Merge branch 'refs/heads/main' into CW-1010-implement-tari-support

# Conflicts:
#	lib/di.dart
#	lib/src/screens/settings/other_settings_page.dart
#	macos/Podfile.lock
#	res/values/strings_ko.arb
This commit is contained in:
Konstantin Ullrich 2025-05-13 13:04:38 +02:00
commit 7cbc0b4a45
No known key found for this signature in database
GPG key ID: 6B3199AD9B3D23B8
219 changed files with 10081 additions and 4261 deletions

View file

@ -11,3 +11,4 @@ Please include a summary of the changes and which issue is fixed / feature is ad
- [ ] Format code
- [ ] Look for code duplication
- [ ] Clear naming for variables and methods
- [ ] Manual tests in accessibility mode (TalkBack on Android) passed

View file

@ -42,6 +42,14 @@ android {
disable 'InvalidPackage'
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
namespace "com.cakewallet.cake_wallet"
defaultConfig {
@ -73,7 +81,6 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
@ -92,6 +99,7 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}
configurations {
implementation.exclude module:'proto-google-common-protos'

View file

@ -7,3 +7,96 @@
-keep class io.flutter.plugins.** { *; }
-dontwarn io.flutter.embedding.**
-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication
# start reown
-dontwarn com.github.luben.zstd.BufferPool
-dontwarn com.github.luben.zstd.ZstdInputStream
-dontwarn com.github.luben.zstd.ZstdOutputStream
-dontwarn com.google.api.client.http.GenericUrl
-dontwarn com.google.api.client.http.HttpHeaders
-dontwarn com.google.api.client.http.HttpRequest
-dontwarn com.google.api.client.http.HttpRequestFactory
-dontwarn com.google.api.client.http.HttpResponse
-dontwarn com.google.api.client.http.HttpTransport
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
-dontwarn java.awt.Color
-dontwarn java.awt.Dimension
-dontwarn java.awt.Graphics2D
-dontwarn java.awt.Graphics
-dontwarn java.awt.Image
-dontwarn java.awt.Point
-dontwarn java.awt.Polygon
-dontwarn java.awt.Shape
-dontwarn java.awt.color.ColorSpace
-dontwarn java.awt.geom.AffineTransform
-dontwarn java.awt.image.BufferedImage
-dontwarn java.awt.image.ColorModel
-dontwarn java.awt.image.ComponentColorModel
-dontwarn java.awt.image.ComponentSampleModel
-dontwarn java.awt.image.DataBuffer
-dontwarn java.awt.image.DataBufferByte
-dontwarn java.awt.image.DataBufferInt
-dontwarn java.awt.image.DataBufferUShort
-dontwarn java.awt.image.ImageObserver
-dontwarn java.awt.image.MultiPixelPackedSampleModel
-dontwarn java.awt.image.Raster
-dontwarn java.awt.image.RenderedImage
-dontwarn java.awt.image.SampleModel
-dontwarn java.awt.image.SinglePixelPackedSampleModel
-dontwarn java.awt.image.WritableRaster
-dontwarn java.beans.BeanInfo
-dontwarn java.beans.FeatureDescriptor
-dontwarn java.beans.IntrospectionException
-dontwarn java.beans.Introspector
-dontwarn java.beans.PropertyDescriptor
-dontwarn java.lang.reflect.InaccessibleObjectException
-dontwarn javax.imageio.IIOImage
-dontwarn javax.imageio.ImageIO
-dontwarn javax.imageio.ImageWriteParam
-dontwarn javax.imageio.ImageWriter
-dontwarn javax.imageio.metadata.IIOMetadata
-dontwarn javax.imageio.stream.ImageOutputStream
-dontwarn javax.swing.JComponent
-dontwarn javax.swing.JFileChooser
-dontwarn javax.swing.JFrame
-dontwarn javax.swing.JPanel
-dontwarn javax.swing.ProgressMonitor
-dontwarn javax.swing.SwingUtilities
-dontwarn org.brotli.dec.BrotliInputStream
-dontwarn org.joda.time.Instant
-dontwarn org.objectweb.asm.AnnotationVisitor
-dontwarn org.objectweb.asm.Attribute
-dontwarn org.objectweb.asm.ClassReader
-dontwarn org.objectweb.asm.ClassVisitor
-dontwarn org.objectweb.asm.FieldVisitor
-dontwarn org.objectweb.asm.Label
-dontwarn org.objectweb.asm.MethodVisitor
-dontwarn org.objectweb.asm.Type
-dontwarn org.tukaani.xz.ARMOptions
-dontwarn org.tukaani.xz.ARMThumbOptions
-dontwarn org.tukaani.xz.DeltaOptions
-dontwarn org.tukaani.xz.FilterOptions
-dontwarn org.tukaani.xz.FinishableOutputStream
-dontwarn org.tukaani.xz.FinishableWrapperOutputStream
-dontwarn org.tukaani.xz.IA64Options
-dontwarn org.tukaani.xz.LZMA2InputStream
-dontwarn org.tukaani.xz.LZMA2Options
-dontwarn org.tukaani.xz.LZMAInputStream
-dontwarn org.tukaani.xz.LZMAOutputStream
-dontwarn org.tukaani.xz.MemoryLimitException
-dontwarn org.tukaani.xz.PowerPCOptions
-dontwarn org.tukaani.xz.SPARCOptions
-dontwarn org.tukaani.xz.SingleXZInputStream
-dontwarn org.tukaani.xz.UnsupportedOptionsException
-dontwarn org.tukaani.xz.X86Options
-dontwarn org.tukaani.xz.XZ
-dontwarn org.tukaani.xz.XZInputStream
-dontwarn org.tukaani.xz.XZOutputStream
-dontwarn us.hebi.matlab.mat.ejml.Mat5Ejml
-dontwarn us.hebi.matlab.mat.format.Mat5
-dontwarn us.hebi.matlab.mat.format.Mat5File
-dontwarn us.hebi.matlab.mat.types.Array
-dontwarn us.hebi.matlab.mat.types.MatFile$Entry
-dontwarn us.hebi.matlab.mat.types.MatFile
# end reown

View file

@ -24,6 +24,10 @@
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:name=".Application"
@ -35,6 +39,10 @@
android:versionName="__versionName__"
android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true">
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<activity
android:name=".MainActivity"
android:launchMode="singleInstance"

View file

@ -2,6 +2,7 @@ allprojects {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

View file

@ -0,0 +1,22 @@
<svg width="158" height="158" viewBox="0 0 158 158" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3076_287)">
<path d="M59.8674 111.213C61.104 109.782 62.8874 109.123 64.6415 109.237L69.6321 103.461C69.1122 101.951 69.369 100.216 70.4903 98.9183C71.6101 97.6223 73.2894 97.1164 74.8608 97.4098L79.8316 91.6568L65.7827 79.5181L42.7631 106.16C41.9574 107.093 41.5571 108.305 41.6467 109.532L41.9828 114.163L48.3733 119.684L53.4019 119.732C54.767 119.746 56.0685 119.159 56.9607 118.127L58.6055 116.223C58.2383 114.504 58.6308 112.644 59.8674 111.213Z" fill="url(#paint0_linear_3076_287)"/>
<path d="M115.362 82.4834C126.257 69.8745 124.867 50.823 112.258 39.9285C99.6491 29.0339 80.5975 30.4236 69.703 43.0325C69.1902 43.6261 68.7318 44.2453 68.2752 44.866L68.1857 44.7886L56.0469 58.8375C52.6951 62.7168 53.1228 68.5796 57.002 71.9314L88.6121 99.2435C92.4913 102.595 98.3541 102.168 101.706 98.2884L113.845 84.2395L113.755 84.1621C114.303 83.6203 114.849 83.077 115.362 82.4834ZM104.671 48.709C108.551 52.0608 108.978 57.9236 105.626 61.8029C102.275 65.6821 96.4118 66.1098 92.5325 62.758C88.6532 59.4061 88.2256 53.5434 91.5774 49.6641C94.9292 45.7849 100.792 45.3572 104.671 48.709Z" fill="url(#paint1_linear_3076_287)"/>
</g>
<defs>
<linearGradient id="paint0_linear_3076_287" x1="72.8481" y1="85.54" x2="45.4677" y2="117.229" gradientUnits="userSpaceOnUse">
<stop stop-color="#E5A505"/>
<stop offset="0.01" stop-color="#E9A804"/>
<stop offset="0.06" stop-color="#F4B102"/>
<stop offset="0.129" stop-color="#FBB600"/>
<stop offset="0.323" stop-color="#FDB700"/>
</linearGradient>
<linearGradient id="paint1_linear_3076_287" x1="91.8949" y1="31.3914" x2="84.3571" y2="99.6065" gradientUnits="userSpaceOnUse">
<stop stop-color="#FEDE00"/>
<stop offset="1" stop-color="#FFD000"/>
</linearGradient>
<clipPath id="clip0_3076_287">
<rect width="111.4" height="111.4" fill="white" transform="translate(73.146) rotate(40.8281)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/images/payjoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -1,6 +1,4 @@
Monero 12-word seed support (Wallet Groups support as well)
Integrate DFX's OpenCryptoPay
Exchange flow enhancements
Hardware Wallets flow enhancements
Minor UI enhancements
Background sync improvements
Payment notifications
UI/UX improvements
Bug fixes

View file

@ -1,6 +1,5 @@
Monero 12-word seed support (Wallet Groups support as well)
Integrate DFX's OpenCryptoPay
Exchange flow enhancements
Hardware Wallets flow enhancements
Minor UI enhancements
Background sync improvements
Payment notifications
WalletConnect enhancements
UI/UX improvements
Bug fixes

View file

@ -2,22 +2,36 @@ import 'package:bitcoin_base/bitcoin_base.dart';
String addressFromOutputScript(Script script, BasedUtxoNetwork network) {
try {
switch (script.getAddressType()) {
case P2pkhAddressType.p2pkh:
return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network);
case P2shAddressType.p2pkInP2sh:
return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2wpkh:
return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network);
case P2shAddressType.p2pkhInP2sh:
return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2wsh:
return P2wshAddress.fromScriptPubkey(script: script).toAddress(network);
case SegwitAddresType.p2tr:
return P2trAddress.fromScriptPubkey(script: script).toAddress(network);
default:
}
return addressFromScript(script, network).toAddress(network);
} catch (_) {}
return '';
}
BitcoinBaseAddress addressFromScript(Script script,
[BasedUtxoNetwork network = BitcoinNetwork.mainnet]) {
final addressType = script.getAddressType();
if (addressType == null) {
throw ArgumentError("Invalid script");
}
switch (addressType) {
case P2pkhAddressType.p2pkh:
return P2pkhAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
case P2shAddressType.p2pkhInP2sh:
return P2shAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
case SegwitAddresType.p2wpkh:
return P2wpkhAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
case SegwitAddresType.p2wsh:
return P2wshAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
case SegwitAddresType.p2tr:
return P2trAddress.fromScriptPubkey(
script: script, network: BitcoinNetwork.mainnet);
}
throw ArgumentError("Invalid script");
}

View file

@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:mobx/mobx.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
@ -16,7 +17,7 @@ abstract class BaseBitcoinAddressRecord {
}) : _txCount = txCount,
_balance = balance,
_name = name,
_isUsed = isUsed;
_isUsed = Observable(isUsed);
@override
bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address;
@ -27,7 +28,7 @@ abstract class BaseBitcoinAddressRecord {
int _txCount;
int _balance;
String _name;
bool _isUsed;
final Observable<bool> _isUsed;
BasedUtxoNetwork? network;
int get txCount => _txCount;
@ -40,9 +41,9 @@ abstract class BaseBitcoinAddressRecord {
set balance(int value) => _balance = value;
bool get isUsed => _isUsed;
bool get isUsed => _isUsed.value;
void setAsUsed() => _isUsed = true;
void setAsUsed() => _isUsed.value = true;
void setNewName(String label) => _name = label;
int get hashCode => address.hashCode;

View file

@ -3,11 +3,17 @@ import 'package:cw_core/output_info.dart';
import 'package:cw_core/unspent_coin_type.dart';
class BitcoinTransactionCredentials {
BitcoinTransactionCredentials(this.outputs,
{required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any});
BitcoinTransactionCredentials(
this.outputs, {
required this.priority,
this.feeRate,
this.coinTypeToSpendFrom = UnspentCoinType.any,
this.payjoinUri,
});
final List<OutputInfo> outputs;
final BitcoinTransactionPriority? priority;
final int? feeRate;
final UnspentCoinType coinTypeToSpendFrom;
final String? payjoinUri;
}

View file

@ -3,22 +3,33 @@ import 'dart:convert';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/payjoin/manager.dart';
import 'package:cw_bitcoin/payjoin/storage.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/psbt/signer.dart';
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
import 'package:cw_bitcoin/psbt/v0_finalizer.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:mobx/mobx.dart';
@ -31,6 +42,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required EncryptionFileUtils encryptionFileUtils,
Uint8List? seedBytes,
String? mnemonic,
@ -71,8 +83,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
// String derivationPath = walletInfo.derivationInfo!.derivationPath!;
// String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
// final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
walletAddresses = BitcoinWalletAddresses(
walletInfo,
payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
walletAddresses = BitcoinWalletAddresses(walletInfo,
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
@ -84,7 +97,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
masterHd:
seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
isHardwareWallet: walletInfo.isHardwareWallet,
);
payjoinManager: payjoinManager);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress =
@ -100,6 +113,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
String? addressPageType,
@ -122,9 +136,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
break;
case DerivationType.electrum:
default:
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
seedBytes =
await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break;
}
return BitcoinWallet(
mnemonic: mnemonic,
passphrase: passphrase ?? "",
@ -141,6 +157,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType,
networkParam: network,
payjoinBox: payjoinBox,
);
}
@ -148,6 +165,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String name,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Box<PayjoinSession> payjoinBox,
required String password,
required EncryptionFileUtils encryptionFileUtils,
required bool alwaysScan,
@ -204,7 +222,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
if (mnemonic != null) {
switch (walletInfo.derivationInfo!.derivationType) {
case DerivationType.electrum:
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
seedBytes =
await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break;
case DerivationType.bip39:
default:
@ -234,7 +253,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
addressPageType: snp?.addressPageType,
networkParam: network,
alwaysScan: alwaysScan,
);
payjoinBox: payjoinBox);
}
LedgerConnection? _ledgerConnection;
@ -247,20 +266,25 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
derivationPath: walletInfo.derivationInfo!.derivationPath!);
}
@override
Future<BtcTransaction> buildHardwareWalletTransaction({
late final PayjoinManager payjoinManager;
bool get isPayjoinAvailable => unspentCoinsInfo.values
.where((element) =>
element.walletId == id && element.isSending && !element.isFrozen)
.isNotEmpty;
Future<PsbtV2> buildPsbt({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
required Uint8List masterFingerprint,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
for (final utxo in utxos) {
final rawTx =
@ -278,13 +302,128 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
));
}
final psbt = PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
return PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF)
.psbt;
}
final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt);
@override
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();
final psbt = await buildPsbt(
outputs: outputs,
fee: fee,
network: network,
utxos: utxos,
publicKeys: publicKeys,
masterFingerprint: masterFingerprint,
memo: memo,
enableRBF: enableRBF,
inputOrdering: inputOrdering,
outputOrdering: outputOrdering,
);
final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt);
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
credentials = credentials as BitcoinTransactionCredentials;
final tx = (await super.createTransaction(credentials))
as PendingBitcoinTransaction;
final payjoinUri = credentials.payjoinUri;
if (payjoinUri == null) return tx;
final transaction = await buildPsbt(
utxos: tx.utxos,
outputs: tx.outputs
.map((e) => BitcoinOutput(
address: addressFromScript(e.scriptPubKey),
value: e.amount,
isSilentPayment: e.isSilentPayment,
isChange: e.isChange,
))
.toList(),
fee: BigInt.from(tx.fee),
network: network,
memo: credentials.outputs.first.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
publicKeys: tx.publicKeys!,
masterFingerprint: Uint8List(0));
final originalPsbt = await signPsbt(
base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
tx.commitOverride = () async {
final sender = await payjoinManager.initSender(
payjoinUri, originalPsbt, int.parse(tx.feeRate));
payjoinManager.spawnNewSender(
sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount));
};
return tx;
}
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
.where((e) => (e.isSending && !e.isFrozen))
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
.toList();
Future<void> commitPsbt(String finalizedPsbt) {
final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
final btcTx =
BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract()));
return PendingBitcoinTransaction(
btcTx,
type,
electrumClient: electrumClient,
amount: 0,
fee: 0,
feeRate: "",
network: network,
hasChange: true,
).commit();
}
Future<String> signPsbt(
String preProcessedPsbt, List<UtxoWithPrivateKey> utxos) async {
final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt));
await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) {
return utxo.utxo.isP2tr()
? key.signTapRoot(
txDigest,
sighash: sighash,
tweak: utxo.utxo.isSilentPayment != true,
)
: key.signInput(txDigest, sigHash: sighash);
}, (txId, vout) async {
final txHex = await electrumClient.getTransactionHex(hash: txId);
final output = BtcTransaction.fromRaw(txHex).outputs[vout];
return TaprootAmountScriptPair(output.amount, output.scriptPubKey);
});
psbt.finalizeV0();
return base64Encode(psbt.asPsbtV0());
}
@override
Future<String> signMessage(String message, {String? address = null}) async {
if (walletInfo.isHardwareWallet) {

View file

@ -1,10 +1,13 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/bip/bip/bip32/bip32.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/payjoin/manager.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
import 'package:payjoin_flutter/receive.dart' as payjoin;
part 'bitcoin_wallet_addresses.g.dart';
@ -17,6 +20,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
required super.sideHd,
required super.network,
required super.isHardwareWallet,
required this.payjoinManager,
super.initialAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
@ -25,6 +29,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
super.masterHd,
}) : super(walletInfo);
final PayjoinManager payjoinManager;
@observable
payjoin.Receiver? currentPayjoinReceiver;
@computed
String? get payjoinEndpoint =>
currentPayjoinReceiver?.pjUriBuilder().build().pjEndpoint();
@override
String getAddress(
{required int index,
@ -45,4 +58,17 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
return generateP2WPKHAddress(hd: hd, index: index, network: network);
}
Future<void> initPayjoin() async {
currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
payjoinManager.resumeSessions();
}
Future<void> newPayjoinReceiver() async {
currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
printV("Initializing new Payjoin Receiver");
payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!);
}
}

View file

@ -5,6 +5,7 @@ import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
@ -21,10 +22,12 @@ class BitcoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource,
this.payjoinSessionSource, this.alwaysScan, this.isDirect);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final Box<PayjoinSession> payjoinSessionSource;
final bool alwaysScan;
final bool isDirect;
@ -55,6 +58,7 @@ class BitcoinWalletService extends WalletService<
passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -79,6 +83,7 @@ class BitcoinWalletService extends WalletService<
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -92,6 +97,7 @@ class BitcoinWalletService extends WalletService<
name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -126,6 +132,7 @@ class BitcoinWalletService extends WalletService<
name: currentName,
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -147,7 +154,6 @@ class BitcoinWalletService extends WalletService<
credentials.walletInfo?.network = network.value;
credentials.walletInfo?.derivationInfo?.derivationPath =
credentials.hwAccountData.derivationPath;
final wallet = await BitcoinWallet(
password: credentials.password!,
xpub: credentials.hwAccountData.xpub,
@ -155,6 +161,7 @@ class BitcoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
payjoinBox: payjoinSessionSource,
);
await wallet.save();
await wallet.init();
@ -182,6 +189,7 @@ class BitcoinWalletService extends WalletService<
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
payjoinBox: payjoinSessionSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);

View file

@ -1188,6 +1188,7 @@ abstract class ElectrumWalletBase
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
utxos: estimatedTx.utxos,
publicKeys: estimatedTx.publicKeys
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
if (estimatedTx.spendsSilentPayment) {
@ -1965,6 +1966,11 @@ abstract class ElectrumWalletBase
}
}
bool isMine(Script script) {
final derivedAddress = addressFromOutputScript(script, network);
return addressesSet.contains(derivedAddress);
}
@override
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try {

View file

@ -144,27 +144,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return silentAddress.toString();
}
String receiveAddress;
final typeMatchingAddresses = _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList();
final typeMatchingReceiveAddresses = typeMatchingAddresses.where((addr) => !addr.isUsed).toList();
final typeMatchingReceiveAddresses =
receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed);
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
typeMatchingReceiveAddresses.isEmpty) {
receiveAddress = generateNewAddress().address;
} else {
final previousAddressMatchesType =
previousAddressRecord != null && previousAddressRecord!.type == addressPageType;
if (previousAddressMatchesType &&
typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) {
receiveAddress = previousAddressRecord!.address;
} else {
receiveAddress = typeMatchingReceiveAddresses.first.address;
}
if (!isEnabledAutoGenerateSubaddress) {
if (previousAddressRecord != null &&
previousAddressRecord!.type == addressPageType) {
return previousAddressRecord!.address;
}
return receiveAddress;
if (typeMatchingAddresses.isNotEmpty) {
return typeMatchingAddresses.first.address;
}
return generateNewAddress().address;
}
if (typeMatchingAddresses.isEmpty || typeMatchingReceiveAddresses.isEmpty) {
return generateNewAddress().address;
}
final prev = previousAddressRecord;
if (prev != null && prev.type == addressPageType && !prev.isUsed) {
return prev.address;
}
return typeMatchingReceiveAddresses.first.address;
}
@observable

View file

@ -0,0 +1,298 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/payjoin/payjoin_receive_worker.dart';
import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart';
import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
import 'package:cw_bitcoin/payjoin/storage.dart';
import 'package:cw_bitcoin/psbt/signer.dart';
import 'package:cw_bitcoin/psbt/utils.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:payjoin_flutter/common.dart';
import 'package:payjoin_flutter/receive.dart';
import 'package:payjoin_flutter/send.dart';
import 'package:payjoin_flutter/uri.dart' as PayjoinUri;
class PayjoinManager {
PayjoinManager(this._payjoinStorage, this._wallet);
final PayjoinStorage _payjoinStorage;
final BitcoinWalletBase _wallet;
final Map<String, PayjoinPollerSession> _activePollers = {};
static const List<String> ohttpRelayUrls = [
'https://pj.bobspacebkk.com',
'https://ohttp.achow101.com',
];
static Future<PayjoinUri.Url> randomOhttpRelayUrl() => PayjoinUri.Url.fromStr(
ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]);
static const payjoinDirectoryUrl = 'https://payjo.in';
Future<void> resumeSessions() async {
final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id);
final spawnedSessions = allSessions.map((session) {
if (session.isSenderSession) {
printV("Resuming Payjoin Sender Session ${session.pjUri!}");
return _spawnSender(
sender: Sender.fromJson(session.sender!),
pjUri: session.pjUri!,
);
}
final receiver = Receiver.fromJson(session.receiver!);
printV("Resuming Payjoin Receiver Session ${receiver.id()}");
return _spawnReceiver(receiver: receiver);
});
printV("Resumed ${spawnedSessions.length} Payjoin Sessions");
await Future.wait(spawnedSessions);
}
Future<Sender> initSender(
String pjUriString, String originalPsbt, int networkFeesSatPerVb) async {
try {
final pjUri =
(await PayjoinUri.Uri.fromStr(pjUriString)).checkPjSupported();
final minFeeRateSatPerKwu = BigInt.from(networkFeesSatPerVb * 250);
final senderBuilder = await SenderBuilder.fromPsbtAndUri(
psbtBase64: originalPsbt,
pjUri: pjUri,
);
return senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu);
} catch (e) {
throw Exception('Error initializing Payjoin Sender: $e');
}
}
Future<void> spawnNewSender({
required Sender sender,
required String pjUrl,
required BigInt amount,
bool isTestnet = false,
}) async {
final pjUri = Uri.parse(pjUrl).queryParameters['pj']!;
await _payjoinStorage.insertSenderSession(
sender, pjUri, _wallet.id, amount);
return _spawnSender(isTestnet: isTestnet, sender: sender, pjUri: pjUri);
}
Future<void> _spawnSender({
required Sender sender,
required String pjUri,
bool isTestnet = false,
}) async {
final completer = Completer();
final receivePort = ReceivePort();
receivePort.listen((message) async {
if (message is Map<String, dynamic>) {
try {
switch (message['type'] as PayjoinSenderRequestTypes) {
case PayjoinSenderRequestTypes.requestPosted:
return;
case PayjoinSenderRequestTypes.psbtToSign:
final proposalPsbt = message['psbt'] as String;
final utxos = _wallet.getUtxoWithPrivateKeys();
final finalizedPsbt = await _wallet.signPsbt(proposalPsbt, utxos);
final txId = getTxIdFromPsbtV0(finalizedPsbt);
_wallet.commitPsbt(finalizedPsbt);
_cleanupSession(pjUri);
await _payjoinStorage.markSenderSessionComplete(pjUri, txId);
completer.complete();
}
} catch (e) {
_cleanupSession(pjUri);
printV(e);
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
completer.completeError(e);
}
} else if (message is PayjoinSessionError) {
_cleanupSession(pjUri);
if (message is UnrecoverableError) {
printV(message.message);
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
completer.complete();
} else if (message is RecoverableError) {
completer.complete();
} else {
completer.completeError(message);
}
}
});
final isolate = await Isolate.spawn(
PayjoinSenderWorker.run,
[receivePort.sendPort, sender.toJson(), pjUri],
);
_activePollers[pjUri] = PayjoinPollerSession(isolate, receivePort);
return completer.future;
}
Future<Receiver> initReceiver(String address,
[bool isTestnet = false]) async {
try {
final payjoinDirectory =
await PayjoinUri.Url.fromStr(payjoinDirectoryUrl);
final ohttpKeys = await PayjoinUri.fetchOhttpKeys(
ohttpRelay: await randomOhttpRelayUrl(),
payjoinDirectory: payjoinDirectory,
);
final receiver = await Receiver.create(
address: address,
network: isTestnet ? Network.testnet : Network.bitcoin,
directory: payjoinDirectory,
ohttpKeys: ohttpKeys,
ohttpRelay: await randomOhttpRelayUrl(),
);
await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
return receiver;
} catch (e) {
throw Exception('Error initializing Payjoin Receiver: $e');
}
}
Future<void> spawnNewReceiver({
required Receiver receiver,
bool isTestnet = false,
}) async {
await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
return _spawnReceiver(isTestnet: isTestnet, receiver: receiver);
}
Future<void> _spawnReceiver({
required Receiver receiver,
bool isTestnet = false,
}) async {
final completer = Completer();
final receivePort = ReceivePort();
SendPort? mainToIsolateSendPort;
List<UtxoWithPrivateKey> utxos = [];
String rawAmount = '0';
receivePort.listen((message) async {
if (message is Map<String, dynamic>) {
try {
switch (message['type'] as PayjoinReceiverRequestTypes) {
case PayjoinReceiverRequestTypes.processOriginalTx:
final tx = message['tx'] as String;
rawAmount = getOutputAmountFromTx(tx, _wallet);
break;
case PayjoinReceiverRequestTypes.checkIsOwned:
(_wallet.walletAddresses as BitcoinWalletAddresses).newPayjoinReceiver();
_payjoinStorage.markReceiverSessionInProgress(receiver.id());
final inputScript = message['input_script'] as Uint8List;
final isOwned =
_wallet.isMine(Script.fromRaw(byteData: inputScript));
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
'result': isOwned,
});
break;
case PayjoinReceiverRequestTypes.checkIsReceiverOutput:
final outputScript = message['output_script'] as Uint8List;
final isReceiverOutput =
_wallet.isMine(Script.fromRaw(byteData: outputScript));
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
'result': isReceiverOutput,
});
break;
case PayjoinReceiverRequestTypes.getCandidateInputs:
utxos = _wallet.getUtxoWithPrivateKeys();
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
'result': utxos,
});
break;
case PayjoinReceiverRequestTypes.processPsbt:
final psbt = message['psbt'] as String;
final signedPsbt = await _wallet.signPsbt(psbt, utxos);
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
'result': signedPsbt,
});
break;
case PayjoinReceiverRequestTypes.proposalSent:
_cleanupSession(receiver.id());
final psbt = message['psbt'] as String;
await _payjoinStorage.markReceiverSessionComplete(
receiver.id(), getTxIdFromPsbtV0(psbt), rawAmount);
completer.complete();
}
} catch (e) {
_cleanupSession(receiver.id());
await _payjoinStorage.markReceiverSessionUnrecoverable(
receiver.id(), e.toString());
completer.completeError(e);
}
} else if (message is PayjoinSessionError) {
_cleanupSession(receiver.id());
if (message is UnrecoverableError) {
await _payjoinStorage.markReceiverSessionUnrecoverable(
receiver.id(), message.message);
completer.complete();
} else if (message is RecoverableError) {
completer.complete();
} else {
completer.completeError(message);
}
} else if (message is SendPort) {
mainToIsolateSendPort = message;
}
});
final isolate = await Isolate.spawn(
PayjoinReceiverWorker.run,
[receivePort.sendPort, receiver.toJson()],
);
_activePollers[receiver.id()] = PayjoinPollerSession(isolate, receivePort);
return completer.future;
}
void cleanupSessions() {
final sessionIds = _activePollers.keys.toList();
for (final sessionId in sessionIds) {
_cleanupSession(sessionId);
}
}
void _cleanupSession(String sessionId) {
_activePollers[sessionId]?.close();
_activePollers.remove(sessionId);
}
}
class PayjoinPollerSession {
final Isolate isolate;
final ReceivePort port;
PayjoinPollerSession(this.isolate, this.port);
void close() {
isolate.kill();
port.close();
}
}

View file

@ -0,0 +1,219 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
import 'package:cw_bitcoin/psbt/signer.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:http/http.dart' as http;
import 'package:payjoin_flutter/bitcoin_ffi.dart';
import 'package:payjoin_flutter/common.dart';
import 'package:payjoin_flutter/receive.dart';
import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj;
enum PayjoinReceiverRequestTypes {
processOriginalTx,
proposalSent,
getCandidateInputs,
checkIsOwned,
checkIsReceiverOutput,
processPsbt;
}
class PayjoinReceiverWorker {
final SendPort sendPort;
final pendingRequests = <String, Completer<dynamic>>{};
PayjoinReceiverWorker._(this.sendPort);
static Future<void> run(List<Object> args) async {
await pj.core.init();
final sendPort = args[0] as SendPort;
final receiverJson = args[1] as String;
final worker = PayjoinReceiverWorker._(sendPort);
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen(worker.handleMessage);
try {
final httpClient = http.Client();
final receiver = Receiver.fromJson(receiverJson);
final uncheckedProposal =
await worker.receiveUncheckedProposal(httpClient, receiver);
final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast();
sendPort.send({
'type': PayjoinReceiverRequestTypes.processOriginalTx,
'tx': BytesUtils.toHexString(originalTx),
});
final payjoinProposal = await worker.processPayjoinProposal(
uncheckedProposal,
);
final psbt = await worker.sendFinalProposal(httpClient, payjoinProposal);
sendPort.send({
'type': PayjoinReceiverRequestTypes.proposalSent,
'psbt': psbt,
});
} catch (e) {
if (e is HttpException ||
(e is http.ClientException &&
e.message.contains("Software caused connection abort"))) {
sendPort.send(PayjoinSessionError.recoverable(e.toString()));
} else {
sendPort.send(PayjoinSessionError.unrecoverable(e.toString()));
}
}
}
void handleMessage(dynamic message) async {
if (message is Map<String, dynamic>) {
final requestId = message['requestId'] as String?;
if (requestId != null && pendingRequests.containsKey(requestId)) {
pendingRequests[requestId]!.complete(message['result']);
pendingRequests.remove(requestId);
}
}
}
Future<dynamic> _sendRequest(PayjoinReceiverRequestTypes type,
[Map<String, dynamic> data = const {}]) async {
final completer = Completer<dynamic>();
final requestId = DateTime.now().millisecondsSinceEpoch.toString();
pendingRequests[requestId] = completer;
sendPort.send({
...data,
'type': type,
'requestId': requestId,
});
return completer.future;
}
Future<UncheckedProposal> receiveUncheckedProposal(
http.Client httpClient, Receiver session) async {
while (true) {
printV("Polling for Proposal (${session.id()})");
final extractReq = await session.extractReq();
final request = extractReq.$1;
final url = Uri.parse(request.url.asString());
final httpRequest = await httpClient.post(url,
headers: {'Content-Type': request.contentType}, body: request.body);
final proposal = await session.processRes(
body: httpRequest.bodyBytes, ctx: extractReq.$2);
if (proposal != null) return proposal;
}
}
Future<String> sendFinalProposal(
http.Client httpClient, PayjoinProposal finalProposal) async {
final req = await finalProposal.extractV2Req();
final proposalReq = req.$1;
final proposalCtx = req.$2;
final request = await httpClient.post(
Uri.parse(proposalReq.url.asString()),
headers: {"Content-Type": proposalReq.contentType},
body: proposalReq.body,
);
await finalProposal.processRes(
res: request.bodyBytes,
ohttpContext: proposalCtx,
);
return await finalProposal.psbt();
}
Future<PayjoinProposal> processPayjoinProposal(
UncheckedProposal proposal) async {
await proposal.extractTxToScheduleBroadcast();
// TODO Handle this. send to the main port on a timer?
try {
// Receive Check 1: can broadcast
final pj1 = await proposal.assumeInteractiveReceiver();
// Receive Check 2: original PSBT has no receiver-owned inputs
final pj2 = await pj1.checkInputsNotOwned(
isOwned: (inputScript) async {
final result = await _sendRequest(
PayjoinReceiverRequestTypes.checkIsOwned,
{'input_script': inputScript},
);
return result as bool;
},
);
// Receive Check 3: sender inputs have not been seen before (prevent probing attacks)
final pj3 = await pj2.checkNoInputsSeenBefore(isKnown: (input) => false);
// Identify receiver outputs
final pj4 = await pj3.identifyReceiverOutputs(
isReceiverOutput: (outputScript) async {
final result = await _sendRequest(
PayjoinReceiverRequestTypes.checkIsReceiverOutput,
{'output_script': outputScript},
);
return result as bool;
},
);
final pj5 = await pj4.commitOutputs();
final listUnspent =
await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs);
final unspent = listUnspent as List<UtxoWithPrivateKey>;
if (unspent.isEmpty) throw Exception('No unspent outputs available');
final selectedUtxo = await _inputPairFromUtxo(unspent[0]);
final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]);
final pj7 = await pj6.commitInputs();
// Finalize proposal
final payjoinProposal = await pj7.finalizeProposal(
processPsbt: (String psbt) async {
final result = await _sendRequest(
PayjoinReceiverRequestTypes.processPsbt, {'psbt': psbt});
return result as String;
},
// TODO set maxFeeRateSatPerVb
maxFeeRateSatPerVb: BigInt.from(10000),
);
return payjoinProposal;
} catch (e) {
printV('Error occurred while finalizing proposal: $e');
rethrow;
}
}
Future<InputPair> _inputPairFromUtxo(UtxoWithPrivateKey utxo) async {
final txout = TxOut(
value: utxo.utxo.value,
scriptPubkey: Uint8List.fromList(
utxo.ownerDetails.address.toScriptPubKey().toBytes()),
);
final psbtin =
PsbtInput(witnessUtxo: txout, redeemScript: null, witnessScript: null);
final previousOutput =
OutPoint(txid: utxo.utxo.txHash, vout: utxo.utxo.vout);
final txin = TxIn(
previousOutput: previousOutput,
scriptSig: await Script.newInstance(rawOutputScript: []),
witness: [],
sequence: 0,
);
return InputPair.newInstance(txin, psbtin);
}
}

View file

@ -0,0 +1,119 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:cw_bitcoin/payjoin/manager.dart';
import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:http/http.dart' as http;
import 'package:payjoin_flutter/common.dart';
import 'package:payjoin_flutter/send.dart';
import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj;
enum PayjoinSenderRequestTypes {
requestPosted,
psbtToSign;
}
class PayjoinSenderWorker {
final SendPort sendPort;
final pendingRequests = <String, Completer<dynamic>>{};
final String pjUrl;
PayjoinSenderWorker._(this.sendPort, this.pjUrl);
static Future<void> run(List<Object> args) async {
await pj.core.init();
final sendPort = args[0] as SendPort;
final senderJson = args[1] as String;
final pjUrl = args[2] as String;
final sender = Sender.fromJson(senderJson);
final worker = PayjoinSenderWorker._(sendPort, pjUrl);
try {
final proposalPsbt = await worker.runSender(sender);
sendPort.send({
'type': PayjoinSenderRequestTypes.psbtToSign,
'psbt': proposalPsbt,
});
} catch (e) {
sendPort.send(e);
}
}
/// Run a payjoin sender (V2 protocol first, fallback to V1).
Future<String> runSender(Sender sender) async {
final httpClient = http.Client();
try {
return await _runSenderV2(sender, httpClient);
} catch (e) {
printV(e);
if (e is PayjoinException &&
// TODO condition on error type instead of message content
e.message?.contains('parse receiver public key') == true) {
return await _runSenderV1(sender, httpClient);
} else if (e is HttpException) {
printV(e);
throw Exception(PayjoinSessionError.recoverable(e.toString()));
} else {
throw Exception(PayjoinSessionError.unrecoverable(e.toString()));
}
}
}
/// Attempt to send payjoin using the V2 of the protocol.
Future<String> _runSenderV2(Sender sender, http.Client httpClient) async {
try {
final postRequest = await sender.extractV2(
ohttpProxyUrl: await PayjoinManager.randomOhttpRelayUrl(),
);
final postResult = await _postRequest(httpClient, postRequest.$1);
final getContext =
await postRequest.$2.processResponse(response: postResult);
sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted, "pj": pjUrl});
while (true) {
printV('Polling V2 Proposal Request (${pjUrl})');
final getRequest = await getContext.extractReq(
ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(),
);
final getRes = await _postRequest(httpClient, getRequest.$1);
final proposalPsbt = await getContext.processResponse(
response: getRes,
ohttpCtx: getRequest.$2,
);
printV("$proposalPsbt");
if (proposalPsbt != null) return proposalPsbt;
}
} catch (e) {
rethrow;
}
}
/// Attempt to send payjoin using the V1 of the protocol.
Future<String> _runSenderV1(Sender sender, http.Client httpClient) async {
try {
final postRequest = await sender.extractV1();
final response = await _postRequest(httpClient, postRequest.$1);
sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted});
return await postRequest.$2.processResponse(response: response);
} catch (e) {
throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e');
}
}
Future<List<int>> _postRequest(http.Client client, Request req) async {
final httpRequest = await client.post(Uri.parse(req.url.asString()),
headers: {'Content-Type': req.contentType}, body: req.body);
return httpRequest.bodyBytes;
}
}

View file

@ -0,0 +1,16 @@
class PayjoinSessionError {
final String message;
const PayjoinSessionError._(this.message);
factory PayjoinSessionError.recoverable(String message) = RecoverableError;
factory PayjoinSessionError.unrecoverable(String message) = UnrecoverableError;
}
class RecoverableError extends PayjoinSessionError {
const RecoverableError(super.message) : super._();
}
class UnrecoverableError extends PayjoinSessionError {
const UnrecoverableError(super.message) : super._();
}

View file

@ -0,0 +1,95 @@
import 'package:cw_core/payjoin_session.dart';
import 'package:hive/hive.dart';
import 'package:payjoin_flutter/receive.dart';
import 'package:payjoin_flutter/send.dart';
class PayjoinStorage {
PayjoinStorage(this._payjoinSessionSources);
final Box<PayjoinSession> _payjoinSessionSources;
static const String _receiverPrefix = 'pj_recv_';
static const String _senderPrefix = 'pj_send_';
Future<void> insertReceiverSession(
Receiver receiver,
String walletId,
) =>
_payjoinSessionSources.put(
"$_receiverPrefix${receiver.id()}",
PayjoinSession(
walletId: walletId,
receiver: receiver.toJson(),
),
);
Future<void> markReceiverSessionComplete(
String sessionId, String txId, String amount) async {
final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
session.status = PayjoinSessionStatus.success.name;
session.txId = txId;
session.rawAmount = amount;
await session.save();
}
Future<void> markReceiverSessionUnrecoverable(
String sessionId, String reason) async {
final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
session.status = PayjoinSessionStatus.unrecoverable.name;
session.error = reason;
await session.save();
}
Future<void> markReceiverSessionInProgress(String sessionId) async {
final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
session.status = PayjoinSessionStatus.inProgress.name;
session.inProgressSince = DateTime.now();
await session.save();
}
Future<void> insertSenderSession(
Sender sender,
String pjUrl,
String walletId,
BigInt amount,
) =>
_payjoinSessionSources.put(
"$_senderPrefix$pjUrl",
PayjoinSession(
walletId: walletId,
pjUri: pjUrl,
sender: sender.toJson(),
status: PayjoinSessionStatus.inProgress.name,
inProgressSince: DateTime.now(),
rawAmount: amount.toString(),
),
);
Future<void> markSenderSessionComplete(String pjUrl, String txId) async {
final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
session.status = PayjoinSessionStatus.success.name;
session.txId = txId;
await session.save();
}
Future<void> markSenderSessionUnrecoverable(String pjUrl) async {
final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
session.status = PayjoinSessionStatus.unrecoverable.name;
await session.save();
}
List<PayjoinSession> readAllOpenSessions(String walletId) =>
_payjoinSessionSources.values
.where((session) =>
session.walletId == walletId &&
![
PayjoinSessionStatus.success.name,
PayjoinSessionStatus.unrecoverable.name
].contains(session.status))
.toList();
}

View file

@ -1,3 +1,4 @@
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:grpc/grpc.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
@ -25,6 +26,8 @@ class PendingBitcoinTransaction with PendingTransaction {
this.hasTaprootInputs = false,
this.isMweb = false,
this.utxos = const [],
this.publicKeys,
this.commitOverride,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
@ -43,6 +46,8 @@ class PendingBitcoinTransaction with PendingTransaction {
String? idOverride;
String? hexOverride;
List<String>? outputAddresses;
final Map<String, PublicKeyWithDerivationPath>? publicKeys;
Future<void> Function()? commitOverride;
@override
String get id => idOverride ?? _tx.txId();
@ -129,6 +134,10 @@ class PendingBitcoinTransaction with PendingTransaction {
@override
Future<void> commit() async {
if (commitOverride != null) {
return commitOverride?.call();
}
if (isMweb) {
await _ltcCommit();
} else {

View file

@ -0,0 +1,263 @@
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_bitcoin/src/utils/buffer_writer.dart';
extension PsbtSigner on PsbtV2 {
Uint8List extractUnsignedTX({bool getSegwit = true}) {
final tx = BufferWriter()..writeUInt32(getGlobalTxVersion());
final isSegwit = getInputWitnessUtxo(0) != null;
if (isSegwit && getSegwit) {
tx.writeSlice(Uint8List.fromList([0, 1]));
}
final inputCount = getGlobalInputCount();
tx.writeVarInt(inputCount);
for (var i = 0; i < inputCount; i++) {
tx
..writeSlice(getInputPreviousTxid(i))
..writeUInt32(getInputOutputIndex(i))
..writeVarSlice(Uint8List(0))
..writeUInt32(getInputSequence(i));
}
final outputCount = getGlobalOutputCount();
tx.writeVarInt(outputCount);
for (var i = 0; i < outputCount; i++) {
tx.writeUInt64(getOutputAmount(i));
tx.writeVarSlice(getOutputScript(i));
}
tx.writeUInt32(getGlobalFallbackLocktime() ?? 0);
return tx.buffer();
}
Future<void> signWithUTXO(
List<UtxoWithPrivateKey> utxos, UTXOSignerCallBack signer,
[UTXOGetterCallBack? getTaprootPair]) async {
final raw = BytesUtils.toHexString(extractUnsignedTX(getSegwit: false));
final tx = BtcTransaction.fromRaw(raw);
/// when the transaction is taproot and we must use getTaproot transaction
/// digest we need all of inputs amounts and owner script pub keys
List<BigInt> taprootAmounts = [];
List<Script> taprootScripts = [];
if (utxos.any((e) => e.utxo.isP2tr())) {
for (final input in tx.inputs) {
final utxo = utxos.firstWhereOrNull(
(u) => u.utxo.txHash == input.txId && u.utxo.vout == input.txIndex);
if (utxo == null) {
final trPair = await getTaprootPair!.call(input.txId, input.txIndex);
taprootAmounts.add(trPair.value);
taprootScripts.add(trPair.script);
continue;
}
taprootAmounts.add(utxo.utxo.value);
taprootScripts.add(_findLockingScript(utxo, true));
}
}
for (var i = 0; i < tx.inputs.length; i++) {
final utxo = utxos.firstWhereOrNull((e) =>
e.utxo.txHash == tx.inputs[i].txId &&
e.utxo.vout == tx.inputs[i].txIndex); // ToDo: More robust verify
if (utxo == null) continue;
/// We receive the owner's ScriptPubKey
final script = _findLockingScript(utxo, false);
final int sighash = utxo.utxo.isP2tr()
? BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL
: BitcoinOpCodeConst.SIGHASH_ALL;
/// We generate transaction digest for current input
final digest = _generateTransactionDigest(
script, i, utxo.utxo, tx, taprootAmounts, taprootScripts);
/// now we need sign the transaction digest
final sig = signer(digest, utxo, utxo.privateKey, sighash);
if (utxo.utxo.isP2tr()) {
setInputTapKeySig(i, Uint8List.fromList(BytesUtils.fromHexString(sig)));
} else {
setInputPartialSig(
i,
Uint8List.fromList(BytesUtils.fromHexString(utxo.public().toHex())),
Uint8List.fromList(BytesUtils.fromHexString(sig)));
}
}
}
List<int> _generateTransactionDigest(
Script scriptPubKeys,
int input,
BitcoinUtxo utxo,
BtcTransaction transaction,
List<BigInt> taprootAmounts,
List<Script> tapRootPubKeys) {
if (utxo.isSegwit()) {
if (utxo.isP2tr()) {
return transaction.getTransactionTaprootDigset(
txIndex: input,
scriptPubKeys: tapRootPubKeys,
amounts: taprootAmounts,
);
}
return transaction.getTransactionSegwitDigit(
txInIndex: input, script: scriptPubKeys, amount: utxo.value);
}
return transaction.getTransactionDigest(
txInIndex: input, script: scriptPubKeys);
}
Script _findLockingScript(UtxoWithAddress utxo, bool isTaproot) {
if (utxo.isMultiSig()) {
throw Exception("MultiSig is not supported yet");
}
final senderPub = utxo.public();
switch (utxo.utxo.scriptType) {
case PubKeyAddressType.p2pk:
return senderPub.toRedeemScript();
case SegwitAddresType.p2wsh:
if (isTaproot) {
return senderPub.toP2wshAddress().toScriptPubKey();
}
return senderPub.toP2wshRedeemScript();
case P2pkhAddressType.p2pkh:
return senderPub.toP2pkhAddress().toScriptPubKey();
case SegwitAddresType.p2wpkh:
if (isTaproot) {
return senderPub.toP2wpkhAddress().toScriptPubKey();
}
return senderPub.toP2pkhAddress().toScriptPubKey();
case SegwitAddresType.p2tr:
return senderPub
.toTaprootAddress(tweak: utxo.utxo.isSilentPayment != true)
.toScriptPubKey();
case SegwitAddresType.mweb:
return Script(script: []);
case P2shAddressType.p2pkhInP2sh:
if (isTaproot) {
return senderPub.toP2pkhInP2sh().toScriptPubKey();
}
return senderPub.toP2pkhAddress().toScriptPubKey();
case P2shAddressType.p2wpkhInP2sh:
if (isTaproot) {
return senderPub.toP2wpkhInP2sh().toScriptPubKey();
}
return senderPub.toP2pkhAddress().toScriptPubKey();
case P2shAddressType.p2wshInP2sh:
if (isTaproot) {
return senderPub.toP2wshInP2sh().toScriptPubKey();
}
return senderPub.toP2wshRedeemScript();
case P2shAddressType.p2pkInP2sh:
if (isTaproot) {
return senderPub.toP2pkInP2sh().toScriptPubKey();
}
return senderPub.toRedeemScript();
}
throw Exception("invalid bitcoin address type");
}
}
typedef UTXOSignerCallBack = String Function(List<int> trDigest,
UtxoWithAddress utxo, ECPrivate privateKey, int sighash);
typedef UTXOGetterCallBack = Future<TaprootAmountScriptPair> Function(
String txId, int vout);
class TaprootAmountScriptPair {
final BigInt value;
final Script script;
const TaprootAmountScriptPair(this.value, this.script);
}
class UtxoWithPrivateKey extends UtxoWithAddress {
final ECPrivate privateKey;
UtxoWithPrivateKey({
required super.utxo,
required super.ownerDetails,
required this.privateKey,
});
factory UtxoWithPrivateKey.fromUtxo(
UtxoWithAddress input, List<ECPrivateInfo> inputPrivateKeyInfos) {
ECPrivateInfo? key;
if (inputPrivateKeyInfos.isEmpty) {
throw Exception("No private keys generated.");
} else {
key = inputPrivateKeyInfos.firstWhereOrNull((element) {
final elemPubkey = element.privkey.getPublic().toHex();
if (elemPubkey == input.public().toHex()) {
return true;
} else {
return false;
}
});
}
if (key == null) {
throw Exception("${input.utxo.txHash} No Key found");
}
return UtxoWithPrivateKey(
utxo: input.utxo,
ownerDetails: input.ownerDetails,
privateKey: key.privkey);
}
factory UtxoWithPrivateKey.fromUnspent(
BitcoinUnspent input, BitcoinWalletBase wallet) {
final address =
RegexUtils.addressTypeFromStr(input.address, BitcoinNetwork.mainnet);
final newHd =
input.bitcoinAddressRecord.isHidden ? wallet.sideHd : wallet.hd;
ECPrivate privkey;
if (input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress =
input.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
privkey = wallet.walletAddresses.silentAddress!.b_spend.tweakAdd(
BigintUtils.fromBytes(
BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!),
),
);
} else {
privkey = generateECPrivate(
hd: newHd,
index: input.bitcoinAddressRecord.index,
network: BitcoinNetwork.mainnet);
}
return UtxoWithPrivateKey(
utxo: BitcoinUtxo(
txHash: input.hash,
value: BigInt.from(input.value),
vout: input.vout,
scriptType: input.bitcoinAddressRecord.type,
isSilentPayment:
input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord,
),
ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(),
address: address,
),
privateKey: privkey);
}
}

View file

@ -0,0 +1,41 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:ledger_bitcoin/psbt.dart';
String getTxIdFromPsbtV0(String psbt) {
final psbtV2 = PsbtV2()..deserializeV0(base64.decode(psbt));
return BtcTransaction.fromRaw(
BytesUtils.toHexString(psbtV2.extractUnsignedTX(false)))
.txId();
}
String getOutputAmountFromPsbt(String psbtV0, BitcoinWalletBase wallet) {
printV(psbtV0);
final psbt = PsbtV2()..deserializeV0(base64.decode(psbtV0));
int amount = 0;
for (var i = 0; i < psbt.getGlobalOutputCount(); i++) {
final script = psbt.getOutputScript(i);
if (wallet.isMine(Script.fromRaw(byteData: script))) {
amount += psbt.getOutputAmount(i);
}
}
return amount.toString();
}
String getOutputAmountFromTx(String originalTx, BitcoinWalletBase wallet) {
final tx = BtcTransaction.fromRaw(originalTx);
BigInt amount = BigInt.zero;
for (final output in tx.outputs) {
if (wallet.isMine(output.scriptPubKey)) {
amount += output.amount;
}
}
printV(amount);
return amount.toString();
}

View file

@ -0,0 +1,52 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_bitcoin/src/psbt/map_extension.dart';
import 'package:ledger_bitcoin/src/utils/buffer_reader.dart';
import 'package:ledger_bitcoin/src/utils/uint8list_extension.dart' as ext;
extension PsbtSigner on PsbtV2 {
void deserializeV0(Uint8List psbt) {
final bufferReader = BufferReader(psbt);
if (!listEquals(bufferReader.readSlice(5), Uint8List.fromList([0x70, 0x73, 0x62, 0x74, 0xff]))) {
throw Exception("Invalid magic bytes");
}
while (_readKeyPair(globalMap, bufferReader)) {}
final tx = BtcTransaction.fromRaw(BytesUtils.toHexString(globalMap['00']!));
setGlobalInputCount(tx.inputs.length);
setGlobalOutputCount(tx.outputs.length);
setGlobalTxVersion(Uint8List.fromList(tx.version).readUint32LE(0));
for (var i = 0; i < getGlobalInputCount(); i++) {
inputMaps.insert(i, <String, Uint8List>{});
while (_readKeyPair(inputMaps[i], bufferReader)) {}
final input = tx.inputs[i];
setInputOutputIndex(i, input.txIndex);
setInputPreviousTxId(i, Uint8List.fromList(BytesUtils.fromHexString(input.txId).reversed.toList()));
setInputSequence(i, Uint8List.fromList(input.sequence).readUint32LE(0));
}
for (var i = 0; i < getGlobalOutputCount(); i++) {
outputMaps.insert(i, <String, Uint8List>{});
while (_readKeyPair(outputMaps[i], bufferReader)) {}
final output = tx.outputs[i];
setOutputAmount(i, output.amount.toInt());
setOutputScript(i, Uint8List.fromList(output.scriptPubKey.toBytes()));
}
}
bool _readKeyPair(Map<String, Uint8List> map, BufferReader bufferReader) {
final keyLen = bufferReader.readVarInt();
if (keyLen == 0) return false;
final keyType = bufferReader.readUInt8();
final keyData = bufferReader.readSlice(keyLen - 1);
final value = bufferReader.readVarSlice();
map.set(keyType, keyData, value);
return true;
}
}

View file

@ -0,0 +1,143 @@
import "dart:typed_data";
import "package:ledger_bitcoin/src/psbt/constants.dart";
import "package:ledger_bitcoin/src/psbt/psbtv2.dart";
import "package:ledger_bitcoin/src/utils/buffer_writer.dart";
/// This roughly implements the "input finalizer" role of BIP370 (PSBTv2
/// https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However
/// the role is documented in BIP174 (PSBTv0
/// https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki).
///
/// Verify that all inputs have a signature, and set inputFinalScriptwitness
/// and/or inputFinalScriptSig depending on the type of the spent outputs. Clean
/// fields that aren't useful anymore, partial signatures, redeem script and
/// derivation paths.
///
/// @param psbt The psbt with all signatures added as partial sigs, either
/// through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG
extension InputFinalizer on PsbtV2 {
void finalizeV0() {
// First check that each input has a signature
for (var i = 0; i < getGlobalInputCount(); i++) {
if (_isFinalized(i)) continue;
final legacyPubkeys = getInputKeyDatas(i, PSBTIn.partialSig);
final taprootSig = getInputTapKeySig(i);
if (legacyPubkeys.isEmpty && taprootSig == null) {
continue;
// throw Exception('No signature for input $i present');
}
if (legacyPubkeys.isNotEmpty) {
if (legacyPubkeys.length > 1) {
throw Exception(
'Expected exactly one signature, got ${legacyPubkeys.length}');
}
if (taprootSig != null) {
throw Exception('Both taproot and non-taproot signatures present.');
}
final isSegwitV0 = getInputWitnessUtxo(i) != null;
final redeemScript = getInputRedeemScript(i);
final isWrappedSegwit = redeemScript != null;
final signature = getInputPartialSig(i, legacyPubkeys[0]);
if (signature == null) {
throw Exception('Expected partial signature for input $i');
}
if (isSegwitV0) {
final witnessBuf = BufferWriter()
..writeVarInt(2)
..writeVarInt(signature.length)
..writeSlice(signature)
..writeVarInt(legacyPubkeys[0].length)
..writeSlice(legacyPubkeys[0]);
setInputFinalScriptwitness(i, witnessBuf.buffer());
if (isWrappedSegwit) {
if (redeemScript.isEmpty) {
throw Exception(
"Expected non-empty redeemscript. Can't finalize intput $i");
}
final scriptSigBuf = BufferWriter()
..writeUInt8(redeemScript.length) // Push redeemScript length
..writeSlice(redeemScript);
setInputFinalScriptsig(i, scriptSigBuf.buffer());
}
} else {
// Legacy input
final scriptSig = BufferWriter();
_writePush(scriptSig, signature);
_writePush(scriptSig, legacyPubkeys[0]);
setInputFinalScriptsig(i, scriptSig.buffer());
}
} else {
// Taproot input
final signature = getInputTapKeySig(i);
if (signature == null) {
throw Exception("No taproot signature found");
}
if (signature.length != 64 && signature.length != 65) {
throw Exception("Unexpected length of schnorr signature.");
}
final witnessBuf = BufferWriter()
..writeVarInt(1)
..writeVarSlice(signature);
setInputFinalScriptwitness(i, witnessBuf.buffer());
}
clearFinalizedInput(i);
}
}
/// Deletes fields that are no longer neccesary from the psbt.
///
/// Note, the spec doesn't say anything about removing ouput fields
/// like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them
/// without actually knowing why. I think we should remove them too.
void clearFinalizedInput(int inputIndex) {
final keyTypes = [
PSBTIn.bip32Derivation,
PSBTIn.partialSig,
PSBTIn.tapBip32Derivation,
PSBTIn.tapKeySig,
];
final witnessUtxoAvailable = getInputWitnessUtxo(inputIndex) != null;
final nonWitnessUtxoAvailable = getInputNonWitnessUtxo(inputIndex) != null;
if (witnessUtxoAvailable && nonWitnessUtxoAvailable) {
// Remove NON_WITNESS_UTXO for segwit v0 as it's only needed while signing.
// Segwit v1 doesn't have NON_WITNESS_UTXO set.
// See https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#cite_note-7
keyTypes.add(PSBTIn.nonWitnessUTXO);
}
deleteInputEntries(inputIndex, keyTypes);
}
/// Writes a script push operation to buf, which looks different
/// depending on the size of the data. See
/// https://en.bitcoin.it/wiki/Script#finalants
///
/// [buf] the BufferWriter to write to
/// [data] the Buffer to be pushed.
void _writePush(BufferWriter buf, Uint8List data) {
if (data.length <= 75) {
buf.writeUInt8(data.length);
} else if (data.length <= 256) {
buf.writeUInt8(76);
buf.writeUInt8(data.length);
} else if (data.length <= 256 * 256) {
buf.writeUInt8(77);
final b = ByteData(2)..setUint16(0, data.length, Endian.little);
buf.writeSlice(b.buffer.asUint8List());
}
buf.writeSlice(data);
}
bool _isFinalized(int i) {
if (getInputFinalScriptsig(i) != null) return true;
try {
getInputFinalScriptwitness(i);
return true;
} catch (_) {
return false;
}
}
}

View file

@ -125,6 +125,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172
url: "https://pub.dev"
source: hosted
version: "2.1.0"
build_config:
dependency: transitive
description:
@ -377,6 +385,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_rust_bridge:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00"
url: "https://pub.dev"
source: hosted
version: "2.7.1"
flutter_test:
dependency: "direct dev"
description: flutter
@ -395,6 +411,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
@ -559,7 +583,7 @@ packages:
dependency: "direct main"
description:
path: "packages/ledger-bitcoin"
ref: HEAD
ref: trunk
resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8
url: "https://github.com/cake-tech/ledger-flutter-plus-plugins"
source: git
@ -726,6 +750,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
payjoin_flutter:
dependency: "direct main"
description:
path: "."
ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c"
resolved-ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c"
url: "https://github.com/konstantinullrich/payjoin-flutter"
source: git
version: "0.21.0"
petitparser:
dependency: transitive
description:
@ -940,6 +973,14 @@ packages:
url: "https://github.com/cake-tech/sp_scanner"
source: git
version: "0.0.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@ -1036,6 +1077,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math:
dependency: transitive
description:
@ -1118,4 +1167,4 @@ packages:
version: "2.2.2"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -40,11 +40,16 @@ dependencies:
bech32:
git:
url: https://github.com/cake-tech/bech32.git
payjoin_flutter:
git:
url: https://github.com/konstantinullrich/payjoin-flutter
ref: 6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c #cake-v1
ledger_flutter_plus: ^1.4.1
ledger_bitcoin:
git:
url: https://github.com/cake-tech/ledger-flutter-plus-plugins
path: packages/ledger-bitcoin
ref: trunk
ledger_litecoin:
git:
url: https://github.com/cake-tech/ledger-flutter-plus-plugins

View file

@ -21,3 +21,4 @@ const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
const MWEB_UTXO_TYPE_ID = 20;
const HAVEN_SEED_STORE_TYPE_ID = 21;
const ZANO_ASSET_TYPE_ID = 22;
const PAYJOIN_SESSION_TYPE_ID = 23;

View file

@ -0,0 +1,67 @@
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'payjoin_session.g.dart';
@HiveType(typeId: PAYJOIN_SESSION_TYPE_ID)
class PayjoinSession extends HiveObject {
PayjoinSession({
required this.walletId,
this.receiver,
this.sender,
this.pjUri,
this.status = "created",
this.inProgressSince,
this.rawAmount,
}) {
if (receiver == null) {
assert(sender != null);
assert(pjUri != null);
} else {
assert(receiver != null);
}
}
static const typeId = PAYJOIN_SESSION_TYPE_ID;
static const boxName = 'PayjoinSessions';
@HiveField(0)
final String walletId;
@HiveField(1)
final String? sender;
@HiveField(2)
final String? receiver;
@HiveField(3)
final String? pjUri;
@HiveField(4)
String status;
@HiveField(5)
DateTime? inProgressSince;
@HiveField(6)
String? txId;
@HiveField(7)
String? rawAmount;
@HiveField(8)
String? error;
bool get isSenderSession => sender != null;
BigInt get amount => BigInt.parse(rawAmount ?? "0");
set amount(BigInt amount) => rawAmount = amount.toString();
}
enum PayjoinSessionStatus {
created,
inProgress,
success,
unrecoverable,
}

View file

@ -810,4 +810,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/print_verbose.dart';
@ -602,7 +603,25 @@ abstract class DecredWalletBase
throw "wallet already exists at $newDirPath";
}
await Directory(currentDirPath).rename(newDirPath);
final sourceDir = Directory(currentDirPath);
final targetDir = Directory(newDirPath);
if (!targetDir.existsSync()) {
await targetDir.create(recursive: true);
}
await for (final entity in sourceDir.list(recursive: true)) {
final relativePath = entity.path.substring(sourceDir.path.length+1);
final targetPath = p.join(targetDir.path, relativePath);
if (entity is File) {
await entity.rename(targetPath);
} else if (entity is Directory) {
await Directory(targetPath).create(recursive: true);
}
}
await sourceDir.delete(recursive: true);
}
@override

View file

@ -118,6 +118,10 @@ class DecredWalletService extends WalletService<
currentWalletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet
? testnet
: mainnet;
if (libwallet == null) {
libwallet = await Libwallet.spawn();
libwallet!.initLibdcrwallet("", "err");
}
final currentWallet = DecredWallet(
currentWalletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);

View file

@ -849,4 +849,4 @@ packages:
version: "2.2.2"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -2,31 +2,31 @@ import 'dart:async';
import 'package:cw_monero/api/wallet.dart';
import 'package:cw_monero/monero_account_list.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/wallet2.dart';
import 'package:monero/src/monero.dart';
monero.wallet? wptr = null;
bool get isViewOnly => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0;
Wallet2Wallet? currentWallet = null;
bool get isViewOnly => int.tryParse(currentWallet!.secretSpendKey()) == 0;
int _wlptrForW = 0;
monero.WalletListener? _wlptr = null;
Wallet2WalletListener? _wlptr = null;
monero.WalletListener? getWlptr() {
if (wptr == null) return null;
if (wptr!.address == _wlptrForW) return _wlptr!;
_wlptrForW = wptr!.address;
_wlptr = monero.MONERO_cw_getWalletListener(wptr!);
Wallet2WalletListener? getWlptr() {
if (currentWallet == null) return null;
_wlptrForW = currentWallet!.ffiAddress();
_wlptr = currentWallet!.getWalletListener();
return _wlptr!;
}
monero.SubaddressAccount? subaddressAccount;
Wallet2SubaddressAccount? subaddressAccount;
bool isUpdating = false;
void refreshAccounts() {
try {
isUpdating = true;
subaddressAccount = monero.Wallet_subaddressAccount(wptr!);
monero.SubaddressAccount_refresh(subaddressAccount!);
subaddressAccount = currentWallet!.subaddressAccount();
subaddressAccount!.refresh();
isUpdating = false;
} catch (e) {
isUpdating = false;
@ -34,45 +34,28 @@ void refreshAccounts() {
}
}
List<monero.SubaddressAccountRow> getAllAccount() {
List<Wallet2SubaddressAccountRow> getAllAccount() {
// final size = monero.Wallet_numSubaddressAccounts(wptr!);
refreshAccounts();
int size = monero.SubaddressAccount_getAll_size(subaddressAccount!);
int size = subaddressAccount!.getAll_size();
if (size == 0) {
monero.Wallet_addSubaddressAccount(wptr!);
monero.Wallet_status(wptr!);
currentWallet!.addSubaddressAccount();
currentWallet!.status();
return [];
}
return List.generate(size, (index) {
return monero.SubaddressAccount_getAll_byIndex(subaddressAccount!, index: index);
return subaddressAccount!.getAll_byIndex(index);
});
}
void addAccountSync({required String label}) {
monero.Wallet_addSubaddressAccount(wptr!, label: label);
void addAccount({required String label}) {
currentWallet!.addSubaddressAccount(label: label);
unawaited(store());
}
void setLabelForAccountSync({required int accountIndex, required String label}) {
monero.SubaddressAccount_setLabel(subaddressAccount!, accountIndex: accountIndex, label: label);
MoneroAccountListBase.cachedAccounts[wptr!.address] = [];
void setLabelForAccount({required int accountIndex, required String label}) {
subaddressAccount!.setLabel(accountIndex: accountIndex, label: label);
MoneroAccountListBase.cachedAccounts[currentWallet!.ffiAddress()] = [];
refreshAccounts();
}
void _addAccount(String label) => addAccountSync(label: label);
void _setLabelForAccount(Map<String, dynamic> args) {
final label = args['label'] as String;
final accountIndex = args['accountIndex'] as int;
setLabelForAccountSync(label: label, accountIndex: accountIndex);
}
Future<void> addAccount({required String label}) async {
_addAccount(label);
unawaited(store());
}
Future<void> setLabelForAccount({required int accountIndex, required String label}) async {
_setLabelForAccount({'accountIndex': accountIndex, 'label': label});
unawaited(store());
}

View file

@ -3,17 +3,18 @@ import 'dart:isolate';
import 'package:cw_monero/api/account_list.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/wallet2.dart';
import 'package:mutex/mutex.dart';
monero.Coins? coins = null;
Wallet2Coins? coins = null;
final coinsMutex = Mutex();
Future<void> refreshCoins(int accountIndex) async {
if (coinsMutex.isLocked) {
return;
}
coins = monero.Wallet_coins(wptr!);
final coinsPtr = coins!.address;
coins = currentWallet!.coins();
final coinsPtr = coins!.ffiAddress();
await coinsMutex.acquire();
await Isolate.run(() => monero.Coins_refresh(Pointer.fromAddress(coinsPtr)));
coinsMutex.release();
@ -21,14 +22,14 @@ Future<void> refreshCoins(int accountIndex) async {
Future<int> countOfCoins() async {
await coinsMutex.acquire();
final count = monero.Coins_count(coins!);
final count = coins!.count();
coinsMutex.release();
return count;
}
Future<monero.CoinsInfo> getCoin(int index) async {
Future<Wallet2CoinsInfo> getCoin(int index) async {
await coinsMutex.acquire();
final coin = monero.Coins_coin(coins!, index);
final coin = coins!.coin(index);
coinsMutex.release();
return coin;
}
@ -37,7 +38,7 @@ Future<int?> getCoinByKeyImage(String keyImage) async {
final count = await countOfCoins();
for (int i = 0; i < count; i++) {
final coin = await getCoin(i);
final coinAddress = monero.CoinsInfo_keyImage(coin);
final coinAddress = coin.keyImage;
if (keyImage == coinAddress) {
return i;
}
@ -47,14 +48,14 @@ Future<int?> getCoinByKeyImage(String keyImage) async {
Future<void> freezeCoin(int index) async {
await coinsMutex.acquire();
final coinsPtr = coins!.address;
final coinsPtr = coins!.ffiAddress();
await Isolate.run(() => monero.Coins_setFrozen(Pointer.fromAddress(coinsPtr), index: index));
coinsMutex.release();
}
Future<void> thawCoin(int index) async {
await coinsMutex.acquire();
final coinsPtr = coins!.address;
final coinsPtr = coins!.ffiAddress();
await Isolate.run(() => monero.Coins_thaw(Pointer.fromAddress(coinsPtr), index: index));
coinsMutex.release();
}

View file

@ -2,7 +2,8 @@
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/transaction_history.dart';
import 'package:cw_monero/api/wallet.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/monero.dart';
import 'package:monero/src/monero.dart';
bool isUpdating = false;
@ -16,7 +17,7 @@ class SubaddressInfoMetadata {
SubaddressInfoMetadata? subaddress = null;
String getRawLabel({required int accountIndex, required int addressIndex}) {
return monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
return currentWallet!.getSubaddressLabel(accountIndex: accountIndex, addressIndex: addressIndex);
}
void refreshSubaddresses({required int accountIndex}) {
@ -46,7 +47,7 @@ class Subaddress {
final int received;
final int txCount;
String get label {
final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
final localLabel = currentWallet!.getSubaddressLabel(accountIndex: accountIndex, addressIndex: addressIndex);
if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen
return "#$addressIndex ${localLabel}".trim();
}
@ -66,26 +67,26 @@ int lastTxCount = 0;
List<TinyTransactionDetails> ttDetails = [];
List<Subaddress> getAllSubaddresses() {
txhistory = monero.Wallet_history(wptr!);
final txCount = monero.TransactionHistory_count(txhistory!);
if (lastTxCount != txCount && lastWptr != wptr!.address) {
txhistory = currentWallet!.history();
final txCount = txhistory!.count();
if (lastTxCount != txCount && lastWptr != currentWallet!.ffiAddress()) {
final List<TinyTransactionDetails> newttDetails = [];
lastTxCount = txCount;
lastWptr = wptr!.address;
lastWptr = currentWallet!.ffiAddress();
for (var i = 0; i < txCount; i++) {
final tx = monero.TransactionHistory_transaction(txhistory!, index: i);
if (monero.TransactionInfo_direction(tx) == monero.TransactionInfo_Direction.Out) continue;
final subaddrs = monero.TransactionInfo_subaddrIndex(tx).split(",");
final account = monero.TransactionInfo_subaddrAccount(tx);
final tx = txhistory!.transaction(i);
if (tx.direction() == TransactionInfo_Direction.Out.index) continue;
final subaddrs = tx.subaddrIndex().split(",");
final account = tx.subaddrAccount();
newttDetails.add(TinyTransactionDetails(
address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)),
amount: monero.TransactionInfo_amount(tx),
amount: tx.amount(),
));
}
ttDetails.clear();
ttDetails.addAll(newttDetails);
}
final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
final size = currentWallet!.numSubaddresses(accountIndex: subaddress!.accountIndex);
final list = List.generate(size, (index) {
final ttDetailsLocal = ttDetails.where((element) {
final address = getAddress(
@ -119,46 +120,17 @@ List<Subaddress> getAllSubaddresses() {
}
int numSubaddresses(int subaccountIndex) {
return monero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex);
}
void addSubaddressSync({required int accountIndex, required String label}) {
monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label);
refreshSubaddresses(accountIndex: accountIndex);
}
void setLabelForSubaddressSync(
{required int accountIndex, required int addressIndex, required String label}) {
monero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label);
}
void _addSubaddress(Map<String, dynamic> args) {
final label = args['label'] as String;
final accountIndex = args['accountIndex'] as int;
addSubaddressSync(accountIndex: accountIndex, label: label);
}
void _setLabelForSubaddress(Map<String, dynamic> args) {
final label = args['label'] as String;
final accountIndex = args['accountIndex'] as int;
final addressIndex = args['addressIndex'] as int;
setLabelForSubaddressSync(
accountIndex: accountIndex, addressIndex: addressIndex, label: label);
return currentWallet!.numSubaddresses(accountIndex: subaccountIndex);
}
Future<void> addSubaddress({required int accountIndex, required String label}) async {
_addSubaddress({'accountIndex': accountIndex, 'label': label});
currentWallet!.addSubaddress(accountIndex: accountIndex, label: label);
refreshSubaddresses(accountIndex: accountIndex);
await store();
}
Future<void> setLabelForSubaddress(
{required int accountIndex, required int addressIndex, required String label}) async {
_setLabelForSubaddress({
'accountIndex': accountIndex,
'addressIndex': addressIndex,
'label': label
});
currentWallet!.setSubaddressLabel(accountIndex: accountIndex, addressIndex: addressIndex, label: label);
await store();
}

View file

@ -9,36 +9,38 @@ import 'package:cw_monero/api/structs/pending_transaction.dart';
import 'package:cw_monero/api/wallet.dart';
import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart';
import 'package:ffi/ffi.dart';
import 'package:monero/src/monero.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/wallet2.dart';
import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
import 'package:mutex/mutex.dart';
Map<int, Map<String, String>> txKeys = {};
String getTxKey(String txId) {
txKeys[wptr!.address] ??= {};
if (txKeys[wptr!.address]![txId] != null) {
return txKeys[wptr!.address]![txId]!;
txKeys[currentWallet!.ffiAddress()] ??= {};
if (txKeys[currentWallet!.ffiAddress()]![txId] != null) {
return txKeys[currentWallet!.ffiAddress()]![txId]!;
}
final txKey = monero.Wallet_getTxKey(wptr!, txid: txId);
final status = monero.Wallet_status(wptr!);
final txKey = currentWallet!.getTxKey(txid: txId);
final status = currentWallet!.status();
if (status != 0) {
monero.Wallet_errorString(wptr!);
txKeys[wptr!.address]![txId] = "";
currentWallet!.errorString();
txKeys[currentWallet!.ffiAddress()]![txId] = "";
return "";
}
txKeys[wptr!.address]![txId] = txKey;
txKeys[currentWallet!.ffiAddress()]![txId] = txKey;
return txKey;
}
final txHistoryMutex = Mutex();
monero.TransactionHistory? txhistory;
Wallet2TransactionHistory? txhistory;
bool isRefreshingTx = false;
Future<void> refreshTransactions() async {
if (isRefreshingTx == true) return;
isRefreshingTx = true;
txhistory ??= monero.Wallet_history(wptr!);
final ptr = txhistory!.address;
txhistory ??= currentWallet!.history();
final ptr = txhistory!.ffiAddress();
await txHistoryMutex.acquire();
await Isolate.run(() {
monero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
@ -48,14 +50,14 @@ Future<void> refreshTransactions() async {
isRefreshingTx = false;
}
int countOfTransactions() => monero.TransactionHistory_count(txhistory!);
int countOfTransactions() => txhistory!.count();
Future<List<Transaction>> getAllTransactions() async {
List<Transaction> dummyTxs = [];
await txHistoryMutex.acquire();
txhistory ??= monero.Wallet_history(wptr!);
final startAddress = txhistory!.address * wptr!.address;
txhistory ??= currentWallet!.history();
final startAddress = txhistory!.ffiAddress() * currentWallet!.ffiAddress();
int size = countOfTransactions();
final list = <Transaction>[];
for (int index = 0; index < size; index++) {
@ -63,21 +65,21 @@ Future<List<Transaction>> getAllTransactions() async {
// Give main thread a chance to do other things.
await Future.delayed(Duration.zero);
}
if (txhistory!.address * wptr!.address != startAddress) {
if (txhistory!.ffiAddress() * currentWallet!.ffiAddress() != startAddress) {
printV("Loop broken because txhistory!.address * wptr!.address != startAddress");
break;
}
final txInfo = monero.TransactionHistory_transaction(txhistory!, index: index);
final txHash = monero.TransactionInfo_hash(txInfo);
txCache[wptr!.address] ??= {};
txCache[wptr!.address]![txHash] = Transaction(txInfo: txInfo);
list.add(txCache[wptr!.address]![txHash]!);
final txInfo = txhistory!.transaction(index);
final txHash = txInfo.hash();
txCache[currentWallet!.ffiAddress()] ??= {};
txCache[currentWallet!.ffiAddress()]![txHash] = Transaction(txInfo: txInfo);
list.add(txCache[currentWallet!.ffiAddress()]![txHash]!);
}
txHistoryMutex.release();
final accts = monero.Wallet_numSubaddressAccounts(wptr!);
final accts = currentWallet!.numSubaddressAccounts();
for (var i = 0; i < accts; i++) {
final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i);
final availBalance = monero.Wallet_unlockedBalance(wptr!, accountIndex: i);
final fullBalance = currentWallet!.balance(accountIndex: i);
final availBalance = currentWallet!.unlockedBalance(accountIndex: i);
if (fullBalance > availBalance) {
if (list.where((element) => element.accountIndex == i && element.isConfirmed == false).isEmpty) {
dummyTxs.add(
@ -95,7 +97,7 @@ Future<List<Transaction>> getAllTransactions() async {
isSpend: false,
hash: "pending",
key: "",
txInfo: Pointer.fromAddress(0),
txInfo: DummyTransaction(),
)..timeStamp = DateTime.now()
);
}
@ -105,16 +107,21 @@ Future<List<Transaction>> getAllTransactions() async {
return list;
}
class DummyTransaction implements Wallet2TransactionInfo {
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
Map<int, Map<String, Transaction>> txCache = {};
Future<Transaction> getTransaction(String txId) async {
if (txCache[wptr!.address] != null && txCache[wptr!.address]![txId] != null) {
return txCache[wptr!.address]![txId]!;
if (txCache[currentWallet!.ffiAddress()] != null && txCache[currentWallet!.ffiAddress()]![txId] != null) {
return txCache[currentWallet!.ffiAddress()]![txId]!;
}
await txHistoryMutex.acquire();
final tx = monero.TransactionHistory_transactionById(txhistory!, txid: txId);
final tx = txhistory!.transactionById(txId);
final txDart = Transaction(txInfo: tx);
txCache[wptr!.address] ??= {};
txCache[wptr!.address]![txId] = txDart;
txCache[currentWallet!.ffiAddress()] ??= {};
txCache[currentWallet!.ffiAddress()]![txId] = txDart;
txHistoryMutex.release();
return txDart;
}
@ -127,9 +134,9 @@ Future<PendingTransactionDescription> createTransactionSync(
int accountIndex = 0,
List<String> preferredInputs = const []}) async {
final amt = amount == null ? 0 : monero.Wallet_amountFromString(amount);
final amt = amount == null ? 0 : currentWallet!.amountFromString(amount);
final waddr = wptr!.address;
final waddr = currentWallet!.ffiAddress();
// force reconnection in case the os killed the connection?
// fixes failed to get block height error.
@ -149,7 +156,7 @@ Future<PendingTransactionDescription> createTransactionSync(
final paymentIdAddr = paymentId_.address;
final preferredInputsAddr = preferredInputs_.address;
final spaddr = monero.defaultSeparator.address;
final pendingTx = Pointer<Void>.fromAddress(await Isolate.run(() {
final pendingTxPtr = Pointer<Void>.fromAddress(await Isolate.run(() {
final tx = monero_gen.MoneroC(DynamicLibrary.open(monero.libPath)).MONERO_Wallet_createTransaction(
Pointer.fromAddress(waddr),
Pointer.fromAddress(addraddr).cast(),
@ -163,15 +170,16 @@ Future<PendingTransactionDescription> createTransactionSync(
);
return tx.address;
}));
final Wallet2PendingTransaction pendingTx = MoneroPendingTransaction(pendingTxPtr);
calloc.free(address_);
calloc.free(paymentId_);
calloc.free(preferredInputs_);
final String? error = (() {
final status = monero.PendingTransaction_status(pendingTx);
final status = pendingTx.status();
if (status == 0) {
return null;
}
return monero.PendingTransaction_errorString(pendingTx);
return pendingTx.errorString();
})();
if (error != null) {
@ -182,10 +190,10 @@ Future<PendingTransactionDescription> createTransactionSync(
throw CreationTransactionException(message: message);
}
final rAmt = monero.PendingTransaction_amount(pendingTx);
final rFee = monero.PendingTransaction_fee(pendingTx);
final rHash = monero.PendingTransaction_txid(pendingTx, '');
final rHex = monero.PendingTransaction_hex(pendingTx, '');
final rAmt = pendingTx.amount();
final rFee = pendingTx.fee();
final rHash = pendingTx.txid('');
final rHex = pendingTx.hex('');
final rTxKey = rHash;
return PendingTransactionDescription(
@ -194,7 +202,7 @@ Future<PendingTransactionDescription> createTransactionSync(
hash: rHash,
hex: rHex,
txKey: rTxKey,
pointerAddress: pendingTx.address,
pointerAddress: pendingTx.ffiAddress(),
);
}
@ -206,9 +214,9 @@ Future<PendingTransactionDescription> createTransactionMultDest(
List<String> preferredInputs = const []}) async {
final dstAddrs = outputs.map((e) => e.address).toList();
final amounts = outputs.map((e) => monero.Wallet_amountFromString(e.amount)).toList();
final amounts = outputs.map((e) => currentWallet!.amountFromString(e.amount)).toList();
final waddr = wptr!.address;
final waddr = currentWallet!.ffiAddress();
// force reconnection in case the os killed the connection
Isolate.run(() async {
@ -227,49 +235,50 @@ Future<PendingTransactionDescription> createTransactionMultDest(
).address;
}));
if (monero.PendingTransaction_status(txptr) != 0) {
throw CreationTransactionException(message: monero.PendingTransaction_errorString(txptr));
final Wallet2PendingTransaction tx = MoneroPendingTransaction(txptr);
if (tx.status() != 0) {
throw CreationTransactionException(message: tx.errorString());
}
return PendingTransactionDescription(
amount: monero.PendingTransaction_amount(txptr),
fee: monero.PendingTransaction_fee(txptr),
hash: monero.PendingTransaction_txid(txptr, ''),
hex: monero.PendingTransaction_hex(txptr, ''),
txKey: monero.PendingTransaction_txid(txptr, ''),
pointerAddress: txptr.address,
amount: tx.amount(),
fee: tx.fee(),
hash: tx.txid(''),
hex: tx.hex(''),
txKey: tx.txid(''),
pointerAddress: tx.ffiAddress(),
);
}
String? commitTransactionFromPointerAddress({required int address, required bool useUR}) =>
commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address), useUR: useUR);
commitTransaction(tx: MoneroPendingTransaction(Pointer.fromAddress(address)), useUR: useUR);
String? commitTransaction({required monero.PendingTransaction transactionPointer, required bool useUR}) {
final transactionPointerAddress = transactionPointer.address;
String? commitTransaction({required Wallet2PendingTransaction tx, required bool useUR}) {
final txCommit = useUR
? monero.PendingTransaction_commitUR(transactionPointer, 120)
? tx.commitUR(120)
: Isolate.run(() {
monero.PendingTransaction_commit(
Pointer.fromAddress(transactionPointerAddress),
Pointer.fromAddress(tx.ffiAddress()),
filename: '',
overwrite: false,
);
});
String? error = (() {
final status = monero.PendingTransaction_status(transactionPointer.cast());
final status = tx.status();
if (status == 0) {
return null;
}
return monero.PendingTransaction_errorString(transactionPointer.cast());
return tx.errorString();
})();
if (error == null) {
error = (() {
final status = monero.Wallet_status(wptr!);
final status = currentWallet!.status();
if (status == 0) {
return null;
}
return monero.Wallet_errorString(wptr!);
return currentWallet!.errorString();
})();
}
@ -283,43 +292,9 @@ String? commitTransaction({required monero.PendingTransaction transactionPointer
}
}
Future<PendingTransactionDescription> _createTransactionSync(Map args) async {
final address = args['address'] as String;
final paymentId = args['paymentId'] as String;
final amount = args['amount'] as String?;
final priorityRaw = args['priorityRaw'] as int;
final accountIndex = args['accountIndex'] as int;
final preferredInputs = args['preferredInputs'] as List<String>;
return createTransactionSync(
address: address,
paymentId: paymentId,
amount: amount,
priorityRaw: priorityRaw,
accountIndex: accountIndex,
preferredInputs: preferredInputs);
}
Future<PendingTransactionDescription> createTransaction(
{required String address,
required int priorityRaw,
String? amount,
String paymentId = '',
int accountIndex = 0,
List<String> preferredInputs = const []}) async =>
_createTransactionSync({
'address': address,
'paymentId': paymentId,
'amount': amount,
'priorityRaw': priorityRaw,
'accountIndex': accountIndex,
'preferredInputs': preferredInputs
});
class Transaction {
final String displayLabel;
late final String subaddressLabel = monero.Wallet_getSubaddressLabel(
wptr!,
late final String subaddressLabel = currentWallet!.getSubaddressLabel(
accountIndex: accountIndex,
addressIndex: addressIndex,
);
@ -372,26 +347,26 @@ class Transaction {
// final SubAddress? subAddress;
// List<Transfer> transfers = [];
// final int txIndex;
final monero.TransactionInfo txInfo;
final Wallet2TransactionInfo txInfo;
Transaction({
required this.txInfo,
}) : displayLabel = monero.TransactionInfo_label(txInfo),
hash = monero.TransactionInfo_hash(txInfo),
}) : displayLabel = txInfo.label(),
hash = txInfo.hash(),
timeStamp = DateTime.fromMillisecondsSinceEpoch(
monero.TransactionInfo_timestamp(txInfo) * 1000,
txInfo.timestamp() * 1000,
),
isSpend = monero.TransactionInfo_direction(txInfo) ==
monero.TransactionInfo_Direction.Out,
amount = monero.TransactionInfo_amount(txInfo),
paymentId = monero.TransactionInfo_paymentId(txInfo),
accountIndex = monero.TransactionInfo_subaddrAccount(txInfo),
addressIndex = int.tryParse(monero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
addressIndexList = monero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
blockheight = monero.TransactionInfo_blockHeight(txInfo),
confirmations = monero.TransactionInfo_confirmations(txInfo),
fee = monero.TransactionInfo_fee(txInfo),
description = monero.TransactionInfo_description(txInfo),
key = getTxKey(monero.TransactionInfo_hash(txInfo));
isSpend = txInfo.direction() ==
monero.TransactionInfo_Direction.Out.index,
amount = txInfo.amount(),
paymentId = txInfo.paymentId(),
accountIndex = txInfo.subaddrAccount(),
addressIndex = int.tryParse(txInfo.subaddrIndex().split(", ")[0]) ?? 0,
addressIndexList = txInfo.subaddrIndex().split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
blockheight = txInfo.blockHeight(),
confirmations = txInfo.confirmations(),
fee = txInfo.fee(),
description = txInfo.description(),
key = getTxKey(txInfo.hash());
Transaction.dummy({
required this.displayLabel,

View file

@ -5,8 +5,6 @@ import 'dart:isolate';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
import 'package:cw_monero/api/wallet_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:monero/monero.dart' as monero;
import 'package:mutex/mutex.dart';
import 'package:polyseed/polyseed.dart';
@ -15,36 +13,37 @@ bool debugMonero = false;
int getSyncingHeight() {
// final height = monero.MONERO_cw_WalletListener_height(getWlptr());
final h2 = monero.Wallet_blockChainHeight(wptr!);
if (currentWallet == null) return 0;
final h2 = currentWallet!.blockChainHeight();
// printV("height: $height / $h2");
return h2;
}
bool isNeededToRefresh() {
final wlptr = getWlptr();
if (wlptr == null) return false;
final ret = monero.MONERO_cw_WalletListener_isNeedToRefresh(wlptr);
monero.MONERO_cw_WalletListener_resetNeedToRefresh(wlptr);
final wl = getWlptr();
if (wl == null) return false;
final ret = wl.isNeedToRefresh();
wl.resetNeedToRefresh();
return ret;
}
bool isNewTransactionExist() {
final wlptr = getWlptr();
if (wlptr == null) return false;
final ret = monero.MONERO_cw_WalletListener_isNewTransactionExist(wlptr);
monero.MONERO_cw_WalletListener_resetIsNewTransactionExist(wlptr);
final ret = wlptr.isNewTransactionExist();
wlptr.resetIsNewTransactionExist();
return ret;
}
String getFilename() => monero.Wallet_filename(wptr!);
String getFilename() => currentWallet!.filename();
String getSeed() {
// monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
final cakepolyseed =
monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
currentWallet!.getCacheAttribute(key: "cakewallet.seed");
final cakepassphrase = getPassphrase();
final weirdPolyseed = monero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase);
final weirdPolyseed = currentWallet!.getPolyseed(passphrase: cakepassphrase);
if (weirdPolyseed != "") return weirdPolyseed;
if (cakepolyseed != "") {
@ -63,7 +62,7 @@ String getSeed() {
return cakepolyseed;
}
final bip39 = monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed.bip39");
final bip39 = currentWallet!.getCacheAttribute(key: "cakewallet.seed.bip39");
if(bip39.isNotEmpty) return bip39;
@ -85,29 +84,29 @@ String? getSeedLanguage(String? language) {
String getSeedLegacy(String? language) {
final cakepassphrase = getPassphrase();
language = getSeedLanguage(language);
var legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
if (monero.Wallet_status(wptr!) != 0) {
if (monero.Wallet_errorString(wptr!).contains("seed_language")) {
monero.Wallet_setSeedLanguage(wptr!, language: "English");
legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
var legacy = currentWallet!.seed(seedOffset: cakepassphrase);
if (currentWallet!.status() != 0) {
if (currentWallet!.errorString().contains("seed_language")) {
currentWallet!.setSeedLanguage(language: "English");
legacy = currentWallet!.seed(seedOffset: cakepassphrase);
}
}
if (language != null) {
monero.Wallet_setSeedLanguage(wptr!, language: language);
final status = monero.Wallet_status(wptr!);
currentWallet!.setSeedLanguage(language: language);
final status = currentWallet!.status();
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
final err = currentWallet!.errorString();
if (legacy.isNotEmpty) {
return "$err\n\n$legacy";
}
return err;
}
legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
legacy = currentWallet!.seed(seedOffset: cakepassphrase);
}
if (monero.Wallet_status(wptr!) != 0) {
final err = monero.Wallet_errorString(wptr!);
if (currentWallet!.status() != 0) {
final err = currentWallet!.errorString();
if (legacy.isNotEmpty) {
return "$err\n\n$legacy";
}
@ -117,7 +116,7 @@ String getSeedLegacy(String? language) {
}
String getPassphrase() {
return monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.passphrase");
return currentWallet!.getCacheAttribute(key: "cakewallet.passphrase");
}
Map<int, Map<int, Map<int, String>>> addressCache = {};
@ -125,31 +124,31 @@ Map<int, Map<int, Map<int, String>>> addressCache = {};
String getAddress({int accountIndex = 0, int addressIndex = 0}) {
// printV("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}");
// this could be a while loop, but I'm in favor of making it if to not cause freezes
if (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) {
if (monero.Wallet_numSubaddressAccounts(wptr!) < accountIndex) {
monero.Wallet_addSubaddressAccount(wptr!);
if (currentWallet!.numSubaddresses(accountIndex: accountIndex)-1 < addressIndex) {
if (currentWallet!.numSubaddressAccounts() < accountIndex) {
currentWallet!.addSubaddressAccount();
} else {
monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex);
currentWallet!.addSubaddress(accountIndex: accountIndex);
}
}
addressCache[wptr!.address] ??= {};
addressCache[wptr!.address]![accountIndex] ??= {};
addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!,
addressCache[currentWallet!.ffiAddress()] ??= {};
addressCache[currentWallet!.ffiAddress()]![accountIndex] ??= {};
addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex] ??= currentWallet!.address(
accountIndex: accountIndex, addressIndex: addressIndex);
return addressCache[wptr!.address]![accountIndex]![addressIndex]!;
return addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex]!;
}
int getFullBalance({int accountIndex = 0}) =>
monero.Wallet_balance(wptr!, accountIndex: accountIndex);
currentWallet!.balance(accountIndex: accountIndex);
int getUnlockedBalance({int accountIndex = 0}) =>
monero.Wallet_unlockedBalance(wptr!, accountIndex: accountIndex);
currentWallet!.unlockedBalance(accountIndex: accountIndex);
int getCurrentHeight() => monero.Wallet_blockChainHeight(wptr!);
int getCurrentHeight() => currentWallet!.blockChainHeight();
int getNodeHeightSync() => monero.Wallet_daemonBlockChainHeight(wptr!);
int getNodeHeightSync() => currentWallet!.daemonBlockChainHeight();
bool isConnectedSync() => monero.Wallet_connected(wptr!) != 0;
bool isConnectedSync() => currentWallet!.connected() != 0;
Future<bool> setupNodeSync(
{required String address,
@ -168,7 +167,7 @@ Future<bool> setupNodeSync(
daemonPassword: $password ?? ''
}
''');
final addr = wptr!.address;
final addr = currentWallet!.ffiAddress();
printV("init: start");
await Isolate.run(() {
monero.Wallet_init(Pointer.fromAddress(addr),
@ -180,10 +179,10 @@ Future<bool> setupNodeSync(
});
printV("init: end");
final status = monero.Wallet_status(wptr!);
final status = currentWallet!.status();
if (status != 0) {
final error = monero.Wallet_errorString(wptr!);
final error = currentWallet!.errorString();
if (error != "no tx keys found for this txid") {
printV("error: $error");
throw SetupWalletException(message: error);
@ -191,8 +190,8 @@ Future<bool> setupNodeSync(
}
if (true) {
monero.Wallet_init3(
wptr!, argv0: '',
currentWallet!.init3(
argv0: '',
defaultLogBaseName: 'moneroc',
console: true,
logPath: '',
@ -203,19 +202,19 @@ Future<bool> setupNodeSync(
}
void startRefreshSync() {
monero.Wallet_refreshAsync(wptr!);
monero.Wallet_startRefresh(wptr!);
currentWallet!.refreshAsync();
currentWallet!.startRefresh();
}
void setRefreshFromBlockHeight({required int height}) {
monero.Wallet_setRefreshFromBlockHeight(wptr!,
currentWallet!.setRefreshFromBlockHeight(
refresh_from_block_height: height);
}
void setRecoveringFromSeed({required bool isRecovery}) {
monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
monero.Wallet_store(wptr!);
currentWallet!.setRecoveringFromSeed(recoveringFromSeed: isRecovery);
currentWallet!.store();
}
final storeMutex = Mutex();
@ -224,18 +223,18 @@ final storeMutex = Mutex();
int lastStorePointer = 0;
int lastStoreHeight = 0;
void storeSync({bool force = false}) async {
final addr = wptr!.address;
final addr = currentWallet!.ffiAddress();
final synchronized = await Isolate.run(() {
return monero.Wallet_synchronized(Pointer.fromAddress(addr));
});
if (lastStorePointer == wptr!.address &&
lastStoreHeight + 5000 > monero.Wallet_blockChainHeight(wptr!) &&
if (lastStorePointer == addr &&
lastStoreHeight + 5000 > currentWallet!.blockChainHeight() &&
!synchronized &&
!force) {
return;
}
lastStorePointer = wptr!.address;
lastStoreHeight = monero.Wallet_blockChainHeight(wptr!);
lastStorePointer = currentWallet!.ffiAddress();
lastStoreHeight = currentWallet!.blockChainHeight();
await storeMutex.acquire();
await Isolate.run(() {
monero.Wallet_store(Pointer.fromAddress(addr));
@ -244,25 +243,25 @@ void storeSync({bool force = false}) async {
}
void setPasswordSync(String password) {
monero.Wallet_setPassword(wptr!, password: password);
currentWallet!.setPassword(password: password);
final status = monero.Wallet_status(wptr!);
final status = currentWallet!.status();
if (status != 0) {
throw Exception(monero.Wallet_errorString(wptr!));
throw Exception(currentWallet!.errorString());
}
}
void closeCurrentWallet() {
monero.Wallet_stop(wptr!);
currentWallet!.stop();
}
String getSecretViewKey() => monero.Wallet_secretViewKey(wptr!);
String getSecretViewKey() => currentWallet!.secretViewKey();
String getPublicViewKey() => monero.Wallet_publicViewKey(wptr!);
String getPublicViewKey() => currentWallet!.publicViewKey();
String getSecretSpendKey() => monero.Wallet_secretSpendKey(wptr!);
String getSecretSpendKey() => currentWallet!.secretSpendKey();
String getPublicSpendKey() => monero.Wallet_publicSpendKey(wptr!);
String getPublicSpendKey() => currentWallet!.publicSpendKey();
class SyncListener {
SyncListener(this.onNewBlock, this.onNewTransaction)
@ -360,52 +359,32 @@ Future<bool> _setupNodeSync(Map<String, Object?> args) async {
socksProxyAddress: socksProxyAddress);
}
bool _isConnected(Object _) => isConnectedSync();
int _getNodeHeight(Object _) => getNodeHeightSync();
void startRefresh() => startRefreshSync();
Future<void> setupNode(
{required String address,
String? login,
String? password,
bool useSSL = false,
String? socksProxyAddress,
bool isLightWallet = false}) async =>
_setupNodeSync({
'address': address,
'login': login,
'password': password,
'useSSL': useSSL,
'isLightWallet': isLightWallet,
'socksProxyAddress': socksProxyAddress
});
Future<void> store() async => _storeSync(0);
Future<bool> isConnected() async => _isConnected(0);
Future<bool> isConnected() async => isConnectedSync();
Future<int> getNodeHeight() async => _getNodeHeight(0);
Future<int> getNodeHeight() async => getNodeHeightSync();
void rescanBlockchainAsync() => monero.Wallet_rescanBlockchainAsync(wptr!);
void rescanBlockchainAsync() => currentWallet!.rescanBlockchainAsync();
String getSubaddressLabel(int accountIndex, int addressIndex) {
return monero.Wallet_getSubaddressLabel(wptr!,
return currentWallet!.getSubaddressLabel(
accountIndex: accountIndex, addressIndex: addressIndex);
}
Future setTrustedDaemon(bool trusted) async =>
monero.Wallet_setTrustedDaemon(wptr!, arg: trusted);
currentWallet!.setTrustedDaemon(arg: trusted);
Future<bool> trustedDaemon() async => monero.Wallet_trustedDaemon(wptr!);
Future<bool> trustedDaemon() async => currentWallet!.trustedDaemon();
String signMessage(String message, {String address = ""}) {
return monero.Wallet_signMessage(wptr!, message: message, address: address);
return currentWallet!.signMessage(message: message, address: address);
}
bool verifyMessage(String message, String address, String signature) {
return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
return currentWallet!.verifySignedMessage(message: message, address: address, signature: signature);
}
Map<String, List<int>> debugCallLength() => monero.debugCallLength;

View file

@ -12,6 +12,8 @@ import 'package:cw_monero/api/transaction_history.dart';
import 'package:cw_monero/api/wallet.dart';
import 'package:cw_monero/ledger.dart';
import 'package:flutter/foundation.dart';
import 'package:monero/src/monero.dart';
import 'package:monero/src/wallet2.dart';
import 'package:monero/monero.dart' as monero;
class MoneroCException implements Exception {
@ -24,9 +26,10 @@ class MoneroCException implements Exception {
}
void checkIfMoneroCIsFine() {
final cppCsCpp = monero.MONERO_checksum_wallet2_api_c_cpp();
final cppCsH = monero.MONERO_checksum_wallet2_api_c_h();
final cppCsExp = monero.MONERO_checksum_wallet2_api_c_exp();
final checksum = MoneroWalletChecksum();
final cppCsCpp = checksum.checksum_wallet2_api_c_cpp();
final cppCsH = checksum.checksum_wallet2_api_c_h();
final cppCsExp = checksum.checksum_wallet2_api_c_exp();
final dartCsCpp = monero.wallet2_api_c_cpp_sha256;
final dartCsH = monero.wallet2_api_c_h_sha256;
@ -44,36 +47,35 @@ void checkIfMoneroCIsFine() {
throw MoneroCException("monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'");
}
}
monero.WalletManager? _wmPtr;
final monero.WalletManager wmPtr = Pointer.fromAddress((() {
Wallet2WalletManager? _wmPtr;
Wallet2WalletManager wmPtr = (() {
try {
// Problems with the wallet? Crashes? Lags? this will print all calls to xmr
// codebase, so it will be easier to debug what happens. At least easier
// than plugging gdb in. Especially on windows/android.
monero.printStarts = false;
if (kDebugMode && debugMonero) {
monero.WalletManagerFactory_setLogLevel(4);
MoneroWalletManagerFactory().setLogLevel(4);
}
_wmPtr ??= monero.WalletManagerFactory_getWalletManager();
_wmPtr ??= MoneroWalletManagerFactory().getWalletManager();
if (kDebugMode && debugMonero) {
monero.WalletManagerFactory_setLogLevel(4);
MoneroWalletManagerFactory().setLogLevel(4);
}
printV("ptr: $_wmPtr");
} catch (e) {
printV(e);
rethrow;
}
return _wmPtr!.address;
})());
return _wmPtr!;
})();
void createWalletPointer() {
final newWptr = monero.WalletManager_createWallet(wmPtr,
Wallet2Wallet createWalletPointer() {
final newWptr = wmPtr.createWallet(
path: "", password: "", language: "", networkType: 0);
wptr = newWptr;
return newWptr;
}
void createWalletSync(
void createWallet(
{required String path,
required String password,
required String language,
@ -81,28 +83,25 @@ void createWalletSync(
int nettype = 0}) {
txhistory = null;
language = getSeedLanguage(language)!;
final newWptr = monero.WalletManager_createWallet(wmPtr,
final newW = wmPtr.createWallet(
path: path, password: password, language: language, networkType: 0);
int status = monero.Wallet_status(newWptr);
int status = newW.status();
if (status != 0) {
throw WalletCreationException(message: monero.Wallet_errorString(newWptr));
throw WalletCreationException(message: newW.errorString());
}
setupBackgroundSync(password, newWptr);
setupBackgroundSync(password, newW);
wptr = newWptr;
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
monero.Wallet_store(wptr!, path: path);
openedWalletsByPath[path] = wptr!;
currentWallet = newW;
currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase);
currentWallet!.store(path: path);
openedWalletsByPath[path] = currentWallet!;
_lastOpenedWallet = path;
// is the line below needed?
// setupNodeSync(address: "node.moneroworld.com:18089");
}
bool isWalletExistSync({required String path}) {
return monero.WalletManager_walletExists(wmPtr, path);
bool isWalletExist({required String path}) {
return wmPtr.walletExists(path);
}
void restoreWalletFromSeedSync(
@ -113,8 +112,7 @@ void restoreWalletFromSeedSync(
int nettype = 0,
int restoreHeight = 0}) {
txhistory = null;
final newWptr = monero.WalletManager_recoveryWallet(
wmPtr,
final newW = wmPtr.recoveryWallet(
path: path,
password: password,
mnemonic: seed,
@ -123,10 +121,10 @@ void restoreWalletFromSeedSync(
networkType: 0,
);
final status = monero.Wallet_status(newWptr);
final status = newW.status();
if (status != 0) {
final error = monero.Wallet_errorString(newWptr);
final error = newW.errorString();
if (error.contains('word list failed verification')) {
throw WalletRestoreFromSeedException(
message: "Seed verification failed, please make sure you entered the correct seed with the correct words order",
@ -134,20 +132,20 @@ void restoreWalletFromSeedSync(
}
throw WalletRestoreFromSeedException(message: error);
}
wptr = newWptr;
currentWallet = newW;
setRefreshFromBlockHeight(height: restoreHeight);
setupBackgroundSync(password, newWptr);
setupBackgroundSync(password, newW);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase);
openedWalletsByPath[path] = wptr!;
openedWalletsByPath[path] = currentWallet!;
monero.Wallet_store(wptr!);
currentWallet!.store(path: path);
_lastOpenedWallet = path;
}
void restoreWalletFromKeysSync(
void restoreWalletFromKeys(
{required String path,
required String password,
required String language,
@ -157,8 +155,8 @@ void restoreWalletFromKeysSync(
int nettype = 0,
int restoreHeight = 0}) {
txhistory = null;
var newWptr = (spendKey != "")
? monero.WalletManager_createDeterministicWalletFromSpendKey(wmPtr,
var newW = (spendKey != "")
? wmPtr.createDeterministicWalletFromSpendKey(
path: path,
password: password,
language: language,
@ -166,8 +164,7 @@ void restoreWalletFromKeysSync(
newWallet: true,
// TODO(mrcyjanek): safe to remove
restoreHeight: restoreHeight)
: monero.WalletManager_createWalletFromKeys(
wmPtr,
: wmPtr.createWalletFromKeys(
path: path,
password: password,
restoreHeight: restoreHeight,
@ -177,22 +174,21 @@ void restoreWalletFromKeysSync(
nettype: 0,
);
int status = monero.Wallet_status(newWptr);
int status = newW.status();
if (status != 0) {
throw WalletRestoreFromKeysException(
message: monero.Wallet_errorString(newWptr));
message: newW.errorString());
}
// CW-712 - Try to restore deterministic wallet first, if the view key doesn't
// match the view key provided
if (spendKey != "") {
final viewKeyRestored = monero.Wallet_secretViewKey(newWptr);
final viewKeyRestored = newW.secretViewKey();
if (viewKey != viewKeyRestored && viewKey != "") {
monero.WalletManager_closeWallet(wmPtr, newWptr, false);
wmPtr.closeWallet(newW, false);
File(path).deleteSync();
File(path + ".keys").deleteSync();
newWptr = monero.WalletManager_createWalletFromKeys(
wmPtr,
newW = wmPtr.createWalletFromKeys(
path: path,
password: password,
restoreHeight: restoreHeight,
@ -201,19 +197,19 @@ void restoreWalletFromKeysSync(
spendKeyString: spendKey,
nettype: 0,
);
int status = monero.Wallet_status(newWptr);
int status = newW.status();
if (status != 0) {
throw WalletRestoreFromKeysException(
message: monero.Wallet_errorString(newWptr));
message: newW.errorString());
}
setupBackgroundSync(password, newWptr);
setupBackgroundSync(password, newW);
}
}
wptr = newWptr;
currentWallet = newW;
openedWalletsByPath[path] = wptr!;
openedWalletsByPath[path] = currentWallet!;
_lastOpenedWallet = path;
}
@ -228,8 +224,7 @@ void restoreWalletFromPolyseedWithOffset(
int nettype = 0}) {
txhistory = null;
final newWptr = monero.WalletManager_createWalletFromPolyseed(
wmPtr,
final newW = wmPtr.createWalletFromPolyseed(
path: path,
password: password,
networkType: nettype,
@ -240,24 +235,24 @@ void restoreWalletFromPolyseedWithOffset(
kdfRounds: 1,
);
int status = monero.Wallet_status(newWptr);
int status = newW.status();
if (status != 0) {
final err = monero.Wallet_errorString(newWptr);
final err = newW.errorString();
printV("err: $err");
throw WalletRestoreFromKeysException(message: err);
}
wptr = newWptr;
currentWallet = newW;
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: seedOffset);
monero.Wallet_store(wptr!);
currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed);
currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: seedOffset);
currentWallet!.store(path: path);
setupBackgroundSync(password, newWptr);
setupBackgroundSync(password, currentWallet!);
storeSync();
openedWalletsByPath[path] = wptr!;
openedWalletsByPath[path] = currentWallet!;
}
@ -282,8 +277,7 @@ void restoreWalletFromSpendKeySync(
// );
txhistory = null;
final newWptr = monero.WalletManager_createDeterministicWalletFromSpendKey(
wmPtr,
final newW = wmPtr.createDeterministicWalletFromSpendKey(
path: path,
password: password,
language: language,
@ -292,23 +286,23 @@ void restoreWalletFromSpendKeySync(
restoreHeight: restoreHeight,
);
int status = monero.Wallet_status(newWptr);
int status = newW.status();
if (status != 0) {
final err = monero.Wallet_errorString(newWptr);
final err = newW.errorString();
printV("err: $err");
throw WalletRestoreFromKeysException(message: err);
}
wptr = newWptr;
currentWallet = newW;
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed);
storeSync();
setupBackgroundSync(password, newWptr);
setupBackgroundSync(password, currentWallet!);
openedWalletsByPath[path] = wptr!;
openedWalletsByPath[path] = currentWallet!;
_lastOpenedWallet = path;
}
@ -321,41 +315,42 @@ Future<void> restoreWalletFromHardwareWallet(
int nettype = 0,
int restoreHeight = 0}) async {
txhistory = null;
final wmPtr = MoneroWalletManagerFactory().getWalletManager().ffiAddress();
final newWptrAddr = await Isolate.run(() {
return monero.WalletManager_createWalletFromDevice(wmPtr,
return monero.WalletManager_createWalletFromDevice(Pointer.fromAddress(wmPtr),
path: path,
password: password,
restoreHeight: restoreHeight,
deviceName: deviceName)
.address;
});
final newWptr = Pointer<Void>.fromAddress(newWptrAddr);
final newW = MoneroWallet(Pointer.fromAddress(newWptrAddr));
final status = monero.Wallet_status(newWptr);
final status = newW.status();
if (status != 0) {
final error = monero.Wallet_errorString(newWptr);
final error = newW.errorString();
throw WalletRestoreFromSeedException(message: error);
}
wptr = newWptr;
currentWallet = newW;
currentWallet!.store(path: path);
_lastOpenedWallet = path;
openedWalletsByPath[path] = wptr!;
openedWalletsByPath[path] = currentWallet!;
}
Map<String, monero.wallet> openedWalletsByPath = {};
Map<String, Wallet2Wallet> openedWalletsByPath = {};
Future<void> loadWallet(
{required String path, required String password, int nettype = 0}) async {
if (openedWalletsByPath[path] != null) {
txhistory = null;
wptr = openedWalletsByPath[path]!;
currentWallet = openedWalletsByPath[path]!;
return;
}
if (wptr == null || path != _lastOpenedWallet) {
if (wptr != null) {
final addr = wptr!.address;
if (currentWallet == null || path != _lastOpenedWallet) {
if (currentWallet != null) {
final addr = currentWallet!.ffiAddress();
Isolate.run(() {
monero.Wallet_store(Pointer.fromAddress(addr));
});
@ -366,20 +361,25 @@ Future<void> loadWallet(
/// 0: Software Wallet
/// 1: Ledger
/// 2: Trezor
late final deviceType;
var deviceType = 0;
if (Platform.isAndroid || Platform.isIOS) {
deviceType = monero.WalletManager_queryWalletDevice(
wmPtr,
deviceType = wmPtr.queryWalletDevice(
keysFileName: "$path.keys",
password: password,
kdfRounds: 1,
);
final status = monero.WalletManager_errorString(wmPtr);
final status = wmPtr.errorString();
if (status != "") {
printV("loadWallet:"+status);
// This is most likely closeWallet call leaking error. This is fine.
if (status.contains("failed to save file")) {
printV("loadWallet: error leaked: $status");
deviceType = 0;
} else {
throw WalletOpeningException(message: status);
}
}
} else {
deviceType = 0;
}
@ -388,107 +388,47 @@ Future<void> loadWallet(
if (gLedger == null) {
throw Exception("Tried to open a ledger wallet with no ledger connected");
}
final dummyWPtr = wptr ??
monero.WalletManager_openWallet(wmPtr, path: '', password: '');
final dummyWPtr = (currentWallet ??
wmPtr.openWallet(path: '', password: ''));
enableLedgerExchange(dummyWPtr, gLedger!);
}
final addr = wmPtr.address;
final addr = wmPtr.ffiAddress();
final newWptrAddr = await Isolate.run(() {
return monero.WalletManager_openWallet(Pointer.fromAddress(addr),
path: path, password: password)
.address;
});
final newWptr = Pointer<Void>.fromAddress(newWptrAddr);
final newW = MoneroWallet(Pointer.fromAddress(newWptrAddr));
int status = monero.Wallet_status(newWptr);
int status = newW.status();
if (status != 0) {
final err = monero.Wallet_errorString(newWptr);
final err = newW.errorString();
printV("loadWallet:"+err);
throw WalletOpeningException(message: err);
}
if (deviceType == 0) {
setupBackgroundSync(password, newWptr);
setupBackgroundSync(password, newW);
}
wptr = newWptr;
currentWallet = newW;
_lastOpenedWallet = path;
openedWalletsByPath[path] = wptr!;
openedWalletsByPath[path] = currentWallet!;
}
}
void setupBackgroundSync(String password, Pointer<Void>? wptrOverride) {
if (isViewOnlyBySpendKey(wptrOverride)) {
void setupBackgroundSync(String password, Wallet2Wallet wallet) {
if (isViewOnlyBySpendKey(wallet)) {
return;
}
monero.Wallet_setupBackgroundSync(wptrOverride ?? wptr!, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: '');
if (monero.Wallet_status(wptrOverride ?? wptr!) != 0) {
wallet.setupBackgroundSync(backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: '');
if (wallet.status() != 0) {
// We simply ignore the error.
printV("setupBackgroundSync: ${monero.Wallet_errorString(wptrOverride ?? wptr!)}");
printV("setupBackgroundSync: ${wallet.errorString()}");
}
}
void _createWallet(Map<String, dynamic> args) {
final path = args['path'] as String;
final password = args['password'] as String;
final language = args['language'] as String;
final passphrase = args['passphrase'] as String;
createWalletSync(path: path, password: password, language: language, passphrase: passphrase);
}
void _restoreFromSeed(Map<String, dynamic> args) {
final path = args['path'] as String;
final password = args['password'] as String;
final passphrase = args['passphrase'] as String;
final seed = args['seed'] as String;
final restoreHeight = args['restoreHeight'] as int;
return restoreWalletFromSeedSync(
path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight);
}
void _restoreFromKeys(Map<String, dynamic> args) {
final path = args['path'] as String;
final password = args['password'] as String;
final language = args['language'] as String;
final restoreHeight = args['restoreHeight'] as int;
final address = args['address'] as String;
final viewKey = args['viewKey'] as String;
final spendKey = args['spendKey'] as String;
restoreWalletFromKeysSync(
path: path,
password: password,
language: language,
restoreHeight: restoreHeight,
address: address,
viewKey: viewKey,
spendKey: spendKey);
}
void _restoreFromSpendKey(Map<String, dynamic> args) {
final path = args['path'] as String;
final password = args['password'] as String;
final seed = args['seed'] as String;
final language = args['language'] as String;
final spendKey = args['spendKey'] as String;
final restoreHeight = args['restoreHeight'] as int;
restoreWalletFromSpendKeySync(
path: path,
password: password,
seed: seed,
language: language,
restoreHeight: restoreHeight,
spendKey: spendKey);
}
Future<void> _openWallet(Map<String, String> args) async => loadWallet(
path: args['path'] as String, password: args['password'] as String);
bool _isWalletExist(String path) => isWalletExistSync(path: path);
Future<void> openWallet(
{required String path,
@ -496,77 +436,4 @@ Future<void> openWallet(
int nettype = 0}) async =>
loadWallet(path: path, password: password, nettype: nettype);
Future<void> openWalletAsync(Map<String, String> args) async =>
_openWallet(args);
Future<void> createWallet(
{required String path,
required String password,
required String language,
required String passphrase,
int nettype = 0}) async =>
_createWallet({
'path': path,
'password': password,
'language': language,
'passphrase': passphrase,
'nettype': nettype
});
void restoreFromSeed(
{required String path,
required String password,
required String passphrase,
required String seed,
int nettype = 0,
int restoreHeight = 0}) =>
_restoreFromSeed({
'path': path,
'password': password,
'passphrase': passphrase,
'seed': seed,
'nettype': nettype,
'restoreHeight': restoreHeight
});
Future<void> restoreFromKeys(
{required String path,
required String password,
required String language,
required String address,
required String viewKey,
required String spendKey,
int nettype = 0,
int restoreHeight = 0}) async =>
_restoreFromKeys({
'path': path,
'password': password,
'language': language,
'address': address,
'viewKey': viewKey,
'spendKey': spendKey,
'nettype': nettype,
'restoreHeight': restoreHeight
});
Future<void> restoreFromSpendKey(
{required String path,
required String password,
required String seed,
required String language,
required String spendKey,
int nettype = 0,
int restoreHeight = 0}) async =>
_restoreFromSpendKey({
'path': path,
'password': password,
'seed': seed,
'language': language,
'spendKey': spendKey,
'nettype': nettype,
'restoreHeight': restoreHeight
});
bool isWalletExist({required String path}) => _isWalletExist(path);
bool isViewOnlyBySpendKey(Pointer<Void>? wptrOverride) => int.tryParse(monero.Wallet_secretSpendKey(wptrOverride ?? wptr!)) == 0;
bool isViewOnlyBySpendKey(Wallet2Wallet? wallet) => int.tryParse((wallet??currentWallet!).secretSpendKey()) == 0;

View file

@ -7,26 +7,26 @@ import 'package:cw_core/utils/print_verbose.dart';
import 'package:ffi/ffi.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus_dart.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/wallet2.dart';
LedgerConnection? gLedger;
Timer? _ledgerExchangeTimer;
Timer? _ledgerKeepAlive;
void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) {
void enableLedgerExchange(Wallet2Wallet wallet, LedgerConnection connection) {
_ledgerExchangeTimer?.cancel();
_ledgerExchangeTimer = Timer.periodic(Duration(milliseconds: 1), (_) async {
final ledgerRequestLength = monero.Wallet_getSendToDeviceLength(ptr);
final ledgerRequest = monero.Wallet_getSendToDevice(ptr)
final ledgerRequestLength = wallet.getSendToDeviceLength();
final ledgerRequest = wallet.getSendToDevice()
.cast<Uint8>()
.asTypedList(ledgerRequestLength);
if (ledgerRequestLength > 0) {
_ledgerKeepAlive?.cancel();
final Pointer<Uint8> emptyPointer = malloc<Uint8>(0);
monero.Wallet_setDeviceSendData(
ptr, emptyPointer.cast<UnsignedChar>(), 0);
wallet.setDeviceSendData(
emptyPointer.cast<UnsignedChar>(), 0);
malloc.free(emptyPointer);
_logLedgerCommand(ledgerRequest, false);
@ -45,8 +45,8 @@ void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) {
result.asTypedList(response.length)[i] = response[i];
}
monero.Wallet_setDeviceReceivedData(
ptr, result.cast<UnsignedChar>(), response.length);
wallet.setDeviceReceivedData(
result.cast<UnsignedChar>(), response.length);
malloc.free(result);
keepAlive(connection);
}

View file

@ -4,7 +4,7 @@ import 'package:cw_monero/api/wallet_manager.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/account.dart';
import 'package:cw_monero/api/account_list.dart' as account_list;
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/monero.dart';
part 'monero_account_list.g.dart';
@ -50,32 +50,32 @@ abstract class MoneroAccountListBase with Store {
List<Account> getAll() {
final allAccounts = account_list.getAllAccount();
final currentCount = allAccounts.length;
cachedAccounts[account_list.wptr!.address] ??= [];
cachedAccounts[account_list.currentWallet!.ffiAddress()] ??= [];
if (cachedAccounts[account_list.wptr!.address]!.length == currentCount) {
return cachedAccounts[account_list.wptr!.address]!;
if (cachedAccounts[account_list.currentWallet!.ffiAddress()]!.length == currentCount) {
return cachedAccounts[account_list.currentWallet!.ffiAddress()]!;
}
cachedAccounts[account_list.wptr!.address] = allAccounts.map((accountRow) {
final balance = monero.SubaddressAccountRow_getUnlockedBalance(accountRow);
cachedAccounts[account_list.currentWallet!.ffiAddress()] = allAccounts.map((accountRow) {
final balance = accountRow.getUnlockedBalance();
return Account(
id: monero.SubaddressAccountRow_getRowId(accountRow),
label: monero.SubaddressAccountRow_getLabel(accountRow),
balance: moneroAmountToString(amount: monero.Wallet_amountFromString(balance)),
id: accountRow.getRowId(),
label: accountRow.getLabel(),
balance: moneroAmountToString(amount: account_list.currentWallet!.amountFromString(balance)),
);
}).toList();
return cachedAccounts[account_list.wptr!.address]!;
return cachedAccounts[account_list.currentWallet!.ffiAddress()]!;
}
Future<void> addAccount({required String label}) async {
await account_list.addAccount(label: label);
void addAccount({required String label}) {
account_list.addAccount(label: label);
update();
}
Future<void> setLabelAccount({required int accountIndex, required String label}) async {
await account_list.setLabelForAccount(accountIndex: accountIndex, label: label);
void setLabelAccount({required int accountIndex, required String label}) {
account_list.setLabelForAccount(accountIndex: accountIndex, label: label);
update();
}

View file

@ -1,7 +1,7 @@
import 'package:cw_core/unspent_transaction_output.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_monero/api/coins_info.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/monero.dart';
class MoneroUnspent extends Unspent {
static Future<MoneroUnspent> fromUnspent(String address, String hash, String keyImage, int value, bool isFrozen, bool isUnlocked) async {

View file

@ -39,6 +39,7 @@ import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:mobx/mobx.dart';
import 'package:monero/src/monero.dart' as m;
import 'package:monero/monero.dart' as monero;
part 'monero_wallet.g.dart';
@ -84,7 +85,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
monero_wallet.getUnlockedBalance(accountIndex: account.id))
});
_updateSubAddress(isEnabledAutoGenerateSubaddress, account: account);
_askForUpdateTransactionHistory();
unawaited(updateTransactions());
});
reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
@ -139,7 +140,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
passphrase: monero_wallet.getPassphrase());
int? get restoreHeight =>
transactionHistory.transactions.values.firstOrNull?.height ?? monero.Wallet_getRefreshFromBlockHeight(wptr!);
transactionHistory.transactions.values.firstOrNull?.height ?? currentWallet?.getRefreshFromBlockHeight();
monero_wallet.SyncListener? _listener;
ReactionDisposer? _onAccountChangeReaction;
@ -169,7 +170,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
if (monero_wallet.getCurrentHeight() <= 1) {
monero_wallet.setRefreshFromBlockHeight(
height: walletInfo.restoreHeight);
setupBackgroundSync(password, wptr!);
setupBackgroundSync(password, currentWallet!);
}
}
@ -189,14 +190,23 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
final currentWalletDirPath = await pathForWalletDir(name: name, type: type);
if (openedWalletsByPath["$currentWalletDirPath/$name"] != null) {
printV("closing wallet");
final wmaddr = wmPtr.address;
final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.address;
final wmaddr = wmPtr.ffiAddress();
final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.ffiAddress();
openedWalletsByPath.remove("$currentWalletDirPath/$name");
if (Platform.isWindows) {
await Isolate.run(() {
monero.WalletManager_closeWallet(
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
});
openedWalletsByPath.remove("$currentWalletDirPath/$name");
wptr = null;
} else {
unawaited(Isolate.run(() {
monero.WalletManager_closeWallet(
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
}));
}
currentWallet = null;
printV("wallet closed");
}
}
@ -211,7 +221,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
await monero_wallet.setupNode(
await monero_wallet.setupNodeSync(
address: node.uri.toString(),
login: node.login,
password: node.password,
@ -237,10 +247,10 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
isBackgroundSyncRunning = true;
await save();
monero.Wallet_startBackgroundSync(wptr!);
final status = monero.Wallet_status(wptr!);
currentWallet!.startBackgroundSync();
final status = currentWallet!.status();
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
final err = currentWallet!.errorString();
isBackgroundSyncRunning = false;
printV("startBackgroundSync: $err");
}
@ -256,8 +266,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
Future<void> stopSync() async {
if (isBackgroundSyncRunning) {
printV("Stopping background sync");
await save();
monero.Wallet_stopBackgroundSync(wptr!, '');
currentWallet!.store();
currentWallet!.stopBackgroundSync('');
currentWallet!.store();
isBackgroundSyncRunning = false;
}
await save();
@ -268,9 +279,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
Future<void> stopBackgroundSync(String password) async {
if (isBackgroundSyncRunning) {
printV("Stopping background sync");
await save();
monero.Wallet_stopBackgroundSync(wptr!, password);
await save();
currentWallet!.store();
currentWallet!.stopBackgroundSync(password);
currentWallet!.store();
isBackgroundSyncRunning = false;
}
}
@ -307,44 +318,44 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
Future<bool> submitTransactionUR(String ur) async {
final retStatus = monero.Wallet_submitTransactionUR(wptr!, ur);
final status = monero.Wallet_status(wptr!);
final retStatus = currentWallet!.submitTransactionUR(ur);
final status = currentWallet!.status();
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
final err = currentWallet!.errorString();
throw MoneroTransactionCreationException("unable to broadcast signed transaction: $err");
}
return retStatus;
}
bool importKeyImagesUR(String ur) {
final retStatus = monero.Wallet_importKeyImagesUR(wptr!, ur);
final status = monero.Wallet_status(wptr!);
final retStatus = currentWallet!.importKeyImagesUR(ur);
final status = currentWallet!.status();
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
final err = currentWallet!.errorString();
throw Exception("unable to import key images: $err");
}
return retStatus;
}
String exportOutputsUR(bool all) {
final str = monero.Wallet_exportOutputsUR(wptr!, all: all);
final status = monero.Wallet_status(wptr!);
final str = currentWallet!.exportOutputsUR(all: all);
final status = currentWallet!.status();
if (status != 0) {
final err = monero.Wallet_errorString(wptr!);
final err = currentWallet!.errorString();
throw MoneroTransactionCreationException("unable to export UR: $err");
}
return str;
}
bool needExportOutputs(int amount) {
if (int.tryParse(monero.Wallet_secretSpendKey(wptr!)) != 0) {
if (int.tryParse(currentWallet!.secretSpendKey()) != 0) {
return false;
}
// viewOnlyBalance - balance that we can spend
// TODO(mrcyjanek): remove hasUnknownKeyImages when we cleanup coin control
return (monero.Wallet_viewOnlyBalance(wptr!,
return (currentWallet!.viewOnlyBalance(
accountIndex: walletAddresses.account!.id) < amount) ||
monero.Wallet_hasUnknownKeyImages(wptr!);
currentWallet!.hasUnknownKeyImages();
}
@override
@ -424,12 +435,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
if (inputs.isEmpty) MoneroTransactionCreationException(
'No inputs selected');
pendingTransactionDescription =
await transaction_history.createTransaction(
await transaction_history.createTransactionSync(
address: address!,
amount: amount,
priorityRaw: _credentials.priority.serialize(),
accountIndex: walletAddresses.account!.id,
preferredInputs: inputs);
preferredInputs: inputs,
paymentId: '');
}
// final status = monero.PendingTransaction_status(pendingTransactionDescription);
@ -484,14 +496,25 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
final currentWalletDirPath = await pathForWalletDir(name: name, type: type);
if (openedWalletsByPath["$currentWalletDirPath/$name"] != null) {
// NOTE: this is realistically only required on windows.
// That's why we await it only on that platform - other platforms actually understand
// the concept of a file properly...
printV("closing wallet");
final wmaddr = wmPtr.address;
final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.address;
final wmaddr = wmPtr.ffiAddress();
final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.ffiAddress();
openedWalletsByPath.remove("$currentWalletDirPath/$name");
if (Platform.isWindows) {
await Isolate.run(() {
monero.WalletManager_closeWallet(
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
});
openedWalletsByPath.remove("$currentWalletDirPath/$name");
} else {
unawaited(Isolate.run(() {
monero.WalletManager_closeWallet(
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
}));
}
printV("wallet closed");
}
try {
@ -500,32 +523,33 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
Directory(await pathForWalletDir(name: name, type: type));
final newWalletDirPath =
await pathForWalletDir(name: newWalletName, type: type);
await currentWalletDir.rename(newWalletDirPath);
// -- use new waller folder to rename files with old names still --
final renamedWalletPath = newWalletDirPath + '/$name';
// Create new directory if it doesn't exist
await Directory(newWalletDirPath).create(recursive: true);
final currentCacheFile = File(renamedWalletPath);
final currentKeysFile = File('$renamedWalletPath.keys');
final currentAddressListFile = File('$renamedWalletPath.address.txt');
final backgroundSyncFile = File('$renamedWalletPath.background');
// -- use new waller folder to copy files with old names still --
final currentWalletPath = currentWalletDir.path + '/$name';
final newWalletPath =
await pathForWallet(name: newWalletName, type: type);
final currentCacheFile = File(currentWalletPath);
final currentKeysFile = File('$currentWalletPath.keys');
final currentAddressListFile = File('$currentWalletPath.address.txt');
final backgroundSyncFile = File('$currentWalletPath.background');
if (currentCacheFile.existsSync()) {
await currentCacheFile.rename(newWalletPath);
await currentCacheFile.copy("${newWalletDirPath}/$newWalletName");
}
if (currentKeysFile.existsSync()) {
await currentKeysFile.rename('$newWalletPath.keys');
await currentKeysFile.copy("${newWalletDirPath}/$newWalletName.keys");
}
if (currentAddressListFile.existsSync()) {
await currentAddressListFile.rename('$newWalletPath.address.txt');
await currentAddressListFile.copy("${newWalletDirPath}/$newWalletName.address.txt");
}
if (backgroundSyncFile.existsSync()) {
await backgroundSyncFile.rename('$newWalletPath.background');
await backgroundSyncFile.copy("${newWalletDirPath}/$newWalletName.background");
}
await currentWalletDir.delete(recursive: true);
await backupWalletFiles(newWalletName);
} catch (e) {
final currentWalletPath = await pathForWallet(name: name, type: type);
@ -571,12 +595,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
walletInfo.restoreHeight = height;
walletInfo.isRecovery = true;
monero_wallet.setRefreshFromBlockHeight(height: height);
setupBackgroundSync(password, wptr!);
setupBackgroundSync(password, currentWallet!);
monero_wallet.rescanBlockchainAsync();
await startSync();
_askForUpdateBalance();
walletAddresses.accountList.update();
await _askForUpdateTransactionHistory();
await updateTransactions();
await save();
await walletInfo.save();
}
@ -590,15 +614,15 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
final coinCount = await countOfCoins();
for (var i = 0; i < coinCount; i++) {
final coin = await getCoin(i);
final coinSpent = monero.CoinsInfo_spent(coin);
if (coinSpent == false && monero.CoinsInfo_subaddrAccount(coin) == walletAddresses.account!.id) {
final coinSpent = coin.spent();
if (coinSpent == false && coin.subaddrAccount() == walletAddresses.account!.id) {
final unspent = await MoneroUnspent.fromUnspent(
monero.CoinsInfo_address(coin),
monero.CoinsInfo_hash(coin),
monero.CoinsInfo_keyImage(coin),
monero.CoinsInfo_amount(coin),
monero.CoinsInfo_frozen(coin),
monero.CoinsInfo_unlocked(coin),
coin.address(),
coin.hash(),
coin.keyImage(),
coin.amount(),
coin.frozen(),
coin.unlocked(),
);
// TODO: double-check the logic here
if (unspent.hash.isNotEmpty) {
@ -703,6 +727,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
acc[tx.id] = tx;
return acc;
});
// This is needed to update the transaction history when new transaction is made.
unawaited(updateTransactions());
return resp;
}
@ -791,7 +817,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
monero_wallet.setRecoveringFromSeed(isRecovery: true);
monero_wallet.setRefreshFromBlockHeight(height: height);
setupBackgroundSync(password, wptr!);
setupBackgroundSync(password, currentWallet!);
}
int _getHeightDistance(DateTime date) {
@ -830,9 +856,6 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
}
Future<void> _askForUpdateTransactionHistory() async =>
await updateTransactions();
int _getUnlockedBalance() => monero_wallet.getUnlockedBalance(
accountIndex: walletAddresses.account!.id);
@ -851,13 +874,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
printV("onNewBlock: $height, $blocksLeft, $ptc");
try {
if (walletInfo.isRecovery) {
await _askForUpdateTransactionHistory();
await updateTransactions();
_askForUpdateBalance();
walletAddresses.accountList.update();
}
if (blocksLeft < 100) {
await _askForUpdateTransactionHistory();
await updateTransactions();
_askForUpdateBalance();
walletAddresses.accountList.update();
syncStatus = SyncedSyncStatus();
@ -880,7 +903,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
void _onNewTransaction() async {
try {
await _askForUpdateTransactionHistory();
await updateTransactions();
_askForUpdateBalance();
await Future<void>.delayed(Duration(seconds: 1));
} catch (e) {
@ -916,8 +939,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
void setLedgerConnection(LedgerConnection connection) {
final dummyWPtr = wptr ??
monero.WalletManager_openWallet(wmPtr, path: '', password: '');
final dummyWPtr = createWalletPointer();
enableLedgerExchange(dummyWPtr, connection);
}

View file

@ -130,8 +130,8 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
final transactions = _moneroTransactionHistory.transactions.values.toList();
transactions.forEach((element) {
final accountIndex = element.accountIndex;
final addressIndex = element.addressIndex;
final accountIndex = element.additionalInfo['accountIndex'] as int? ?? 0;
final addressIndex = element.additionalInfo['addressIndex'] as int? ?? 0;
usedAddresses.add(getAddress(accountIndex: accountIndex, addressIndex: addressIndex));
});
}

View file

@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:collection/collection.dart';
import 'package:cw_core/get_height_by_date.dart';
@ -20,6 +22,7 @@ import 'package:cw_monero/ledger.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:monero/src/monero.dart' as m;
import 'package:monero/monero.dart' as monero;
import 'package:polyseed/polyseed.dart';
@ -139,7 +142,7 @@ class MoneroWalletService extends WalletService<
overrideHeight: heightOverride, passphrase: credentials.passphrase);
}
await monero_wallet_manager.createWallet(
monero_wallet_manager.createWallet(
path: path,
password: credentials.password!,
language: credentials.language,
@ -179,7 +182,7 @@ class MoneroWalletService extends WalletService<
if (walletFilesExist(path)) await repairOldAndroidWallet(name);
await monero_wallet_manager
.openWalletAsync({'path': path, 'password': password});
.openWallet(path: path, password: password);
final walletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = MoneroWallet(
@ -217,13 +220,23 @@ class MoneroWalletService extends WalletService<
if (openedWalletsByPath["$path/$wallet"] != null) {
// NOTE: this is realistically only required on windows.
printV("closing wallet");
final wmaddr = wmPtr.address;
final waddr = openedWalletsByPath["$path/$wallet"]!.address;
// await Isolate.run(() {
monero.WalletManager_closeWallet(
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), false);
// });
final w = openedWalletsByPath["$path/$wallet"]!;
final wmaddr = wmPtr.ffiAddress();
final waddr = w.ffiAddress();
openedWalletsByPath.remove("$path/$wallet");
if (Platform.isWindows) {
await Isolate.run(() {
monero.WalletManager_closeWallet(
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
});
} else {
unawaited(Isolate.run(() {
monero.WalletManager_closeWallet(
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
}));
}
printV("wallet closed");
}
@ -263,7 +276,7 @@ class MoneroWalletService extends WalletService<
{bool? isTestnet}) async {
try {
final path = await pathForWallet(name: credentials.name, type: getType());
await monero_wallet_manager.restoreFromKeys(
monero_wallet_manager.restoreWalletFromKeys(
path: path,
password: credentials.password!,
language: credentials.language,
@ -293,9 +306,13 @@ class MoneroWalletService extends WalletService<
final password = credentials.password;
final height = credentials.height;
if (wptr == null) monero_wallet_manager.createWalletPointer();
if (currentWallet == null) {
final tmpWptr = monero_wallet_manager.createWalletPointer();
enableLedgerExchange(tmpWptr, credentials.ledgerConnection);
} else {
enableLedgerExchange(currentWallet!, credentials.ledgerConnection);
}
enableLedgerExchange(wptr!, credentials.ledgerConnection);
await monero_wallet_manager.restoreWalletFromHardwareWallet(
path: path,
password: password!,
@ -352,7 +369,7 @@ class MoneroWalletService extends WalletService<
try {
final path = await pathForWallet(name: credentials.name, type: getType());
monero_wallet_manager.restoreFromSeed(
monero_wallet_manager.restoreWalletFromSeedSync(
path: path,
password: credentials.password!,
passphrase: credentials.passphrase,
@ -393,7 +410,7 @@ class MoneroWalletService extends WalletService<
walletInfo.isRecovery = true;
walletInfo.restoreHeight = height;
monero_wallet_manager.restoreFromSeed(
monero_wallet_manager.restoreWalletFromSeedSync(
path: path,
password: password,
passphrase: '',
@ -401,12 +418,12 @@ class MoneroWalletService extends WalletService<
restoreHeight: height,
);
monero.Wallet_setCacheAttribute(wptr!,
currentWallet!.setCacheAttribute(
key: "cakewallet.seed.bip39", value: mnemonic);
monero.Wallet_setCacheAttribute(wptr!,
currentWallet!.setCacheAttribute(
key: "cakewallet.passphrase", value: passphrase ?? '');
monero.Wallet_store(wptr!);
currentWallet!.store();
final wallet = MoneroWallet(
walletInfo: walletInfo,
@ -472,7 +489,7 @@ class MoneroWalletService extends WalletService<
walletInfo.isRecovery = true;
walletInfo.restoreHeight = height;
await monero_wallet_manager.restoreFromSpendKey(
monero_wallet_manager.restoreWalletFromSpendKeySync(
path: path,
password: password,
seed: seed,
@ -481,8 +498,8 @@ class MoneroWalletService extends WalletService<
spendKey: spendKey);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??'');
currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed);
currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase??'');
final wallet = MoneroWallet(
walletInfo: walletInfo,
@ -529,7 +546,7 @@ class MoneroWalletService extends WalletService<
if (walletFilesExist(path)) await repairOldAndroidWallet(name);
await monero_wallet_manager
.openWalletAsync({'path': path, 'password': password});
.openWallet(path: path, password: password);
final walletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = MoneroWallet(

View file

@ -573,8 +573,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: "84e52393e395d75f449bcd81e23028889538118f"
resolved-ref: "84e52393e395d75f449bcd81e23028889538118f"
ref: b335585a7fb94b315eb52bd88f2da6d3489fa508
resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
@ -978,4 +978,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -27,7 +27,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: 84e52393e395d75f449bcd81e23028889538118f
ref: b335585a7fb94b315eb52bd88f2da6d3489fa508
path: impls/monero.dart
mutex: ^3.1.0
ledger_flutter_plus: ^1.4.1

View file

@ -946,4 +946,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -9,7 +9,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath 'com.android.tools.build:gradle:8.7.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View file

@ -171,6 +171,53 @@ class SolanaWalletClient {
if (programId == SystemProgramConst.programId) {
// For native solana transactions
if (txResponse.version == TransactionType.legacy) {
// For legacy transfers, the fee payer (index 0) is the sender.
sender = message.accountKeys[0].address;
final senderPreBalance = meta.preBalances[0];
final senderPostBalance = meta.postBalances[0];
final feeForTx = fee / SolanaUtils.lamportsPerSol;
// The loss on the sender's account would include both the transfer amount and the fee.
// So we would subtract the fee to calculate the actual amount that was transferred (in lamports).
final transferLamports = (senderPreBalance - senderPostBalance) - BigInt.from(fee);
// Next, we attempt to find the receiver by comparing the balance changes.
// (The index 0 is for the sender so we skip it.)
bool foundReceiver = false;
for (int i = 1; i < meta.preBalances.length; i++) {
// The increase in balance on the receiver account should correspond to the transfer amount we calculated earlieer.
final pre = meta.preBalances[i];
final post = meta.postBalances[i];
if ((post - pre) == transferLamports) {
receiver = message.accountKeys[i].address;
foundReceiver = true;
break;
}
}
if (!foundReceiver) {
// Optionally (and rarely), if no account shows the exact expected change,
// we set the receiver address to unknown.
receiver = "unknown";
}
final amount = transferLamports / BigInt.from(1e9);
return SolanaTransactionModel(
isOutgoingTx: sender == walletAddress,
from: sender,
to: receiver,
id: signature,
amount: amount.abs(),
programId: SystemProgramConst.programId.address,
tokenSymbol: 'SOL',
blockTimeInInt: blockTime?.toInt() ?? 0,
fee: feeForTx,
);
} else {
if (instruction.accounts.length < 2) continue;
final senderIndex = instruction.accounts[0];
final receiverIndex = instruction.accounts[1];
@ -202,6 +249,7 @@ class SolanaWalletClient {
blockTimeInInt: blockTime?.toInt() ?? 0,
fee: feeForTx,
);
}
} else if (programId == SPLTokenProgramConst.tokenProgramId) {
// For SPL Token transactions
if (instruction.accounts.length < 2) continue;
@ -842,6 +890,7 @@ class SolanaWalletClient {
}) async {
/// Sign the transaction with the owner's private key.
final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage());
transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature);
/// Serialize the transaction.

View file

@ -428,7 +428,7 @@ class TronClient {
if (!request.isSuccess) {
log("Tron TRC20 error: ${request.error} \n ${request.respose}");
throw Exception(
'An error occured while creating the transfer request. Please try again.',
'An error occurred while creating the transfer request. Please try again.',
);
}

View file

@ -480,8 +480,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: "84e52393e395d75f449bcd81e23028889538118f"
resolved-ref: "84e52393e395d75f449bcd81e23028889538118f"
ref: b335585a7fb94b315eb52bd88f2da6d3489fa508
resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
@ -845,4 +845,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -25,7 +25,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: 84e52393e395d75f449bcd81e23028889538118f # monero_c hash
ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 # monero_c hash
path: impls/monero.dart
mutex: ^3.1.0

View file

@ -1,5 +1,5 @@
class Destination {
final BigInt amount; // transfered as string
final BigInt amount; // transferred as string
final String address;
final String assetId;

View file

@ -1,7 +1,6 @@
import 'dart:math';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_zano/zano_wallet_api.dart';
import 'package:decimal/decimal.dart';
import 'package:decimal/intl.dart';
import 'package:fluttertoast/fluttertoast.dart';
@ -57,7 +56,9 @@ class ZanoFormatter {
static int parseAmount(String amount, [int decimalPoint = defaultDecimalPoint]) {
final resultBigInt = (Decimal.parse(amount) * Decimal.fromBigInt(BigInt.from(10).pow(decimalPoint))).toBigInt();
if (!resultBigInt.isValidInt) {
try {
Fluttertoast.showToast(msg: 'Cannot transfer $amount. Maximum is ${intAmountToString(resultBigInt.toInt(), decimalPoint)}.');
} catch (_) {}
}
return resultBigInt.toInt();
}

View file

@ -485,8 +485,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: "84e52393e395d75f449bcd81e23028889538118f"
resolved-ref: "84e52393e395d75f449bcd81e23028889538118f"
ref: b335585a7fb94b315eb52bd88f2da6d3489fa508
resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
@ -842,4 +842,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View file

@ -26,7 +26,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: 84e52393e395d75f449bcd81e23028889538118f # monero_c hash
ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 # monero_c hash
path: impls/monero.dart
dev_dependencies:
flutter_test:

View file

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
platform :ios, '13.0'
source 'https://github.com/CocoaPods/Specs.git'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
@ -43,7 +43,7 @@ post_install do |installer|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',

View file

@ -1,7 +1,6 @@
PODS:
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- CryptoSwift (1.8.4)
- cw_decred (0.0.1):
- Flutter
@ -59,6 +58,8 @@ PODS:
- OrderedSet (~> 6.0.3)
- flutter_local_authentication (1.2.0):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_mailer (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
@ -77,7 +78,9 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- ReachabilitySwift (5.2.4)
- reown_yttrium (0.0.1):
- Flutter
- YttriumWrapper (= 0.8.35)
- SDWebImage (5.20.0):
- SDWebImage/Core (= 5.20.0)
- SDWebImage/Core (5.20.0)
@ -100,6 +103,7 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- YttriumWrapper (0.8.35)
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
@ -114,6 +118,7 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
@ -122,6 +127,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- reown_yttrium (from `.symlinks/plugins/reown_yttrium/ios`)
- sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@ -137,9 +143,9 @@ SPEC REPOS:
- DKImagePickerController
- DKPhotoGallery
- OrderedSet
- ReachabilitySwift
- SDWebImage
- SwiftyGif
- YttriumWrapper
EXTERNAL SOURCES:
connectivity_plus:
@ -164,6 +170,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_local_authentication:
:path: ".symlinks/plugins/flutter_local_authentication/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_mailer:
:path: ".symlinks/plugins/flutter_mailer/ios"
flutter_secure_storage:
@ -180,6 +188,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
reown_yttrium:
:path: ".symlinks/plugins/reown_yttrium/ios"
sensitive_clipboard:
:path: ".symlinks/plugins/sensitive_clipboard/ios"
share_plus:
@ -233,6 +243,6 @@ SPEC CHECKSUMS:
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: e448f662d4c41f0c0b1ccbb78afd57dbf895a597
PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
COCOAPODS: 1.16.2

View file

@ -505,7 +505,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -653,7 +653,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -694,7 +694,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View file

@ -109,9 +109,12 @@ class CWBitcoin extends Bitcoin {
required TransactionPriority priority,
int? feeRate,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
String? payjoinUri,
}) {
final bitcoinFeeRate =
priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
priority == BitcoinTransactionPriority.custom && feeRate != null
? feeRate
: null;
return BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
@ -128,7 +131,7 @@ class CWBitcoin extends Bitcoin {
priority: priority as BitcoinTransactionPriority,
feeRate: bitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
payjoinUri: payjoinUri);
}
@override
@ -224,9 +227,14 @@ class CWBitcoin extends Bitcoin {
await bitcoinWallet.updateAllUnspents();
}
WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
WalletService createBitcoinWalletService(
Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource,
Box<PayjoinSession> payjoinSessionSource,
bool alwaysScan,
bool isDirect) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource,
payjoinSessionSource, alwaysScan, isDirect);
}
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
@ -550,6 +558,10 @@ class CWBitcoin extends Bitcoin {
return option is BitcoinReceivePageOption;
}
@override
bool isPayjoinAvailable(Object wallet) =>
(wallet is BitcoinWallet) && (wallet as BitcoinWallet).isPayjoinAvailable;
@override
BitcoinAddressType getOptionToType(ReceivePageOption option) {
return (option as BitcoinReceivePageOption).toType();
@ -706,4 +718,34 @@ class CWBitcoin extends Bitcoin {
return null;
}
}
@override
String getPayjoinEndpoint(Object wallet) {
final _wallet = wallet as ElectrumWallet;
if (!isPayjoinAvailable(wallet)) return '';
return (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint ?? '';
}
@override
void updatePayjoinState(Object wallet, bool value) {
final _wallet = wallet as ElectrumWallet;
if (value) {
(_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
} else {
stopPayjoinSessions(wallet);
}
}
@override
void resumePayjoinSessions(Object wallet) {
final _wallet = wallet as ElectrumWallet;
(_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
}
@override
void stopPayjoinSessions(Object wallet) {
final _wallet = wallet as ElectrumWallet;
(_wallet.walletAddresses as BitcoinWalletAddresses).payjoinManager.cleanupSessions();
(_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null;
}
}

View file

@ -11,7 +11,6 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
@ -61,9 +60,15 @@ class OnRamperBuyProvider extends BuyProvider {
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
final params = {'type': isBuyAction ? 'buy' : 'sell'};
final normalizedCryptoCurrency =
cryptoCurrency.title + _getNormalizeNetwork(cryptoCurrency);
final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params);
final sourceCurrency = (isBuyAction ? fiatCurrency : normalizedCryptoCurrency).toLowerCase();
final destinationCurrency = (isBuyAction ? normalizedCryptoCurrency : fiatCurrency).toLowerCase();
final params = {'type': isBuyAction ? 'buy' : 'sell', 'destination' : destinationCurrency};
final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$sourceCurrency', params);
try {
final response =
@ -76,7 +81,9 @@ class OnRamperBuyProvider extends BuyProvider {
.map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
.toList();
} else {
printV('Failed to fetch available payment types');
final responseBody =
jsonDecode(response.body) as Map<String, dynamic>;
printV('Failed to fetch available payment types: ${responseBody['message']}');
return [];
}
} catch (e) {
@ -242,7 +249,7 @@ class OnRamperBuyProvider extends BuyProvider {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch URL');
throw Exception('Could not launch URL ${uri.toString()}');
}
}
@ -276,7 +283,7 @@ class OnRamperBuyProvider extends BuyProvider {
if (currency.tag != null) return '_' + _tagToNetwork(currency.tag!);
return '_' + (currency.fullName?.replaceAll(' ', '') ?? currency.title);;
return '_' + (currency.fullName?.replaceAll(' ', '') ?? currency.title);
}
String? normalizePaymentMethod(PaymentType paymentType) {

View file

@ -1,26 +1,101 @@
import 'dart:async';
import 'dart:math';
import 'dart:io';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
class BackgroundSync {
final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
bool _isInitialized = false;
Future<void> _initializeNotifications() async {
if (_isInitialized) return;
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initializationSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _notificationsPlugin.initialize(initializationSettings);
_isInitialized = true;
}
Future<bool> requestPermissions() async {
if (Platform.isIOS || Platform.isMacOS) {
return await _notificationsPlugin
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
) ?? false;
} else if (Platform.isAndroid) {
return await _notificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.areNotificationsEnabled() ?? false;
}
return false;
}
Future<void> showNotification(String title, String content) async {
await _initializeNotifications();
final hasPermission = await requestPermissions();
if (!hasPermission) {
printV('Notification permissions not granted');
return;
}
const androidDetails = AndroidNotificationDetails(
'transactions',
'Transactions',
channelDescription: 'Channel for notifications about transactions',
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
);
const iosDetails = DarwinNotificationDetails();
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _notificationsPlugin.show(
DateTime.now().millisecondsSinceEpoch.hashCode,
title,
content,
notificationDetails,
);
}
Future<void> sync() async {
printV("Background sync started");
await _syncMonero();
await _syncWallets();
printV("Background sync completed");
}
Future<void> _syncMonero() async {
Future<void> _syncWallets() async {
final walletLoadingService = getIt.get<WalletLoadingService>();
final walletListViewModel = getIt.get<WalletListViewModel>();
final settingsStore = getIt.get<SettingsStore>();
@ -28,10 +103,10 @@ class BackgroundSync {
final List<WalletListItem> moneroWallets = walletListViewModel.wallets
.where((element) => !element.isHardware)
.where((element) => [WalletType.monero].contains(element.type))
.where((element) => ![WalletType.haven, WalletType.decred].contains(element.type))
.toList();
for (int i = 0; i < moneroWallets.length; i++) {
final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name);
final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name, isBackground: true);
int syncedTicks = 0;
final keyService = getIt.get<KeyService>();
@ -75,7 +150,7 @@ class BackgroundSync {
} else {
syncedTicks = 0;
}
if (kDebugMode) {
if (FeatureFlag.hasDevOptions) {
if (syncStatus is SyncingSyncStatus) {
final blocksLeft = syncStatus.blocksLeft;
printV("$blocksLeft Blocks Left");
@ -100,6 +175,27 @@ class BackgroundSync {
}
}
}
final txs = wallet.transactionHistory;
final sortedTxs = txs.transactions.values.toList()..sort((a, b) => a.date.compareTo(b.date));
final sharedPreferences = await SharedPreferences.getInstance();
for (final tx in sortedTxs) {
final lastTriggerString = sharedPreferences.getString(PreferencesKey.backgroundSyncLastTrigger(wallet.name));
final lastTriggerDate = lastTriggerString != null
? DateTime.parse(lastTriggerString)
: DateTime.now();
final keys = sharedPreferences.getKeys();
if (tx.date.isBefore(lastTriggerDate)) {
printV("w: ${wallet.name}, tx: ${tx.date} is before $lastTriggerDate (lastTriggerString: $lastTriggerString) (k: ${keys.length})");
continue;
}
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), tx.date.add(Duration(minutes: 1)).toIso8601String());
final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
await showNotification("$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amountFormatted()}");
}
printV("${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}");
}
wallet.id;
await wallet.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name));
await wallet.close(shouldCleanup: true);
}

View file

@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/get_encryption_key.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
@ -164,6 +163,31 @@ class $BackupService {
}
final data = json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>;
try { // shouldn't throw an error but just in case, so it doesn't stop the backup restore
for (var entry in data.entries) {
String key = entry.key;
dynamic value = entry.value;
// Check the type of the value and save accordingly
if (value is String) {
await sharedPreferences.setString(key, value);
} else if (value is int) {
await sharedPreferences.setInt(key, value);
} else if (value is double) {
await sharedPreferences.setDouble(key, value);
} else if (value is bool) {
await sharedPreferences.setBool(key, value);
} else if (value is List<String>) {
await sharedPreferences.setStringList(key, value);
} else {
if (kDebugMode) {
printV('Skipping individual save for key "$key": Unsupported type (${value.runtimeType}). Value: $value');
}
}
}
} catch (_) {}
String currentWalletName = data[PreferencesKey.currentWalletName] as String;
int currentWalletType = data[PreferencesKey.currentWalletType] as int;
@ -175,151 +199,10 @@ class $BackupService {
currentWalletType = serializeToInt(correctWallets.first.type);
}
final currentNodeId = data[PreferencesKey.currentNodeIdKey] as int?;
final currentBalanceDisplayMode = data[PreferencesKey.currentBalanceDisplayModeKey] as int?;
final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?;
final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?;
final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?;
final currentTransactionPriorityKeyLegacy =
data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
final currentBitcoinElectrumSererId =
data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?;
final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?;
final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?;
final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?;
final currentTheme = data[PreferencesKey.currentTheme] as int?;
final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?;
final currentDefaultSettingsMigrationVersion =
data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?;
final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?;
final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?;
final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?;
final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?;
final useEtherscan = data[PreferencesKey.useEtherscan] as bool?;
final defaultNanoRep = data[PreferencesKey.defaultNanoRep] as String?;
final defaultBananoRep = data[PreferencesKey.defaultBananoRep] as String?;
final lookupsTwitter = data[PreferencesKey.lookupsTwitter] as bool?;
final lookupsMastodon = data[PreferencesKey.lookupsMastodon] as bool?;
final lookupsYatService = data[PreferencesKey.lookupsYatService] as bool?;
final lookupsUnstoppableDomains = data[PreferencesKey.lookupsUnstoppableDomains] as bool?;
final lookupsOpenAlias = data[PreferencesKey.lookupsOpenAlias] as bool?;
final lookupsENS = data[PreferencesKey.lookupsENS] as bool?;
final lookupsWellKnown = data[PreferencesKey.lookupsWellKnown] as bool?;
final syncAll = data[PreferencesKey.syncAllKey] as bool?;
final syncMode = data[PreferencesKey.syncModeKey] as int?;
final autoGenerateSubaddressStatus =
data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?;
await sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
if (currentNodeId != null)
await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId);
if (currentBalanceDisplayMode != null)
await sharedPreferences.setInt(
PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode);
await sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType);
if (currentFiatCurrency != null)
await sharedPreferences.setString(
PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency);
if (shouldSaveRecipientAddress != null)
await sharedPreferences.setBool(
PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress);
if (isAppSecure != null)
await sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
if (disableTradeOption != null)
await sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption);
if (currentTransactionPriorityKeyLegacy != null)
await sharedPreferences.setInt(
PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy);
if (currentBitcoinElectrumSererId != null)
await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId);
if (currentLanguageCode != null)
await sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode);
if (displayActionListMode != null)
await sharedPreferences.setInt(
PreferencesKey.displayActionListModeKey, displayActionListMode);
if (fiatApiMode != null)
await sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode);
if (autoGenerateSubaddressStatus != null)
await sharedPreferences.setInt(
PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus);
if (currentTheme != null && DeviceInfo.instance.isMobile) {
await sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme);
// enforce dark theme on desktop platforms until the design is ready:
} else if (DeviceInfo.instance.isDesktop) {
if (DeviceInfo.instance.isDesktop) {
await sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw);
}
if (exchangeStatus != null)
await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus);
if (currentDefaultSettingsMigrationVersion != null)
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion,
currentDefaultSettingsMigrationVersion);
if (moneroTransactionPriority != null)
await sharedPreferences.setInt(
PreferencesKey.moneroTransactionPriority, moneroTransactionPriority);
if (bitcoinTransactionPriority != null)
await sharedPreferences.setInt(
PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority);
if (sortBalanceTokensBy != null)
await sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy);
if (pinNativeTokenAtTop != null)
await sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop);
if (useEtherscan != null)
await sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan);
if (defaultNanoRep != null)
await sharedPreferences.setString(PreferencesKey.defaultNanoRep, defaultNanoRep);
if (defaultBananoRep != null)
await sharedPreferences.setString(PreferencesKey.defaultBananoRep, defaultBananoRep);
if (syncAll != null) await sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll);
if (lookupsTwitter != null)
await sharedPreferences.setBool(PreferencesKey.lookupsTwitter, lookupsTwitter);
if (lookupsMastodon != null)
await sharedPreferences.setBool(PreferencesKey.lookupsMastodon, lookupsMastodon);
if (lookupsYatService != null)
await sharedPreferences.setBool(PreferencesKey.lookupsYatService, lookupsYatService);
if (lookupsUnstoppableDomains != null)
await sharedPreferences.setBool(
PreferencesKey.lookupsUnstoppableDomains, lookupsUnstoppableDomains);
if (lookupsOpenAlias != null)
await sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, lookupsOpenAlias);
if (lookupsENS != null) await sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS);
if (lookupsWellKnown != null)
await sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, lookupsWellKnown);
if (syncAll != null) await sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll);
if (syncMode != null) await sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode);
await preferencesFile.delete();
}
@ -368,6 +251,22 @@ class $BackupService {
await importWalletKeychainInfo(info);
});
for (var key in (keychainJSON['_all'] as Map<String, dynamic>).keys) {
try {
if (!key.startsWith('MONERO_WALLET_')) continue;
final decodedPassword = decodeWalletPassword(password: keychainJSON['_all'][key].toString());
final walletName = key.split('_WALLET_')[1];
final walletType = key.split('_WALLET_')[0].toLowerCase();
await importWalletKeychainInfo({
'name': walletName,
'type': "WalletType.$walletType",
'password': decodedPassword,
});
} catch (e) {
printV('Error importing wallet ($key) password: $e');
}
}
keychainDumpFile.deleteSync();
}
@ -378,83 +277,45 @@ class $BackupService {
await keyService.saveWalletPassword(walletName: name, password: password);
}
@Deprecated('Use v2 instead')
Future<Uint8List> _exportKeychainDumpV1(String password,
{required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async =>
throw Exception('Deprecated');
Future<Uint8List> exportKeychainDumpV2(String password,
{String keychainSalt = secrets.backupKeychainSalt}) async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final wallets = await Future.wait(walletInfoSource.values.map((walletInfo) async {
try {
return {
'name': walletInfo.name,
'type': walletInfo.type.toString(),
'password': await keyService.getWalletPassword(walletName: walletInfo.name)
};
} catch (e) {
return {
'name': walletInfo.name,
'type': walletInfo.type.toString(),
'password': ''
};
}
}));
final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
final backupPassword = await _secureStorage.read(key: backupPasswordKey);
final data = utf8.encode(
json.encode({'wallets': wallets, backupPasswordKey: backupPassword}));
json.encode({'wallets': wallets, backupPasswordKey: backupPassword, '_all': await _secureStorage.readAll()}));
final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password');
return encrypted;
}
static const List<String> _excludedPrefsKeys = [
PreferencesKey.currentPinLength,
PreferencesKey.showCameraConsent,
PreferencesKey.lastSeenAppVersion,
PreferencesKey.failedTotpTokenTrials,
];
Future<String> exportPreferencesJSON() async {
final preferences = <String, dynamic>{
PreferencesKey.currentWalletName:
sharedPreferences.getString(PreferencesKey.currentWalletName),
PreferencesKey.currentNodeIdKey: sharedPreferences.getInt(PreferencesKey.currentNodeIdKey),
PreferencesKey.currentBalanceDisplayModeKey:
sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey),
PreferencesKey.currentWalletType: sharedPreferences.getInt(PreferencesKey.currentWalletType),
PreferencesKey.currentFiatCurrencyKey:
sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
PreferencesKey.shouldSaveRecipientAddressKey:
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
PreferencesKey.disableTradeOption: sharedPreferences.getBool(PreferencesKey.disableTradeOption),
PreferencesKey.currentTransactionPriorityKeyLegacy:
sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
PreferencesKey.currentBitcoinElectrumSererIdKey:
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey),
PreferencesKey.currentLanguageCode:
sharedPreferences.getString(PreferencesKey.currentLanguageCode),
PreferencesKey.displayActionListModeKey:
sharedPreferences.getInt(PreferencesKey.displayActionListModeKey),
PreferencesKey.currentTheme: sharedPreferences.getInt(PreferencesKey.currentTheme),
PreferencesKey.exchangeStatusKey: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey),
PreferencesKey.currentDefaultSettingsMigrationVersion:
sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion),
PreferencesKey.bitcoinTransactionPriority:
sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority),
PreferencesKey.moneroTransactionPriority:
sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority),
PreferencesKey.currentFiatApiModeKey:
sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey),
PreferencesKey.sortBalanceBy: sharedPreferences.getInt(PreferencesKey.sortBalanceBy),
PreferencesKey.pinNativeTokenAtTop:
sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop),
PreferencesKey.useEtherscan: sharedPreferences.getBool(PreferencesKey.useEtherscan),
PreferencesKey.defaultNanoRep: sharedPreferences.getString(PreferencesKey.defaultNanoRep),
PreferencesKey.defaultBananoRep:
sharedPreferences.getString(PreferencesKey.defaultBananoRep),
PreferencesKey.lookupsTwitter: sharedPreferences.getBool(PreferencesKey.lookupsTwitter),
PreferencesKey.lookupsMastodon: sharedPreferences.getBool(PreferencesKey.lookupsMastodon),
PreferencesKey.lookupsYatService:
sharedPreferences.getBool(PreferencesKey.lookupsYatService),
PreferencesKey.lookupsUnstoppableDomains:
sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains),
PreferencesKey.lookupsOpenAlias: sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias),
PreferencesKey.lookupsENS: sharedPreferences.getBool(PreferencesKey.lookupsENS),
PreferencesKey.lookupsWellKnown:
sharedPreferences.getBool(PreferencesKey.lookupsWellKnown),
PreferencesKey.syncModeKey: sharedPreferences.getInt(PreferencesKey.syncModeKey),
PreferencesKey.syncAllKey: sharedPreferences.getBool(PreferencesKey.syncAllKey),
PreferencesKey.autoGenerateSubaddressStatusKey:
sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey),
};
final preferences = <String, dynamic>{};
sharedPreferences.getKeys().forEach((key) => preferences[key] = sharedPreferences.get(key));
_excludedPrefsKeys.forEach((key) => preferences.remove(key));
return json.encode(preferences);
}
@ -466,10 +327,6 @@ class $BackupService {
return Uint8List.fromList(bytes);
}
@Deprecated('Use v2 instead')
Future<Uint8List> _encryptV1(Uint8List data, String secretKeySource, String nonceBase64) async =>
throw Exception('Deprecated');
Future<Uint8List> _decryptV1(Uint8List data, String secretKeySource, String nonceBase64,
{int macLength = 16}) async {
final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource));

View file

@ -19,7 +19,7 @@ class NodePathValidator extends TextValidator {
}
// NodeAddressValidatorDecredBlankException allows decred to send a blank ip
// address which effectively clears the current set persistant peer.
// address which effectively clears the current set persistent peer.
class NodeAddressValidatorDecredBlankException extends TextValidator {
NodeAddressValidatorDecredBlankException()
: super(

View file

@ -1,5 +0,0 @@
abstract class ChainService {
String getNamespace();
String getChainId();
List<String> getEvents();
}

View file

@ -1,304 +0,0 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
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:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
import 'package:convert/convert.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:http/http.dart' as http;
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:web3dart/web3dart.dart';
import '../chain_service.dart';
import '../../wallet_connect_key_service.dart';
class EvmChainServiceImpl implements ChainService {
final AppStore appStore;
final BottomSheetService bottomSheetService;
final Web3Wallet wallet;
final WalletConnectKeyService wcKeyService;
static const namespace = 'eip155';
static const pSign = 'personal_sign';
static const eSign = 'eth_sign';
static const eSignTransaction = 'eth_signTransaction';
static const eSignTypedData = 'eth_signTypedData_v4';
static const eSendTransaction = 'eth_sendTransaction';
final EVMChainId reference;
final Web3Client ethClient;
EvmChainServiceImpl({
required this.reference,
required this.appStore,
required this.wcKeyService,
required this.bottomSheetService,
required this.wallet,
Web3Client? web3Client,
}) : ethClient = web3Client ??
Web3Client(
appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(),
http.Client(),
) {
for (final String event in getEvents()) {
wallet.registerEventEmitter(chainId: getChainId(), event: event);
}
wallet.registerRequestHandler(
chainId: getChainId(),
method: pSign,
handler: personalSign,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: eSign,
handler: ethSign,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: eSignTransaction,
handler: ethSignTransaction,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: eSendTransaction,
handler: ethSignTransaction,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: eSignTypedData,
handler: ethSignTypedData,
);
}
@override
String getNamespace() {
return namespace;
}
@override
String getChainId() {
return reference.chain();
}
@override
List<String> getEvents() {
return ['chainChanged', 'accountsChanged'];
}
Future<String?> requestAuthorization(String? text) async {
// Show the bottom sheet
final bool? isApproved = await bottomSheetService.queueBottomSheet(
widget: Web3RequestModal(
child: ConnectionWidget(
title: S.current.signTransaction,
info: [
ConnectionModel(
text: text,
),
],
),
),
) as bool?;
if (isApproved != null && isApproved == false) {
return 'User rejected signature';
}
return null;
}
Future<String> personalSign(String topic, dynamic parameters) async {
log('received personal sign request: $parameters');
final String message;
if (parameters[0] == null) {
message = '';
} else {
message = parameters[0].toString().utf8Message;
}
final String? authError = await requestAuthorization(message);
if (authError != null) {
return authError;
}
try {
// Load the private key
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
final String signature = hex.encode(
credentials.signPersonalMessageToUint8List(Uint8List.fromList(utf8.encode(message))),
);
return '0x$signature';
} catch (e) {
log(e.toString());
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: '${S.current.errorGettingCredentials} ${e.toString()}',
),
);
return 'Failed: Error while getting credentials';
}
}
Future<String> ethSign(String topic, dynamic parameters) async {
log('received eth sign request: $parameters');
final String message;
if (parameters[1] == null) {
message = '';
} else {
message = parameters[1].toString().utf8Message;
}
final String? authError = await requestAuthorization(message);
if (authError != null) {
return authError;
}
try {
// Load the private key
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
final EthPrivateKey credentials = EthPrivateKey.fromHex(keys[0].privateKey);
final String signature = hex.encode(
credentials.signPersonalMessageToUint8List(
Uint8List.fromList(utf8.encode(message)),
chainId: getChainIdBasedOnWalletType(appStore.wallet!.type),
),
);
log(signature);
return '0x$signature';
} catch (e) {
log('error: ${e.toString()}');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(message: '${S.current.error}: ${e.toString()}'),
);
return 'Failed';
}
}
Future<String> ethSignTransaction(String topic, dynamic parameters) async {
log('received eth sign transaction request: $parameters');
final paramsData = parameters[0] as Map<String, dynamic>;
final message = _convertToReadable(paramsData);
final String? authError = await requestAuthorization(message);
if (authError != null) {
return authError;
}
// Load the private key
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
WCEthereumTransactionModel ethTransaction =
WCEthereumTransactionModel.fromJson(parameters[0] as Map<String, dynamic>);
final transaction = Transaction(
from: EthereumAddress.fromHex(ethTransaction.from),
to: EthereumAddress.fromHex(ethTransaction.to),
maxGas: ethTransaction.gasLimit != null ? int.tryParse(ethTransaction.gasLimit ?? "") : null,
gasPrice: ethTransaction.gasPrice != null
? EtherAmount.inWei(BigInt.parse(ethTransaction.gasPrice ?? ""))
: null,
value: EtherAmount.inWei(BigInt.parse(ethTransaction.value)),
data: hexToBytes(ethTransaction.data ?? ""),
nonce: ethTransaction.nonce != null ? int.tryParse(ethTransaction.nonce ?? "") : null,
);
try {
final result = await ethClient.sendTransaction(
credentials,
transaction,
chainId: getChainIdBasedOnWalletType(appStore.wallet!.type),
);
log('Result: $result');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.awaitDAppProcessing,
isError: false,
),
);
return result;
} catch (e) {
log('An error has occurred while signing transaction: ${e.toString()}');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: '${S.current.errorSigningTransaction}: ${e.toString()}',
),
);
return 'Failed';
}
}
Future<String> ethSignTypedData(String topic, dynamic parameters) async {
log('received eth sign typed data request: $parameters');
final String? data = parameters[1] as String?;
final String? authError = await requestAuthorization(data);
if (authError != null) {
return authError;
}
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
return EthSigUtil.signTypedData(
privateKey: keys[0].privateKey,
jsonData: data ?? '',
version: TypedDataVersion.V4,
);
}
String _convertToReadable(Map<String, dynamic> data) {
final tokenName = getTokenNameBasedOnWalletType(appStore.wallet!.type);
String gas = int.parse((data['gas'] as String).substring(2), radix: 16).toString();
String value = data['value'] != null
? (int.parse((data['value'] as String).substring(2), radix: 16) / 1e18).toString() +
' $tokenName'
: '0 $tokenName';
String from = data['from'] as String;
String to = data['to'] as String;
return '''
Gas: $gas\n
Value: $value\n
From: $from\n
To: $to
''';
}
}

View file

@ -1,28 +0,0 @@
class SolanaSignMessage {
final String pubkey;
final String message;
SolanaSignMessage({
required this.pubkey,
required this.message,
});
factory SolanaSignMessage.fromJson(Map<String, dynamic> json) {
return SolanaSignMessage(
pubkey: json['pubkey'] as String,
message: json['message'] as String,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'pubkey': pubkey,
'message': message,
};
}
@override
String toString() {
return 'SolanaSignMessage(pubkey: $pubkey, message: $message)';
}
}

View file

@ -1,106 +0,0 @@
class SolanaSignTransaction {
final String? feePayer;
final String? recentBlockhash;
final String transaction;
final List<SolanaInstruction>? instructions;
SolanaSignTransaction({
required this.feePayer,
required this.recentBlockhash,
required this.instructions,
required this.transaction,
});
factory SolanaSignTransaction.fromJson(Map<String, dynamic> json) {
return SolanaSignTransaction(
feePayer:json['feePayer'] !=null ? json['feePayer'] as String: null,
recentBlockhash: json['recentBlockhash']!=null? json['recentBlockhash'] as String: null,
instructions:json['instructions']!=null? (json['instructions'] as List<dynamic>)
.map((e) => SolanaInstruction.fromJson(e as Map<String, dynamic>))
.toList(): null,
transaction: json['transaction'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'feePayer': feePayer,
'recentBlockhash': recentBlockhash,
'instructions': instructions,
'transaction': transaction,
};
}
@override
String toString() {
return 'SolanaSignTransaction(feePayer: $feePayer, recentBlockhash: $recentBlockhash, instructions: $instructions, transaction: $transaction)';
}
}
class SolanaInstruction {
final String programId;
final List<SolanaKeyMetadata> keys;
final List<int> data;
SolanaInstruction({
required this.programId,
required this.keys,
required this.data,
});
factory SolanaInstruction.fromJson(Map<String, dynamic> json) {
return SolanaInstruction(
programId: json['programId'] as String,
keys: (json['keys'] as List<dynamic>)
.map((e) => SolanaKeyMetadata.fromJson(e as Map<String, dynamic>))
.toList(),
data: (json['data'] as List<dynamic>).map((e) => e as int).toList(),
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'programId': programId,
'keys': keys,
'data': data,
};
}
@override
String toString() {
return 'SolanaInstruction(programId: $programId, keys: $keys, data: $data)';
}
}
class SolanaKeyMetadata {
final String pubkey;
final bool isSigner;
final bool isWritable;
SolanaKeyMetadata({
required this.pubkey,
required this.isSigner,
required this.isWritable,
});
factory SolanaKeyMetadata.fromJson(Map<String, dynamic> json) {
return SolanaKeyMetadata(
pubkey: json['pubkey'] as String,
isSigner: json['isSigner'] as bool,
isWritable: json['isWritable'] as bool,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'pubkey': pubkey,
'isSigner': isSigner,
'isWritable': isWritable,
};
}
@override
String toString() {
return 'SolanaKeyMetadata(pubkey: $pubkey, isSigner: $isSigner, isWritable: $isWritable)';
}
}

View file

@ -1,30 +0,0 @@
import 'solana_chain_service.dart';
enum SolanaChainId {
mainnet,
// testnet,
// devnet,
}
extension SolanaChainIdX on SolanaChainId {
String chain() {
String name = '';
switch (this) {
case SolanaChainId.mainnet:
name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ';
// solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
break;
// case SolanaChainId.devnet:
// name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
// // solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
// break;
// case SolanaChainId.testnet:
// name = '';
// // solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z
// break;
}
return '${SolanaChainServiceImpl.namespace}:$name';
}
}

View file

@ -1,170 +0,0 @@
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';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
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/solana_rpc_http_service.dart';
import 'package:cw_core/utils/print_verbose.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';
import 'entities/solana_sign_transaction.dart';
class SolanaChainServiceImpl implements ChainService {
final BottomSheetService bottomSheetService;
final Web3Wallet wallet;
final WalletConnectKeyService wcKeyService;
static const namespace = 'solana';
static const solSignTransaction = 'solana_signTransaction';
static const solSignMessage = 'solana_signMessage';
final SolanaChainId reference;
final SolanaRPC solanaProvider;
final SolanaPrivateKey? ownerPrivateKey;
SolanaChainServiceImpl({
required this.reference,
required this.wcKeyService,
required this.bottomSheetService,
required this.wallet,
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);
}
wallet.registerRequestHandler(
chainId: getChainId(),
method: solSignTransaction,
handler: solanaSignTransaction,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: solSignMessage,
handler: solanaSignMessage,
);
}
@override
String getNamespace() {
return namespace;
}
@override
String getChainId() {
return reference.chain();
}
@override
List<String> getEvents() {
return ['chainChanged', 'accountsChanged'];
}
Future<String?> requestAuthorization(String? text) async {
// Show the bottom sheet
final bool? isApproved = await bottomSheetService.queueBottomSheet(
widget: Web3RequestModal(
child: ConnectionWidget(
title: S.current.signTransaction,
info: [
ConnectionModel(
text: text,
),
],
),
),
) as bool?;
if (isApproved != null && isApproved == false) {
return 'User rejected signature';
}
return null;
}
Future<String> solanaSignTransaction(String topic, dynamic parameters) async {
log('received solana sign transaction request $parameters');
final solanaSignTx = SolanaSignTransaction.fromJson(parameters as Map<String, dynamic>);
final String? authError = await requestAuthorization('Confirm request to sign transaction?');
if (authError != null) {
return authError;
}
try {
// Convert transaction string to bytes
List<int> transactionBytes = base64Decode(solanaSignTx.transaction);
final message = SolanaTransactionUtils.deserializeMessageLegacy(transactionBytes);
final sign = ownerPrivateKey!.sign(message.serialize());
final signature = solanaProvider.request(
SolanaRPCSendTransaction(
encodedTransaction: Base58Encoder.encode(sign),
commitment: Commitment.confirmed,
),
);
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.awaitDAppProcessing,
isError: false,
),
);
return signature;
} catch (e) {
log('An error has occurred while signing transaction: ${e.toString()}');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: '${S.current.errorSigningTransaction}: ${e.toString()}',
),
);
return 'Failed';
}
}
Future<String> solanaSignMessage(String topic, dynamic parameters) async {
log('received solana sign message request: $parameters');
final solanaSignMessage = SolanaSignMessage.fromJson(parameters as Map<String, dynamic>);
final String? authError = await requestAuthorization('Confirm request to sign message?');
if (authError != null) {
return authError;
}
List<int>? sign;
try {
sign = ownerPrivateKey!.sign(Base58Decoder.decode(solanaSignMessage.message));
} catch (e) {
printV(e);
}
if (sign == null) {
return '';
}
final signature = Base58Encoder.encode(sign);
return signature;
}
}

View file

@ -1,60 +0,0 @@
class WCEthereumTransactionModel {
final String from;
final String to;
final String value;
final String? nonce;
final String? gasPrice;
final String? maxFeePerGas;
final String? maxPriorityFeePerGas;
final String? gas;
final String? gasLimit;
final String? data;
WCEthereumTransactionModel({
required this.from,
required this.to,
required this.value,
this.nonce,
this.gasPrice,
this.maxFeePerGas,
this.maxPriorityFeePerGas,
this.gas,
this.gasLimit,
this.data,
});
factory WCEthereumTransactionModel.fromJson(Map<String, dynamic> json) {
return WCEthereumTransactionModel(
from: json['from'] as String,
to: json['to'] as String,
value: json['value'] as String,
nonce: json['nonce'] as String?,
gasPrice: json['gasPrice'] as String?,
maxFeePerGas: json['maxFeePerGas'] as String?,
maxPriorityFeePerGas: json['maxPriorityFeePerGas'] as String?,
gas: json['gas'] as String?,
gasLimit: json['gasLimit'] as String?,
data: json['data'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'from': from,
'to': to,
'value': value,
'nonce': nonce,
'gasPrice': gasPrice,
'maxFeePerGas': maxFeePerGas,
'maxPriorityFeePerGas': maxPriorityFeePerGas,
'gas': gas,
'gasLimit': gasLimit,
'data': data,
};
}
@override
String toString() {
return 'EthereumTransactionModel(from: $from, to: $to, nonce: $nonce, gasPrice: $gasPrice, maxFeePerGas: $maxFeePerGas, maxPriorityFeePerGas: $maxPriorityFeePerGas, gas: $gas, gasLimit: $gasLimit, value: $value, data: $data)';
}
}

View file

@ -1,16 +0,0 @@
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
class AuthRequestModel {
final String iss;
final AuthRequest request;
AuthRequestModel({
required this.iss,
required this.request,
});
@override
String toString() {
return 'AuthRequestModel(iss: $iss, request: $request)';
}
}

View file

@ -1,16 +0,0 @@
class ChainKeyModel {
final List<String> chains;
final String privateKey;
final String publicKey;
ChainKeyModel({
required this.chains,
required this.privateKey,
required this.publicKey,
});
@override
String toString() {
return 'ChainKeyModel(chains: $chains, privateKey: $privateKey, publicKey: $publicKey)';
}
}

View file

@ -1,14 +0,0 @@
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
class SessionRequestModel {
final ProposalData request;
SessionRequestModel({
required this.request,
});
@override
String toString() {
return 'SessionRequestModel(request: $request)';
}
}

View file

@ -1,416 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/utils/print_verbose.dart';
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';
import 'chain_service/solana/solana_chain_id.dart';
import 'chain_service/solana/solana_chain_service.dart';
import 'wc_bottom_sheet_service.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
part 'web3wallet_service.g.dart';
class Web3WalletService = Web3WalletServiceBase with _$Web3WalletService;
abstract class Web3WalletServiceBase with Store {
final AppStore appStore;
final SharedPreferences sharedPreferences;
final BottomSheetService _bottomSheetHandler;
final WalletConnectKeyService walletKeyService;
late Web3Wallet _web3Wallet;
@observable
bool isInitialized;
/// The list of requests from the dapp
/// Potential types include, but aren't limited to:
/// [SessionProposalEvent], [AuthRequest]
@observable
ObservableList<PairingInfo> pairings;
@observable
ObservableList<SessionData> sessions;
@observable
ObservableList<StoredCacao> auth;
Web3WalletServiceBase(
this._bottomSheetHandler, this.walletKeyService, this.appStore, this.sharedPreferences)
: pairings = ObservableList<PairingInfo>(),
sessions = ObservableList<SessionData>(),
auth = ObservableList<StoredCacao>(),
isInitialized = false;
@action
void create() {
// Create the web3wallet client
_web3Wallet = Web3Wallet(
core: Core(projectId: secrets.walletConnectProjectId),
metadata: const PairingMetadata(
name: 'Cake Wallet',
description: 'Cake Wallet',
url: 'https://cakewallet.com',
icons: ['https://cakewallet.com/assets/image/cake_logo.png'],
),
);
// Setup our accounts
List<ChainKeyModel> chainKeys = walletKeyService.getKeys(appStore.wallet!);
for (final chainKey in chainKeys) {
for (final chainId in chainKey.chains) {
_web3Wallet.registerAccount(
chainId: chainId,
accountAddress: chainKey.publicKey,
);
}
}
// Setup our listeners
log('Created instance of web3wallet');
_web3Wallet.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid);
_web3Wallet.core.pairing.onPairingCreate.subscribe(_onPairingCreate);
_web3Wallet.core.pairing.onPairingDelete.subscribe(_onPairingDelete);
_web3Wallet.core.pairing.onPairingExpire.subscribe(_onPairingDelete);
_web3Wallet.pairings.onSync.subscribe(_onPairingsSync);
_web3Wallet.onSessionProposal.subscribe(_onSessionProposal);
_web3Wallet.onSessionProposalError.subscribe(_onSessionProposalError);
_web3Wallet.onSessionConnect.subscribe(_onSessionConnect);
_web3Wallet.onAuthRequest.subscribe(_onAuthRequest);
}
@action
Future<void> init() async {
// Await the initialization of the web3wallet
log('Intializing web3wallet');
if (!isInitialized) {
try {
await _web3Wallet.init();
log('Initialized');
isInitialized = true;
} catch (e) {
log('Experimentallllll: $e');
isInitialized = false;
}
}
_refreshPairings();
final newSessions = _web3Wallet.sessions.getAll();
sessions.addAll(newSessions);
final newAuthRequests = _web3Wallet.completeRequests.getAll();
auth.addAll(newAuthRequests);
if (isEVMCompatibleChain(appStore.wallet!.type)) {
for (final cId in EVMChainId.values) {
EvmChainServiceImpl(
reference: cId,
appStore: appStore,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
);
}
}
if (appStore.wallet!.type == WalletType.solana) {
for (final cId in SolanaChainId.values) {
final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
String formattedUrl;
String protocolUsed = node.isSSL ? "https" : "http";
if (node.uriRaw == 'rpc.ankr.com') {
String ankrApiKey = secrets.ankrApiKey;
formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey';
} else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') {
String chainStackApiKey = secrets.chainStackApiKey;
formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey';
} else {
formattedUrl = '$protocolUsed://${node.uriRaw}';
}
SolanaChainServiceImpl(
reference: cId,
formattedRPCUrl: formattedUrl,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
ownerPrivateKey: SolanaPrivateKey.fromSeedHex(solana!.getPrivateKey(appStore.wallet!)),
);
}
}
}
@action
FutureOr<void> onDispose() {
log('web3wallet dispose');
_web3Wallet.core.pairing.onPairingInvalid.unsubscribe(_onPairingInvalid);
_web3Wallet.pairings.onSync.unsubscribe(_onPairingsSync);
_web3Wallet.onSessionProposal.unsubscribe(_onSessionProposal);
_web3Wallet.onSessionProposalError.unsubscribe(_onSessionProposalError);
_web3Wallet.onSessionConnect.unsubscribe(_onSessionConnect);
_web3Wallet.onAuthRequest.unsubscribe(_onAuthRequest);
_web3Wallet.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete);
_web3Wallet.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete);
isInitialized = false;
}
Web3Wallet getWeb3Wallet() {
return _web3Wallet;
}
void _onPairingsSync(StoreSyncEvent? args) {
if (args != null) {
_refreshPairings();
}
}
void _onPairingDelete(PairingEvent? event) {
_refreshPairings();
}
Future<void> _onSessionProposalError(SessionProposalErrorEvent? args) async {
log(args.toString());
}
void _onSessionProposal(SessionProposalEvent? args) async {
if (args != null) {
final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type);
final Widget modalWidget = Web3RequestModal(
child: ConnectionRequestWidget(
chaindIdNamespace: chaindIdNamespace,
wallet: _web3Wallet,
sessionProposal: SessionRequestModel(request: args.params),
),
);
// show the bottom sheet
final bool? isApproved = await _bottomSheetHandler.queueBottomSheet(
widget: modalWidget,
) as bool?;
if (isApproved != null && isApproved) {
_web3Wallet.approveSession(
id: args.id,
namespaces: args.params.generatedNamespaces!,
);
} else {
_web3Wallet.rejectSession(
id: args.id,
reason: Errors.getSdkError(
Errors.USER_REJECTED,
),
);
}
}
}
@action
void _onPairingInvalid(PairingInvalidEvent? args) {
log('Pairing Invalid Event: $args');
_bottomSheetHandler.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(message: '${S.current.pairingInvalidEvent}: $args'),
);
}
@action
Future<void> pairWithUri(Uri uri) async {
try {
log('Pairing with URI: $uri');
await _web3Wallet.pair(uri: uri);
} on WalletConnectError catch (e) {
_bottomSheetHandler.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(message: e.message),
);
} catch (e) {
_bottomSheetHandler.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(message: e.toString()),
);
}
}
@action
void _refreshPairings() {
printV('Refreshing pairings');
pairings.clear();
final allPairings = _web3Wallet.pairings.getAll();
final keyForWallet = getKeyForStoringTopicsForWallet();
if (keyForWallet.isEmpty) return;
final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet);
final filteredPairings =
allPairings.where((pairing) => currentTopicsForWallet.contains(pairing.topic)).toList();
pairings.addAll(filteredPairings);
}
void _onPairingCreate(PairingEvent? args) {
log('Pairing Create Event: $args');
}
@action
Future<void> _onSessionConnect(SessionConnect? args) async {
if (args != null) {
log('Session Connected $args');
await savePairingTopicToLocalStorage(args.session.pairingTopic);
sessions.add(args.session);
_refreshPairings();
}
}
@action
Future<void> _onAuthRequest(AuthRequest? args) async {
if (args != null) {
final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type);
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
// Create the message to be signed
final String iss = 'did:pkh:$chaindIdNamespace:${chainKeys.first.publicKey}';
final Widget modalWidget = Web3RequestModal(
child: ConnectionRequestWidget(
chaindIdNamespace: chaindIdNamespace,
wallet: _web3Wallet,
authRequest: AuthRequestModel(iss: iss, request: args),
),
);
final bool? isAuthenticated = await _bottomSheetHandler.queueBottomSheet(
widget: modalWidget,
) as bool?;
if (isAuthenticated != null && isAuthenticated) {
final String message = _web3Wallet.formatAuthMessage(
iss: iss,
cacaoPayload: CacaoRequestPayload.fromPayloadParams(
args.payloadParams,
),
);
final String sig = EthSigUtil.signPersonalMessage(
message: Uint8List.fromList(message.codeUnits),
privateKey: chainKeys.first.privateKey,
);
await _web3Wallet.respondAuthRequest(
id: args.id,
iss: iss,
signature: CacaoSignature(
t: CacaoSignature.EIP191,
s: sig,
),
);
} else {
await _web3Wallet.respondAuthRequest(
id: args.id,
iss: iss,
error: Errors.getSdkError(
Errors.USER_REJECTED_AUTH,
),
);
}
}
}
@action
Future<void> disconnectSession(String topic) async {
final session = sessions.firstWhere((element) => element.pairingTopic == topic);
await _web3Wallet.core.pairing.disconnect(topic: topic);
await _web3Wallet.disconnectSession(
topic: session.topic, reason: Errors.getSdkError(Errors.USER_DISCONNECTED));
}
@action
List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
}
String getKeyForStoringTopicsForWallet() {
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
if (chainKeys.isEmpty) {
return '';
}
final keyForPairingTopic =
PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
return keyForPairingTopic;
}
List<String> getPairingTopicsForWallet(String key) {
// Get the JSON-encoded string from shared preferences
final jsonString = sharedPreferences.getString(key);
// If the string is null, return an empty list
if (jsonString == null) {
return [];
}
// Decode the JSON string to a list of strings
final List<dynamic> jsonList = jsonDecode(jsonString) as List<dynamic>;
// Cast each item to a string
return jsonList.map((item) => item as String).toList();
}
Future<void> savePairingTopicToLocalStorage(String pairingTopic) async {
// Get key specific to the current wallet
final key = getKeyForStoringTopicsForWallet();
if (key.isEmpty) return;
// Get all pairing topics attached to this key
final pairingTopicsForWallet = getPairingTopicsForWallet(key);
printV(pairingTopicsForWallet);
bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
printV('Is Pairing Topic Saved: $isPairingTopicAlreadySaved');
if (!isPairingTopicAlreadySaved) {
// Update the list with the most recent pairing topic
pairingTopicsForWallet.add(pairingTopic);
// Convert the list of updated pairing topics to a JSON-encoded string
final jsonString = jsonEncode(pairingTopicsForWallet);
// Save the encoded string to shared preferences
await sharedPreferences.setString(key, jsonString);
}
}
}

View file

@ -32,17 +32,18 @@ class WalletLoadingService {
Future<void> renameWallet(WalletType type, String name, String newName,
{String? password}) async {
try {
final walletService = walletServiceFactory.call(type);
final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name));
// Save the current wallet's password to the new wallet name's key
await keyService.saveWalletPassword(walletName: newName, password: walletPassword);
await walletService.rename(name, walletPassword, newName);
// Delete previous wallet name from keyService to keep only new wallet's name
// otherwise keeps duplicate (old and new names)
await keyService.deleteWalletPassword(walletName: name);
await walletService.rename(name, walletPassword, newName);
// set shared preferences flag based on previous wallet name
if (type == WalletType.monero) {
final oldNameKey = PreferencesKey.moneroWalletUpdateV1Key(name);
@ -50,10 +51,17 @@ class WalletLoadingService {
final newNameKey = PreferencesKey.moneroWalletUpdateV1Key(newName);
await sharedPreferences.setBool(newNameKey, isPasswordUpdated);
}
} catch (error, stack) {
await ExceptionHandler.resetLastPopupDate();
await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
}
}
Future<WalletBase> load(WalletType type, String name, {String? password}) async {
Future<WalletBase> load(WalletType type, String name, {String? password, bool isBackground = false}) async {
try {
if (!isBackground) {
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(name), DateTime.now().toIso8601String());
}
final walletService = walletServiceFactory.call(type);
final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name));
final wallet = await walletService.openWallet(name, walletPassword);

View file

@ -20,9 +20,6 @@ import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/core/yat_service.dart';
@ -33,11 +30,19 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/haven/cw_haven.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/tari/tari.dart';
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
import 'package:cake_wallet/view_model/dev/secure_preferences.dart';
import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
@ -47,6 +52,7 @@ import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
@ -54,7 +60,10 @@ import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
@ -266,6 +275,8 @@ import 'buy/kryptonim/kryptonim.dart';
import 'buy/meld/meld_buy_provider.dart';
import 'src/screens/buy/buy_sell_page.dart';
import 'cake_pay/cake_pay_payment_credantials.dart';
import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
final getIt = GetIt.instance;
@ -280,6 +291,7 @@ late Box<ExchangeTemplate> _exchangeTemplates;
late Box<TransactionDescription> _transactionDescriptionBox;
late Box<Order> _ordersSource;
late Box<UnspentCoinsInfo> _unspentCoinsInfoSource;
late Box<PayjoinSession> _payjoinSessionSource;
late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
Future<void> setup({
@ -293,6 +305,7 @@ Future<void> setup({
required Box<TransactionDescription> transactionDescriptionBox,
required Box<Order> ordersSource,
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
required Box<PayjoinSession> payjoinSessionSource,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource,
required SecureStorage secureStorage,
required GlobalKey<NavigatorState> navigatorKey,
@ -307,6 +320,7 @@ Future<void> setup({
_transactionDescriptionBox = transactionDescriptionBox;
_ordersSource = ordersSource;
_unspentCoinsInfoSource = unspentCoinsInfoSource;
_payjoinSessionSource = payjoinSessionSource;
_anonpayInvoiceInfoSource = anonpayInvoiceInfoSource;
if (!_isSetupFinished) {
@ -348,6 +362,8 @@ Future<void> setup({
TradesStore(tradesSource: _tradesSource, settingsStore: getIt.get<SettingsStore>()));
getIt.registerSingleton<OrdersStore>(
OrdersStore(ordersSource: _ordersSource, settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory(() =>
PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource));
getIt.registerSingleton<TradeFilterStore>(TradeFilterStore());
getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore(getIt.get<AppStore>()));
getIt.registerSingleton<FiatConversionStore>(FiatConversionStore());
@ -501,6 +517,7 @@ Future<void> setup({
yatStore: getIt.get<YatStore>(),
ordersStore: getIt.get<OrdersStore>(),
anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>(),
payjoinTransactionsStore: getIt.get<PayjoinTransactionsStore>(),
sharedPreferences: getIt.get<SharedPreferences>(),
keyService: getIt.get<KeyService>()));
@ -636,13 +653,18 @@ Future<void> setup({
getIt.registerLazySingleton<WalletConnectKeyService>(() => KeyServiceImpl());
getIt.registerLazySingleton<Web3WalletService>(() {
final Web3WalletService web3WalletService = Web3WalletService(getIt.get<BottomSheetService>(),
getIt.get<WalletConnectKeyService>(), appStore, getIt.get<SharedPreferences>());
web3WalletService.create();
return web3WalletService;
getIt.registerLazySingleton<WalletKitService>(() {
final WalletKitService walletKitService = WalletKitService(
getIt.get<BottomSheetService>(),
getIt.get<WalletConnectKeyService>(),
appStore,
getIt.get<SharedPreferences>(),
);
walletKitService.create();
return walletKitService;
});
getIt.registerFactory(() => NFTViewModel(appStore, getIt.get<BottomSheetService>()));
getIt.registerFactory(() => BalancePage(
nftViewModel: getIt.get<NFTViewModel>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
@ -828,7 +850,7 @@ Future<void> setup({
if (wallet.type == WalletType.monero ||
wallet.type == WalletType.wownero ||
wallet.type == WalletType.haven) {
return MoneroAccountListViewModel(wallet);
return MoneroAccountListViewModel(wallet,getIt.get<SettingsStore>());
}
throw Exception(
'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel');
@ -879,9 +901,8 @@ Future<void> setup({
nanoAccountCreationViewModel:
getIt.get<NanoAccountEditOrCreateViewModel>(param1: account)));
getIt.registerFactory(() {
return DisplaySettingsViewModel(getIt.get<SettingsStore>());
});
getIt.registerFactory(() =>
DisplaySettingsViewModel(getIt.get<SettingsStore>()));
getIt.registerFactory(() =>
SilentPaymentsSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
@ -889,22 +910,20 @@ Future<void> setup({
getIt.registerFactory(
() => MwebSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
getIt.registerFactory(() {
return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!);
});
getIt.registerFactory(() =>
PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
getIt.registerFactory(() => TrocadorExchangeProvider());
getIt.registerFactory(() => TrocadorProvidersViewModel(
getIt.get<SettingsStore>(), getIt.get<TrocadorExchangeProvider>()));
getIt.registerFactory(() {
return OtherSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!,
getIt.get<SendViewModel>());});
getIt.registerFactory(() =>
OtherSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!,
getIt.get<SendViewModel>()));
getIt.registerFactory(() {
return SecuritySettingsViewModel(getIt.get<SettingsStore>());
});
getIt.registerFactory(() =>
SecuritySettingsViewModel(getIt.get<SettingsStore>()));
getIt.registerFactory(() => WalletSeedViewModel(getIt.get<AppStore>().wallet!));
@ -912,6 +931,10 @@ Future<void> setup({
getIt.registerFactory(() => DevMoneroBackgroundSync(getIt.get<AppStore>().wallet!));
getIt.registerFactory(() => DevSharedPreferences());
getIt.registerFactory(() => DevSecurePreferences());
getIt.registerFactoryParam<WalletSeedPage, bool, void>((bool isWalletCreated, _) =>
WalletSeedPage(getIt.get<WalletSeedViewModel>(), isNewWalletCreated: isWalletCreated));
@ -1083,6 +1106,7 @@ Future<void> setup({
return bitcoin!.createBitcoinWalletService(
_walletInfoSource,
_unspentCoinsInfoSource,
_payjoinSessionSource,
getIt.get<SettingsStore>().silentPaymentsAlwaysScan,
SettingsStoreBase.walletPasswordDirectInput,
);
@ -1118,8 +1142,9 @@ Future<void> setup({
return decred!.createDecredWalletService(_walletInfoSource, _unspentCoinsInfoSource);
case WalletType.tari:
return tari!.createTariWalletService(_walletInfoSource);
case WalletType.none:
case WalletType.haven:
return HavenWalletService(_walletInfoSource);
case WalletType.none:
throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
}
});
@ -1412,6 +1437,15 @@ Future<void> setup({
settingsStore: getIt.get<SettingsStore>(),
));
getIt.registerFactoryParam<PayjoinDetailsViewModel, String, TransactionInfo?>(
(String sessionId, TransactionInfo? transactionInfo) =>
PayjoinDetailsViewModel(
sessionId,
transactionInfo,
payjoinSessionSource: _payjoinSessionSource,
settingsStore: getIt.get<SettingsStore>(),
));
getIt.registerFactoryParam<AnonPayReceivePage, AnonpayInfoBase, void>(
(AnonpayInfoBase anonpayInvoiceInfo, _) =>
AnonPayReceivePage(invoiceInfo: anonpayInvoiceInfo));
@ -1420,6 +1454,11 @@ Future<void> setup({
(AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsPage(
anonpayDetailsViewModel: getIt.get<AnonpayDetailsViewModel>(param1: anonpayInvoiceInfo)));
getIt.registerFactoryParam<PayjoinDetailsPage, String, TransactionInfo?>(
(String sessionId, TransactionInfo? transactionInfo) => PayjoinDetailsPage(
payjoinDetailsViewModel: getIt.get<PayjoinDetailsViewModel>(
param1: sessionId, param2: transactionInfo)));
getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>
HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel)));
@ -1442,9 +1481,9 @@ Future<void> setup({
});
getIt.registerFactory(
() => WalletConnectConnectionsView(web3walletService: getIt.get<Web3WalletService>()));
() => WalletConnectConnectionsView(walletKitService: getIt.get<WalletKitService>()),
);
getIt.registerFactory(() => NFTViewModel(appStore, getIt.get<BottomSheetService>()));
getIt.registerFactory<TorPage>(() => TorPage(getIt.get<AppStore>()));
getIt.registerFactory(() => SignViewModel(getIt.get<AppStore>().wallet!));
@ -1452,6 +1491,16 @@ Future<void> setup({
getIt.registerFactory(() => SeedVerificationPage(getIt.get<WalletSeedViewModel>()));
getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get<DevMoneroBackgroundSync>()));
getIt.registerFactory(() => DevMoneroCallProfilerPage());
getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get<DevSharedPreferences>()));
getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get<DevSecurePreferences>()));
getIt.registerFactory(() => BackgroundSyncLogsViewModel());
getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get<BackgroundSyncLogsViewModel>()));
_isSetupFinished = true;
}

View file

@ -9,8 +9,7 @@ class MainActions {
final bool Function(DashboardViewModel viewModel)? isEnabled;
final bool Function(DashboardViewModel viewModel)? canShow;
final Future<void> Function(
BuildContext context, DashboardViewModel viewModel) onTap;
final Future<void> Function(BuildContext context, DashboardViewModel viewModel) onTap;
MainActions._({
required this.name,
@ -32,7 +31,12 @@ class MainActions {
name: (context) => S.of(context).wallets,
image: 'assets/images/wallet_new.png',
onTap: (BuildContext context, DashboardViewModel viewModel) async {
Navigator.pushNamed(context, Routes.walletList);
Navigator.pushNamed(
context,
Routes.walletList,
arguments: (BuildContext context) =>
Navigator.of(context).pushNamedAndRemoveUntil(Routes.dashboard, (route) => false),
);
},
);
@ -65,7 +69,6 @@ class MainActions {
},
);
static MainActions tradeAction = MainActions._(
name: (context) => S.of(context).exchange,
image: 'assets/images/buy_sell.png',

View file

@ -84,6 +84,7 @@ class PreferencesKey {
static const lookupsOpenAlias = 'looks_up_open_alias';
static const lookupsENS = 'looks_up_ens';
static const lookupsWellKnown = 'looks_up_well_known';
static const usePayjoin = 'use_payjoin';
static const showCameraConsent = 'show_camera_consent';
static const showDecredInfoCard = 'show_decred_info_card';
@ -107,4 +108,6 @@ class PreferencesKey {
static const walletConnectPairingTopicsList = 'wallet_connect_pairing_topics_list';
static String walletConnectPairingTopicsListForWallet(String publicKey) =>
'${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}';
static String backgroundSyncLastTrigger(String walletId) => 'background_sync_last_trigger_${walletId}';
static const backgroundSyncNotificationsEnabled = 'background_sync_notifications_enabled';
}

View file

@ -8,10 +8,10 @@ class MoneroSeedType extends EnumerableItem<int> with Serializable<int> {
static const defaultSeedType = polyseed;
static const legacy = MoneroSeedType(raw: 0, title: 'Legacy');
static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed');
static const legacy = MoneroSeedType(raw: 0, title: 'Legacy (25 words)');
static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed (16 words)');
static const wowneroSeed = MoneroSeedType(raw: 2, title: 'Wownero');
static const bip39 = MoneroSeedType(raw: 3, title: 'BIP39');
static const bip39 = MoneroSeedType(raw: 3, title: 'BIP39 (12 words)');
static MoneroSeedType deserialize({required int raw}) {
switch (raw) {

View file

@ -47,10 +47,10 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
String get title => 'SwapTrade';
@override
bool get isAvailable => true;
bool get isAvailable => false;
@override
bool get isEnabled => true;
bool get isEnabled => false;
@override
bool get supportsFixedRate => false;
@ -59,7 +59,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
ExchangeProviderDescription get description => ExchangeProviderDescription.swapTrade;
@override
Future<bool> checkIsAvailable() async => true;
Future<bool> checkIsAvailable() async => false;
@override
Future<Limits> fetchLimits({

View file

@ -41,8 +41,8 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging');
static const sending = TradeState(raw: 'sending', title: 'Sending');
static const success = TradeState(raw: 'success', title: 'Success');
static TradeState deserialize({required String raw}) {
static TradeState deserialize({required String raw}) {
switch (raw) {
case '1':
return unpaid;
@ -138,7 +138,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
case 'awaiting':
return awaiting;
default:
throw Exception('Unexpected token: $raw in TradeState deserialize');
return TradeState(raw: raw, title: raw);
}
}

View file

@ -1,348 +1,78 @@
part of 'haven.dart';
import 'dart:io';
class CWHavenAccountList extends HavenAccountList {
CWHavenAccountList(this._wallet);
import 'package:cw_core/balance.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
final Object _wallet;
class HavenWalletService extends WalletService {
final Box<WalletInfo> walletInfoSource;
HavenWalletService(this.walletInfoSource);
@override
@computed
ObservableList<Account> get accounts {
final havenWallet = _wallet as HavenWallet;
final accounts = havenWallet.walletAddresses.accountList.accounts
.map((acc) => Account(id: acc.id, label: acc.label))
.toList();
return ObservableList<Account>.of(accounts);
WalletType getType() => WalletType.haven;
@override
Future<void> remove(String wallet) async {
final path = await pathForWalletDir(name: wallet, type: WalletType.haven);
final file = Directory(path);
final isExist = file.existsSync();
if (isExist) {
await file.delete(recursive: true);
}
final walletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(wallet, getType()));
await walletInfoSource.delete(walletInfo.key);
}
@override
void update(Object wallet) {
final havenWallet = wallet as HavenWallet;
havenWallet.walletAddresses.accountList.update();
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>> create(
WalletCredentials credentials,
{bool? isTestnet}) {
throw UnimplementedError();
}
@override
void refresh(Object wallet) {
final havenWallet = wallet as HavenWallet;
havenWallet.walletAddresses.accountList.refresh();
Future<bool> isWalletExit(String name) {
throw UnimplementedError();
}
@override
List<Account> getAll(Object wallet) {
final havenWallet = wallet as HavenWallet;
return havenWallet.walletAddresses.accountList
.getAll()
.map((acc) => Account(id: acc.id, label: acc.label))
.toList();
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>> openWallet(
String name, String password) {
throw UnimplementedError();
}
@override
Future<void> addAccount(Object wallet, {required String label}) async {
final havenWallet = wallet as HavenWallet;
await havenWallet.walletAddresses.accountList.addAccount(label: label);
Future<void> rename(String currentName, String password, String newName) {
throw UnimplementedError();
}
@override
Future<void> setLabelAccount(Object wallet,
{required int accountIndex, required String label}) async {
final havenWallet = wallet as HavenWallet;
await havenWallet.walletAddresses.accountList
.setLabelAccount(accountIndex: accountIndex, label: label);
}
}
class CWHavenSubaddressList extends MoneroSubaddressList {
CWHavenSubaddressList(this._wallet);
final Object _wallet;
@override
@computed
ObservableList<Subaddress> get subaddresses {
final havenWallet = _wallet as HavenWallet;
final subAddresses = havenWallet.walletAddresses.subaddressList.subaddresses
.map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label))
.toList();
return ObservableList<Subaddress>.of(subAddresses);
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>>
restoreFromHardwareWallet(WalletCredentials credentials) {
throw UnimplementedError();
}
@override
void update(Object wallet, {required int accountIndex}) {
final havenWallet = wallet as HavenWallet;
havenWallet.walletAddresses.subaddressList.update(accountIndex: accountIndex);
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>>
restoreFromKeys(WalletCredentials credentials, {bool? isTestnet}) {
throw UnimplementedError();
}
@override
void refresh(Object wallet, {required int accountIndex}) {
final havenWallet = wallet as HavenWallet;
havenWallet.walletAddresses.subaddressList.refresh(accountIndex: accountIndex);
}
@override
List<Subaddress> getAll(Object wallet) {
final havenWallet = wallet as HavenWallet;
return havenWallet.walletAddresses.subaddressList
.getAll()
.map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address))
.toList();
}
@override
Future<void> addSubaddress(Object wallet,
{required int accountIndex, required String label}) async {
final havenWallet = wallet as HavenWallet;
await havenWallet.walletAddresses.subaddressList
.addSubaddress(accountIndex: accountIndex, label: label);
}
@override
Future<void> setLabelSubaddress(Object wallet,
{required int accountIndex, required int addressIndex, required String label}) async {
final havenWallet = wallet as HavenWallet;
await havenWallet.walletAddresses.subaddressList
.setLabelSubaddress(accountIndex: accountIndex, addressIndex: addressIndex, label: label);
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>>
restoreFromSeed(WalletCredentials credentials, {bool? isTestnet}) {
throw UnimplementedError();
}
}
class CWHavenWalletDetails extends HavenWalletDetails {
CWHavenWalletDetails(this._wallet);
final Object _wallet;
@computed
@override
Account get account {
final havenWallet = _wallet as HavenWallet;
final acc = havenWallet.walletAddresses.account as monero_account.Account;
return Account(id: acc.id, label: acc.label);
}
@computed
@override
HavenBalance get balance {
final havenWallet = _wallet as HavenWallet;
final balance = havenWallet.balance;
throw Exception('Unimplemented');
//return HavenBalance(
// fullBalance: balance.fullBalance,
// unlockedBalance: balance.unlockedBalance);
}
}
class CWHaven extends Haven {
@override
HavenAccountList getAccountList(Object wallet) {
return CWHavenAccountList(wallet);
}
@override
MoneroSubaddressList getSubaddressList(Object wallet) {
return CWHavenSubaddressList(wallet);
}
@override
TransactionHistoryBase getTransactionHistory(Object wallet) {
final havenWallet = wallet as HavenWallet;
return havenWallet.transactionHistory;
}
@override
HavenWalletDetails getMoneroWalletDetails(Object wallet) {
return CWHavenWalletDetails(wallet);
}
@override
int getHeightByDate({required DateTime date}) => getHavenHeightByDate(date: date);
@override
Future<int> getCurrentHeight() => getHavenCurrentHeight();
@override
TransactionPriority getDefaultTransactionPriority() {
return MoneroTransactionPriority.automatic;
}
@override
TransactionPriority deserializeMoneroTransactionPriority({required int raw}) {
return MoneroTransactionPriority.deserialize(raw: raw);
}
@override
List<TransactionPriority> getTransactionPriorities() {
return MoneroTransactionPriority.all;
}
@override
List<String> getMoneroWordList(String language) {
switch (language.toLowerCase()) {
case 'english':
return EnglishMnemonics.words;
case 'chinese (simplified)':
return ChineseSimplifiedMnemonics.words;
case 'dutch':
return DutchMnemonics.words;
case 'german':
return GermanMnemonics.words;
case 'japanese':
return JapaneseMnemonics.words;
case 'portuguese':
return PortugueseMnemonics.words;
case 'russian':
return RussianMnemonics.words;
case 'spanish':
return SpanishMnemonics.words;
case 'french':
return FrenchMnemonics.words;
case 'italian':
return ItalianMnemonics.words;
default:
return EnglishMnemonics.words;
}
}
@override
WalletCredentials createHavenRestoreWalletFromKeysCredentials(
{required String name,
required String spendKey,
required String viewKey,
required String address,
required String password,
required String language,
required int height}) {
return HavenRestoreWalletFromKeysCredentials(
name: name,
spendKey: spendKey,
viewKey: viewKey,
address: address,
password: password,
language: language,
height: height);
}
@override
WalletCredentials createHavenRestoreWalletFromSeedCredentials(
{required String name,
required String password,
required int height,
required String mnemonic}) {
return HavenRestoreWalletFromSeedCredentials(
name: name, password: password, height: height, mnemonic: mnemonic);
}
@override
WalletCredentials createHavenNewWalletCredentials(
{required String name, required String language, String? password}) {
return HavenNewWalletCredentials(name: name, password: password, language: language);
}
@override
Map<String, String> getKeys(Object wallet) {
final havenWallet = wallet as HavenWallet;
final keys = havenWallet.keys;
return <String, String>{
'privateSpendKey': keys.privateSpendKey,
'privateViewKey': keys.privateViewKey,
'publicSpendKey': keys.publicSpendKey,
'publicViewKey': keys.publicViewKey
};
}
@override
Object createHavenTransactionCreationCredentials(
{required List<Output> outputs,
required TransactionPriority priority,
required String assetType}) {
return HavenTransactionCreationCredentials(
outputs: outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
.toList(),
priority: priority as MoneroTransactionPriority,
assetType: assetType);
}
@override
String formatterMoneroAmountToString({required int amount}) {
return moneroAmountToString(amount: amount);
}
@override
double formatterMoneroAmountToDouble({required int amount}) {
return moneroAmountToDouble(amount: amount);
}
@override
int formatterMoneroParseAmount({required String amount}) {
return moneroParseAmount(amount: amount);
}
@override
Account getCurrentAccount(Object wallet) {
final havenWallet = wallet as HavenWallet;
final acc = havenWallet.walletAddresses.account as monero_account.Account;
return Account(id: acc.id, label: acc.label);
}
@override
void setCurrentAccount(Object wallet, int id, String label) {
final havenWallet = wallet as HavenWallet;
havenWallet.walletAddresses.account = monero_account.Account(id: id, label: label);
}
@override
void onStartup() {
monero_wallet_api.onStartup();
}
@override
int getTransactionInfoAccountId(TransactionInfo tx) {
final havenTransactionInfo = tx as HavenTransactionInfo;
return havenTransactionInfo.accountIndex;
}
@override
Future<void> backupHavenSeeds(Box<HavenSeedStore> havenSeedStore) async {
final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
final wallets = walletInfoSource.values
.where((element) => element.type == WalletType.haven);
for (var w in wallets) {
final walletService = HavenWalletService(walletInfoSource);
final flutterSecureStorage = secureStorageShared;
final keyService = KeyService(flutterSecureStorage);
final password = await keyService.getWalletPassword(walletName: w.name);
final wallet = await walletService.openWallet(w.name, password);
await havenSeedStore.add(HavenSeedStore(id: wallet.id, seed: wallet.seed));
wallet.close();
}
await havenSeedStore.flush();
}
@override
WalletService createHavenWalletService(Box<WalletInfo> walletInfoSource) {
return HavenWalletService(walletInfoSource);
}
@override
String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) {
final havenWallet = wallet as HavenWallet;
return havenWallet.getTransactionAddress(accountIndex, addressIndex);
}
@override
CryptoCurrency assetOfTransaction(TransactionInfo tx) {
final transaction = tx as HavenTransactionInfo;
final asset = CryptoCurrency.fromString(transaction.assetType);
return asset;
}
@override
List<AssetRate> getAssetRate() =>
getRate().map((rate) => AssetRate(rate.getAssetType(), rate.getRate())).toList();
}

View file

@ -35,6 +35,7 @@ import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
@ -178,6 +179,10 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
CakeHive.registerAdapter(MwebUtxoAdapter());
}
if (!CakeHive.isAdapterRegistered(PayjoinSession.typeId)) {
CakeHive.registerAdapter(PayjoinSessionAdapter());
}
final secureStorage = secureStorageShared;
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
@ -197,6 +202,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
final payjoinSessionSource = await CakeHive.openBox<PayjoinSession>(PayjoinSession.boxName);
final havenSeedStoreBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey);
@ -219,6 +225,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 49,
@ -241,6 +248,7 @@ Future<void> initialSetup(
required SecureStorage secureStorage,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
required Box<PayjoinSession> payjoinSessionSource,
required Box<HavenSeedStore> havenSeedStore,
int initialMigrationVersion = 15, }) async {
LanguageService.loadLocaleList();
@ -266,6 +274,7 @@ Future<void> initialSetup(
ordersSource: ordersSource,
anonpayInvoiceInfoSource: anonpayInvoiceInfo,
unspentCoinsInfoSource: unspentCoinsInfoSource,
payjoinSessionSource: payjoinSessionSource,
navigatorKey: navigatorKey,
secureStorage: secureStorage,
);

View file

@ -39,14 +39,14 @@ class CWMoneroAccountList extends MoneroAccountList {
@override
Future<void> addAccount(Object wallet, {required String label}) async {
final moneroWallet = wallet as MoneroWallet;
await moneroWallet.walletAddresses.accountList.addAccount(label: label);
moneroWallet.walletAddresses.accountList.addAccount(label: label);
}
@override
Future<void> setLabelAccount(Object wallet,
{required int accountIndex, required String label}) async {
final moneroWallet = wallet as MoneroWallet;
await moneroWallet.walletAddresses.accountList
moneroWallet.walletAddresses.accountList
.setLabelAccount(accountIndex: accountIndex, label: label);
}
}

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
import 'package:cake_wallet/entities/load_current_wallet.dart';
@ -8,6 +7,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -71,7 +71,7 @@ void startAuthenticationStateChange(
buttonAction: () => Navigator.of(context).pop()),
);
await loadCurrentWallet();
getIt.get<BottomSheetService>().resetCurrentSheet();
getIt.get<BottomSheetService>().showNext();
await navigatorKey.currentState!
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
},

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
@ -78,6 +79,10 @@ void startCurrentWalletChangeReaction(
_setAutoGenerateSubaddressStatus(wallet, settingsStore);
}
if (wallet.type == WalletType.bitcoin) {
bitcoin!.updatePayjoinState(wallet, settingsStore.usePayjoin);
}
await wallet.connectToNode(node: node);
if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
final powNode = settingsStore.getCurrentPowNode(wallet.type);

View file

@ -1,5 +1,7 @@
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_supported_methods.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart';
import 'package:cw_core/wallet_type.dart';
bool isEVMCompatibleChain(WalletType walletType) {
@ -47,6 +49,19 @@ String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) {
}
}
List<String> getChainSupportedMethodsOnWalletType(WalletType walletType) {
switch (walletType) {
case WalletType.ethereum:
return EVMSupportedMethods.values.map((e) => e.name).toList();
case WalletType.polygon:
return EVMSupportedMethods.values.map((e) => e.name).toList();
case WalletType.solana:
return SolanaSupportedMethods.values.map((e) => e.name).toList();
default:
return [];
}
}
int getChainIdBasedOnWalletType(WalletType walletType) {
switch (walletType) {
case WalletType.polygon:

View file

@ -4,7 +4,6 @@ import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
@ -37,6 +36,9 @@ import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
@ -56,6 +58,7 @@ import 'package:cake_wallet/src/screens/new_wallet/wallet_group_existing_seed_de
import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
@ -106,6 +109,7 @@ import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart';
import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
@ -716,6 +720,14 @@ Route<dynamic> createRoute(RouteSettings settings) {
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<AnonpayDetailsPage>(param1: anonInvoiceViewData));
case Routes.payjoinDetails:
final arguments = settings.arguments as List;
final sessionId = arguments.first as String;
final transactionInfo = arguments[1] as TransactionInfo?;
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<PayjoinDetailsPage>(
param1: sessionId, param2: transactionInfo));
case Routes.desktop_actions:
return PageRouteBuilder(
opaque: false,
@ -777,7 +789,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.walletConnectConnectionsListing:
return MaterialPageRoute<void>(
builder: (_) => WalletConnectConnectionsView(
web3walletService: getIt.get<Web3WalletService>(),
walletKitService: getIt.get<WalletKitService>(),
launchUri: settings.arguments as Uri?,
));
@ -836,12 +848,26 @@ Route<dynamic> createRoute(RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (_) => getIt.get<DevMoneroBackgroundSyncPage>(),
);
case Routes.devSharedPreferences:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<DevSharedPreferencesPage>(),
);
case Routes.devBackgroundSyncLogs:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<DevBackgroundSyncLogsPage>(),
);
case Routes.devMoneroCallProfiler:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<DevMoneroCallProfilerPage>(),
);
case Routes.devSecurePreferences:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<DevSecurePreferencesPage>(),
);
default:
return MaterialPageRoute<void>(
builder: (_) => Scaffold(

View file

@ -15,6 +15,7 @@ class Routes {
static const dashboard = '/dashboard';
static const send = '/send';
static const transactionDetails = '/transaction_info';
static const payjoinDetails = '/transaction_info/payjoin';
static const bumpFeePage = '/bump_fee_page';
static const receive = '/receive';
static const newSubaddress = '/new_subaddress';
@ -111,8 +112,13 @@ class Routes {
static const importNFTPage = '/import_nft_page';
static const torPage = '/tor_page';
static const backgroundSync = '/background_sync';
static const devMoneroBackgroundSync = '/dev/monero_background_sync';
static const devMoneroCallProfiler = '/dev/monero_call_profiler';
static const devSharedPreferences = '/dev/shared_preferences';
static const devSecurePreferences = '/dev/secure_preferences';
static const devBackgroundSyncLogs = '/dev/background_sync_logs';
static const signPage = '/sign_page';
static const connectDevices = '/device/connect';
static const urqrAnimatedPage = '/urqr/animated_page';

View file

@ -490,11 +490,19 @@ class BuySellPage extends BasePage {
return DesktopExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
onBuyTap: () => null,
onSellTap: () =>
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
isBuySellOption: true,
);
} else {
return DesktopExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
onBuyTap: () =>
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
onSellTap: () => null,
isBuySellOption: true,
);
}
},

View file

@ -1,10 +1,10 @@
import 'dart:async';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/src/widgets/haven_wallet_removal_popup.dart';
import 'package:cake_wallet/src/widgets/services_updates_widget.dart';

View file

@ -1,4 +1,3 @@
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@ -8,7 +7,8 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sideba
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/widgets/services_updates_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';

View file

@ -87,10 +87,7 @@ class BalanceRowWidget extends StatelessWidget {
// ],
),
child: TextButton(
onPressed: () => Fluttertoast.showToast(
msg: S.current.show_balance_toast,
backgroundColor: Color.fromRGBO(0, 0, 0, 0.85),
),
onPressed: _showToast,
onLongPress: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
@ -366,10 +363,7 @@ class BalanceRowWidget extends StatelessWidget {
// ],
),
child: TextButton(
onPressed: () => Fluttertoast.showToast(
msg: S.current.show_balance_toast,
backgroundColor: Color.fromRGBO(0, 0, 0, 0.85),
),
onPressed: _showToast,
onLongPress: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
@ -709,4 +703,13 @@ class BalanceRowWidget extends StatelessWidget {
void _showBalanceDescription(BuildContext context, String content) {
showPopUp<void>(context: context, builder: (_) => InformationPage(information: content));
}
void _showToast() async {
try {
await Fluttertoast.showToast(
msg: S.current.show_balance_toast,
backgroundColor: Color.fromRGBO(0, 0, 0, 0.85),
);
} catch (_) {}
}
}

View file

@ -11,11 +11,27 @@ import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cw_core/wallet_type.dart';
class NFTListingPage extends StatelessWidget {
class NFTListingPage extends StatefulWidget {
final NFTViewModel nftViewModel;
const NFTListingPage({super.key, required this.nftViewModel});
@override
State<NFTListingPage> createState() => _NFTListingPageState();
}
class _NFTListingPageState extends State<NFTListingPage> {
@override
void initState() {
super.initState();
fetchNFTsForWallet();
}
Future<void> fetchNFTsForWallet() async {
await widget.nftViewModel.getNFTAssetByWallet();
}
@override
Widget build(BuildContext context) {
final dashboardTheme = Theme.of(context).extension<DashboardPageTheme>()!;
@ -36,11 +52,11 @@ class NFTListingPage extends StatelessWidget {
onPressed: () => Navigator.pushNamed(
context,
Routes.importNFTPage,
arguments: nftViewModel,
arguments: widget.nftViewModel,
),
),
),
if (nftViewModel.isLoading)
if (widget.nftViewModel.isLoading)
Expanded(
child: Center(
child: CircularProgressIndicator(
@ -53,7 +69,7 @@ class NFTListingPage extends StatelessWidget {
)
else
Expanded(
child: NFTListWidget(nftViewModel: nftViewModel),
child: NFTListWidget(nftViewModel: widget.nftViewModel),
),
],
);

View file

@ -1,12 +1,14 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/payjoin_transaction_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart';
import 'package:cake_wallet/themes/extensions/placeholder_theme.dart';
import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
@ -143,6 +145,25 @@ class TransactionsPage extends StatelessWidget {
);
}
if (item is PayjoinTransactionListItem) {
final session = item.session;
return PayjoinTransactionRow(
key: item.key,
onTap: () => Navigator.of(context).pushNamed(
Routes.payjoinDetails,
arguments: [item.sessionId, item.transaction],
),
currency: "BTC",
state: item.status,
amount: bitcoin!.formatterBitcoinAmountToString(
amount: session.amount.toInt()),
createdAt: DateFormat('HH:mm')
.format(session.inProgressSince!),
isSending: session.isSenderSession,
);
}
if (item is TradeListItem) {
final trade = item.trade;

Some files were not shown because too many files have changed in this diff Show more