diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart index 21b5ef79d7..f7ce60aac7 100644 --- a/cw_solana/lib/default_spl_tokens.dart +++ b/cw_solana/lib/default_spl_tokens.dart @@ -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)', diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index e014460008..5102ea51f7 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -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 => ''; diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 431f5f7fbe..2e84b77db1 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -9,49 +9,54 @@ import 'package:cw_solana/pending_solana_transaction.dart'; import 'package:cw_solana/solana_balance.dart'; import 'package:cw_solana/solana_exceptions.dart'; import 'package:cw_solana/solana_transaction_model.dart'; +import 'package:cw_solana/solana_rpc_service.dart'; +import 'package:cw_solana/spl_token.dart'; import 'package:http/http.dart' as http; -import 'package:solana/dto.dart'; -import 'package:solana/encoder.dart'; -import 'package:solana/solana.dart'; +import 'package:on_chain/solana/solana.dart'; +import 'package:on_chain/solana/src/models/pda/pda.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import '.secrets.g.dart' as secrets; class SolanaWalletClient { final httpClient = http.Client(); - SolanaClient? _client; + SolanaRPC? _provider; bool connect(Node node) { try { - Uri rpcUri = node.uri; - String webSocketUrl = 'wss://${node.uriRaw}'; + String formattedUrl; + String protocolUsed = node.isSSL ? "https" : "http"; if (node.uriRaw == 'rpc.ankr.com') { String ankrApiKey = secrets.ankrApiKey; - rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); - webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; + formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey'; } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') { String chainStackApiKey = secrets.chainStackApiKey; - rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey'); - webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey'; + formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey'; + } else { + formattedUrl = '$protocolUsed://${node.uriRaw}'; } - _client = SolanaClient( - rpcUrl: rpcUri, - websocketUrl: Uri.parse(webSocketUrl), - timeout: const Duration(minutes: 2), - ); + _provider = SolanaRPC(SolanaRPCHTTPService(url: formattedUrl)); + return true; } catch (e) { return false; } } - Future getBalance(String address) async { + Future getBalance(String walletAddress) async { try { - final balance = await _client!.rpcClient.getBalance(address); + final balance = await _provider!.requestWithContext( + SolanaRPCGetBalance( + account: SolAddress(walletAddress), + ), + ); + + final balInLamp = balance.result.toDouble(); - final solBalance = balance.value / lamportsPerSol; + final solBalance = balInLamp / SolanaUtils.lamportsPerSol; return solBalance; } catch (_) { @@ -59,37 +64,42 @@ class SolanaWalletClient { } } - Future getSPLTokenAccounts(String mintAddress, String publicKey) async { + Future?> getSPLTokenAccounts( + String mintAddress, String publicKey) async { try { - final tokenAccounts = await _client!.rpcClient.getTokenAccountsByOwner( - publicKey, - TokenAccountsFilter.byMint(mintAddress), - commitment: Commitment.confirmed, - encoding: Encoding.jsonParsed, + final result = await _provider!.request( + SolanaRPCGetTokenAccountsByOwner( + account: SolAddress(publicKey), + mint: SolAddress(mintAddress), + commitment: Commitment.confirmed, + encoding: SolanaRPCEncoding.base64, + ), ); - return tokenAccounts; + + return result; } catch (e) { return null; } } - Future getSplTokenBalance(String mintAddress, String publicKey) async { + Future getSplTokenBalance(String mintAddress, String walletAddress) async { // Fetch the token accounts (a token can have multiple accounts for various uses) - final tokenAccounts = await getSPLTokenAccounts(mintAddress, publicKey); + final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress); // Handle scenario where there is no token account - if (tokenAccounts == null || tokenAccounts.value.isEmpty) { + if (tokenAccounts == null || tokenAccounts.isEmpty) { return null; } // Sum the balances of all accounts with the specified mint address double totalBalance = 0.0; - for (var programAccount in tokenAccounts.value) { - final tokenAmountResult = - await _client!.rpcClient.getTokenAccountBalance(programAccount.pubkey); + for (var tokenAccount in tokenAccounts) { + final tokenAmountResult = await _provider!.request( + SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey), + ); - final balance = tokenAmountResult.value.uiAmountString; + final balance = tokenAmountResult.uiAmountString; final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0; @@ -101,178 +111,236 @@ class SolanaWalletClient { Future getFeeForMessage(String message, Commitment commitment) async { try { - final feeForMessage = - await _client!.rpcClient.getFeeForMessage(message, commitment: commitment); - final fee = (feeForMessage ?? 0.0) / lamportsPerSol; + final feeForMessage = await _provider!.request( + SolanaRPCGetFeeForMessage( + encodedMessage: message, + commitment: commitment, + ), + ); + + final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol; return fee; } catch (_) { return 0.0; } } - Future getEstimatedFee(Ed25519HDKeyPair ownerKeypair) async { - const commitment = Commitment.confirmed; - - final message = - _getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol); - - final latestBlockhash = await _getLatestBlockhash(commitment); + Future getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async { + final message = await _getMessageForNativeTransaction( + publicKey: publicKey, + destinationAddress: publicKey.toAddress().address, + lamports: SolanaUtils.lamportsPerSol, + commitment: commitment, + ); - final estimatedFee = _getFeeFromCompiledMessage( + final estimatedFee = await _getFeeFromCompiledMessage( message, - ownerKeypair.publicKey, - latestBlockhash, commitment, ); return estimatedFee; } + Future parseTransaction({ + VersionedTransactionResponse? txResponse, + required String address, + String? splTokenSymbol, + }) async { + if (txResponse == null) return null; + + try { + final blockTime = txResponse.blockTime; + final meta = txResponse.meta; + final transaction = txResponse.transaction; + + if (meta == null || transaction == null) return null; + + final int fee = meta.fee; + + final message = transaction.message; + final instructions = message.compiledInstructions; + + String sender = ""; + String receiver = ""; + + String signature = (txResponse.transaction?.signatures.isEmpty ?? true) + ? "" + : Base58Encoder.encode(txResponse.transaction!.signatures.first); + + for (final instruction in instructions) { + final programId = message.accountKeys[instruction.programIdIndex]; + + if (programId == SystemProgramConst.programId) { + // For native solana transactions + if (instruction.accounts.length < 2) continue; + final senderIndex = instruction.accounts[0]; + final receiverIndex = instruction.accounts[1]; + + sender = message.accountKeys[senderIndex].address; + receiver = message.accountKeys[receiverIndex].address; + + final feeForTx = fee / SolanaUtils.lamportsPerSol; + + final preBalances = meta.preBalances; + final postBalances = meta.postBalances; + + final amountInString = + (((preBalances[senderIndex] - postBalances[senderIndex]) / BigInt.from(1e9)) + .toDouble() - + feeForTx) + .toStringAsFixed(6); + + final amount = double.parse(amountInString); + + return SolanaTransactionModel( + isOutgoingTx: sender == address, + from: sender, + to: receiver, + id: signature, + amount: amount.abs(), + programId: SystemProgramConst.programId.address, + tokenSymbol: 'SOL', + blockTimeInInt: blockTime?.toInt() ?? 0, + fee: feeForTx, + ); + } else if (programId == SPLTokenProgramConst.tokenProgramId) { + // For SPL Token transactions + if (instruction.accounts.length < 2) continue; + + final preBalances = meta.preTokenBalances; + final postBalances = meta.postTokenBalances; + + double amount = 0.0; + if (preBalances != null && + preBalances.isNotEmpty && + postBalances != null && + postBalances.isNotEmpty) { + final amountInString = ((preBalances.first.uiTokenAmount.uiAmount ?? 0) - + (postBalances.first.uiTokenAmount.uiAmount ?? 0)) + .toStringAsFixed(6); + + amount = double.parse(amountInString); + } + + sender = message.accountKeys[instruction.accounts[0]].address; + receiver = message.accountKeys[instruction.accounts[1]].address; + final mintAddress = message.accountKeys[4].address; + + String? tokenSymbol = splTokenSymbol; + + if (tokenSymbol == null) { + final token = await fetchSPLTokenInfo(mintAddress); + + tokenSymbol = token?.symbol; + } + + final model = SolanaTransactionModel( + isOutgoingTx: sender == address, + from: sender, + to: receiver, + id: signature, + amount: amount, + programId: SPLTokenProgramConst.tokenProgramId.address, + blockTimeInInt: blockTime?.toInt() ?? 0, + tokenSymbol: tokenSymbol ?? '', + fee: fee / SolanaUtils.lamportsPerSol, + ); + return model; + } else { + return null; + } + } + } catch (e, s) { + printV("Error parsing transaction: $e\n$s"); + } + + return null; + } + /// Load the Address's transactions into the account Future> fetchTransactions( - Ed25519HDPublicKey publicKey, { + SolAddress address, { String? splTokenSymbol, int? splTokenDecimal, + Commitment? commitment, }) async { List transactions = []; try { - final signatures = await _client!.rpcClient.getSignaturesForAddress( - publicKey.toBase58(), - commitment: Commitment.confirmed, + final signatures = await _provider!.request( + SolanaRPCGetSignaturesForAddress( + account: address, + commitment: commitment, + ), ); - final List transactionDetails = []; + final List transactionDetails = []; + for (int i = 0; i < signatures.length; i += 20) { - final response = await _client!.rpcClient.getMultipleTransactions( - signatures.sublist(i, math.min(i + 20, signatures.length)), - commitment: Commitment.confirmed, - encoding: Encoding.jsonParsed, - ); - transactionDetails.addAll(response); + final batch = signatures.skip(i).take(20).toList(); // Get the next 20 signatures + + final batchResponses = await Future.wait(batch.map((signature) async { + try { + return await _provider!.request( + SolanaRPCGetTransaction( + transactionSignature: signature['signature'], + encoding: SolanaRPCEncoding.jsonParsed, + maxSupportedTransactionVersion: 0, + ), + ); + } catch (e) { + printV("Error fetching transaction: $e"); + return null; + } + })); + + transactionDetails.addAll(batchResponses.whereType()); // to avoid reaching the node RPS limit - await Future.delayed(Duration(milliseconds: 500)); + if (i + 20 < signatures.length) { + await Future.delayed(const Duration(milliseconds: 500)); + } } for (final tx in transactionDetails) { - if (tx.transaction is ParsedTransaction) { - final parsedTx = (tx.transaction as ParsedTransaction); - final message = parsedTx.message; - - final fee = (tx.meta?.fee ?? 0) / lamportsPerSol; - - for (final instruction in message.instructions) { - if (instruction is ParsedInstruction) { - instruction.map( - system: (systemData) { - systemData.parsed.map( - transfer: (transferData) { - ParsedSystemTransferInformation transfer = transferData.info; - bool isOutgoingTx = transfer.source == publicKey.toBase58(); - - double amount = transfer.lamports.toDouble() / lamportsPerSol; - - transactions.add( - SolanaTransactionModel( - id: parsedTx.signatures.first, - from: transfer.source, - to: transfer.destination, - amount: amount, - isOutgoingTx: isOutgoingTx, - blockTimeInInt: tx.blockTime!, - fee: fee, - programId: SystemProgram.programId, - tokenSymbol: 'SOL', - ), - ); - }, - transferChecked: (_) {}, - unsupported: (_) {}, - ); - }, - splToken: (splTokenData) { - if (splTokenSymbol != null) { - splTokenData.parsed.map( - transfer: (transferData) { - SplTokenTransferInfo transfer = transferData.info; - bool isOutgoingTx = transfer.source == publicKey.toBase58(); - - double amount = (double.tryParse(transfer.amount) ?? 0.0) / - math.pow(10, splTokenDecimal ?? 9); - - transactions.add( - SolanaTransactionModel( - id: parsedTx.signatures.first, - fee: fee, - from: transfer.source, - to: transfer.destination, - amount: amount, - isOutgoingTx: isOutgoingTx, - programId: TokenProgram.programId, - blockTimeInInt: tx.blockTime!, - tokenSymbol: splTokenSymbol, - ), - ); - }, - transferChecked: (transferCheckedData) { - SplTokenTransferCheckedInfo transfer = transferCheckedData.info; - bool isOutgoingTx = transfer.source == publicKey.toBase58(); - double amount = - double.tryParse(transfer.tokenAmount.uiAmountString ?? '0.0') ?? 0.0; - - transactions.add( - SolanaTransactionModel( - id: parsedTx.signatures.first, - fee: fee, - from: transfer.source, - to: transfer.destination, - amount: amount, - isOutgoingTx: isOutgoingTx, - programId: TokenProgram.programId, - blockTimeInInt: tx.blockTime!, - tokenSymbol: splTokenSymbol, - ), - ); - }, - generic: (genericData) {}, - ); - } - }, - memo: (_) {}, - unsupported: (a) {}, - ); - } - } + final parsedTx = await parseTransaction( + txResponse: tx, + splTokenSymbol: splTokenSymbol, + address: address.address, + ); + if (parsedTx != null) { + transactions.add(parsedTx); } } return transactions; - } catch (err) { + } catch (err, s) { + printV('Error fetching transactions: $err \n$s'); return []; } } - Future> getSPLTokenTransfers( - String address, - String splTokenSymbol, - int splTokenDecimal, - Ed25519HDKeyPair ownerKeypair, - ) async { - final tokenMint = Ed25519HDPublicKey.fromBase58(address); - - ProgramAccount? associatedTokenAccount; + Future> getSPLTokenTransfers({ + required String address, + required String splTokenSymbol, + required int splTokenDecimal, + required SolanaPrivateKey privateKey, + }) async { + ProgramDerivedAddress? associatedTokenAccount; try { - associatedTokenAccount = await _client!.getAssociatedTokenAccount( - mint: tokenMint, - owner: ownerKeypair.publicKey, - commitment: Commitment.confirmed, + associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( + payerPrivateKey: privateKey, + mintAddress: SolAddress(address), + ownerAddress: privateKey.publicKey().toAddress(), + shouldCreateATA: false, ); - } catch (_) {} + } catch (e, s) { + printV('$e \n $s'); + } if (associatedTokenAccount == null) return []; - final accountPublicKey = Ed25519HDPublicKey.fromBase58(associatedTokenAccount.pubkey); + final accountPublicKey = associatedTokenAccount.address; final tokenTransactions = await fetchTransactions( accountPublicKey, @@ -283,16 +351,51 @@ class SolanaWalletClient { return tokenTransactions; } + Future fetchSPLTokenInfo(String mintAddress) async { + final programAddress = + MetaplexTokenMetaDataProgramUtils.findMetadataPda(mint: SolAddress(mintAddress)); + + final token = await _provider!.request( + SolanaRPCGetMetadataAccount( + account: programAddress.address, + commitment: Commitment.confirmed, + ), + ); + + if (token == null) { + return null; + } + + final metadata = token.data; + + String? iconPath; + //TODO(Further explore fetching images) + // try { + // iconPath = await _client.getIconImageFromTokenUri(metadata.uri); + // } catch (_) {} + + String filteredTokenSymbol = + metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + + return SPLToken.fromMetadata( + name: metadata.name, + mint: metadata.symbol, + symbol: filteredTokenSymbol, + mintAddress: token.mint.address, + iconPath: iconPath, + ); + } + void stop() {} - SolanaClient? get getSolanaClient => _client; + SolanaRPC? get getSolanaProvider => _provider; Future signSolanaTransaction({ required String tokenTitle, required int tokenDecimals, required double inputAmount, required String destinationAddress, - required Ed25519HDKeyPair ownerKeypair, + required SolanaPrivateKey ownerPrivateKey, required bool isSendAll, required double solBalance, String? tokenMint, @@ -302,11 +405,9 @@ class SolanaWalletClient { if (tokenTitle == CryptoCurrency.sol.title) { final pendingNativeTokenTransaction = await _signNativeTokenTransaction( - tokenTitle: tokenTitle, - tokenDecimals: tokenDecimals, inputAmount: inputAmount, destinationAddress: destinationAddress, - ownerKeypair: ownerKeypair, + ownerPrivateKey: ownerPrivateKey, commitment: commitment, isSendAll: isSendAll, solBalance: solBalance, @@ -314,12 +415,11 @@ class SolanaWalletClient { return pendingNativeTokenTransaction; } else { final pendingSPLTokenTransaction = _signSPLTokenTransaction( - tokenTitle: tokenTitle, tokenDecimals: tokenDecimals, tokenMint: tokenMint!, inputAmount: inputAmount, + ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, - ownerKeypair: ownerKeypair, commitment: commitment, solBalance: solBalance, ); @@ -327,47 +427,72 @@ class SolanaWalletClient { } } - Future _getLatestBlockhash(Commitment commitment) async { - final latestBlockHashResult = - await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value; - - final latestBlockhash = LatestBlockhash( - blockhash: latestBlockHashResult.blockhash, - lastValidBlockHeight: latestBlockHashResult.lastValidBlockHeight, + Future _getLatestBlockhash(Commitment commitment) async { + final latestBlockhash = await _provider!.request( + const SolanaRPCGetLatestBlockhash(), ); - return latestBlockhash; + return latestBlockhash.blockhash; } - Message _getMessageForNativeTransaction( - Ed25519HDKeyPair ownerKeypair, - String destinationAddress, - int lamports, - ) { + Future _getMessageForNativeTransaction({ + required SolanaPublicKey publicKey, + required String destinationAddress, + required int lamports, + required Commitment commitment, + }) async { final instructions = [ - SystemInstruction.transfer( - fundingAccount: ownerKeypair.publicKey, - recipientAccount: Ed25519HDPublicKey.fromBase58(destinationAddress), - lamports: lamports, + SystemProgram.transfer( + from: publicKey.toAddress(), + layout: SystemTransferLayout(lamports: BigInt.from(lamports)), + to: SolAddress(destinationAddress), ), ]; - final message = Message(instructions: instructions); + final latestBlockhash = await _getLatestBlockhash(commitment); + + final message = Message.compile( + transactionInstructions: instructions, + payer: publicKey.toAddress(), + recentBlockhash: latestBlockhash, + ); return message; } - Future _getFeeFromCompiledMessage( - Message message, - Ed25519HDPublicKey feePayer, - LatestBlockhash latestBlockhash, - Commitment commitment, - ) async { - final compile = message.compile( - recentBlockhash: latestBlockhash.blockhash, - feePayer: feePayer, + Future _getMessageForSPLTokenTransaction({ + required SolAddress ownerAddress, + required SolAddress destinationAddress, + required int tokenDecimals, + required SolAddress mintAddress, + required SolAddress sourceAccount, + required int amount, + required Commitment commitment, + }) async { + final instructions = [ + SPLTokenProgram.transferChecked( + layout: SPLTokenTransferCheckedLayout( + amount: BigInt.from(123456455345234234), + decimals: tokenDecimals, + ), + mint: mintAddress, + source: sourceAccount, + destination: destinationAddress, + owner: ownerAddress, + ) + ]; + + final latestBlockhash = await _getLatestBlockhash(commitment); + + final message = Message.compile( + transactionInstructions: instructions, + payer: ownerAddress, + recentBlockhash: latestBlockhash, ); + return message; + } - final base64Message = base64Encode(compile.toByteArray().toList()); + Future _getFeeFromCompiledMessage(Message message, Commitment commitment) async { + final base64Message = base64Encode(message.serialize()); final fee = await getFeeForMessage(base64Message, commitment); @@ -379,43 +504,43 @@ class SolanaWalletClient { required double solBalance, required double fee, }) async { - return true; - // TODO: this is not doing what the name inclines - // final rent = - // await _client!.getMinimumBalanceForMintRentExemption(commitment: Commitment.confirmed); - // - // final rentInSol = (rent / lamportsPerSol).toDouble(); - // - // final remnant = solBalance - (inputAmount + fee); - // - // if (remnant > rentInSol) return true; - // - // return false; + final rent = await _provider!.request( + SolanaRPCGetMinimumBalanceForRentExemption( + size: SolanaTokenAccountUtils.accountSize, + ), + ); + + final rentInSol = (rent.toDouble() / SolanaUtils.lamportsPerSol).toDouble(); + + final remnant = solBalance - (inputAmount + fee); + + if (remnant > rentInSol) return true; + + return false; } Future _signNativeTokenTransaction({ - required String tokenTitle, - required int tokenDecimals, required double inputAmount, required String destinationAddress, - required Ed25519HDKeyPair ownerKeypair, + required SolanaPrivateKey ownerPrivateKey, required Commitment commitment, required bool isSendAll, required double solBalance, }) async { // Convert SOL to lamport - int lamports = (inputAmount * lamportsPerSol).toInt(); - - Message message = _getMessageForNativeTransaction(ownerKeypair, destinationAddress, lamports); + int lamports = (inputAmount * SolanaUtils.lamportsPerSol).toInt(); - final signers = [ownerKeypair]; + Message message = await _getMessageForNativeTransaction( + publicKey: ownerPrivateKey.publicKey(), + destinationAddress: destinationAddress, + lamports: lamports, + commitment: commitment, + ); - LatestBlockhash latestBlockhash = await _getLatestBlockhash(commitment); + SolAddress latestBlockhash = await _getLatestBlockhash(commitment); final fee = await _getFeeFromCompiledMessage( message, - signers.first.publicKey, - latestBlockhash, commitment, ); @@ -429,37 +554,44 @@ class SolanaWalletClient { throw SolanaSignNativeTokenTransactionRentException(); } - SignedTx signedTx; + String serializedTransaction; if (isSendAll) { - final feeInLamports = (fee * lamportsPerSol).toInt(); + final feeInLamports = (fee * SolanaUtils.lamportsPerSol).toInt(); final updatedLamports = lamports - feeInLamports; - final updatedMessage = - _getMessageForNativeTransaction(ownerKeypair, destinationAddress, updatedLamports); - - signedTx = await _signTransactionInternal( - message: updatedMessage, - signers: signers, - commitment: commitment, + final transaction = _constructNativeTransaction( + ownerPrivateKey: ownerPrivateKey, + destinationAddress: destinationAddress, latestBlockhash: latestBlockhash, + lamports: updatedLamports, + ); + + serializedTransaction = await _signTransactionInternal( + ownerPrivateKey: ownerPrivateKey, + transaction: transaction, ); } else { - signedTx = await _signTransactionInternal( - message: message, - signers: signers, - commitment: commitment, + final transaction = _constructNativeTransaction( + ownerPrivateKey: ownerPrivateKey, + destinationAddress: destinationAddress, latestBlockhash: latestBlockhash, + lamports: lamports, + ); + + serializedTransaction = await _signTransactionInternal( + ownerPrivateKey: ownerPrivateKey, + transaction: transaction, ); } sendTx() async => await sendTransaction( - signedTransaction: signedTx, + serializedTransaction: serializedTransaction, commitment: commitment, ); final pendingTransaction = PendingSolanaTransaction( amount: inputAmount, - signedTransaction: signedTx, + serializedTransaction: serializedTransaction, destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, @@ -468,108 +600,170 @@ class SolanaWalletClient { return pendingTransaction; } + SolanaTransaction _constructNativeTransaction({ + required SolanaPrivateKey ownerPrivateKey, + required String destinationAddress, + required SolAddress latestBlockhash, + required int lamports, + }) { + final owner = ownerPrivateKey.publicKey().toAddress(); + + /// Create a transfer instruction to move funds from the owner to the receiver. + final transferInstruction = SystemProgram.transfer( + from: owner, + layout: SystemTransferLayout(lamports: BigInt.from(lamports)), + to: SolAddress(destinationAddress), + ); + + /// Construct a Solana transaction with the transfer instruction. + return SolanaTransaction( + instructions: [transferInstruction], + recentBlockhash: latestBlockhash, + payerKey: ownerPrivateKey.publicKey().toAddress(), + type: TransactionType.v0, + ); + } + + Future _getOrCreateAssociatedTokenAccount({ + required SolanaPrivateKey payerPrivateKey, + required SolAddress ownerAddress, + required SolAddress mintAddress, + required bool shouldCreateATA, + }) async { + final associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + mint: mintAddress, + owner: ownerAddress, + ); + + SolanaAccountInfo? accountInfo; + try { + accountInfo = await _provider!.request( + SolanaRPCGetAccountInfo(account: associatedTokenAccount.address), + ); + } catch (e) { + accountInfo = null; + } + + // If aacountInfo is null, signifies that the associatedTokenAccount has only been created locally and not been broadcasted to the blockchain. + if (accountInfo != null) return associatedTokenAccount; + + if (!shouldCreateATA) return null; + + final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount( + payer: payerPrivateKey.publicKey().toAddress(), + associatedToken: associatedTokenAccount.address, + owner: ownerAddress, + mint: mintAddress, + ); + + final blockhash = await _getLatestBlockhash(Commitment.confirmed); + + final transaction = SolanaTransaction( + payerKey: payerPrivateKey.publicKey().toAddress(), + instructions: [createAssociatedTokenAccount], + recentBlockhash: blockhash, + ); + + transaction.sign([payerPrivateKey]); + + await sendTransaction( + serializedTransaction: transaction.serializeString(), + commitment: Commitment.confirmed, + ); + + // Delay for propagation on the blockchain for newly created associated token addresses + await Future.delayed(const Duration(seconds: 2)); + + return associatedTokenAccount; + } + Future _signSPLTokenTransaction({ - required String tokenTitle, required int tokenDecimals, required String tokenMint, required double inputAmount, required String destinationAddress, - required Ed25519HDKeyPair ownerKeypair, + required SolanaPrivateKey ownerPrivateKey, required Commitment commitment, required double solBalance, }) async { - final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress); - final mint = Ed25519HDPublicKey.fromBase58(tokenMint); + final mintAddress = SolAddress(tokenMint); // Input by the user final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt(); - - ProgramAccount? associatedRecipientAccount; - ProgramAccount? associatedSenderAccount; - - associatedRecipientAccount = await _client!.getAssociatedTokenAccount( - mint: mint, - owner: destinationOwner, - commitment: commitment, - ); - - associatedSenderAccount = await _client!.getAssociatedTokenAccount( - owner: ownerKeypair.publicKey, - mint: mint, - commitment: commitment, - ); + ProgramDerivedAddress? associatedSenderAccount; + try { + associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + mint: mintAddress, + owner: ownerPrivateKey.publicKey().toAddress(), + ); + } catch (e) { + associatedSenderAccount = null; + } // Throw an appropriate exception if the sender has no associated // token account if (associatedSenderAccount == null) { - throw SolanaNoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58()); + throw SolanaNoAssociatedTokenAccountException( + ownerPrivateKey.publicKey().toAddress().address, + mintAddress.address, + ); } + ProgramDerivedAddress? associatedRecipientAccount; try { - if (associatedRecipientAccount == null) { - final derivedAddress = await findAssociatedTokenAddress( - owner: destinationOwner, - mint: mint, - ); - - final instruction = AssociatedTokenAccountInstruction.createAccount( - mint: mint, - address: derivedAddress, - owner: destinationOwner, - funder: ownerKeypair.publicKey, - ); - - final _signedTx = await _signTransactionInternal( - message: Message.only(instruction), - signers: [ownerKeypair], - commitment: commitment, - latestBlockhash: await _getLatestBlockhash(commitment), - ); - - await sendTransaction( - signedTransaction: _signedTx, - commitment: commitment, - ); + associatedRecipientAccount = await _getOrCreateAssociatedTokenAccount( + payerPrivateKey: ownerPrivateKey, + mintAddress: mintAddress, + ownerAddress: SolAddress(destinationAddress), + shouldCreateATA: true, + ); + } catch (e) { + associatedRecipientAccount = null; - associatedRecipientAccount = ProgramAccount( - pubkey: derivedAddress.toBase58(), - account: Account( - owner: destinationOwner.toBase58(), - lamports: 0, - executable: false, - rentEpoch: BigInt.zero, - data: null, - ), - ); + throw SolanaCreateAssociatedTokenAccountException( + 'Error fetching recipient associated token account: ${e.toString()}', + ); + } - await Future.delayed(Duration(seconds: 5)); - } - } catch (e) { - throw SolanaCreateAssociatedTokenAccountException(e.toString()); + if (associatedRecipientAccount == null) { + throw SolanaCreateAssociatedTokenAccountException( + 'Error fetching recipient associated token account', + ); } - final instruction = TokenInstruction.transfer( - source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey), - destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey), - owner: ownerKeypair.publicKey, - amount: amount, + final transferInstructions = SPLTokenProgram.transferChecked( + layout: SPLTokenTransferCheckedLayout( + amount: BigInt.from(amount), + decimals: tokenDecimals, + ), + mint: mintAddress, + source: associatedSenderAccount.address, + destination: associatedRecipientAccount.address, + owner: ownerPrivateKey.publicKey().toAddress(), ); - final message = Message(instructions: [instruction]); + final latestBlockHash = await _getLatestBlockhash(commitment); - final signers = [ownerKeypair]; - - LatestBlockhash latestBlockhash = await _getLatestBlockhash(commitment); + final transaction = SolanaTransaction( + payerKey: ownerPrivateKey.publicKey().toAddress(), + instructions: [transferInstructions], + recentBlockhash: latestBlockHash, + ); - final fee = await _getFeeFromCompiledMessage( - message, - signers.first.publicKey, - latestBlockhash, - commitment, + final message = await _getMessageForSPLTokenTransaction( + ownerAddress: ownerPrivateKey.publicKey().toAddress(), + tokenDecimals: tokenDecimals, + mintAddress: mintAddress, + destinationAddress: associatedRecipientAccount.address, + sourceAccount: associatedSenderAccount.address, + amount: amount, + commitment: commitment, ); + final fee = await _getFeeFromCompiledMessage(message, commitment); + bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( - inputAmount: inputAmount, + inputAmount: 0, fee: fee, solBalance: solBalance, ); @@ -578,25 +772,19 @@ class SolanaWalletClient { throw SolanaSignSPLTokenTransactionRentException(); } - final signedTx = await _signTransactionInternal( - message: message, - signers: signers, - commitment: commitment, - latestBlockhash: latestBlockhash, + final serializedTransaction = await _signTransactionInternal( + ownerPrivateKey: ownerPrivateKey, + transaction: transaction, ); - sendTx() async { - await Future.delayed(Duration(seconds: 3)); - - return await sendTransaction( - signedTransaction: signedTx, + sendTx() async => await sendTransaction( + serializedTransaction: serializedTransaction, commitment: commitment, ); - } final pendingTransaction = PendingSolanaTransaction( amount: inputAmount, - signedTransaction: signedTx, + serializedTransaction: serializedTransaction, destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, @@ -604,37 +792,41 @@ class SolanaWalletClient { return pendingTransaction; } - Future _signTransactionInternal({ - required Message message, - required List signers, - required Commitment commitment, - required LatestBlockhash latestBlockhash, + Future _signTransactionInternal({ + required SolanaPrivateKey ownerPrivateKey, + required SolanaTransaction transaction, }) async { - final signedTx = await signTransaction(latestBlockhash, message, signers); + /// Sign the transaction with the owner's private key. + final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage()); + transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature); - return signedTx; + /// Serialize the transaction. + final serializedTransaction = transaction.serializeString(); + + return serializedTransaction; } Future sendTransaction({ - required SignedTx signedTransaction, + required String serializedTransaction, required Commitment commitment, }) async { try { - final signature = await _client!.rpcClient.sendTransaction( - signedTransaction.encode(), - preflightCommitment: commitment, + /// Send the transaction to the Solana network. + final signature = await _provider!.request( + SolanaRPCSendTransaction( + encodedTransaction: serializedTransaction, + commitment: commitment, + ), ); - - _client!.waitForSignatureStatus(signature, status: commitment); - return signature; } catch (e) { - printV('Error while sending transaction: ${e.toString()}'); throw Exception(e); } } Future getIconImageFromTokenUri(String uri) async { + if (uri.isEmpty || uri == '…') return null; + try { final response = await httpClient.get(Uri.parse(uri)); diff --git a/cw_solana/lib/solana_rpc_service.dart b/cw_solana/lib/solana_rpc_service.dart new file mode 100644 index 0000000000..fbe9a29dcc --- /dev/null +++ b/cw_solana/lib/solana_rpc_service.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; +import 'package:http/http.dart'; +import 'package:on_chain/solana/solana.dart'; + +class SolanaRPCHTTPService implements SolanaJSONRPCService { + SolanaRPCHTTPService( + {required this.url, Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)}) + : client = client ?? Client(); + @override + final String url; + final Client client; + final Duration defaultRequestTimeout; + + @override + Future> call(SolanaRequestDetails params, [Duration? timeout]) async { + final response = await client.post( + Uri.parse(url), + body: params.toRequestBody(), + headers: { + 'Content-Type': 'application/json', + }, + ).timeout(timeout ?? defaultRequestTimeout); + final data = json.decode(response.body) as Map; + return data; + } +} diff --git a/cw_solana/lib/solana_transaction_info.dart b/cw_solana/lib/solana_transaction_info.dart index 7a0844e52a..f51c55dad6 100644 --- a/cw_solana/lib/solana_transaction_info.dart +++ b/cw_solana/lib/solana_transaction_info.dart @@ -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'; } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 15c0659185..c69ddd8805 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -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 _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; - @override - String? get seed => _mnemonic; + late final SolanaPublicKey _solanaPublicKey; - @override - String get privateKey { - final privateKeyBytes = _keyPairData!.bytes; + SolanaPublicKey get solanaPublicKey => _solanaPublicKey; - final publicKeyBytes = _keyPairData!.publicKey.bytes; + SolanaPrivateKey get solanaPrivateKey => _solanaPrivateKey; - final encodedBytes = privateKeyBytes + publicKeyBytes; + String get solanaAddress => _solanaPublicKey.toAddress().address; - final privateKey = base58encode(encodedBytes); + @override + String? get seed => _mnemonic; - return privateKey; - } + @override + 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(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 getWalletPair({String? mnemonic, String? privateKey}) async { + Future 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 _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 _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!, + address: 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 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 _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 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 pubKeyBytes = base58decode(base58EncodedPublicKeyString); + List 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; diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index aff75373e6..a33cebb3a2 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -33,6 +33,7 @@ class SolanaWalletService extends WalletService 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); -} diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml index 807acdca8a..82b5a2bc07 100644 --- a/cw_solana/pubspec.yaml +++ b/cw_solana/pubspec.yaml @@ -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: diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index e69fd7ca04..80ea7ee518 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: path: ../cw_evm on_chain: git: - url: https://github.com/cake-tech/On_chain + url: https://github.com/cake-tech/on_chain.git ref: cake-update-v2 blockchain_utils: git: diff --git a/lib/core/wallet_connect/chain_service/solana/rpc_provider.dart b/lib/core/wallet_connect/chain_service/solana/rpc_provider.dart new file mode 100644 index 0000000000..b747389292 --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/rpc_provider.dart @@ -0,0 +1,22 @@ +import 'dart:convert'; +import 'package:http/http.dart'; +import 'package:on_chain/solana/solana.dart'; + +class SolanaRPCHTTPService implements SolanaJSONRPCService { + SolanaRPCHTTPService( + {required this.url, Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)}) + : client = client ?? Client(); + @override + final String url; + final Client client; + final Duration defaultRequestTimeout; + + @override + Future> call(SolanaRequestDetails params, [Duration? timeout]) async { + final response = await client.get(Uri.parse(url), headers: { + 'Content-Type': 'application/json', + }).timeout(timeout ?? defaultRequestTimeout); + final data = json.decode(response.body) as Map; + return data; + } +} diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart index d7fe53c73d..9395c63a73 100644 --- a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart +++ b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart @@ -1,5 +1,7 @@ +import 'dart:convert'; import 'dart:developer'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart'; import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; @@ -9,8 +11,8 @@ import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:solana/base58.dart'; -import 'package:solana/solana.dart'; +import 'package:cw_solana/solana_rpc_service.dart'; +import 'package:on_chain/solana/solana.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import '../chain_service.dart'; import '../../wallet_connect_key_service.dart'; @@ -27,25 +29,19 @@ class SolanaChainServiceImpl implements ChainService { final SolanaChainId reference; - final SolanaClient solanaClient; + final SolanaRPC solanaProvider; - final Ed25519HDKeyPair? ownerKeyPair; + final SolanaPrivateKey? ownerPrivateKey; SolanaChainServiceImpl({ required this.reference, required this.wcKeyService, required this.bottomSheetService, required this.wallet, - required this.ownerKeyPair, - required String webSocketUrl, - required Uri rpcUrl, - SolanaClient? solanaClient, - }) : solanaClient = solanaClient ?? - SolanaClient( - rpcUrl: rpcUrl, - websocketUrl: Uri.parse(webSocketUrl), - timeout: const Duration(minutes: 5), - ) { + required this.ownerPrivateKey, + required String formattedRPCUrl, + SolanaRPC? solanaProvider, + }) : solanaProvider = solanaProvider ?? SolanaRPC(SolanaRPCHTTPService(url: formattedRPCUrl)) { for (final String event in getEvents()) { wallet.registerEventEmitter(chainId: getChainId(), event: event); } @@ -110,22 +106,18 @@ class SolanaChainServiceImpl implements ChainService { } try { - final message = - await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction); + // Convert transaction string to bytes + List transactionBytes = base64Decode(solanaSignTx.transaction); - final sign = await ownerKeyPair?.signMessage( - message: message, - recentBlockhash: solanaSignTx.recentBlockhash ?? '', - ); + final message = SolanaTransactionUtils.deserializeMessageLegacy(transactionBytes); - if (sign == null) { - return ''; - } + ownerPrivateKey!.sign(message.serialize()); - String signature = await solanaClient.sendAndConfirmTransaction( - message: message, - signers: [ownerKeyPair!], - commitment: Commitment.confirmed, + final signature = solanaProvider.request( + SolanaRPCSendTransaction( + encodedTransaction: message.serializeHex(), + commitment: Commitment.confirmed, + ), ); printV(signature); @@ -161,10 +153,10 @@ class SolanaChainServiceImpl implements ChainService { if (authError != null) { return authError; } - Signature? sign; + List? sign; try { - sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message)); + sign = ownerPrivateKey!.sign(Base58Decoder.decode(solanaSignMessage.message)); } catch (e) { printV(e); } @@ -173,7 +165,7 @@ class SolanaChainServiceImpl implements ChainService { return ''; } - String signature = sign.toBase58(); + final signature = Base58Encoder.encode(sign); return signature; } diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart index 3740d3dfea..898433c621 100644 --- a/lib/core/wallet_connect/web3wallet_service.dart +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -22,6 +22,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; +import 'package:on_chain/solana/solana.dart' hide Store; import 'package:shared_preferences/shared_preferences.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; @@ -140,29 +141,28 @@ abstract class Web3WalletServiceBase with Store { for (final cId in SolanaChainId.values) { final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type); - Uri rpcUri = node.uri; - String webSocketUrl = 'wss://${node.uriRaw}'; + String formattedUrl; + String protocolUsed = node.isSSL ? "https" : "http"; if (node.uriRaw == 'rpc.ankr.com') { String ankrApiKey = secrets.ankrApiKey; - rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); - webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; + formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey'; } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') { String chainStackApiKey = secrets.chainStackApiKey; - rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey'); - webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey'; + formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey'; + } else { + formattedUrl = '$protocolUsed://${node.uriRaw}'; } SolanaChainServiceImpl( reference: cId, - rpcUrl: rpcUri, - webSocketUrl: webSocketUrl, + formattedRPCUrl: formattedUrl, wcKeyService: walletKeyService, bottomSheetService: _bottomSheetHandler, wallet: _web3Wallet, - ownerKeyPair: solana!.getWalletKeyPair(appStore.wallet!), + ownerPrivateKey: SolanaPrivateKey.fromSeedHex(solana!.getPrivateKey(appStore.wallet!)), ); } } diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 7894f77edb..f8718d79cf 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -54,11 +54,8 @@ class CWSolana extends Solana { String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey; @override - String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).keys.publicKey.toBase58(); - - @override - Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet) => (wallet as SolanaWallet).walletKeyPair; - + String getPublicKey(WalletBase wallet) => + (wallet as SolanaWallet).solanaPublicKey.toAddress().address; Object createSolanaTransactionCredentials( List outputs, { required CryptoCurrency currency, diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index ab6762f8d2..be19721060 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -326,7 +326,7 @@ class _WalletKeysPageBodyState extends State ), ); } - + Widget _buildBottomActionPanel({ required String titleForClipboard, required String dataToCopy, diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 72c17a0795..767f0b1f38 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -106,12 +106,19 @@ dependencies: flutter_svg: ^2.0.9 polyseed: ^0.0.6 nostr_tools: ^1.0.9 - solana: ^0.31.0+1 ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76 hashlib: ^1.19.2 + 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: diff --git a/tool/configure.dart b/tool/configure.dart index 7fb72d5e4c..13b96a4750 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1258,7 +1258,6 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; -import 'package:solana/solana.dart'; """; const solanaCWHeaders = """ @@ -1285,7 +1284,6 @@ abstract class Solana { String getAddress(WalletBase wallet); String getPrivateKey(WalletBase wallet); String getPublicKey(WalletBase wallet); - Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet); Object createSolanaTransactionCredentials( List outputs, {