From 376632e22dcfb8ec05f9c489934f880f9d28bae7 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sat, 20 Apr 2024 10:07:50 +0100 Subject: [PATCH 01/31] Allow the app to run without building monero --- cw_haven/android/build.gradle | 1 - cw_monero/android/build.gradle | 1 - .../src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt | 1 - lib/main.dart | 1 - 4 files changed, 4 deletions(-) diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle index fb941f657..1319e4ad4 100644 --- a/cw_haven/android/build.gradle +++ b/cw_haven/android/build.gradle @@ -35,7 +35,6 @@ android { } externalNativeBuild { cmake { - path "CMakeLists.txt" } } } diff --git a/cw_monero/android/build.gradle b/cw_monero/android/build.gradle index fc4835e81..46b1b4315 100644 --- a/cw_monero/android/build.gradle +++ b/cw_monero/android/build.gradle @@ -39,7 +39,6 @@ android { } externalNativeBuild { cmake { - path "CMakeLists.txt" } } } diff --git a/cw_monero/android/src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt b/cw_monero/android/src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt index 37684a16a..57eec7d00 100644 --- a/cw_monero/android/src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt +++ b/cw_monero/android/src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt @@ -26,7 +26,6 @@ class CwMoneroPlugin: MethodCallHandler { val main = Handler(Looper.getMainLooper()); init { - System.loadLibrary("cw_monero") } @JvmStatic diff --git a/lib/main.dart b/lib/main.dart index b80c9eb85..cf9a766a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -207,7 +207,6 @@ Future<void> initialSetup( unspentCoinsInfoSource: unspentCoinsInfoSource, secureStorage: secureStorage); await bootstrap(navigatorKey); - monero?.onStartup(); } class App extends StatefulWidget { From 9e9ff7095e91b500114cd47196033fb84d96d55a Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sun, 21 Apr 2024 00:20:52 +0100 Subject: [PATCH 02/31] Add mwebd --- cw_bitcoin/lib/litecoin_wallet.dart | 15 + cw_bitcoin/lib/litecoin_wallet_service.dart | 6 + cw_bitcoin/pubspec.lock | 47 ++ cw_bitcoin/pubspec.yaml | 3 + cw_mweb/.gitignore | 31 + cw_mweb/.metadata | 36 + cw_mweb/CHANGELOG.md | 3 + cw_mweb/LICENSE | 1 + cw_mweb/README.md | 15 + cw_mweb/analysis_options.yaml | 4 + cw_mweb/android/.gitignore | 9 + cw_mweb/android/build.gradle | 76 ++ cw_mweb/android/settings.gradle | 1 + cw_mweb/android/src/main/AndroidManifest.xml | 3 + .../com/cakewallet/mweb/CwMwebPlugin.kt | 43 + .../cakewallet/cw_mweb/CwMwebPluginTest.kt | 27 + cw_mweb/ios/.gitignore | 38 + cw_mweb/ios/Assets/.gitkeep | 0 cw_mweb/ios/Classes/CwMwebPlugin.swift | 19 + cw_mweb/ios/cw_mweb.podspec | 23 + cw_mweb/lib/cw_mweb.dart | 17 + cw_mweb/lib/cw_mweb_method_channel.dart | 17 + cw_mweb/lib/cw_mweb_platform_interface.dart | 29 + cw_mweb/lib/mwebd.pb.dart | 773 ++++++++++++++++++ cw_mweb/lib/mwebd.pbgrpc.dart | 159 ++++ cw_mweb/macos/Classes/CwMwebPlugin.swift | 19 + cw_mweb/macos/cw_mweb.podspec | 23 + cw_mweb/pubspec.yaml | 74 ++ cw_mweb/test/cw_mweb_method_channel_test.dart | 27 + cw_mweb/test/cw_mweb_test.dart | 29 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + 31 files changed, 1569 insertions(+) create mode 100644 cw_mweb/.gitignore create mode 100644 cw_mweb/.metadata create mode 100644 cw_mweb/CHANGELOG.md create mode 100644 cw_mweb/LICENSE create mode 100644 cw_mweb/README.md create mode 100644 cw_mweb/analysis_options.yaml create mode 100644 cw_mweb/android/.gitignore create mode 100644 cw_mweb/android/build.gradle create mode 100644 cw_mweb/android/settings.gradle create mode 100644 cw_mweb/android/src/main/AndroidManifest.xml create mode 100644 cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt create mode 100644 cw_mweb/android/src/test/kotlin/com/cakewallet/cw_mweb/CwMwebPluginTest.kt create mode 100644 cw_mweb/ios/.gitignore create mode 100644 cw_mweb/ios/Assets/.gitkeep create mode 100644 cw_mweb/ios/Classes/CwMwebPlugin.swift create mode 100644 cw_mweb/ios/cw_mweb.podspec create mode 100644 cw_mweb/lib/cw_mweb.dart create mode 100644 cw_mweb/lib/cw_mweb_method_channel.dart create mode 100644 cw_mweb/lib/cw_mweb_platform_interface.dart create mode 100644 cw_mweb/lib/mwebd.pb.dart create mode 100644 cw_mweb/lib/mwebd.pbgrpc.dart create mode 100644 cw_mweb/macos/Classes/CwMwebPlugin.swift create mode 100644 cw_mweb/macos/cw_mweb.podspec create mode 100644 cw_mweb/pubspec.yaml create mode 100644 cw_mweb/test/cw_mweb_method_channel_test.dart create mode 100644 cw_mweb/test/cw_mweb_test.dart diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index d2379d5a5..f218ddd5d 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -14,6 +15,8 @@ import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/litecoin_network.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:cw_mweb/mwebd.pb.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; part 'litecoin_wallet.g.dart'; @@ -103,6 +106,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } + @action + @override + Future<void> startSync() async { + super.startSync(); + final stub = CwMweb.stub(); + Timer.periodic( + const Duration(seconds: 1), (timer) async { + final resp = await stub.status(StatusRequest()); + print(resp.blockHeaderHeight); + }); + } + @override int feeRate(TransactionPriority priority) { if (priority is LitecoinTransactionPriority) { diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index ee3b0e628..7f2066c30 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:path_provider/path_provider.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; @@ -10,6 +11,7 @@ import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_mweb/cw_mweb.dart'; import 'package:collection/collection.dart'; class LitecoinWalletService extends WalletService< @@ -26,6 +28,8 @@ class LitecoinWalletService extends WalletService< @override Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { + final appDir = await getApplicationSupportDirectory(); + await CwMweb.start(appDir.path); final wallet = await LitecoinWalletBase.create( mnemonic: await generateMnemonic(), password: credentials.password!, @@ -43,6 +47,8 @@ class LitecoinWalletService extends WalletService< @override Future<LitecoinWallet> openWallet(String name, String password) async { + final appDir = await getApplicationSupportDirectory(); + await CwMweb.start(appDir.path); final walletInfo = walletInfoSource.values.firstWhereOrNull( (info) => info.id == WalletBase.idFor(name, getType()))!; diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 50cd432c0..20851ea86 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.0" + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" args: dependency: transitive description: @@ -252,6 +260,13 @@ packages: relative: true source: path version: "0.0.1" + cw_mweb: + dependency: "direct main" + description: + path: "../cw_mweb" + relative: true + source: path + version: "0.0.1" dart_style: dependency: transitive description: @@ -334,6 +349,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da + url: "https://pub.dev" + source: hosted + version: "1.4.1" graphs: dependency: transitive description: @@ -342,6 +365,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + grpc: + dependency: "direct main" + description: + name: grpc + sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 + url: "https://pub.dev" + source: hosted + version: "3.2.4" hex: dependency: transitive description: @@ -374,6 +405,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + http2: + dependency: transitive + description: + name: http2 + sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" + url: "https://pub.dev" + source: hosted + version: "2.3.0" http_multi_server: dependency: transitive description: @@ -582,6 +621,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" provider: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 632a3140a..033d1f84e 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -35,6 +35,9 @@ dependencies: url: https://github.com/cake-tech/bitcoin_base.git ref: cake-update-v2 blockchain_utils: ^2.1.1 + cw_mweb: + path: ../cw_mweb + grpc: ^3.2.4 dev_dependencies: flutter_test: diff --git a/cw_mweb/.gitignore b/cw_mweb/.gitignore new file mode 100644 index 000000000..8959f5d70 --- /dev/null +++ b/cw_mweb/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ +libs/ diff --git a/cw_mweb/.metadata b/cw_mweb/.metadata new file mode 100644 index 000000000..606303914 --- /dev/null +++ b/cw_mweb/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_mweb/CHANGELOG.md b/cw_mweb/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_mweb/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_mweb/LICENSE b/cw_mweb/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_mweb/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_mweb/README.md b/cw_mweb/README.md new file mode 100644 index 000000000..8a839b1ec --- /dev/null +++ b/cw_mweb/README.md @@ -0,0 +1,15 @@ +# cw_mweb + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/cw_mweb/analysis_options.yaml b/cw_mweb/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_mweb/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_mweb/android/.gitignore b/cw_mweb/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/cw_mweb/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/cw_mweb/android/build.gradle b/cw_mweb/android/build.gradle new file mode 100644 index 000000000..7e67b98ad --- /dev/null +++ b/cw_mweb/android/build.gradle @@ -0,0 +1,76 @@ +group 'com.cakewallet.mweb' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.allprojects { + repositories { + flatDir { + dirs project(':cw_mweb').file('libs') + } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation (name: 'mwebd', ext: 'aar') +} diff --git a/cw_mweb/android/settings.gradle b/cw_mweb/android/settings.gradle new file mode 100644 index 000000000..88fbd66fb --- /dev/null +++ b/cw_mweb/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cw_mweb' diff --git a/cw_mweb/android/src/main/AndroidManifest.xml b/cw_mweb/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..fd3746a8c --- /dev/null +++ b/cw_mweb/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.cakewallet.mweb"> +</manifest> diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt new file mode 100644 index 000000000..394b607ab --- /dev/null +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -0,0 +1,43 @@ +package com.cakewallet.mweb + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +import mwebd.Mwebd + +/** CwMwebPlugin */ +class CwMwebPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + private var started = false + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "start") { + if (started) return + val dataDir = call.argument("dataDir") ?: "" + val server = Mwebd.newServer("mainnet", dataDir, "") + server.start(12345, true) + started = true + result.success(true) + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/cw_mweb/android/src/test/kotlin/com/cakewallet/cw_mweb/CwMwebPluginTest.kt b/cw_mweb/android/src/test/kotlin/com/cakewallet/cw_mweb/CwMwebPluginTest.kt new file mode 100644 index 000000000..baa81332f --- /dev/null +++ b/cw_mweb/android/src/test/kotlin/com/cakewallet/cw_mweb/CwMwebPluginTest.kt @@ -0,0 +1,27 @@ +package com.cakewallet.mweb + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlin.test.Test +import org.mockito.Mockito + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class CwMwebPluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = CwMwebPlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/cw_mweb/ios/.gitignore b/cw_mweb/ios/.gitignore new file mode 100644 index 000000000..0c885071e --- /dev/null +++ b/cw_mweb/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_mweb/ios/Assets/.gitkeep b/cw_mweb/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_mweb/ios/Classes/CwMwebPlugin.swift b/cw_mweb/ios/Classes/CwMwebPlugin.swift new file mode 100644 index 000000000..d742aab61 --- /dev/null +++ b/cw_mweb/ios/Classes/CwMwebPlugin.swift @@ -0,0 +1,19 @@ +import Flutter +import UIKit + +public class CwMwebPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger()) + let instance = CwMwebPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_mweb/ios/cw_mweb.podspec b/cw_mweb/ios/cw_mweb.podspec new file mode 100644 index 000000000..cd0600fa1 --- /dev/null +++ b/cw_mweb/ios/cw_mweb.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_mweb.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_mweb' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart new file mode 100644 index 000000000..e73887aaf --- /dev/null +++ b/cw_mweb/lib/cw_mweb.dart @@ -0,0 +1,17 @@ +import 'package:grpc/grpc.dart'; +import 'cw_mweb_platform_interface.dart'; +import 'mwebd.pbgrpc.dart'; + +class CwMweb { + static Future<bool?> start(String dataDir) { + return CwMwebPlatform.instance.start(dataDir); + } + + static stub() { + final channel = ClientChannel('127.0.0.1', + port: 12345, + options: const ChannelOptions( + credentials: ChannelCredentials.insecure())); + return RpcClient(channel); + } +} diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart new file mode 100644 index 000000000..112dcfaa7 --- /dev/null +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_mweb_platform_interface.dart'; + +/// An implementation of [CwMwebPlatform] that uses method channels. +class MethodChannelCwMweb extends CwMwebPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_mweb'); + + @override + Future<bool?> start(String dataDir) async { + final result = await methodChannel.invokeMethod<bool>('start', {'dataDir': dataDir}); + return result; + } +} diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart new file mode 100644 index 000000000..ce518402f --- /dev/null +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_mweb_method_channel.dart'; + +abstract class CwMwebPlatform extends PlatformInterface { + /// Constructs a CwMwebPlatform. + CwMwebPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwMwebPlatform _instance = MethodChannelCwMweb(); + + /// The default instance of [CwMwebPlatform] to use. + /// + /// Defaults to [MethodChannelCwMweb]. + static CwMwebPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwMwebPlatform] when + /// they register themselves. + static set instance(CwMwebPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future<bool?> start(String dataDir) { + throw UnimplementedError('start() has not been implemented.'); + } +} diff --git a/cw_mweb/lib/mwebd.pb.dart b/cw_mweb/lib/mwebd.pb.dart new file mode 100644 index 000000000..8d139d7eb --- /dev/null +++ b/cw_mweb/lib/mwebd.pb.dart @@ -0,0 +1,773 @@ +// +// Generated code. Do not modify. +// source: mwebd.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +class StatusRequest extends $pb.GeneratedMessage { + factory StatusRequest() => create(); + StatusRequest._() : super(); + factory StatusRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusRequest', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusRequest clone() => StatusRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusRequest copyWith(void Function(StatusRequest) updates) => super.copyWith((message) => updates(message as StatusRequest)) as StatusRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusRequest create() => StatusRequest._(); + StatusRequest createEmptyInstance() => create(); + static $pb.PbList<StatusRequest> createRepeated() => $pb.PbList<StatusRequest>(); + @$core.pragma('dart2js:noInline') + static StatusRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<StatusRequest>(create); + static StatusRequest? _defaultInstance; +} + +class StatusResponse extends $pb.GeneratedMessage { + factory StatusResponse({ + $core.int? blockHeaderHeight, + $core.int? mwebHeaderHeight, + $core.int? mwebUtxosHeight, + }) { + final $result = create(); + if (blockHeaderHeight != null) { + $result.blockHeaderHeight = blockHeaderHeight; + } + if (mwebHeaderHeight != null) { + $result.mwebHeaderHeight = mwebHeaderHeight; + } + if (mwebUtxosHeight != null) { + $result.mwebUtxosHeight = mwebUtxosHeight; + } + return $result; + } + StatusResponse._() : super(); + factory StatusResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusResponse', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'blockHeaderHeight', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'mwebHeaderHeight', $pb.PbFieldType.O3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'mwebUtxosHeight', $pb.PbFieldType.O3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusResponse clone() => StatusResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusResponse copyWith(void Function(StatusResponse) updates) => super.copyWith((message) => updates(message as StatusResponse)) as StatusResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusResponse create() => StatusResponse._(); + StatusResponse createEmptyInstance() => create(); + static $pb.PbList<StatusResponse> createRepeated() => $pb.PbList<StatusResponse>(); + @$core.pragma('dart2js:noInline') + static StatusResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<StatusResponse>(create); + static StatusResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get blockHeaderHeight => $_getIZ(0); + @$pb.TagNumber(1) + set blockHeaderHeight($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasBlockHeaderHeight() => $_has(0); + @$pb.TagNumber(1) + void clearBlockHeaderHeight() => clearField(1); + + @$pb.TagNumber(2) + $core.int get mwebHeaderHeight => $_getIZ(1); + @$pb.TagNumber(2) + set mwebHeaderHeight($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasMwebHeaderHeight() => $_has(1); + @$pb.TagNumber(2) + void clearMwebHeaderHeight() => clearField(2); + + @$pb.TagNumber(3) + $core.int get mwebUtxosHeight => $_getIZ(2); + @$pb.TagNumber(3) + set mwebUtxosHeight($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasMwebUtxosHeight() => $_has(2); + @$pb.TagNumber(3) + void clearMwebUtxosHeight() => clearField(3); +} + +class UtxosRequest extends $pb.GeneratedMessage { + factory UtxosRequest({ + $core.int? fromHeight, + $core.List<$core.int>? scanSecret, + }) { + final $result = create(); + if (fromHeight != null) { + $result.fromHeight = fromHeight; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + return $result; + } + UtxosRequest._() : super(); + factory UtxosRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory UtxosRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UtxosRequest', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromHeight', $pb.PbFieldType.O3) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + UtxosRequest clone() => UtxosRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + UtxosRequest copyWith(void Function(UtxosRequest) updates) => super.copyWith((message) => updates(message as UtxosRequest)) as UtxosRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UtxosRequest create() => UtxosRequest._(); + UtxosRequest createEmptyInstance() => create(); + static $pb.PbList<UtxosRequest> createRepeated() => $pb.PbList<UtxosRequest>(); + @$core.pragma('dart2js:noInline') + static UtxosRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<UtxosRequest>(create); + static UtxosRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get fromHeight => $_getIZ(0); + @$pb.TagNumber(1) + set fromHeight($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFromHeight() => $_has(0); + @$pb.TagNumber(1) + void clearFromHeight() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get scanSecret => $_getN(1); + @$pb.TagNumber(2) + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasScanSecret() => $_has(1); + @$pb.TagNumber(2) + void clearScanSecret() => clearField(2); +} + +class Utxo extends $pb.GeneratedMessage { + factory Utxo({ + $core.int? height, + $fixnum.Int64? value, + $core.String? address, + $core.String? outputId, + }) { + final $result = create(); + if (height != null) { + $result.height = height; + } + if (value != null) { + $result.value = value; + } + if (address != null) { + $result.address = address; + } + if (outputId != null) { + $result.outputId = outputId; + } + return $result; + } + Utxo._() : super(); + factory Utxo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Utxo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Utxo', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'height', $pb.PbFieldType.O3) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOS(3, _omitFieldNames ? '' : 'address') + ..aOS(4, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Utxo clone() => Utxo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Utxo copyWith(void Function(Utxo) updates) => super.copyWith((message) => updates(message as Utxo)) as Utxo; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Utxo create() => Utxo._(); + Utxo createEmptyInstance() => create(); + static $pb.PbList<Utxo> createRepeated() => $pb.PbList<Utxo>(); + @$core.pragma('dart2js:noInline') + static Utxo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Utxo>(create); + static Utxo? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get height => $_getIZ(0); + @$pb.TagNumber(1) + set height($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasHeight() => $_has(0); + @$pb.TagNumber(1) + void clearHeight() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get value => $_getI64(1); + @$pb.TagNumber(2) + set value($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasValue() => $_has(1); + @$pb.TagNumber(2) + void clearValue() => clearField(2); + + @$pb.TagNumber(3) + $core.String get address => $_getSZ(2); + @$pb.TagNumber(3) + set address($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasAddress() => $_has(2); + @$pb.TagNumber(3) + void clearAddress() => clearField(3); + + @$pb.TagNumber(4) + $core.String get outputId => $_getSZ(3); + @$pb.TagNumber(4) + set outputId($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasOutputId() => $_has(3); + @$pb.TagNumber(4) + void clearOutputId() => clearField(4); +} + +class AddressRequest extends $pb.GeneratedMessage { + factory AddressRequest({ + $core.int? fromIndex, + $core.int? toIndex, + $core.List<$core.int>? scanSecret, + $core.List<$core.int>? spendPubkey, + }) { + final $result = create(); + if (fromIndex != null) { + $result.fromIndex = fromIndex; + } + if (toIndex != null) { + $result.toIndex = toIndex; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + if (spendPubkey != null) { + $result.spendPubkey = spendPubkey; + } + return $result; + } + AddressRequest._() : super(); + factory AddressRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressRequest', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromIndex', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'toIndex', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'spendPubkey', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressRequest clone() => AddressRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressRequest copyWith(void Function(AddressRequest) updates) => super.copyWith((message) => updates(message as AddressRequest)) as AddressRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AddressRequest create() => AddressRequest._(); + AddressRequest createEmptyInstance() => create(); + static $pb.PbList<AddressRequest> createRepeated() => $pb.PbList<AddressRequest>(); + @$core.pragma('dart2js:noInline') + static AddressRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AddressRequest>(create); + static AddressRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get fromIndex => $_getIZ(0); + @$pb.TagNumber(1) + set fromIndex($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFromIndex() => $_has(0); + @$pb.TagNumber(1) + void clearFromIndex() => clearField(1); + + @$pb.TagNumber(2) + $core.int get toIndex => $_getIZ(1); + @$pb.TagNumber(2) + set toIndex($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasToIndex() => $_has(1); + @$pb.TagNumber(2) + void clearToIndex() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get scanSecret => $_getN(2); + @$pb.TagNumber(3) + set scanSecret($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasScanSecret() => $_has(2); + @$pb.TagNumber(3) + void clearScanSecret() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get spendPubkey => $_getN(3); + @$pb.TagNumber(4) + set spendPubkey($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasSpendPubkey() => $_has(3); + @$pb.TagNumber(4) + void clearSpendPubkey() => clearField(4); +} + +class AddressResponse extends $pb.GeneratedMessage { + factory AddressResponse({ + $core.Iterable<$core.String>? address, + }) { + final $result = create(); + if (address != null) { + $result.address.addAll(address); + } + return $result; + } + AddressResponse._() : super(); + factory AddressResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressResponse', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'address') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressResponse clone() => AddressResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressResponse copyWith(void Function(AddressResponse) updates) => super.copyWith((message) => updates(message as AddressResponse)) as AddressResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AddressResponse create() => AddressResponse._(); + AddressResponse createEmptyInstance() => create(); + static $pb.PbList<AddressResponse> createRepeated() => $pb.PbList<AddressResponse>(); + @$core.pragma('dart2js:noInline') + static AddressResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AddressResponse>(create); + static AddressResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get address => $_getList(0); +} + +class SpentRequest extends $pb.GeneratedMessage { + factory SpentRequest({ + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + SpentRequest._() : super(); + factory SpentRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpentRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentRequest', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpentRequest clone() => SpentRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpentRequest copyWith(void Function(SpentRequest) updates) => super.copyWith((message) => updates(message as SpentRequest)) as SpentRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpentRequest create() => SpentRequest._(); + SpentRequest createEmptyInstance() => create(); + static $pb.PbList<SpentRequest> createRepeated() => $pb.PbList<SpentRequest>(); + @$core.pragma('dart2js:noInline') + static SpentRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpentRequest>(create); + static SpentRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get outputId => $_getList(0); +} + +class SpentResponse extends $pb.GeneratedMessage { + factory SpentResponse({ + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + SpentResponse._() : super(); + factory SpentResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpentResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentResponse', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpentResponse clone() => SpentResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpentResponse copyWith(void Function(SpentResponse) updates) => super.copyWith((message) => updates(message as SpentResponse)) as SpentResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpentResponse create() => SpentResponse._(); + SpentResponse createEmptyInstance() => create(); + static $pb.PbList<SpentResponse> createRepeated() => $pb.PbList<SpentResponse>(); + @$core.pragma('dart2js:noInline') + static SpentResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpentResponse>(create); + static SpentResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get outputId => $_getList(0); +} + +class CreateRequest extends $pb.GeneratedMessage { + factory CreateRequest({ + $core.List<$core.int>? rawTx, + $core.List<$core.int>? scanSecret, + $core.List<$core.int>? spendSecret, + $fixnum.Int64? feeRatePerKb, + $core.bool? dryRun, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + if (spendSecret != null) { + $result.spendSecret = spendSecret; + } + if (feeRatePerKb != null) { + $result.feeRatePerKb = feeRatePerKb; + } + if (dryRun != null) { + $result.dryRun = dryRun; + } + return $result; + } + CreateRequest._() : super(); + factory CreateRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateRequest', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'spendSecret', $pb.PbFieldType.OY) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'feeRatePerKb', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOB(5, _omitFieldNames ? '' : 'dryRun') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateRequest clone() => CreateRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateRequest copyWith(void Function(CreateRequest) updates) => super.copyWith((message) => updates(message as CreateRequest)) as CreateRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateRequest create() => CreateRequest._(); + CreateRequest createEmptyInstance() => create(); + static $pb.PbList<CreateRequest> createRepeated() => $pb.PbList<CreateRequest>(); + @$core.pragma('dart2js:noInline') + static CreateRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateRequest>(create); + static CreateRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get scanSecret => $_getN(1); + @$pb.TagNumber(2) + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasScanSecret() => $_has(1); + @$pb.TagNumber(2) + void clearScanSecret() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get spendSecret => $_getN(2); + @$pb.TagNumber(3) + set spendSecret($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasSpendSecret() => $_has(2); + @$pb.TagNumber(3) + void clearSpendSecret() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get feeRatePerKb => $_getI64(3); + @$pb.TagNumber(4) + set feeRatePerKb($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasFeeRatePerKb() => $_has(3); + @$pb.TagNumber(4) + void clearFeeRatePerKb() => clearField(4); + + @$pb.TagNumber(5) + $core.bool get dryRun => $_getBF(4); + @$pb.TagNumber(5) + set dryRun($core.bool v) { $_setBool(4, v); } + @$pb.TagNumber(5) + $core.bool hasDryRun() => $_has(4); + @$pb.TagNumber(5) + void clearDryRun() => clearField(5); +} + +class CreateResponse extends $pb.GeneratedMessage { + factory CreateResponse({ + $core.List<$core.int>? rawTx, + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + CreateResponse._() : super(); + factory CreateResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateResponse', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..pPS(2, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateResponse clone() => CreateResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateResponse copyWith(void Function(CreateResponse) updates) => super.copyWith((message) => updates(message as CreateResponse)) as CreateResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateResponse create() => CreateResponse._(); + CreateResponse createEmptyInstance() => create(); + static $pb.PbList<CreateResponse> createRepeated() => $pb.PbList<CreateResponse>(); + @$core.pragma('dart2js:noInline') + static CreateResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateResponse>(create); + static CreateResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.String> get outputId => $_getList(1); +} + +class BroadcastRequest extends $pb.GeneratedMessage { + factory BroadcastRequest({ + $core.List<$core.int>? rawTx, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + return $result; + } + BroadcastRequest._() : super(); + factory BroadcastRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BroadcastRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastRequest', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BroadcastRequest clone() => BroadcastRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BroadcastRequest copyWith(void Function(BroadcastRequest) updates) => super.copyWith((message) => updates(message as BroadcastRequest)) as BroadcastRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BroadcastRequest create() => BroadcastRequest._(); + BroadcastRequest createEmptyInstance() => create(); + static $pb.PbList<BroadcastRequest> createRepeated() => $pb.PbList<BroadcastRequest>(); + @$core.pragma('dart2js:noInline') + static BroadcastRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BroadcastRequest>(create); + static BroadcastRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); +} + +class BroadcastResponse extends $pb.GeneratedMessage { + factory BroadcastResponse({ + $core.String? txid, + }) { + final $result = create(); + if (txid != null) { + $result.txid = txid; + } + return $result; + } + BroadcastResponse._() : super(); + factory BroadcastResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BroadcastResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastResponse', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'txid') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BroadcastResponse clone() => BroadcastResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BroadcastResponse copyWith(void Function(BroadcastResponse) updates) => super.copyWith((message) => updates(message as BroadcastResponse)) as BroadcastResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BroadcastResponse create() => BroadcastResponse._(); + BroadcastResponse createEmptyInstance() => create(); + static $pb.PbList<BroadcastResponse> createRepeated() => $pb.PbList<BroadcastResponse>(); + @$core.pragma('dart2js:noInline') + static BroadcastResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BroadcastResponse>(create); + static BroadcastResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get txid => $_getSZ(0); + @$pb.TagNumber(1) + set txid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTxid() => $_has(0); + @$pb.TagNumber(1) + void clearTxid() => clearField(1); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/cw_mweb/lib/mwebd.pbgrpc.dart b/cw_mweb/lib/mwebd.pbgrpc.dart new file mode 100644 index 000000000..6bc48cfdf --- /dev/null +++ b/cw_mweb/lib/mwebd.pbgrpc.dart @@ -0,0 +1,159 @@ +// +// Generated code. Do not modify. +// source: mwebd.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'mwebd.pb.dart' as $0; + +export 'mwebd.pb.dart'; + +@$pb.GrpcServiceName('Rpc') +class RpcClient extends $grpc.Client { + static final _$status = $grpc.ClientMethod<$0.StatusRequest, $0.StatusResponse>( + '/Rpc/Status', + ($0.StatusRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.StatusResponse.fromBuffer(value)); + static final _$utxos = $grpc.ClientMethod<$0.UtxosRequest, $0.Utxo>( + '/Rpc/Utxos', + ($0.UtxosRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Utxo.fromBuffer(value)); + static final _$addresses = $grpc.ClientMethod<$0.AddressRequest, $0.AddressResponse>( + '/Rpc/Addresses', + ($0.AddressRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.AddressResponse.fromBuffer(value)); + static final _$spent = $grpc.ClientMethod<$0.SpentRequest, $0.SpentResponse>( + '/Rpc/Spent', + ($0.SpentRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.SpentResponse.fromBuffer(value)); + static final _$create = $grpc.ClientMethod<$0.CreateRequest, $0.CreateResponse>( + '/Rpc/Create', + ($0.CreateRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.CreateResponse.fromBuffer(value)); + static final _$broadcast = $grpc.ClientMethod<$0.BroadcastRequest, $0.BroadcastResponse>( + '/Rpc/Broadcast', + ($0.BroadcastRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.BroadcastResponse.fromBuffer(value)); + + RpcClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, + interceptors: interceptors); + + $grpc.ResponseFuture<$0.StatusResponse> status($0.StatusRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$status, request, options: options); + } + + $grpc.ResponseStream<$0.Utxo> utxos($0.UtxosRequest request, {$grpc.CallOptions? options}) { + return $createStreamingCall(_$utxos, $async.Stream.fromIterable([request]), options: options); + } + + $grpc.ResponseFuture<$0.AddressResponse> addresses($0.AddressRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$addresses, request, options: options); + } + + $grpc.ResponseFuture<$0.SpentResponse> spent($0.SpentRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$spent, request, options: options); + } + + $grpc.ResponseFuture<$0.CreateResponse> create($0.CreateRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$create, request, options: options); + } + + $grpc.ResponseFuture<$0.BroadcastResponse> broadcast($0.BroadcastRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$broadcast, request, options: options); + } +} + +@$pb.GrpcServiceName('Rpc') +abstract class RpcServiceBase extends $grpc.Service { + $core.String get $name => 'Rpc'; + + RpcServiceBase() { + $addMethod($grpc.ServiceMethod<$0.StatusRequest, $0.StatusResponse>( + 'Status', + status_Pre, + false, + false, + ($core.List<$core.int> value) => $0.StatusRequest.fromBuffer(value), + ($0.StatusResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.UtxosRequest, $0.Utxo>( + 'Utxos', + utxos_Pre, + false, + true, + ($core.List<$core.int> value) => $0.UtxosRequest.fromBuffer(value), + ($0.Utxo value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.AddressRequest, $0.AddressResponse>( + 'Addresses', + addresses_Pre, + false, + false, + ($core.List<$core.int> value) => $0.AddressRequest.fromBuffer(value), + ($0.AddressResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.SpentRequest, $0.SpentResponse>( + 'Spent', + spent_Pre, + false, + false, + ($core.List<$core.int> value) => $0.SpentRequest.fromBuffer(value), + ($0.SpentResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.CreateRequest, $0.CreateResponse>( + 'Create', + create_Pre, + false, + false, + ($core.List<$core.int> value) => $0.CreateRequest.fromBuffer(value), + ($0.CreateResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.BroadcastRequest, $0.BroadcastResponse>( + 'Broadcast', + broadcast_Pre, + false, + false, + ($core.List<$core.int> value) => $0.BroadcastRequest.fromBuffer(value), + ($0.BroadcastResponse value) => value.writeToBuffer())); + } + + $async.Future<$0.StatusResponse> status_Pre($grpc.ServiceCall call, $async.Future<$0.StatusRequest> request) async { + return status(call, await request); + } + + $async.Stream<$0.Utxo> utxos_Pre($grpc.ServiceCall call, $async.Future<$0.UtxosRequest> request) async* { + yield* utxos(call, await request); + } + + $async.Future<$0.AddressResponse> addresses_Pre($grpc.ServiceCall call, $async.Future<$0.AddressRequest> request) async { + return addresses(call, await request); + } + + $async.Future<$0.SpentResponse> spent_Pre($grpc.ServiceCall call, $async.Future<$0.SpentRequest> request) async { + return spent(call, await request); + } + + $async.Future<$0.CreateResponse> create_Pre($grpc.ServiceCall call, $async.Future<$0.CreateRequest> request) async { + return create(call, await request); + } + + $async.Future<$0.BroadcastResponse> broadcast_Pre($grpc.ServiceCall call, $async.Future<$0.BroadcastRequest> request) async { + return broadcast(call, await request); + } + + $async.Future<$0.StatusResponse> status($grpc.ServiceCall call, $0.StatusRequest request); + $async.Stream<$0.Utxo> utxos($grpc.ServiceCall call, $0.UtxosRequest request); + $async.Future<$0.AddressResponse> addresses($grpc.ServiceCall call, $0.AddressRequest request); + $async.Future<$0.SpentResponse> spent($grpc.ServiceCall call, $0.SpentRequest request); + $async.Future<$0.CreateResponse> create($grpc.ServiceCall call, $0.CreateRequest request); + $async.Future<$0.BroadcastResponse> broadcast($grpc.ServiceCall call, $0.BroadcastRequest request); +} diff --git a/cw_mweb/macos/Classes/CwMwebPlugin.swift b/cw_mweb/macos/Classes/CwMwebPlugin.swift new file mode 100644 index 000000000..9c0dabd40 --- /dev/null +++ b/cw_mweb/macos/Classes/CwMwebPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class CwMwebPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger) + let instance = CwMwebPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_mweb/macos/cw_mweb.podspec b/cw_mweb/macos/cw_mweb.podspec new file mode 100644 index 000000000..8fadcced9 --- /dev/null +++ b/cw_mweb/macos/cw_mweb.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_mweb.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_mweb' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/cw_mweb/pubspec.yaml b/cw_mweb/pubspec.yaml new file mode 100644 index 000000000..5416963d8 --- /dev/null +++ b/cw_mweb/pubspec.yaml @@ -0,0 +1,74 @@ +name: cw_mweb +description: A new Flutter plugin project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.cakewallet.mweb + pluginClass: CwMwebPlugin + ios: + pluginClass: CwMwebPlugin + macos: + pluginClass: CwMwebPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_mweb/test/cw_mweb_method_channel_test.dart b/cw_mweb/test/cw_mweb_method_channel_test.dart new file mode 100644 index 000000000..bb0523758 --- /dev/null +++ b/cw_mweb/test/cw_mweb_method_channel_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:cw_mweb/cw_mweb_method_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelCwMweb platform = MethodChannelCwMweb(); + const MethodChannel channel = MethodChannel('cw_mweb'); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + return '42'; + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/cw_mweb/test/cw_mweb_test.dart b/cw_mweb/test/cw_mweb_test.dart new file mode 100644 index 000000000..3677659a9 --- /dev/null +++ b/cw_mweb/test/cw_mweb_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:cw_mweb/cw_mweb_platform_interface.dart'; +import 'package:cw_mweb/cw_mweb_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockCwMwebPlatform + with MockPlatformInterfaceMixin + implements CwMwebPlatform { + + @override + Future<String?> getPlatformVersion() => Future.value('42'); +} + +void main() { + final CwMwebPlatform initialPlatform = CwMwebPlatform.instance; + + test('$MethodChannelCwMweb is the default instance', () { + expect(initialPlatform, isInstanceOf<MethodChannelCwMweb>()); + }); + + test('getPlatformVersion', () async { + CwMweb cwMwebPlugin = CwMweb(); + MockCwMwebPlatform fakePlatform = MockCwMwebPlatform(); + CwMwebPlatform.instance = fakePlatform; + + expect(await cwMwebPlugin.getPlatformVersion(), '42'); + }); +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 75a78404f..e309e3142 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import connectivity_plus import cw_monero +import cw_mweb import device_info_plus import devicelocale import flutter_inappwebview_macos @@ -23,6 +24,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) CwMoneroPlugin.register(with: registry.registrar(forPlugin: "CwMoneroPlugin")) + CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) From a3aebbdb78efed727b7e254b3311ef7231767855 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sun, 21 Apr 2024 12:19:44 +0100 Subject: [PATCH 03/31] Sync status --- cw_bitcoin/lib/litecoin_wallet.dart | 18 +++++++++++++++--- cw_mweb/.gitignore | 1 - cw_mweb/android/.gitignore | 1 + .../kotlin/com/cakewallet/mweb/CwMwebPlugin.kt | 7 +------ cw_mweb/lib/cw_mweb.dart | 13 +++++++------ cw_mweb/lib/cw_mweb_method_channel.dart | 4 ++-- cw_mweb/lib/cw_mweb_platform_interface.dart | 2 +- 7 files changed, 27 insertions(+), 19 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index f218ddd5d..628391eed 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -3,6 +3,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/sync_status.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -109,12 +110,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @action @override Future<void> startSync() async { - super.startSync(); + await super.startSync(); final stub = CwMweb.stub(); Timer.periodic( - const Duration(seconds: 1), (timer) async { + const Duration(milliseconds: 1500), (timer) async { + final height = await electrumClient.getCurrentBlockChainTip() ?? 0; final resp = await stub.status(StatusRequest()); - print(resp.blockHeaderHeight); + if (resp.blockHeaderHeight < height) { + int h = resp.blockHeaderHeight; + syncStatus = SyncingSyncStatus(height - h, h / height); + } else if (resp.mwebHeaderHeight < height) { + int h = resp.mwebHeaderHeight; + syncStatus = SyncingSyncStatus(height - h, h / height); + } else if (resp.mwebUtxosHeight < height) { + syncStatus = SyncingSyncStatus(1, 0.999); + } else { + syncStatus = SyncedSyncStatus(); + } }); } diff --git a/cw_mweb/.gitignore b/cw_mweb/.gitignore index 8959f5d70..96486fd93 100644 --- a/cw_mweb/.gitignore +++ b/cw_mweb/.gitignore @@ -28,4 +28,3 @@ migrate_working_dir/ .dart_tool/ .packages build/ -libs/ diff --git a/cw_mweb/android/.gitignore b/cw_mweb/android/.gitignore index 161bdcdaf..881f3d95c 100644 --- a/cw_mweb/android/.gitignore +++ b/cw_mweb/android/.gitignore @@ -6,4 +6,5 @@ .DS_Store /build /captures +/libs .cxx diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt index 394b607ab..cf194417b 100644 --- a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -17,7 +17,6 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel : MethodChannel - private var started = false override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb") @@ -26,12 +25,8 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "start") { - if (started) return val dataDir = call.argument("dataDir") ?: "" - val server = Mwebd.newServer("mainnet", dataDir, "") - server.start(12345, true) - started = true - result.success(true) + result.success(Mwebd.newServer("", dataDir, "").start(0)) } else { result.notImplemented() } diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index e73887aaf..fb3960ab0 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -3,15 +3,16 @@ import 'cw_mweb_platform_interface.dart'; import 'mwebd.pbgrpc.dart'; class CwMweb { - static Future<bool?> start(String dataDir) { - return CwMwebPlatform.instance.start(dataDir); + static var port; + + static start(String dataDir) async { + port = port ?? await CwMwebPlatform.instance.start(dataDir); } static stub() { - final channel = ClientChannel('127.0.0.1', - port: 12345, + return RpcClient(ClientChannel('127.0.0.1', + port: port, options: const ChannelOptions( - credentials: ChannelCredentials.insecure())); - return RpcClient(channel); + credentials: ChannelCredentials.insecure()))); } } diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart index 112dcfaa7..cc880c6df 100644 --- a/cw_mweb/lib/cw_mweb_method_channel.dart +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -10,8 +10,8 @@ class MethodChannelCwMweb extends CwMwebPlatform { final methodChannel = const MethodChannel('cw_mweb'); @override - Future<bool?> start(String dataDir) async { - final result = await methodChannel.invokeMethod<bool>('start', {'dataDir': dataDir}); + Future<int?> start(String dataDir) async { + final result = await methodChannel.invokeMethod<int>('start', {'dataDir': dataDir}); return result; } } diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart index ce518402f..974e07284 100644 --- a/cw_mweb/lib/cw_mweb_platform_interface.dart +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -23,7 +23,7 @@ abstract class CwMwebPlatform extends PlatformInterface { _instance = instance; } - Future<bool?> start(String dataDir) { + Future<int?> start(String dataDir) { throw UnimplementedError('start() has not been implemented.'); } } From dbc005dcf3a704b922d77af7c42c1530d773c4ba Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sun, 21 Apr 2024 19:58:45 +0100 Subject: [PATCH 04/31] Fix stub creation --- cw_bitcoin/lib/litecoin_wallet.dart | 2 +- cw_bitcoin/lib/litecoin_wallet_service.dart | 6 ------ cw_mweb/lib/cw_mweb.dart | 13 ++++++------- cw_mweb/pubspec.yaml | 2 ++ 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 628391eed..b6b996b27 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -111,7 +111,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future<void> startSync() async { await super.startSync(); - final stub = CwMweb.stub(); + final stub = await CwMweb.stub(); Timer.periodic( const Duration(milliseconds: 1500), (timer) async { final height = await electrumClient.getCurrentBlockChainTip() ?? 0; diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 7f2066c30..ee3b0e628 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'package:path_provider/path_provider.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; @@ -11,7 +10,6 @@ import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:cw_mweb/cw_mweb.dart'; import 'package:collection/collection.dart'; class LitecoinWalletService extends WalletService< @@ -28,8 +26,6 @@ class LitecoinWalletService extends WalletService< @override Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { - final appDir = await getApplicationSupportDirectory(); - await CwMweb.start(appDir.path); final wallet = await LitecoinWalletBase.create( mnemonic: await generateMnemonic(), password: credentials.password!, @@ -47,8 +43,6 @@ class LitecoinWalletService extends WalletService< @override Future<LitecoinWallet> openWallet(String name, String password) async { - final appDir = await getApplicationSupportDirectory(); - await CwMweb.start(appDir.path); final walletInfo = walletInfoSource.values.firstWhereOrNull( (info) => info.id == WalletBase.idFor(name, getType()))!; diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index fb3960ab0..a54295673 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -1,17 +1,16 @@ import 'package:grpc/grpc.dart'; +import 'package:path_provider/path_provider.dart'; import 'cw_mweb_platform_interface.dart'; import 'mwebd.pbgrpc.dart'; class CwMweb { - static var port; + static Future<int?>? port; - static start(String dataDir) async { - port = port ?? await CwMwebPlatform.instance.start(dataDir); - } - - static stub() { + static Future<RpcClient> stub() async { + final appDir = await getApplicationSupportDirectory(); + port ??= CwMwebPlatform.instance.start(appDir.path); return RpcClient(ClientChannel('127.0.0.1', - port: port, + port: await port ?? 0, options: const ChannelOptions( credentials: ChannelCredentials.insecure()))); } diff --git a/cw_mweb/pubspec.yaml b/cw_mweb/pubspec.yaml index 5416963d8..cfe43c70b 100644 --- a/cw_mweb/pubspec.yaml +++ b/cw_mweb/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + grpc: ^3.2.4 + path_provider: ^2.1.2 plugin_platform_interface: ^2.0.2 dev_dependencies: From 29238effdf17fa5047e8f30c232cab913b1f2482 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Mon, 22 Apr 2024 11:45:16 +0100 Subject: [PATCH 05/31] Generate MWEB addresses --- .../lib/bitcoin_receive_page_option.dart | 8 ++++ cw_bitcoin/lib/electrum_wallet.dart | 4 ++ cw_bitcoin/lib/electrum_wallet_addresses.dart | 8 +++- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 43 +++++++++++++++++-- cw_bitcoin/pubspec.lock | 8 ++-- cw_bitcoin/pubspec.yaml | 4 +- cw_bitcoin_cash/pubspec.yaml | 4 +- lib/bitcoin/cw_bitcoin.dart | 2 + .../screens/dashboard/pages/address_page.dart | 3 +- .../dashboard/receive_option_view_model.dart | 32 +++++++++----- .../wallet_address_list_view_model.dart | 3 +- 11 files changed, 91 insertions(+), 28 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 2d2339a41..714f1da90 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -7,6 +7,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)'); static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); + static const mweb = BitcoinReceivePageOption._('MWEB'); const BitcoinReceivePageOption._(this.value); @@ -24,12 +25,19 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinReceivePageOption.p2pkh ]; + static const allLitecoin = [ + BitcoinReceivePageOption.p2wpkh, + BitcoinReceivePageOption.mweb + ]; + factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) { switch (type) { case SegwitAddresType.p2tr: return BitcoinReceivePageOption.p2tr; case SegwitAddresType.p2wsh: return BitcoinReceivePageOption.p2wsh; + case SegwitAddresType.mweb: + return BitcoinReceivePageOption.mweb; case P2pkhAddressType.p2pkh: return BitcoinReceivePageOption.p2pkh; case P2shAddressType.p2wpkhInP2sh: diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4a76ee5dd..17e9880e9 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1337,6 +1337,8 @@ BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) return P2wshAddress.fromAddress(address: address, network: network); } else if (P2trAddress.regex.hasMatch(address)) { return P2trAddress.fromAddress(address: address, network: network); + } else if (MwebAddress.regex.hasMatch(address)) { + return MwebAddress.fromAddress(address: address, network: network); } else { return P2wpkhAddress.fromAddress(address: address, network: network); } @@ -1351,6 +1353,8 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { return SegwitAddresType.p2wsh; } else if (type is P2trAddress) { return SegwitAddresType.p2tr; + } else if (type is MwebAddress) { + return SegwitAddresType.mweb; } else { return SegwitAddresType.p2wpkh; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index c43d4988a..6a7a9fd18 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -16,6 +16,7 @@ const List<BitcoinAddressType> ADDRESS_TYPES = [ P2pkhAddressType.p2pkh, SegwitAddresType.p2tr, SegwitAddresType.p2wsh, + SegwitAddresType.mweb, P2shAddressType.p2wpkhInP2sh, ]; @@ -154,6 +155,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { await _generateInitialAddresses(); + await _generateInitialAddresses(type: SegwitAddresType.mweb); } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); @@ -217,6 +219,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => ''; + Future<String> getAddressAsync( + {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) async => + getAddress(index: index, hd: hd, addressType: addressType); + @override Future<void> updateAddressesInBox() async { try { @@ -328,7 +334,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( - getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), index: i, isHidden: isHidden, type: type ?? addressPageType, diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 993d17933..fead1de52 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,8 +1,11 @@ +import 'package:convert/convert.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:cw_mweb/mwebd.pb.dart'; import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; @@ -21,8 +24,40 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with super.initialChangeAddressIndex, }) : super(walletInfo); + List<String> mweb_addrs = []; + + Future<void> topUpMweb(int index) async { + while (mweb_addrs.length - index < 1000) { + final length = mweb_addrs.length; + final scanSecret = mainHd.derive(0).privKey!; + final spendPubkey = mainHd.derive(1).pubKey!; + final stub = await CwMweb.stub(); + final resp = await stub.addresses(AddressRequest( + fromIndex: length, + toIndex: index + 1000, + scanSecret: hex.decode(scanSecret), + spendPubkey: hex.decode(spendPubkey), + )); + if (mweb_addrs.length == length) { + mweb_addrs.addAll(resp.address); + } + } + } + @override - String getAddress( - {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => - generateP2WPKHAddress(hd: hd, index: index, network: network); + String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) { + if (addressType == SegwitAddresType.mweb) { + topUpMweb(index); + return hd == sideHd ? mweb_addrs[0] : mweb_addrs[index+1]; + } + return generateP2WPKHAddress(hd: hd, index: index, network: network); + } + + @override + Future<String> getAddressAsync({required int index, required HDWallet hd, BitcoinAddressType? addressType}) async { + if (addressType == SegwitAddresType.mweb) { + await topUpMweb(index); + } + return getAddress(index: index, hd: hd, addressType: addressType); + } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 20851ea86..f93365d89 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -86,11 +86,9 @@ packages: bitcoin_base: dependency: "direct main" description: - path: "." - ref: cake-update-v2 - resolved-ref: "01d844a5f5a520a31df5254e34169af4664aa769" - url: "https://github.com/cake-tech/bitcoin_base.git" - source: git + path: "../../bitcoin_base" + relative: true + source: path version: "4.2.0" bitcoin_flutter: dependency: "direct main" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 033d1f84e..9c3c5d751 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -31,9 +31,7 @@ dependencies: unorm_dart: ^0.2.0 cryptography: ^2.0.5 bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v2 + path: ../../bitcoin_base blockchain_utils: ^2.1.1 cw_mweb: path: ../cw_mweb diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 37827f1ba..e491cea32 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -30,9 +30,7 @@ dependencies: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v2 + path: ../../bitcoin_base diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 707f1157b..77c940ac4 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -242,6 +242,8 @@ class CWBitcoin extends Bitcoin { return SegwitAddresType.p2tr; case BitcoinReceivePageOption.p2wsh: return SegwitAddresType.p2wsh; + case BitcoinReceivePageOption.mweb: + return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: default: return SegwitAddresType.p2wpkh; diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 3c77cad48..0e51f759d 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -219,7 +219,8 @@ class AddressPage extends BasePage { } break; default: - if (addressListViewModel.type == WalletType.bitcoin) { + if (addressListViewModel.type == WalletType.bitcoin || + addressListViewModel.type == WalletType.litecoin) { addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); } } diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 1e4726eee..17e17842c 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; @@ -11,19 +12,30 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) : selectedReceiveOption = initialPageOption ?? - (_wallet.type == WalletType.bitcoin + (_wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.litecoin ? bitcoin!.getSelectedAddressType(_wallet) : ReceivePageOption.mainnet), _options = [] { - final walletType = _wallet.type; - _options = walletType == WalletType.haven - ? [ReceivePageOption.mainnet] - : walletType == WalletType.bitcoin - ? [ - ...bitcoin!.getBitcoinReceivePageOptions(), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ] - : ReceivePageOptions; + switch (_wallet.type) { + case WalletType.bitcoin: + _options = [ + ...bitcoin!.getBitcoinReceivePageOptions(), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; + case WalletType.litecoin: + _options = [ + ...BitcoinReceivePageOption.allLitecoin, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; + case WalletType.haven: + _options = [ReceivePageOption.mainnet]; + break; + default: + _options = ReceivePageOptions; + }; } final WalletBase _wallet; diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 20980f5f0..5f3b2b9c2 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -404,7 +404,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action Future<void> setAddressType(dynamic option) async { - if (wallet.type == WalletType.bitcoin) { + if (wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin) { await bitcoin!.setAddressType(wallet, option); } } From 407b73171e96b7ba74e92435ad2d5c60c25014d2 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Mon, 22 Apr 2024 12:48:11 +0100 Subject: [PATCH 06/31] Fix mweb address derivation --- cw_bitcoin/lib/litecoin_wallet.dart | 1 + cw_bitcoin/lib/litecoin_wallet_addresses.dart | 6 ++++-- lib/view_model/dashboard/receive_option_view_model.dart | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index b6b996b27..1608a3dba 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -54,6 +54,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), + mwebHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/1000'"), network: network, ); autorun((_) { diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index fead1de52..8875efa18 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -17,6 +17,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with WalletInfo walletInfo, { required super.mainHd, required super.sideHd, + required this.mwebHd, required super.network, required super.electrumClient, super.initialAddresses, @@ -24,13 +25,14 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with super.initialChangeAddressIndex, }) : super(walletInfo); + final HDWallet mwebHd; List<String> mweb_addrs = []; Future<void> topUpMweb(int index) async { while (mweb_addrs.length - index < 1000) { final length = mweb_addrs.length; - final scanSecret = mainHd.derive(0).privKey!; - final spendPubkey = mainHd.derive(1).pubKey!; + final scanSecret = mwebHd.derive(0x80000000).privKey!; + final spendPubkey = mwebHd.derive(0x80000001).pubKey!; final stub = await CwMweb.stub(); final resp = await stub.addresses(AddressRequest( fromIndex: length, diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 17e17842c..472918f4a 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -35,7 +35,7 @@ abstract class ReceiveOptionViewModelBase with Store { break; default: _options = ReceivePageOptions; - }; + } } final WalletBase _wallet; From 90588ee88a01c0bb542b62038e8245653dbb07dc Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Mon, 22 Apr 2024 13:11:31 +0100 Subject: [PATCH 07/31] Use camel-case --- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 8875efa18..e1ee81b31 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -26,11 +26,11 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with }) : super(walletInfo); final HDWallet mwebHd; - List<String> mweb_addrs = []; + List<String> mwebAddrs = []; Future<void> topUpMweb(int index) async { - while (mweb_addrs.length - index < 1000) { - final length = mweb_addrs.length; + while (mwebAddrs.length - index < 1000) { + final length = mwebAddrs.length; final scanSecret = mwebHd.derive(0x80000000).privKey!; final spendPubkey = mwebHd.derive(0x80000001).pubKey!; final stub = await CwMweb.stub(); @@ -40,8 +40,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with scanSecret: hex.decode(scanSecret), spendPubkey: hex.decode(spendPubkey), )); - if (mweb_addrs.length == length) { - mweb_addrs.addAll(resp.address); + if (mwebAddrs.length == length) { + mwebAddrs.addAll(resp.address); } } } @@ -50,7 +50,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) { if (addressType == SegwitAddresType.mweb) { topUpMweb(index); - return hd == sideHd ? mweb_addrs[0] : mweb_addrs[index+1]; + return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index+1]; } return generateP2WPKHAddress(hd: hd, index: index, network: network); } From 9aa5ef83316d20f51479b3f66e947490e98a84a0 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Mon, 22 Apr 2024 17:18:18 +0100 Subject: [PATCH 08/31] Show utxos in tx list --- cw_bitcoin/lib/electrum.dart | 9 +++++++-- cw_bitcoin/lib/litecoin_wallet.dart | 31 +++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 0553170cc..359f9b36d 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:convert/convert.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/script_hash.dart'; @@ -263,8 +264,12 @@ class ElectrumClient { await call(method: 'blockchain.transaction.get_merkle', params: [hash, height]) as Map<String, dynamic>; - Future<Map<String, dynamic>> getHeader({required int height}) async => - await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>; + Future<DateTime> getBlockTime({required int height}) async { + final header = await call(method: 'blockchain.block.header', params: [height]) as String; + final bd = ByteData.sublistView(Uint8List.fromList(hex.decode(header))); + final timestamp = bd.getUint32(68, Endian.little) * 1000; + return DateTime.fromMillisecondsSinceEpoch(timestamp, isUtc: true); + } Future<double> estimatefee({required int p}) => call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 1608a3dba..e71a68dee 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'package:convert/convert.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -11,6 +14,7 @@ import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -36,7 +40,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialChangeAddressIndex, - }) : super( + }) : mwebHd = bitcoin.HDWallet.fromSeed(seedBytes, + network: litecoinNetwork).derivePath("m/1000'"), + super( mnemonic: mnemonic, password: password, walletInfo: walletInfo, @@ -54,7 +60,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), - mwebHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/1000'"), + mwebHd: mwebHd, network: network, ); autorun((_) { @@ -62,6 +68,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }); } + final bitcoin.HDWallet mwebHd; + static Future<LitecoinWallet> create( {required String mnemonic, required String password, @@ -129,6 +137,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { syncStatus = SyncedSyncStatus(); } }); + final scanSecret = mwebHd.derive(0x80000000).privKey!; + final req = UtxosRequest(scanSecret: hex.decode(scanSecret)); + await for (var utxo in stub.utxos(req)) { + final status = await stub.status(StatusRequest()); + var date = DateTime.now(); + var confirmations = 0; + if (utxo.height > 0) { + date = await electrumClient.getBlockTime(height: utxo.height); + confirmations = status.blockHeaderHeight - utxo.height + 1; + } + final tx = ElectrumTransactionInfo(WalletType.litecoin, + id: utxo.outputId, height: utxo.height, + amount: utxo.value.toInt(), + direction: TransactionDirection.incoming, + isPending: utxo.height == 0, + date: date, confirmations: confirmations); + transactionHistory.addOne(tx); + await transactionHistory.save(); + } } @override From b07c3d8a54a76dec713a06164086aba615e2846b Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Mon, 22 Apr 2024 18:07:59 +0100 Subject: [PATCH 09/31] A few fixes --- cw_bitcoin/lib/electrum.dart | 2 +- cw_bitcoin/lib/electrum_transaction_info.dart | 4 ++-- cw_bitcoin/lib/litecoin_wallet.dart | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 359f9b36d..d94dccbba 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -268,7 +268,7 @@ class ElectrumClient { final header = await call(method: 'blockchain.block.header', params: [height]) as String; final bd = ByteData.sublistView(Uint8List.fromList(hex.decode(header))); final timestamp = bd.getUint32(68, Endian.little) * 1000; - return DateTime.fromMillisecondsSinceEpoch(timestamp, isUtc: true); + return DateTime.fromMillisecondsSinceEpoch(timestamp); } Future<double> estimatefee({required int p}) => diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index f980bd884..00315319c 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -196,8 +196,8 @@ class ElectrumTransactionInfo extends TransactionInfo { direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), isPending: data['isPending'] as bool, - inputAddresses: data['inputAddresses'] as List<String>, - outputAddresses: data['outputAddresses'] as List<String>, + inputAddresses: List<String>.from(data['inputAddresses'] as List), + outputAddresses: List<String>.from(data['outputAddresses'] as List), confirmations: data['confirmations'] as int); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index e71a68dee..8fa5af63b 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -149,10 +149,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } final tx = ElectrumTransactionInfo(WalletType.litecoin, id: utxo.outputId, height: utxo.height, - amount: utxo.value.toInt(), + amount: utxo.value.toInt(), fee: 0, direction: TransactionDirection.incoming, isPending: utxo.height == 0, - date: date, confirmations: confirmations); + date: date, confirmations: confirmations, + inputAddresses: [], + outputAddresses: [utxo.address]); transactionHistory.addOne(tx); await transactionHistory.save(); } From 4fc1de8cb6b217ba051f62a4065f00c2dda13772 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Tue, 23 Apr 2024 00:26:34 +0100 Subject: [PATCH 10/31] Add spent processing --- cw_bitcoin/lib/litecoin_wallet.dart | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 8fa5af63b..547a2dda5 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -137,9 +138,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { syncStatus = SyncedSyncStatus(); } }); + processMwebUtxos(); + } + + final Map<String, Utxo> mwebUtxos = {}; + + Future<void> processMwebUtxos() async { + final stub = await CwMweb.stub(); final scanSecret = mwebHd.derive(0x80000000).privKey!; final req = UtxosRequest(scanSecret: hex.decode(scanSecret)); await for (var utxo in stub.utxos(req)) { + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + if (!mwebAddrs.contains(utxo.address)) continue; + mwebUtxos[utxo.outputId] = utxo; + final status = await stub.status(StatusRequest()); var date = DateTime.now(); var confirmations = 0; @@ -160,6 +172,44 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } } + Future<void> checkMwebUtxosSpent() async { + final List<String> outputIds = []; + mwebUtxos.forEach((outputId, utxo) { + if (utxo.height > 0) + outputIds.add(outputId); + }); + final stub = await CwMweb.stub(); + final resp = await stub.spent(SpentRequest(outputId: outputIds)); + final spent = resp.outputId; + if (spent.isEmpty) return; + final status = await stub.status(StatusRequest()); + final height = await electrumClient.getCurrentBlockChainTip(); + if (height == null || status.mwebUtxosHeight != height) return; + final date = await electrumClient.getBlockTime(height: height); + int amount = 0; + Set<String> inputAddresses = {}; + var output = AccumulatorSink<Digest>(); + var input = sha256.startChunkedConversion(output); + for (final outputId in spent) { + input.add(hex.decode(outputId)); + amount += mwebUtxos[outputId]!.value.toInt(); + inputAddresses.add(mwebUtxos[outputId]!.address); + mwebUtxos.remove(outputId); + } + input.close(); + var digest = output.events.single; + final tx = ElectrumTransactionInfo(WalletType.litecoin, + id: digest.toString(), height: height, + amount: amount, fee: 0, + direction: TransactionDirection.outgoing, + isPending: false, + date: date, confirmations: 1, + inputAddresses: inputAddresses.toList(), + outputAddresses: []); + transactionHistory.addOne(tx); + await transactionHistory.save(); + } + @override int feeRate(TransactionPriority priority) { if (priority is LitecoinTransactionPriority) { From 50ef9185f53a2f8e9d30c083aadd834eefe7b5ba Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Tue, 23 Apr 2024 13:37:44 +0100 Subject: [PATCH 11/31] Update balance --- cw_bitcoin/lib/electrum_wallet.dart | 6 +++++- cw_bitcoin/lib/litecoin_wallet.dart | 33 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 17e9880e9..6794d93c5 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -740,7 +740,7 @@ abstract class ElectrumWalletBase Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - Future<void> updateUnspent() async { + Future<void> updateUnspentCoins() async { List<BitcoinUnspent> updatedUnspentCoins = []; final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); @@ -759,6 +759,10 @@ abstract class ElectrumWalletBase })))); unspentCoins = updatedUnspentCoins; + } + + Future<void> updateUnspent() async { + await updateUnspentCoins(); if (unspentCoinsInfo.isEmpty) { unspentCoins.forEach((coin) => _addCoinInfo(coin)); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 547a2dda5..1ff5b5ac9 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -4,6 +4,7 @@ import 'package:crypto/crypto.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; @@ -169,6 +170,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { outputAddresses: [utxo.address]); transactionHistory.addOne(tx); await transactionHistory.save(); + await updateUnspent(); + await updateBalance(); } } @@ -210,6 +213,36 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await transactionHistory.save(); } + @override + Future<void> updateUnspentCoins() async { + await super.updateUnspentCoins(); + await checkMwebUtxosSpent(); + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + mwebUtxos.forEach((outputId, utxo) { + final addressRecord = walletAddresses.allAddresses.firstWhere( + (addressRecord) => addressRecord.address == utxo.address); + unspentCoins.add(BitcoinUnspent(addressRecord, outputId, + utxo.value.toInt(), mwebAddrs.indexOf(utxo.address))); + }); + } + + @override + Future<void> updateBalance() async { + await super.updateBalance(); + var confirmed = balance[currency]!.confirmed; + var unconfirmed = balance[currency]!.unconfirmed; + mwebUtxos.values.forEach((utxo) { + if (utxo.height > 0) + confirmed += utxo.value.toInt(); + else + unconfirmed += utxo.value.toInt(); + }); + balance[currency] = ElectrumBalance( + confirmed: confirmed, unconfirmed: unconfirmed, + frozen: balance[currency]!.frozen); + await save(); + } + @override int feeRate(TransactionPriority priority) { if (priority is LitecoinTransactionPriority) { From 05b71a372451e8b10a3329ccf245faba97dbc04d Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Tue, 23 Apr 2024 17:16:47 +0100 Subject: [PATCH 12/31] Balance fixes --- cw_bitcoin/lib/electrum_wallet.dart | 8 +++--- cw_bitcoin/lib/litecoin_wallet.dart | 43 +++++++++++++++++++++-------- cw_bitcoin/pubspec.lock | 8 ++++++ cw_bitcoin/pubspec.yaml | 1 + 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 6794d93c5..a2839b1e3 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -157,7 +157,7 @@ abstract class ElectrumWalletBase await updateTransactions(); _subscribeForUpdates(); await updateUnspent(); - await updateBalance(); + await updateBalance(delay: 5); _feeRates = await electrumClient.feeRates(network: network); Timer.periodic( @@ -1196,7 +1196,7 @@ abstract class ElectrumWalletBase }); } - Future<ElectrumBalance> _fetchBalances() async { + Future<ElectrumBalance> fetchBalances() async { final addresses = walletAddresses.allAddresses.toList(); final balanceFutures = <Future<Map<String, dynamic>>>[]; for (var i = 0; i < addresses.length; i++) { @@ -1240,8 +1240,8 @@ abstract class ElectrumWalletBase confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen); } - Future<void> updateBalance() async { - balance[currency] = await _fetchBalances(); + Future<void> updateBalance({int delay = 1}) async { + balance[currency] = await fetchBalances(); await save(); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 1ff5b5ac9..35bb143f3 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -25,6 +25,7 @@ import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_mweb/cw_mweb.dart'; import 'package:cw_mweb/mwebd.pb.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:queue/queue.dart'; part 'litecoin_wallet.g.dart'; @@ -157,7 +158,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { var date = DateTime.now(); var confirmations = 0; if (utxo.height > 0) { - date = await electrumClient.getBlockTime(height: utxo.height); + while (true) try { + date = await electrumClient.getBlockTime(height: utxo.height); + break; + } catch (err) { + await Future.delayed(Duration(seconds: 1)); + } confirmations = status.blockHeaderHeight - utxo.height + 1; } final tx = ElectrumTransactionInfo(WalletType.litecoin, @@ -169,9 +175,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { inputAddresses: [], outputAddresses: [utxo.address]); transactionHistory.addOne(tx); - await transactionHistory.save(); - await updateUnspent(); - await updateBalance(); + queueUpdate(1); } } @@ -227,20 +231,35 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future<void> updateBalance() async { - await super.updateBalance(); - var confirmed = balance[currency]!.confirmed; - var unconfirmed = balance[currency]!.unconfirmed; + Future<ElectrumBalance> fetchBalances() async { + final balance = await super.fetchBalances(); + var confirmed = balance.confirmed; + var unconfirmed = balance.unconfirmed; mwebUtxos.values.forEach((utxo) { if (utxo.height > 0) confirmed += utxo.value.toInt(); else unconfirmed += utxo.value.toInt(); }); - balance[currency] = ElectrumBalance( - confirmed: confirmed, unconfirmed: unconfirmed, - frozen: balance[currency]!.frozen); - await save(); + return ElectrumBalance(confirmed: confirmed, + unconfirmed: unconfirmed, frozen: balance.frozen); + } + + @override + Future<void> updateBalance({int delay = 1}) async { + queueUpdate(delay); + } + + Timer? _debounceTimer; + final _debounceQueue = Queue(); + + void queueUpdate(int delay) { + if (_debounceTimer?.isActive ?? false) _debounceTimer?.cancel(); + _debounceTimer = Timer(Duration(seconds: delay), () => + _debounceQueue.add(() async { + await updateUnspent(); + await super.updateBalance(); + })); } @override diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index f93365d89..f081d7e87 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -651,6 +651,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + queue: + dependency: "direct main" + description: + name: queue + sha256: "9a41ecadc15db79010108c06eae229a45c56b18db699760f34e8c9ac9b831ff9" + url: "https://pub.dev" + source: hosted + version: "3.1.0+2" rxdart: dependency: "direct main" description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 9c3c5d751..4615f1219 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: cw_mweb: path: ../cw_mweb grpc: ^3.2.4 + queue: ^3.1.0+2 dev_dependencies: flutter_test: From 3e5a5f18faed191245dd99a02df5c3e2d817a661 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Tue, 23 Apr 2024 18:31:30 +0100 Subject: [PATCH 13/31] Update address records --- cw_bitcoin/lib/litecoin_wallet.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 35bb143f3..2da4c407c 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -174,6 +174,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { date: date, confirmations: confirmations, inputAddresses: [], outputAddresses: [utxo.address]); + if (transactionHistory.transactions[utxo.outputId] == null) { + final addressRecord = walletAddresses.allAddresses.firstWhere( + (addressRecord) => addressRecord.address == utxo.address); + addressRecord.txCount++; + addressRecord.balance += utxo.value.toInt(); + } transactionHistory.addOne(tx); queueUpdate(1); } @@ -199,8 +205,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { var input = sha256.startChunkedConversion(output); for (final outputId in spent) { input.add(hex.decode(outputId)); - amount += mwebUtxos[outputId]!.value.toInt(); - inputAddresses.add(mwebUtxos[outputId]!.address); + final utxo = mwebUtxos[outputId]!; + final addressRecord = walletAddresses.allAddresses.firstWhere( + (addressRecord) => addressRecord.address == utxo.address); + if (!inputAddresses.contains(utxo.address)) + addressRecord.txCount++; + addressRecord.balance -= utxo.value.toInt(); + amount += utxo.value.toInt(); + inputAddresses.add(utxo.address); mwebUtxos.remove(outputId); } input.close(); From 37ff38f0dfbc7656be1e6dd1f2d1f8755532641a Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Wed, 24 Apr 2024 11:53:43 +0100 Subject: [PATCH 14/31] Get rid of debounce hack --- cw_bitcoin/lib/electrum.dart | 9 ++---- cw_bitcoin/lib/electrum_wallet.dart | 9 ++++-- cw_bitcoin/lib/litecoin_wallet.dart | 44 +++++++++++------------------ cw_bitcoin/pubspec.lock | 8 ------ cw_bitcoin/pubspec.yaml | 1 - cw_mweb/lib/mwebd.pb.dart | 28 ++++++++++++++++++ 6 files changed, 52 insertions(+), 47 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index d94dccbba..0553170cc 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:convert/convert.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/script_hash.dart'; @@ -264,12 +263,8 @@ class ElectrumClient { await call(method: 'blockchain.transaction.get_merkle', params: [hash, height]) as Map<String, dynamic>; - Future<DateTime> getBlockTime({required int height}) async { - final header = await call(method: 'blockchain.block.header', params: [height]) as String; - final bd = ByteData.sublistView(Uint8List.fromList(hex.decode(header))); - final timestamp = bd.getUint32(68, Endian.little) * 1000; - return DateTime.fromMillisecondsSinceEpoch(timestamp); - } + Future<Map<String, dynamic>> getHeader({required int height}) async => + await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>; Future<double> estimatefee({required int p}) => call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index a2839b1e3..b25069863 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -21,6 +21,7 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/litecoin_network.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/utils.dart'; @@ -156,8 +157,10 @@ abstract class ElectrumWalletBase syncStatus = AttemptingSyncStatus(); await updateTransactions(); _subscribeForUpdates(); - await updateUnspent(); - await updateBalance(delay: 5); + if (!(this is LitecoinWallet)) { + await updateUnspent(); + await updateBalance(); + } _feeRates = await electrumClient.feeRates(network: network); Timer.periodic( @@ -1240,7 +1243,7 @@ abstract class ElectrumWalletBase confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen); } - Future<void> updateBalance({int delay = 1}) async { + Future<void> updateBalance() async { balance[currency] = await fetchBalances(); await save(); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 2da4c407c..7506e368a 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -25,7 +25,6 @@ import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_mweb/cw_mweb.dart'; import 'package:cw_mweb/mwebd.pb.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:queue/queue.dart'; part 'litecoin_wallet.g.dart'; @@ -149,7 +148,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final stub = await CwMweb.stub(); final scanSecret = mwebHd.derive(0x80000000).privKey!; final req = UtxosRequest(scanSecret: hex.decode(scanSecret)); + var initDone = false; await for (var utxo in stub.utxos(req)) { + if (utxo.address.isEmpty) { + await updateUnspent(); + await updateBalance(); + initDone = true; + } final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; if (!mwebAddrs.contains(utxo.address)) continue; mwebUtxos[utxo.outputId] = utxo; @@ -158,12 +163,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { var date = DateTime.now(); var confirmations = 0; if (utxo.height > 0) { - while (true) try { - date = await electrumClient.getBlockTime(height: utxo.height); - break; - } catch (err) { - await Future.delayed(Duration(seconds: 1)); - } + date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000); confirmations = status.blockHeaderHeight - utxo.height + 1; } final tx = ElectrumTransactionInfo(WalletType.litecoin, @@ -179,9 +179,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { (addressRecord) => addressRecord.address == utxo.address); addressRecord.txCount++; addressRecord.balance += utxo.value.toInt(); + addressRecord.setAsUsed(); } transactionHistory.addOne(tx); - queueUpdate(1); + if (initDone) { + await updateUnspent(); + await updateBalance(); + } } } @@ -197,8 +201,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (spent.isEmpty) return; final status = await stub.status(StatusRequest()); final height = await electrumClient.getCurrentBlockChainTip(); - if (height == null || status.mwebUtxosHeight != height) return; - final date = await electrumClient.getBlockTime(height: height); + if (height == null || status.blockHeaderHeight != height) return; + if (status.mwebUtxosHeight != height) return; int amount = 0; Set<String> inputAddresses = {}; var output = AccumulatorSink<Digest>(); @@ -222,7 +226,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { amount: amount, fee: 0, direction: TransactionDirection.outgoing, isPending: false, - date: date, confirmations: 1, + date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000), + confirmations: 1, inputAddresses: inputAddresses.toList(), outputAddresses: []); transactionHistory.addOne(tx); @@ -257,23 +262,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unconfirmed: unconfirmed, frozen: balance.frozen); } - @override - Future<void> updateBalance({int delay = 1}) async { - queueUpdate(delay); - } - - Timer? _debounceTimer; - final _debounceQueue = Queue(); - - void queueUpdate(int delay) { - if (_debounceTimer?.isActive ?? false) _debounceTimer?.cancel(); - _debounceTimer = Timer(Duration(seconds: delay), () => - _debounceQueue.add(() async { - await updateUnspent(); - await super.updateBalance(); - })); - } - @override int feeRate(TransactionPriority priority) { if (priority is LitecoinTransactionPriority) { diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index f081d7e87..f93365d89 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -651,14 +651,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" - queue: - dependency: "direct main" - description: - name: queue - sha256: "9a41ecadc15db79010108c06eae229a45c56b18db699760f34e8c9ac9b831ff9" - url: "https://pub.dev" - source: hosted - version: "3.1.0+2" rxdart: dependency: "direct main" description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 4615f1219..9c3c5d751 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -36,7 +36,6 @@ dependencies: cw_mweb: path: ../cw_mweb grpc: ^3.2.4 - queue: ^3.1.0+2 dev_dependencies: flutter_test: diff --git a/cw_mweb/lib/mwebd.pb.dart b/cw_mweb/lib/mwebd.pb.dart index 8d139d7eb..d0dd486c0 100644 --- a/cw_mweb/lib/mwebd.pb.dart +++ b/cw_mweb/lib/mwebd.pb.dart @@ -51,6 +51,7 @@ class StatusResponse extends $pb.GeneratedMessage { $core.int? blockHeaderHeight, $core.int? mwebHeaderHeight, $core.int? mwebUtxosHeight, + $core.int? blockTime, }) { final $result = create(); if (blockHeaderHeight != null) { @@ -62,6 +63,9 @@ class StatusResponse extends $pb.GeneratedMessage { if (mwebUtxosHeight != null) { $result.mwebUtxosHeight = mwebUtxosHeight; } + if (blockTime != null) { + $result.blockTime = blockTime; + } return $result; } StatusResponse._() : super(); @@ -72,6 +76,7 @@ class StatusResponse extends $pb.GeneratedMessage { ..a<$core.int>(1, _omitFieldNames ? '' : 'blockHeaderHeight', $pb.PbFieldType.O3) ..a<$core.int>(2, _omitFieldNames ? '' : 'mwebHeaderHeight', $pb.PbFieldType.O3) ..a<$core.int>(3, _omitFieldNames ? '' : 'mwebUtxosHeight', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3) ..hasRequiredFields = false ; @@ -122,6 +127,15 @@ class StatusResponse extends $pb.GeneratedMessage { $core.bool hasMwebUtxosHeight() => $_has(2); @$pb.TagNumber(3) void clearMwebUtxosHeight() => clearField(3); + + @$pb.TagNumber(4) + $core.int get blockTime => $_getIZ(3); + @$pb.TagNumber(4) + set blockTime($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasBlockTime() => $_has(3); + @$pb.TagNumber(4) + void clearBlockTime() => clearField(4); } class UtxosRequest extends $pb.GeneratedMessage { @@ -194,6 +208,7 @@ class Utxo extends $pb.GeneratedMessage { $fixnum.Int64? value, $core.String? address, $core.String? outputId, + $core.int? blockTime, }) { final $result = create(); if (height != null) { @@ -208,6 +223,9 @@ class Utxo extends $pb.GeneratedMessage { if (outputId != null) { $result.outputId = outputId; } + if (blockTime != null) { + $result.blockTime = blockTime; + } return $result; } Utxo._() : super(); @@ -219,6 +237,7 @@ class Utxo extends $pb.GeneratedMessage { ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..aOS(3, _omitFieldNames ? '' : 'address') ..aOS(4, _omitFieldNames ? '' : 'outputId') + ..a<$core.int>(5, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3) ..hasRequiredFields = false ; @@ -278,6 +297,15 @@ class Utxo extends $pb.GeneratedMessage { $core.bool hasOutputId() => $_has(3); @$pb.TagNumber(4) void clearOutputId() => clearField(4); + + @$pb.TagNumber(5) + $core.int get blockTime => $_getIZ(4); + @$pb.TagNumber(5) + set blockTime($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasBlockTime() => $_has(4); + @$pb.TagNumber(5) + void clearBlockTime() => clearField(5); } class AddressRequest extends $pb.GeneratedMessage { From 404672f10fdbc5bc780822af69a434d525df01cc Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Wed, 24 Apr 2024 14:49:51 +0100 Subject: [PATCH 15/31] Get sending up to the confirmation box --- cw_bitcoin/lib/electrum_wallet.dart | 2 +- cw_bitcoin/lib/litecoin_wallet.dart | 6 ++++-- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 7 +++++++ lib/core/address_validator.dart | 16 +++++++++------- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index b25069863..37937f45c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -157,7 +157,7 @@ abstract class ElectrumWalletBase syncStatus = AttemptingSyncStatus(); await updateTransactions(); _subscribeForUpdates(); - if (!(this is LitecoinWallet)) { + if (this is! LitecoinWallet) { await updateUnspent(); await updateBalance(); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 7506e368a..adce6e6b7 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -242,8 +242,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mwebUtxos.forEach((outputId, utxo) { final addressRecord = walletAddresses.allAddresses.firstWhere( (addressRecord) => addressRecord.address == utxo.address); - unspentCoins.add(BitcoinUnspent(addressRecord, outputId, - utxo.value.toInt(), mwebAddrs.indexOf(utxo.address))); + final unspent = BitcoinUnspent(addressRecord, outputId, + utxo.value.toInt(), mwebAddrs.indexOf(utxo.address)); + if (unspent.vout == 0) unspent.isChange = true; + unspentCoins.add(unspent); }); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index e1ee81b31..226bd1fc6 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -62,4 +62,11 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with } return getAddress(index: index, hd: hd, addressType: addressType); } + + @action + @override + Future<String> getChangeAddress() async { + await topUpMweb(0); + return mwebAddrs[0]; + } } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 967cf9bf0..bb69d4ecf 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -9,8 +9,9 @@ class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( errorMessage: S.current.error_text_address, - useAdditionalValidation: type == CryptoCurrency.btc - ? (String txt) => validateAddress(address: txt, network: BitcoinNetwork.mainnet) + useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc + ? (String txt) => validateAddress(address: txt, network: + type == CryptoCurrency.btc ? BitcoinNetwork.mainnet : LitecoinNetwork.mainnet) : null, pattern: getPattern(type), length: getLength(type)); @@ -27,6 +28,8 @@ class AddressValidator extends TextValidator { '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; case CryptoCurrency.btc: return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$'; + case CryptoCurrency.ltc: + return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${MwebAddress.regex.pattern}\$'; case CryptoCurrency.nano: return '[0-9a-zA-Z_]'; case CryptoCurrency.banano: @@ -96,8 +99,6 @@ class AddressValidator extends TextValidator { return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$'; case CryptoCurrency.bnb: return '[0-9a-zA-Z]'; - case CryptoCurrency.ltc: - return '^(?!(ltc|LTC)1)[0-9a-zA-Z]*\$|(^LTC1[A-Z0-9]*\$)|(^ltc1[a-z0-9]*\$)'; case CryptoCurrency.hbar: return '[0-9a-zA-Z.]'; case CryptoCurrency.zaddr: @@ -144,6 +145,8 @@ class AddressValidator extends TextValidator { return null; case CryptoCurrency.btc: return null; + case CryptoCurrency.ltc: + return null; case CryptoCurrency.dash: return [34]; case CryptoCurrency.eos: @@ -190,8 +193,6 @@ class AddressValidator extends TextValidator { return [42, 43, 44, 54, 55]; case CryptoCurrency.bnb: return [42]; - case CryptoCurrency.ltc: - return [34, 43, 63]; case CryptoCurrency.nano: return [64, 65]; case CryptoCurrency.banano: @@ -278,7 +279,8 @@ class AddressValidator extends TextValidator { case CryptoCurrency.ltc: return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)((ltc|t)mweb1q[ac-hj-np-z02-9]{90,120})([^0-9a-zA-Z]|\$)'; case CryptoCurrency.eth: return '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.maticpoly: From 4abe70062fbfa7f1896c3d63235a49bafabf4240 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Thu, 25 Apr 2024 15:23:01 +0100 Subject: [PATCH 16/31] Fee estimation --- cw_bitcoin/lib/electrum.dart | 25 ++++++--- cw_bitcoin/lib/electrum_wallet.dart | 52 ++++++++++++------- cw_bitcoin/lib/litecoin_wallet.dart | 44 ++++++++++++++++ .../com/cakewallet/mweb/CwMwebPlugin.kt | 4 +- cw_mweb/lib/cw_mweb.dart | 5 +- 5 files changed, 100 insertions(+), 30 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 0553170cc..2b3f490a3 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -331,14 +331,19 @@ class ElectrumClient { // "height": 520481, // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" // } - Future<int?> getCurrentBlockChainTip() => - call(method: 'blockchain.headers.subscribe').then((result) { - if (result is Map<String, dynamic>) { - return result["height"] as int; - } + BehaviorSubject<Map<String, dynamic>>? tipListener; + int? currentTip; - return null; - }); + Future<int?> getCurrentBlockChainTip() async { + final method = 'blockchain.headers.subscribe'; + final cb = (result) => currentTip = result['height'] as int; + if (tipListener == null) { + tipListener = subscribe(id: method, method: method); + tipListener?.listen(cb); + cb(await call(method: method)); + } + return currentTip; + } BehaviorSubject<Object>? scripthashUpdate(String scripthash) { _id += 1; @@ -424,6 +429,12 @@ class ElectrumClient { void _methodHandler({required String method, required Map<String, dynamic> request}) { switch (method) { + case 'blockchain.headers.subscribe': + final params = request['params'] as List<dynamic>; + final id = 'blockchain.headers.subscribe'; + + _tasks[id]?.subject?.add(params.last); + break; case 'blockchain.scripthash.subscribe': final params = request['params'] as List<dynamic>; final scripthash = params.first as String?; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 37937f45c..1f5c14319 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -392,24 +392,13 @@ abstract class ElectrumWalletBase value: BigInt.from(amountLeftForChangeAndFee), )); - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxos, - outputs: outputs, - network: network, - memo: memo, - ); - } - - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + int fee = await calcFee( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + ); if (fee == 0) { throw BitcoinTransactionNoFeeException(); @@ -499,6 +488,33 @@ abstract class ElectrumWalletBase ); } + Future<int> calcFee({ + required List<UtxoWithAddress> utxos, + required List<BitcoinBaseOutput> outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate}) async { + + int estimatedSize; + if (network is BitcoinCashNetwork) { + estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } else { + estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } + + return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + } + @override Future<PendingTransaction> createTransaction(Object credentials) async { try { diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index adce6e6b7..b84675669 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,12 +1,15 @@ import 'dart:async'; +import 'dart:math'; import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -279,4 +282,45 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return 0; } + + @override + Future<int> calcFee({ + required List<UtxoWithAddress> utxos, + required List<BitcoinBaseOutput> outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate}) async { + + final txb = BitcoinTransactionBuilder(utxos: utxos, + outputs: outputs, fee: BigInt.zero, network: network); + final scanSecret = mwebHd.derive(0x80000000).privKey!; + final spendSecret = mwebHd.derive(0x80000001).privKey!; + final stub = await CwMweb.stub(); + final resp = await stub.create(CreateRequest( + rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(), + scanSecret: hex.decode(scanSecret), + spendSecret: hex.decode(spendSecret), + feeRatePerKb: Int64(feeRate * 1000), + dryRun: true)); + final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + final posUtxos = utxos.where((utxo) => tx.inputs.any((input) => + input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout)).toList(); + final preOutputSum = outputs.fold<int>(0, (acc, output) => acc + output.toOutput.amount.toInt()); + final posOutputSum = tx.outputs.fold<int>(0, (acc, output) => acc + output.amount.toInt()); + final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue(); + final expectedPegin = max(0, preOutputSum - mwebInputSum.toInt()); + var fee = posOutputSum - expectedPegin; + if (expectedPegin > 0) { + fee += await super.calcFee(utxos: posUtxos, outputs: tx.outputs.map((output) => + BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)).toList(), + network: network, memo: memo, feeRate: feeRate) + feeRate * 41; + } + return fee; + } + + @override + Future<PendingTransaction> createTransaction(Object credentials) async { + final tx = await super.createTransaction(credentials); + return tx; + } } diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt index cf194417b..1dfbe12e5 100644 --- a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -17,6 +17,7 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel : MethodChannel + private var port: Long? = null override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb") @@ -26,7 +27,8 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "start") { val dataDir = call.argument("dataDir") ?: "" - result.success(Mwebd.newServer("", dataDir, "").start(0)) + port = port ?: Mwebd.newServer("", dataDir, "").start(0) + result.success(port) } else { result.notImplemented() } diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart index a54295673..f16d451d9 100644 --- a/cw_mweb/lib/cw_mweb.dart +++ b/cw_mweb/lib/cw_mweb.dart @@ -4,13 +4,10 @@ import 'cw_mweb_platform_interface.dart'; import 'mwebd.pbgrpc.dart'; class CwMweb { - static Future<int?>? port; - static Future<RpcClient> stub() async { final appDir = await getApplicationSupportDirectory(); - port ??= CwMwebPlatform.instance.start(appDir.path); return RpcClient(ClientChannel('127.0.0.1', - port: await port ?? 0, + port: await CwMwebPlatform.instance.start(appDir.path) ?? 0, options: const ChannelOptions( credentials: ChannelCredentials.insecure()))); } From c96424256e7dcb6d56c13a188989c96363222dea Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Thu, 25 Apr 2024 19:02:49 +0100 Subject: [PATCH 17/31] Stop the daemon if plugin is unloaded --- .../src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt index 1dfbe12e5..d3d4140ef 100644 --- a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -9,6 +9,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import mwebd.Mwebd +import mwebd.Server /** CwMwebPlugin */ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { @@ -17,6 +18,7 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel : MethodChannel + private var server: Server? = null private var port: Long? = null override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { @@ -27,7 +29,8 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "start") { val dataDir = call.argument("dataDir") ?: "" - port = port ?: Mwebd.newServer("", dataDir, "").start(0) + server = server ?: Mwebd.newServer("", dataDir, "") + port = port ?: server?.start(0) result.success(port) } else { result.notImplemented() @@ -36,5 +39,6 @@ class CwMwebPlugin: FlutterPlugin, MethodCallHandler { override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) + server?.stop() } } From b1caf79c1b4f4a3bb124d7eac24b80e38abf1914 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Thu, 25 Apr 2024 23:13:41 +0100 Subject: [PATCH 18/31] Normal fee for non-mweb txns --- cw_bitcoin/lib/electrum_wallet.dart | 1 + cw_bitcoin/lib/litecoin_wallet.dart | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 1f5c14319..586c73ed8 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1250,6 +1250,7 @@ abstract class ElectrumWalletBase totalConfirmed += confirmed; totalUnconfirmed += unconfirmed; + addressRecord.balance = confirmed + unconfirmed; if (confirmed > 0 || unconfirmed > 0) { addressRecord.setAsUsed(); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index b84675669..c9bee05f1 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -291,6 +291,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? memo, required int feeRate}) async { + final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); + final paysToMweb = outputs.any((output) => + output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); + if (!spendsMweb && !paysToMweb) { + return await super.calcFee(utxos: utxos, outputs: outputs, + network: network, memo: memo, feeRate: feeRate); + } final txb = BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: BigInt.zero, network: network); final scanSecret = mwebHd.derive(0x80000000).privKey!; From fea7e7a09767d2f296a4ec4e09bb0b9f26beba4e Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Fri, 26 Apr 2024 01:07:58 +0100 Subject: [PATCH 19/31] Fix fee estimation for send all --- cw_bitcoin/lib/electrum_wallet.dart | 25 +++++++------------------ cw_bitcoin/lib/litecoin_wallet.dart | 5 +++++ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 586c73ed8..281255e53 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -245,24 +245,13 @@ abstract class ElectrumWalletBase throw BitcoinTransactionNoInputsException(); } - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxos, - outputs: outputs, - network: network, - memo: memo, - ); - } - - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + int fee = await calcFee( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + ); if (fee == 0) { throw BitcoinTransactionNoFeeException(); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index c9bee05f1..bd69b9e05 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -298,6 +298,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return await super.calcFee(utxos: utxos, outputs: outputs, network: network, memo: memo, feeRate: feeRate); } + if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) { + outputs = [BitcoinScriptOutput( + script: outputs[0].toOutput.scriptPubKey, + value: utxos.sumOfUtxosValue())]; + } final txb = BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: BigInt.zero, network: network); final scanSecret = mwebHd.derive(0x80000000).privKey!; From 66d98a282cbbaa4e8aad1596b74bbd0a17795593 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Fri, 26 Apr 2024 09:16:40 +0100 Subject: [PATCH 20/31] Don't hash mweb addresses --- cw_bitcoin/lib/electrum_wallet.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 281255e53..23a15c280 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -113,11 +113,13 @@ abstract class ElectrumWalletBase SyncStatus syncStatus; List<String> get scriptHashes => walletAddresses.addressesByReceiveType + .where((addr) => addressTypeFromStr(addr.address, network) is! MwebAddress) .map((addr) => scriptHash(addr.address, network: network)) .toList(); List<String> get publicScriptHashes => walletAddresses.allAddresses .where((addr) => !addr.isHidden) + .where((addr) => addressTypeFromStr(addr.address, network) is! MwebAddress) .map((addr) => scriptHash(addr.address, network: network)) .toList(); @@ -1205,7 +1207,8 @@ abstract class ElectrumWalletBase } Future<ElectrumBalance> fetchBalances() async { - final addresses = walletAddresses.allAddresses.toList(); + final addresses = walletAddresses.allAddresses.where((address) => + addressTypeFromStr(address.address, network) is! MwebAddress).toList(); final balanceFutures = <Future<Map<String, dynamic>>>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; From 796c95315ff65d8e6251c92d88ea60ce9de80625 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Fri, 26 Apr 2024 11:19:40 +0100 Subject: [PATCH 21/31] More fee fixes --- cw_bitcoin/lib/litecoin_wallet.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index bd69b9e05..fce466b1e 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -303,8 +303,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue())]; } + final preOutputSum = outputs.fold<BigInt>(BigInt.zero, + (acc, output) => acc + output.toOutput.amount); + final fee = utxos.sumOfUtxosValue() - preOutputSum; final txb = BitcoinTransactionBuilder(utxos: utxos, - outputs: outputs, fee: BigInt.zero, network: network); + outputs: outputs, fee: fee, network: network); final scanSecret = mwebHd.derive(0x80000000).privKey!; final spendSecret = mwebHd.derive(0x80000001).privKey!; final stub = await CwMweb.stub(); @@ -317,17 +320,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); final posUtxos = utxos.where((utxo) => tx.inputs.any((input) => input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout)).toList(); - final preOutputSum = outputs.fold<int>(0, (acc, output) => acc + output.toOutput.amount.toInt()); final posOutputSum = tx.outputs.fold<int>(0, (acc, output) => acc + output.amount.toInt()); final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue(); - final expectedPegin = max(0, preOutputSum - mwebInputSum.toInt()); - var fee = posOutputSum - expectedPegin; - if (expectedPegin > 0) { - fee += await super.calcFee(utxos: posUtxos, outputs: tx.outputs.map((output) => + final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt()); + var feeIncrease = posOutputSum - expectedPegin; + if (expectedPegin > 0 && fee == 0) { + feeIncrease += await super.calcFee(utxos: posUtxos, outputs: tx.outputs.map((output) => BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)).toList(), network: network, memo: memo, feeRate: feeRate) + feeRate * 41; } - return fee; + return fee.toInt() + feeIncrease; } @override From f0157101fbfaee74381c974ba7474385d2f22e00 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Fri, 26 Apr 2024 12:32:38 +0100 Subject: [PATCH 22/31] Broadcast mweb --- cw_bitcoin/lib/litecoin_wallet.dart | 16 ++++++++---- .../lib/pending_bitcoin_transaction.dart | 25 ++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index fce466b1e..a07875c1f 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -8,6 +8,7 @@ import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; @@ -308,13 +309,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final fee = utxos.sumOfUtxosValue() - preOutputSum; final txb = BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network); - final scanSecret = mwebHd.derive(0x80000000).privKey!; - final spendSecret = mwebHd.derive(0x80000001).privKey!; final stub = await CwMweb.stub(); final resp = await stub.create(CreateRequest( rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(), - scanSecret: hex.decode(scanSecret), - spendSecret: hex.decode(spendSecret), + scanSecret: hex.decode(mwebHd.derive(0x80000000).privKey!), + spendSecret: hex.decode(mwebHd.derive(0x80000001).privKey!), feeRatePerKb: Int64(feeRate * 1000), dryRun: true)); final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); @@ -334,7 +333,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future<PendingTransaction> createTransaction(Object credentials) async { - final tx = await super.createTransaction(credentials); + final tx = await super.createTransaction(credentials) as PendingBitcoinTransaction; + final stub = await CwMweb.stub(); + final resp = await stub.create(CreateRequest( + rawTx: hex.decode(tx.hex), + scanSecret: hex.decode(mwebHd.derive(0x80000000).privKey!), + spendSecret: hex.decode(mwebHd.derive(0x80000001).privKey!), + feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000)); + tx.hexOverride = hex.encode(resp.rawTx); return tx; } } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index a59b4f429..46f152708 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,11 +1,14 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:cw_mweb/mwebd.pb.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction( @@ -31,12 +34,14 @@ class PendingBitcoinTransaction with PendingTransaction { final bool hasChange; final bool isSendAll; final bool hasTaprootInputs; + String? idOverride; + String? hexOverride; @override - String get id => _tx.txId(); + String get id => idOverride ?? _tx.txId(); @override - String get hex => _tx.serialize(); + String get hex => hexOverride ?? _tx.serialize(); @override String get amountFormatted => bitcoinAmountToString(amount: amount); @@ -49,8 +54,7 @@ class PendingBitcoinTransaction with PendingTransaction { final List<void Function(ElectrumTransactionInfo transaction)> _listeners; - @override - Future<void> commit() async { + Future<void> _commit() async { int? callId; final result = await electrumClient.broadcastTransaction( @@ -78,6 +82,19 @@ class PendingBitcoinTransaction with PendingTransaction { throw BitcoinTransactionCommitFailed(); } + } + + @override + Future<void> commit() async { + if (network is LitecoinNetwork) try { + final stub = await CwMweb.stub(); + final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + idOverride = resp.txid; + } catch (e) { + throw BitcoinTransactionCommitFailed(); + } else { + await _commit(); + } _listeners.forEach((listener) => listener(transactionInfo())); } From 68688ee0d374672ab4dfaadb865e7a6003f08086 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Fri, 26 Apr 2024 13:03:42 +0100 Subject: [PATCH 23/31] Remove test files --- cw_mweb/test/cw_mweb_method_channel_test.dart | 27 ----------------- cw_mweb/test/cw_mweb_test.dart | 29 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 cw_mweb/test/cw_mweb_method_channel_test.dart delete mode 100644 cw_mweb/test/cw_mweb_test.dart diff --git a/cw_mweb/test/cw_mweb_method_channel_test.dart b/cw_mweb/test/cw_mweb_method_channel_test.dart deleted file mode 100644 index bb0523758..000000000 --- a/cw_mweb/test/cw_mweb_method_channel_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:cw_mweb/cw_mweb_method_channel.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - MethodChannelCwMweb platform = MethodChannelCwMweb(); - const MethodChannel channel = MethodChannel('cw_mweb'); - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { - return '42'; - }, - ); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); - }); - - test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); - }); -} diff --git a/cw_mweb/test/cw_mweb_test.dart b/cw_mweb/test/cw_mweb_test.dart deleted file mode 100644 index 3677659a9..000000000 --- a/cw_mweb/test/cw_mweb_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:cw_mweb/cw_mweb.dart'; -import 'package:cw_mweb/cw_mweb_platform_interface.dart'; -import 'package:cw_mweb/cw_mweb_method_channel.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockCwMwebPlatform - with MockPlatformInterfaceMixin - implements CwMwebPlatform { - - @override - Future<String?> getPlatformVersion() => Future.value('42'); -} - -void main() { - final CwMwebPlatform initialPlatform = CwMwebPlatform.instance; - - test('$MethodChannelCwMweb is the default instance', () { - expect(initialPlatform, isInstanceOf<MethodChannelCwMweb>()); - }); - - test('getPlatformVersion', () async { - CwMweb cwMwebPlugin = CwMweb(); - MockCwMwebPlatform fakePlatform = MockCwMwebPlatform(); - CwMwebPlatform.instance = fakePlatform; - - expect(await cwMwebPlugin.getPlatformVersion(), '42'); - }); -} From 70b07b2d4739c9f35bd6c21d4b1836e5896b2ae4 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Fri, 26 Apr 2024 13:16:04 +0100 Subject: [PATCH 24/31] One more --- .../cakewallet/cw_mweb/CwMwebPluginTest.kt | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 cw_mweb/android/src/test/kotlin/com/cakewallet/cw_mweb/CwMwebPluginTest.kt diff --git a/cw_mweb/android/src/test/kotlin/com/cakewallet/cw_mweb/CwMwebPluginTest.kt b/cw_mweb/android/src/test/kotlin/com/cakewallet/cw_mweb/CwMwebPluginTest.kt deleted file mode 100644 index baa81332f..000000000 --- a/cw_mweb/android/src/test/kotlin/com/cakewallet/cw_mweb/CwMwebPluginTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.cakewallet.mweb - -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import kotlin.test.Test -import org.mockito.Mockito - -/* - * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. - * - * Once you have built the plugin's example app, you can run these tests from the command - * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or - * you can run them directly from IDEs that support JUnit such as Android Studio. - */ - -internal class CwMwebPluginTest { - @Test - fun onMethodCall_getPlatformVersion_returnsExpectedValue() { - val plugin = CwMwebPlugin() - - val call = MethodCall("getPlatformVersion", null) - val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) - plugin.onMethodCall(call, mockResult) - - Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) - } -} From b1f03345502aeb8c9a9a3d4e354514f9d8b6d674 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sat, 27 Apr 2024 14:26:43 +0100 Subject: [PATCH 25/31] Confirm sent txns --- cw_bitcoin/lib/litecoin_wallet.dart | 73 +++++++++++++++---- .../lib/pending_bitcoin_transaction.dart | 3 + 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index a07875c1f..483d86645 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; import 'package:fixnum/fixnum.dart'; @@ -122,6 +123,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } + int mwebUtxosHeight = 0; + @action @override Future<void> startSync() async { @@ -142,6 +145,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } else { syncStatus = SyncedSyncStatus(); } + if (resp.mwebUtxosHeight > mwebUtxosHeight) { + mwebUtxosHeight = resp.mwebUtxosHeight; + await checkMwebUtxosSpent(); + } }); processMwebUtxos(); } @@ -170,14 +177,19 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000); confirmations = status.blockHeaderHeight - utxo.height + 1; } - final tx = ElectrumTransactionInfo(WalletType.litecoin, - id: utxo.outputId, height: utxo.height, - amount: utxo.value.toInt(), fee: 0, - direction: TransactionDirection.incoming, - isPending: utxo.height == 0, - date: date, confirmations: confirmations, - inputAddresses: [], - outputAddresses: [utxo.address]); + var tx = transactionHistory.transactions.values.firstWhereOrNull( + (tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false); + if (tx == null) + tx = ElectrumTransactionInfo(WalletType.litecoin, + id: utxo.outputId, height: utxo.height, + amount: utxo.value.toInt(), fee: 0, + direction: TransactionDirection.incoming, + isPending: utxo.height == 0, + date: date, confirmations: confirmations, + inputAddresses: [], + outputAddresses: [utxo.address]); + tx.isPending = utxo.height == 0; + tx.confirmations = confirmations; if (transactionHistory.transactions[utxo.outputId] == null) { final addressRecord = walletAddresses.allAddresses.firstWhere( (addressRecord) => addressRecord.address == utxo.address); @@ -194,11 +206,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future<void> checkMwebUtxosSpent() async { - final List<String> outputIds = []; - mwebUtxos.forEach((outputId, utxo) { - if (utxo.height > 0) - outputIds.add(outputId); - }); + while ((await Future.wait(transactionHistory.transactions.values + .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending) + .map(checkPendingTransaction))).any((x) => x)); + final outputIds = mwebUtxos.values + .where((utxo) => utxo.height > 0) + .map((utxo) => utxo.outputId).toList(); final stub = await CwMweb.stub(); final resp = await stub.spent(SpentRequest(outputId: outputIds)); final spent = resp.outputId; @@ -212,8 +225,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { var output = AccumulatorSink<Digest>(); var input = sha256.startChunkedConversion(output); for (final outputId in spent) { - input.add(hex.decode(outputId)); - final utxo = mwebUtxos[outputId]!; + final utxo = mwebUtxos[outputId]; + if (utxo == null) continue; final addressRecord = walletAddresses.allAddresses.firstWhere( (addressRecord) => addressRecord.address == utxo.address); if (!inputAddresses.contains(utxo.address)) @@ -222,7 +235,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { amount += utxo.value.toInt(); inputAddresses.add(utxo.address); mwebUtxos.remove(outputId); + input.add(hex.decode(outputId)); } + if (inputAddresses.isEmpty) return; input.close(); var digest = output.events.single; final tx = ElectrumTransactionInfo(WalletType.litecoin, @@ -238,6 +253,33 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await transactionHistory.save(); } + Future<bool> checkPendingTransaction(ElectrumTransactionInfo tx) async { + if (!tx.isPending) return false; + final outputId = <String>[], target = <String>{}; + final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch; + final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? []; + final payingToOutputIds = tx.outputAddresses?.where(isHash) ?? []; + outputId.addAll(spendingOutputIds); + outputId.addAll(payingToOutputIds); + target.addAll(spendingOutputIds); + for (final outputId in payingToOutputIds) { + final spendingTx = transactionHistory.transactions.values.firstWhereOrNull( + (tx) => tx.inputAddresses?.contains(outputId) ?? false); + if (spendingTx != null && !spendingTx.isPending) + target.add(outputId); + } + if (outputId.isEmpty) return false; + final stub = await CwMweb.stub(); + final resp = await stub.spent(SpentRequest(outputId: outputId)); + if (resp.outputId.toSet() != target) return false; + final status = await stub.status(StatusRequest()); + if (!tx.isPending) return false; + tx.height = status.mwebUtxosHeight; + tx.confirmations = 1; + tx.isPending = false; + return true; + } + @override Future<void> updateUnspentCoins() async { await super.updateUnspentCoins(); @@ -341,6 +383,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { spendSecret: hex.decode(mwebHd.derive(0x80000001).privKey!), feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000)); tx.hexOverride = hex.encode(resp.rawTx); + tx.outputs = resp.outputId; return tx; } } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 46f152708..69bdaba70 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -36,6 +36,7 @@ class PendingBitcoinTransaction with PendingTransaction { final bool hasTaprootInputs; String? idOverride; String? hexOverride; + List<String>? outputs; @override String get id => idOverride ?? _tx.txId(); @@ -110,5 +111,7 @@ class PendingBitcoinTransaction with PendingTransaction { date: DateTime.now(), isPending: true, confirmations: 0, + inputAddresses: _tx.inputs.map((input) => input.txId).toList(), + outputAddresses: outputs, fee: fee); } From 2c941efcc319e147daf490c67825e3e0daca62f2 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sat, 27 Apr 2024 17:40:47 +0100 Subject: [PATCH 26/31] Couple of fixes --- cw_bitcoin/lib/litecoin_wallet.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 483d86645..ea5176a00 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -188,6 +188,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { date: date, confirmations: confirmations, inputAddresses: [], outputAddresses: [utxo.address]); + tx.height = utxo.height; tx.isPending = utxo.height == 0; tx.confirmations = confirmations; if (transactionHistory.transactions[utxo.outputId] == null) { @@ -271,7 +272,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (outputId.isEmpty) return false; final stub = await CwMweb.stub(); final resp = await stub.spent(SpentRequest(outputId: outputId)); - if (resp.outputId.toSet() != target) return false; + if (!setEquals(resp.outputId.toSet(), target)) return false; final status = await stub.status(StatusRequest()); if (!tx.isPending) return false; tx.height = status.mwebUtxosHeight; From 8964774c5188fd72b6271014f6a603eb53664a36 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sun, 28 Apr 2024 11:17:28 +0100 Subject: [PATCH 27/31] Resign inputs after mweb create --- cw_bitcoin/lib/litecoin_wallet.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index ea5176a00..6444e217d 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; @@ -278,6 +279,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx.height = status.mwebUtxosHeight; tx.confirmations = 1; tx.isPending = false; + await transactionHistory.save(); return true; } @@ -366,7 +368,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue(); final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt()); var feeIncrease = posOutputSum - expectedPegin; - if (expectedPegin > 0 && fee == 0) { + if (expectedPegin > 0 && fee == BigInt.zero) { feeIncrease += await super.calcFee(utxos: posUtxos, outputs: tx.outputs.map((output) => BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)).toList(), network: network, memo: memo, feeRate: feeRate) + feeRate * 41; @@ -383,7 +385,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { scanSecret: hex.decode(mwebHd.derive(0x80000000).privKey!), spendSecret: hex.decode(mwebHd.derive(0x80000001).privKey!), feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000)); - tx.hexOverride = hex.encode(resp.rawTx); + final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + tx.hexOverride = tx2.copyWith(witnesses: tx2.inputs.asMap().entries.map((e) { + final utxo = unspentCoins.firstWhere((utxo) => + utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); + final key = generateECPrivate(hd: utxo.bitcoinAddressRecord.isHidden ? + walletAddresses.sideHd : walletAddresses.mainHd, + index: utxo.bitcoinAddressRecord.index, network: network); + final digest = tx2.getTransactionSegwitDigit(txInIndex: e.key, + script: key.getPublic().toP2pkhAddress().toScriptPubKey(), + amount: BigInt.from(utxo.value)); + return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]); + }).toList()).toHex(); tx.outputs = resp.outputId; return tx; } From b39b2bbb996087fb9b7fdb5a6339d18c03fd7c48 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sun, 28 Apr 2024 14:12:12 +0100 Subject: [PATCH 28/31] Some more fixes --- cw_bitcoin/lib/electrum_wallet.dart | 1 - cw_bitcoin/lib/litecoin_wallet.dart | 11 ++++++----- cw_bitcoin/lib/pending_bitcoin_transaction.dart | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 23a15c280..ffb637004 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -792,7 +792,6 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; - coin.bitcoinAddressRecord.balance += coinInfo.value; } else { _addCoinInfo(coin); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 6444e217d..6cabcc34e 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -133,6 +133,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final stub = await CwMweb.stub(); Timer.periodic( const Duration(milliseconds: 1500), (timer) async { + if (syncStatus is FailedSyncStatus) return; final height = await electrumClient.getCurrentBlockChainTip() ?? 0; final resp = await stub.status(StatusRequest()); if (resp.blockHeaderHeight < height) { @@ -145,10 +146,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { syncStatus = SyncingSyncStatus(1, 0.999); } else { syncStatus = SyncedSyncStatus(); - } - if (resp.mwebUtxosHeight > mwebUtxosHeight) { - mwebUtxosHeight = resp.mwebUtxosHeight; - await checkMwebUtxosSpent(); + if (resp.mwebUtxosHeight > mwebUtxosHeight) { + mwebUtxosHeight = resp.mwebUtxosHeight; + await checkMwebUtxosSpent(); + } } }); processMwebUtxos(); @@ -192,7 +193,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx.height = utxo.height; tx.isPending = utxo.height == 0; tx.confirmations = confirmations; - if (transactionHistory.transactions[utxo.outputId] == null) { + if (transactionHistory.transactions[tx.id] == null) { final addressRecord = walletAddresses.allAddresses.firstWhere( (addressRecord) => addressRecord.address == utxo.address); addressRecord.txCount++; diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 69bdaba70..20cb1ebc6 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,3 +1,4 @@ +import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -91,8 +92,8 @@ class PendingBitcoinTransaction with PendingTransaction { final stub = await CwMweb.stub(); final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); idOverride = resp.txid; - } catch (e) { - throw BitcoinTransactionCommitFailed(); + } on GrpcError catch (e) { + throw BitcoinTransactionCommitFailed(errorMessage: e.message); } else { await _commit(); } From a936a3b6610ca81b025342d290dc26ec95a6f458 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Sun, 28 Apr 2024 19:46:07 +0100 Subject: [PATCH 29/31] Update balance after sending --- cw_bitcoin/lib/litecoin_wallet.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 6cabcc34e..4c589b134 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -399,6 +399,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]); }).toList()).toHex(); tx.outputs = resp.outputId; - return tx; + return tx..addListener((transaction) async { + transaction.inputAddresses?.forEach(mwebUtxos.remove); + await updateUnspent(); + await updateBalance(); + }); } } From d1ccf6eb0b26632854fda3583fa215f3bcfdd534 Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Mon, 29 Apr 2024 12:11:59 +0100 Subject: [PATCH 30/31] Correctly update address records --- cw_bitcoin/lib/litecoin_wallet.dart | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 4c589b134..703571bd5 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -188,15 +188,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { direction: TransactionDirection.incoming, isPending: utxo.height == 0, date: date, confirmations: confirmations, - inputAddresses: [], - outputAddresses: [utxo.address]); + inputAddresses: [], outputAddresses: [utxo.outputId]); tx.height = utxo.height; tx.isPending = utxo.height == 0; tx.confirmations = confirmations; - if (transactionHistory.transactions[tx.id] == null) { + var isNew = transactionHistory.transactions[tx.id] == null; + if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) { + tx.outputAddresses?.add(utxo.address); + isNew = true; + } + if (isNew) { final addressRecord = walletAddresses.allAddresses.firstWhere( (addressRecord) => addressRecord.address == utxo.address); - addressRecord.txCount++; + if (!(tx.inputAddresses?.contains(utxo.address) ?? false)) + addressRecord.txCount++; addressRecord.balance += utxo.value.toInt(); addressRecord.setAsUsed(); } @@ -400,7 +405,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }).toList()).toHex(); tx.outputs = resp.outputId; return tx..addListener((transaction) async { - transaction.inputAddresses?.forEach(mwebUtxos.remove); + final addresses = <String>{}; + transaction.inputAddresses?.forEach((id) { + final utxo = mwebUtxos.remove(id); + if (utxo == null) return; + final addressRecord = walletAddresses.allAddresses.firstWhere( + (addressRecord) => addressRecord.address == utxo.address); + if (!addresses.contains(utxo.address)) { + addressRecord.txCount++; + addresses.add(utxo.address); + } + addressRecord.balance -= utxo.value.toInt(); + }); + transaction.inputAddresses?.addAll(addresses); + transactionHistory.addOne(transaction); await updateUnspent(); await updateBalance(); }); From 7978ad447690b386f2058c10f318f5045b1aa24b Mon Sep 17 00:00:00 2001 From: Hector Chu <hectorchu@gmail.com> Date: Mon, 29 Apr 2024 13:08:05 +0100 Subject: [PATCH 31/31] Update confs --- cw_bitcoin/lib/litecoin_wallet.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 703571bd5..fba4bbc4a 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -149,6 +149,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (resp.mwebUtxosHeight > mwebUtxosHeight) { mwebUtxosHeight = resp.mwebUtxosHeight; await checkMwebUtxosSpent(); + for (final transaction in transactionHistory.transactions.values) { + if (transaction.isPending) continue; + final confirmations = mwebUtxosHeight - transaction.height + 1; + if (transaction.confirmations == confirmations) continue; + transaction.confirmations = confirmations; + transactionHistory.addOne(transaction); + } + await transactionHistory.save(); } } });