Merge branch 'main' of https://github.com/cake-tech/cake_wallet into bitcoin-derivations

This commit is contained in:
Matthew Fosse 2024-01-02 19:31:47 -05:00
commit 45bfcbc09a
234 changed files with 14186 additions and 1647 deletions

View file

@ -3,6 +3,12 @@ name: PR Test Build
on:
pull_request:
branches: [main]
workflow_dispatch:
inputs:
branch:
description: 'Branch name to build'
required: true
default: 'main'
jobs:
PR_test_build:
@ -12,6 +18,14 @@ jobs:
KEY_PASS: test@cake_wallet
steps:
- name: is pr
if: github.event_name == 'pull_request'
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
- name: is not pr
if: github.event_name != 'pull_request'
run: echo "BRANCH_NAME=${{ github.event.inputs.branch }}" >> $GITHUB_ENV
- name: Free Up GitHub Actions Ubuntu Runner Disk Space
run: |
sudo rm -rf /usr/share/dotnet
@ -40,7 +54,7 @@ jobs:
cd /opt/android
-y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install cargo-ndk
git clone https://github.com/cake-tech/cake_wallet.git --branch $GITHUB_HEAD_REF
git clone https://github.com/cake-tech/cake_wallet.git --branch ${{ env.BRANCH_NAME }}
cd cake_wallet/scripts/android/
./install_ndk.sh
source ./app_env.sh cakewallet
@ -97,6 +111,7 @@ jobs:
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_polygon && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Add secrets
@ -104,6 +119,7 @@ jobs:
cd /opt/android/cake_wallet
touch lib/.secrets.g.dart
touch cw_ethereum/lib/.secrets.g.dart
touch cw_polygon/lib/.secrets.g.dart
echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
@ -137,9 +153,10 @@ jobs:
echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart
- name: Rename app
run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties
run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
- name: Build
run: |
@ -154,7 +171,7 @@ jobs:
# appcenter distribute release \
# --group "Testers" \
# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
# --release-notes ${GITHUB_HEAD_REF} \
# --release-notes ${{ env.BRANCH_NAME }} \
# --app Cake-Labs/Cake-Wallet \
# --token ${{ secrets.APP_CENTER_TOKEN }} \
# --quiet
@ -163,7 +180,7 @@ jobs:
run: |
cd /opt/android/cake_wallet/build/app/outputs/apk/release
mkdir test-apk
cp app-release.apk test-apk/$GITHUB_HEAD_REF.apk
cp app-release.apk test-apk/${{env.BRANCH_NAME}}.apk
- name: Upload Artifact
uses: kittaakos/upload-artifact-as-is@v0
@ -177,6 +194,6 @@ jobs:
token: ${{ secrets.SLACK_APP_TOKEN }}
path: /opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk
channel: ${{ secrets.SLACK_APK_CHANNEL }}
title: "${{github.head_ref}}.apk"
filename: ${{github.head_ref}}.apk
title: "${{ env.BRANCH_NAME }}.apk"
filename: ${{ env.BRANCH_NAME }}.apk
initial_comment: ${{ github.event.head_commit.message }}

1
.gitignore vendored
View file

@ -126,6 +126,7 @@ lib/haven/haven.dart
lib/ethereum/ethereum.dart
lib/bitcoin_cash/bitcoin_cash.dart
lib/nano/nano.dart
lib/polygon/polygon.dart
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png

View file

@ -62,6 +62,9 @@
<data android:scheme="bitcoincash" />
<data android:scheme="bitcoincash-wallet" />
<data android:scheme="bitcoincash_wallet" />
<data android:scheme="polygon" />
<data android:scheme="polygon-wallet" />
<data android:scheme="polygon_wallet" />
</intent-filter>
</activity>
<meta-data

BIN
assets/images/dfx_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
assets/images/dfx_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View file

@ -0,0 +1,6 @@
-
uri: polygon-rpc.com
-
uri: polygon-bor.publicnode.com
-
uri: polygon.llamarpc.com

View file

@ -1,7 +1,4 @@
Coin control fixes and enhancements
In-app Tor connection
Accessibility enhancements
Privacy settings enhancements
UI enhancements
Backup flow fixes
Polyseed enhancements
New on-ramp provider DFX
Usability enhancements
Bug fixes

View file

@ -1,7 +1,2 @@
Coin control fixes and enhancements
In-app Tor connection
Accessibility enhancements
Privacy settings enhancements
UI enhancements
Backup flow fixes
Support multiple address types for Bitcoin Cash
Bug fixes

File diff suppressed because it is too large Load diff

View file

@ -10,4 +10,5 @@ cd cw_haven && flutter pub get && flutter packages pub run build_runner build --
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_polygon && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs

View file

@ -4,7 +4,10 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_core/balance.dart';
class ElectrumBalance extends Balance {
const ElectrumBalance({required this.confirmed, required this.unconfirmed, required this.frozen})
const ElectrumBalance(
{required this.confirmed,
required this.unconfirmed,
required this.frozen})
: super(confirmed, unconfirmed);
static ElectrumBalance? fromJSON(String? jsonSource) {
@ -25,16 +28,19 @@ class ElectrumBalance extends Balance {
final int frozen;
@override
String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen);
String get formattedAvailableBalance =>
bitcoinAmountToString(amount: confirmed - unconfirmed.abs() - frozen);
@override
String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
String get formattedAdditionalBalance =>
bitcoinAmountToString(amount: unconfirmed);
String get formattedFrozenBalance {
@override
String get formattedUnAvailableBalance {
final frozenFormatted = bitcoinAmountToString(amount: frozen);
return frozenFormatted == '0.0' ? '' : frozenFormatted;
}
String toJSON() =>
json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen});
String toJSON() => json.encode(
{'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen});
}

View file

@ -1,10 +1,11 @@
import 'dart:convert';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_bitcoin/file.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
part 'electrum_transaction_history.g.dart';

View file

@ -18,7 +18,6 @@ import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/file.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/script_hash.dart';
import 'package:cw_bitcoin/utils.dart';
@ -30,6 +29,7 @@ import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/foundation.dart';
@ -82,7 +82,7 @@ abstract class ElectrumWalletBase
bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/0");
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 146 + outputsCounts * 33 + 8;
inputsCount * 68 + outputsCounts * 34 + 10;
final bitcoin.HDWallet hd;
final String mnemonic;
@ -725,8 +725,7 @@ abstract class ElectrumWalletBase
final index = address != null
? walletAddresses.addresses.firstWhere((element) => element.address == address).index
: null;
return index == null
? base64Encode(hd.sign(message))
: base64Encode(hd.derive(index).sign(message));
final HD = index == null ? hd : hd.derive(index);
return base64Encode(HD.signMessage(message));
}
}

View file

@ -1,9 +1,9 @@
import 'dart:convert';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/file.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_type.dart';
class ElectrumWallletSnapshot {

View file

@ -79,11 +79,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: cake-update-v3
resolved-ref: df9204144011ed9419eff7d9ef3143102a40252d
ref: cake-update-v4
resolved-ref: e19ffb7e7977278a75b27e0479b3c6f4034223b3
url: "https://github.com/cake-tech/bitcoin_flutter.git"
source: git
version: "2.0.2"
version: "2.1.0"
boolean_selector:
dependency: transitive
description:
@ -244,7 +244,7 @@ packages:
source: hosted
version: "2.2.4"
encrypt:
dependency: "direct main"
dependency: transitive
description:
name: encrypt
sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb"
@ -698,15 +698,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tor:
dependency: transitive
description:
path: "."
ref: main
resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5"
url: "https://github.com/cake-tech/tor.git"
source: git
version: "0.0.1"
typed_data:
dependency: transitive
description:

View file

@ -22,7 +22,7 @@ dependencies:
bitcoin_flutter:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git
ref: cake-update-v3
ref: cake-update-v4
bitbox:
git:
url: https://github.com/cake-tech/bitbox-flutter.git
@ -30,7 +30,6 @@ dependencies:
rxdart: ^0.27.5
unorm_dart: ^0.2.0
cryptography: ^2.0.5
encrypt: ^5.0.1
dev_dependencies:
flutter_test:

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
@ -210,9 +211,28 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
txb.addInput(input.hash, input.vout);
});
final String bchPrefix = "bitcoincash:";
outputs.forEach((item) {
final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
String outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
if (!outputAddress.startsWith(bchPrefix)) {
outputAddress = "$bchPrefix$outputAddress";
}
bool isP2sh = outputAddress.startsWith("p", bchPrefix.length);
if (isP2sh) {
final p2sh = P2shAddress.fromAddress(
address: outputAddress,
network: BitcoinCashNetwork.mainnet,
);
txb.addOutput(Uint8List.fromList(p2sh.toScriptPubKey().toBytes()), outputAmount!);
return;
}
txb.addOutput(outputAddress, outputAmount!);
});
@ -302,10 +322,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
final index = address != null
? walletAddresses.addresses
.firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address))
.index
: null;
return index == null
? base64Encode(hd.sign(message))
: base64Encode(hd.derive(index).sign(message));
.index : null;
final HD = index == null ? hd : hd.derive(index);
return base64Encode(HD.signMessage(message));
}
}

View file

@ -1 +0,0 @@
/Users/blazebrain/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/

View file

@ -1 +0,0 @@
/Users/blazebrain/.pub-cache/git/tor-09ba92cb11d4e3cacf97256e57863b805f79f2e5/

View file

@ -1,11 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View file

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -1,24 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
tor
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -1,12 +0,0 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}

View file

@ -1,11 +0,0 @@
// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=C:\Users\borod\flutter
FLUTTER_APPLICATION_PATH=C:\cake_wallet\cw_bitcoin_cash
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=0.0.1
FLUTTER_BUILD_NUMBER=0.0.1
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=.dart_tool/package_config.json

View file

@ -1,12 +0,0 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=C:\Users\borod\flutter"
export "FLUTTER_APPLICATION_PATH=C:\cake_wallet\cw_bitcoin_cash"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=0.0.1"
export "FLUTTER_BUILD_NUMBER=0.0.1"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=.dart_tool/package_config.json"

View file

@ -24,11 +24,12 @@ dependencies:
bitcoin_flutter:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git
ref: cake-update-v3
ref: cake-update-v4
bitbox:
git:
url: https://github.com/cake-tech/bitbox-flutter.git
ref: master
bitcoin_base: ^3.0.1

View file

@ -1,11 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void RegisterPlugins(flutter::PluginRegistry* registry) {
}

View file

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -1,23 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -93,6 +93,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.dydx,
CryptoCurrency.steth,
CryptoCurrency.banano,
CryptoCurrency.usdtPoly,
CryptoCurrency.usdcEPoly,
];
static const havenCurrencies = [
@ -202,6 +204,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png', decimals: 18);
static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png', decimals: 18);
static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29);
static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
static final Map<int, CryptoCurrency> _rawCurrencyMap =
@ -241,7 +245,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
}
static CryptoCurrency fromFullName(String name) {
if (CryptoCurrency._fullNameCurrencyMap[name.toLowerCase()] == null) {
if (CryptoCurrency._fullNameCurrencyMap[name.split("(").first.trim().toLowerCase()] == null) {
final s = 'Unexpected token: $name for CryptoCurrency fromFullName';
throw ArgumentError.value(name, 'Fullname', s);
}

View file

@ -19,6 +19,8 @@ CryptoCurrency currencyForWalletType(WalletType type) {
return CryptoCurrency.nano;
case WalletType.banano:
return CryptoCurrency.banano;
case WalletType.polygon:
return CryptoCurrency.maticpoly;
default:
throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
}

View file

@ -18,6 +18,8 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
bool _enabled;
@HiveField(5)
final String? iconPath;
@HiveField(6)
final String? tag;
bool get enabled => _enabled;
@ -30,37 +32,41 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
required this.decimal,
bool enabled = true,
this.iconPath,
this.tag,
}) : _enabled = enabled,
super(
name: symbol.toLowerCase(),
title: symbol.toUpperCase(),
fullName: name,
tag: "ETH",
tag: tag,
iconPath: iconPath,
decimals: decimal
);
decimals: decimal);
Erc20Token.copyWith(Erc20Token other, String? icon)
Erc20Token.copyWith(Erc20Token other, String? icon, String? tag)
: this.name = other.name,
this.symbol = other.symbol,
this.contractAddress = other.contractAddress,
this.decimal = other.decimal,
this._enabled = other.enabled,
this.tag = tag,
this.iconPath = icon,
super(
name: other.name,
title: other.symbol.toUpperCase(),
fullName: other.name,
tag: "ETH",
tag: tag,
iconPath: icon,
decimals: other.decimal
decimals: other.decimal,
);
static const typeId = ERC20_TOKEN_TYPE_ID;
static const boxName = 'Erc20Tokens';
static const ethereumBoxName = 'EthereumErc20Tokens';
static const polygonBoxName = 'PolygonErc20Tokens';
@override
bool operator ==(other) => (other is Erc20Token && other.contractAddress == contractAddress) ||
bool operator ==(other) =>
(other is Erc20Token && other.contractAddress == contractAddress) ||
(other is CryptoCurrency && other.title == title);
@override

View file

@ -118,7 +118,10 @@ final dates = {
"2023-6": 2898234,
"2023-7": 2919771,
"2023-8": 2942045,
"2023-9": 2964280
"2023-9": 2964280,
"2023-10": 2985937,
"2023-11": 3008178,
"2023-12": 3029759
};
int getMoneroHeigthByDate({required DateTime date}) {

View file

@ -6,7 +6,7 @@ import 'package:hive/hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/io_client.dart' as ioc;
import 'package:tor/tor.dart';
// import 'package:tor/tor.dart';
part 'node.g.dart';
@ -88,6 +88,8 @@ class Node extends HiveObject with Keyable {
} else {
return Uri.http(uriRaw, '');
}
case WalletType.polygon:
return Uri.https(uriRaw, '');
default:
throw Exception('Unexpected type ${type.toString()} for Node uri');
}
@ -146,6 +148,8 @@ class Node extends HiveObject with Keyable {
case WalletType.nano:
case WalletType.banano:
return requestNanoNode();
case WalletType.polygon:
return requestElectrumServer();
default:
return false;
}
@ -210,14 +214,17 @@ class Node extends HiveObject with Keyable {
}
Future<bool> requestNodeWithProxy() async {
if (!isValidProxyAddress && !Tor.instance.enabled) {
if (!isValidProxyAddress /* && !Tor.instance.enabled*/) {
return false;
}
String? proxy = socksProxyAddress;
if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) {
proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}";
// if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) {
// proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}";
// }
if (proxy == null) {
return false;
}
final proxyAddress = proxy!.split(':')[0];
final proxyPort = int.parse(proxy.split(':')[1]);

View file

@ -2,17 +2,8 @@ import 'dart:io';
import 'package:cw_core/key.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
Future<void> write(
{required String path,
required String password,
required String data}) async {
final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last);
final encrypted = await encode(key: key, iv: iv, data: data);
final f = File(path);
f.writeAsStringSync(encrypted);
}
Future<void> write({required String path, required String password, required String data}) async =>
writeData(path: path, password: password, data: data);
Future<void> writeData(
{required String path,

View file

@ -13,6 +13,7 @@ const walletTypes = [
WalletType.bitcoinCash,
WalletType.nano,
WalletType.banano,
WalletType.polygon,
];
@HiveType(typeId: WALLET_TYPE_TYPE_ID)
@ -44,6 +45,8 @@ enum WalletType {
@HiveField(8)
bitcoinCash,
@HiveField(9)
polygon
}
int serializeToInt(WalletType type) {
@ -64,6 +67,8 @@ int serializeToInt(WalletType type) {
return 6;
case WalletType.bitcoinCash:
return 7;
case WalletType.polygon:
return 8;
default:
return -1;
}
@ -87,6 +92,8 @@ WalletType deserializeFromInt(int raw) {
return WalletType.banano;
case 7:
return WalletType.bitcoinCash;
case 8:
return WalletType.polygon;
default:
throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
}
@ -110,6 +117,8 @@ String walletTypeToString(WalletType type) {
return 'Nano';
case WalletType.banano:
return 'Banano';
case WalletType.polygon:
return 'Polygon';
default:
return '';
}
@ -133,6 +142,8 @@ String walletTypeToDisplayName(WalletType type) {
return 'Nano (XNO)';
case WalletType.banano:
return 'Banano (BAN)';
case WalletType.polygon:
return 'Polygon (MATIC)';
default:
return '';
}
@ -156,7 +167,10 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
return CryptoCurrency.nano;
case WalletType.banano:
return CryptoCurrency.banano;
case WalletType.polygon:
return CryptoCurrency.maticpoly;
default:
throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
throw Exception(
'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
}
}

View file

@ -616,15 +616,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tor:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5"
url: "https://github.com/cake-tech/tor.git"
source: git
version: "0.0.1"
typed_data:
dependency: transitive
description:

View file

@ -20,10 +20,10 @@ dependencies:
intl: ^0.18.0
encrypt: ^5.0.1
socks5_proxy: ^1.0.4
tor:
git:
url: https://github.com/cake-tech/tor.git
ref: main
# tor:
# git:
# url: https://github.com/cake-tech/tor.git
# ref: main
dev_dependencies:
flutter_test:

View file

@ -300,10 +300,6 @@ class DefaultErc20Tokens {
.iconPath;
} catch (_) {}
if (iconPath != null) {
return Erc20Token.copyWith(token, iconPath);
}
return token;
return Erc20Token.copyWith(token, iconPath, 'ETH');
}).toList();
}

View file

@ -15,12 +15,12 @@ import 'package:cw_ethereum/ethereum_transaction_priority.dart';
import 'package:cw_ethereum/.secrets.g.dart' as secrets;
class EthereumClient {
final _httpClient = Client();
final httpClient = Client();
Web3Client? _client;
bool connect(Node node) {
try {
_client = Web3Client(node.uri.toString(), _httpClient);
_client = Web3Client(node.uri.toString(), httpClient);
return true;
} catch (e) {
@ -74,29 +74,34 @@ class EthereumClient {
required int exponent,
String? contractAddress,
}) async {
assert(currency == CryptoCurrency.eth || contractAddress != null);
assert(currency == CryptoCurrency.eth ||
currency == CryptoCurrency.maticpoly ||
contractAddress != null);
bool _isEthereum = currency == CryptoCurrency.eth;
bool _isEVMCompatibleChain =
currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
final price = _client!.getGasPrice();
final Transaction transaction = Transaction(
final Transaction transaction = createTransaction(
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
amount: _isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
);
final signedTransaction = await _client!.signTransaction(privateKey, transaction);
final signedTransaction =
await _client!.signTransaction(privateKey, transaction, chainId: chainId);
final Function _sendTransaction;
if (_isEthereum) {
if (_isEVMCompatibleChain) {
_sendTransaction = () async => await sendTransaction(signedTransaction);
} else {
final erc20 = ERC20(
client: _client!,
address: EthereumAddress.fromHex(contractAddress!),
chainId: chainId,
);
_sendTransaction = () async {
@ -118,8 +123,27 @@ class EthereumClient {
);
}
int get chainId => 1;
Transaction createTransaction({
required EthereumAddress from,
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
}) {
return Transaction(
from: from,
to: to,
maxPriorityFeePerGas: maxPriorityFeePerGas,
value: amount,
);
}
Future<String> sendTransaction(Uint8List signedTransaction) async =>
await _client!.sendRawTransaction(prependTransactionType(0x02, signedTransaction));
await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction));
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) =>
prependTransactionType(0x02, signedTransaction);
Future getTransactionDetails(String transactionHash) async {
// Wait for the transaction receipt to become available
@ -198,7 +222,7 @@ I/flutter ( 4474): Gas Used: 53000
Future<List<EthereumTransactionModel>> fetchTransactions(String address,
{String? contractAddress}) async {
try {
final response = await _httpClient.get(Uri.https("api.etherscan.io", "/api", {
final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", {
"module": "account",
"action": contractAddress != null ? "tokentx" : "txlist",
if (contractAddress != null) "contractaddress": contractAddress,

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
@ -34,8 +36,10 @@ class EthereumTransactionInfo extends TransactionInfo {
final String? to;
@override
String amountFormatted() =>
'${formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString())} $tokenSymbol';
String amountFormatted() {
final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString());
return '${amount.substring(0, min(10, amount.length))} $tokenSymbol';
}
@override
String fiatAmount() => _fiatAmount ?? '';
@ -44,7 +48,10 @@ class EthereumTransactionInfo extends TransactionInfo {
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} ETH';
String feeFormatted() {
final amount = (ethFee / BigInt.from(10).pow(18)).toString();
return '${amount.substring(0, min(10, amount.length))} ETH';
}
factory EthereumTransactionInfo.fromJson(Map<String, dynamic> data) {
return EthereumTransactionInfo(

View file

@ -1,3 +1,4 @@
//! Model used for in parsing transactions fetched using etherscan
class EthereumTransactionModel {
final DateTime date;
final String hash;

View file

@ -14,7 +14,7 @@ import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_ethereum/default_erc20_tokens.dart';
import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart';
import 'package:cw_ethereum/erc20_balance.dart';
import 'package:cw_ethereum/ethereum_client.dart';
import 'package:cw_ethereum/ethereum_exceptions.dart';
@ -75,6 +75,8 @@ abstract class EthereumWalletBase
late final Box<Erc20Token> erc20TokensBox;
late final Box<Erc20Token> ethereumErc20TokensBox;
late final EthPrivateKey _ethPrivateKey;
EthPrivateKey get ethPrivateKey => _ethPrivateKey;
@ -102,7 +104,8 @@ abstract class EthereumWalletBase
Completer<SharedPreferences> _sharedPrefs = Completer();
Future<void> init() async {
erc20TokensBox = await CakeHive.openBox<Erc20Token>(Erc20Token.boxName);
await movePreviousErc20BoxConfigsToNewBox();
await walletAddresses.init();
await transactionHistory.init();
_ethPrivateKey = await getPrivateKey(
@ -114,6 +117,33 @@ abstract class EthereumWalletBase
await save();
}
/// Majorly for backward compatibility for previous configs that have been set.
Future<void> movePreviousErc20BoxConfigsToNewBox() async {
// Opens a box specific to this wallet
ethereumErc20TokensBox = await CakeHive.openBox<Erc20Token>(
"${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}");
//Open the previous token configs box
erc20TokensBox = await CakeHive.openBox<Erc20Token>(Erc20Token.boxName);
// Check if it's empty, if it is, we stop the flow and return.
if (erc20TokensBox.isEmpty) {
// If it's empty, but the new wallet specific box is also empty,
// we load the initial tokens to the new box.
if (ethereumErc20TokensBox.isEmpty) addInitialTokens();
return;
}
final allValues = erc20TokensBox.values.toList();
// Clear and delete the old token box
await erc20TokensBox.clear();
await erc20TokensBox.deleteFromDisk();
// Add all the previous tokens with configs to the new box
ethereumErc20TokensBox.addAll(allValues);
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
try {
@ -378,7 +408,7 @@ abstract class EthereumWalletBase
}
Future<void> _fetchErc20Balances() async {
for (var token in erc20TokensBox.values) {
for (var token in ethereumErc20TokensBox.values) {
try {
if (token.enabled) {
balance[token] = await _client.fetchERC20Balances(
@ -413,7 +443,7 @@ abstract class EthereumWalletBase
Future<void>? updateBalance() async => await _updateBalance();
List<Erc20Token> get erc20Currencies => erc20TokensBox.values.toList();
List<Erc20Token> get erc20Currencies => ethereumErc20TokensBox.values.toList();
Future<void> addErc20Token(Erc20Token token) async {
String? iconPath;
@ -429,10 +459,11 @@ abstract class EthereumWalletBase
contractAddress: token.contractAddress,
decimal: token.decimal,
enabled: token.enabled,
tag: token.tag ?? "ETH",
iconPath: iconPath,
);
await erc20TokensBox.put(_token.contractAddress, _token);
await ethereumErc20TokensBox.put(_token.contractAddress, _token);
if (_token.enabled) {
balance[_token] = await _client.fetchERC20Balances(
@ -462,7 +493,7 @@ abstract class EthereumWalletBase
void addInitialTokens() {
final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens;
initialErc20Tokens.forEach((token) => erc20TokensBox.put(token.contractAddress, token));
initialErc20Tokens.forEach((token) => ethereumErc20TokensBox.put(token.contractAddress, token));
}
@override
@ -492,7 +523,7 @@ abstract class EthereumWalletBase
_transactionsUpdateTimer!.cancel();
}
_transactionsUpdateTimer = Timer.periodic(Duration(seconds: 10), (_) {
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
_updateTransactions();
_updateBalance();
});
@ -508,7 +539,7 @@ abstract class EthereumWalletBase
}
@override
String signMessage(String message, {String? address = null}) =>
String signMessage(String message, {String? address}) =>
bytesToHex(_ethPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
Web3Client? getWeb3Client() => _client.getWeb3Client();

View file

@ -20,12 +20,7 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
@override
Future<EthereumWallet> create(EthereumNewWalletCredentials credentials) async {
final strength = (credentials.seedPhraseLength == 12)
? 128
: (credentials.seedPhraseLength == 24)
? 256
: 128;
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final mnemonic = bip39.generateMnemonic(strength: strength);
final wallet = EthereumWallet(
@ -67,8 +62,8 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = walletInfoSource.values.firstWhereOrNull(
(info) => info.id == WalletBase.idFor(wallet, getType()))!;
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);
}

View file

@ -21,8 +21,8 @@ class PendingEthereumTransaction with PendingTransaction {
@override
String get amountFormatted {
final _amount = BigInt.parse(amount) / BigInt.from(pow(10, exponent));
return _amount.toStringAsFixed(min(15, _amount.toString().length));
final _amount = (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString();
return _amount.substring(0, min(10, _amount.length));
}
@override
@ -30,8 +30,8 @@ class PendingEthereumTransaction with PendingTransaction {
@override
String get feeFormatted {
final _fee = fee / BigInt.from(pow(10, 18));
return _fee.toStringAsFixed(min(15, _fee.toString().length));
final _fee = (fee / BigInt.from(pow(10, 18))).toString();
return _fee.substring(0, min(10, _fee.length));
}
@override

View file

@ -17,7 +17,6 @@ dependencies:
mobx: ^2.0.7+4
bip39: ^1.0.6
bip32: ^2.0.0
ed25519_hd_key: ^2.2.0
hex: ^0.2.0
http: ^1.1.0
shared_preferences: ^2.0.15

View file

@ -623,15 +623,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tor:
dependency: transitive
description:
path: "."
ref: main
resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5"
url: "https://github.com/cake-tech/tor.git"
source: git
version: "0.0.1"
typed_data:
dependency: transitive
description:

View file

@ -153,6 +153,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
hashlib:
dependency: transitive
description:
name: hashlib
sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
hashlib_codecs:
dependency: transitive
description:
name: hashlib_codecs
sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
http:
dependency: transitive
description:
@ -308,12 +324,11 @@ packages:
polyseed:
dependency: transitive
description:
path: "."
ref: HEAD
resolved-ref: "504d58a5b147fccd3bc85a25f2e72fb32771ddd7"
url: "https://github.com/cake-tech/polyseed_dart.git"
source: git
version: "0.0.1"
name: polyseed
sha256: "9b48ec535b10863f78f6354ec983b4cc0c88ca69ff48fee469d0fd1954b01d4f"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
process:
dependency: transitive
description:
@ -383,15 +398,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.1"
tor:
dependency: transitive
description:
path: "."
ref: main
resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5"
url: "https://github.com/cake-tech/tor.git"
source: git
version: "0.0.1"
typed_data:
dependency: transitive
description:

View file

@ -385,6 +385,9 @@ extern "C"
(uint64_t)restoreHeight,
std::string(spendKey));
// Cache Raw to support Polyseed
wallet->setCacheAttribute("cakewallet.seed", std::string(seed));
int status;
std::string errorString;
@ -396,9 +399,6 @@ extern "C"
return false;
}
// Cache Raw to support Polyseed
wallet->setCacheAttribute("cakewallet.seed", std::string(seed));
change_current_wallet(wallet);
return true;
}
@ -926,6 +926,8 @@ extern "C"
return m_wallet->trustedDaemon();
}
// Coin Control //
CoinsInfoRow* coin(int index)
{
if (index >= 0 && index < m_coins_info.size()) {
@ -1020,6 +1022,13 @@ extern "C"
m_coins->thaw(index);
}
// Sign Messages //
char *sign_message(char *message, char *address = "")
{
return strdup(get_current_wallet()->signMessage(std::string(message), std::string(address)).c_str());
}
#ifdef __cplusplus
}
#endif

View file

@ -32,6 +32,7 @@ void store(char *path);
void set_trusted_daemon(bool arg);
bool trusted_daemon();
char *sign_message(char *message, char *address);
#ifdef __cplusplus
}

View file

@ -149,3 +149,5 @@ typedef coin = Pointer<CoinsInfoRow> Function(Int32 index);
typedef freeze_coin = Void Function(Int32 index);
typedef thaw_coin = Void Function(Int32 index);
typedef sign_message = Pointer<Utf8> Function(Pointer<Utf8> message, Pointer<Utf8> address);

View file

@ -149,3 +149,5 @@ typedef GetCoin = Pointer<CoinsInfoRow> Function(int);
typedef FreezeCoin = void Function(int);
typedef ThawCoin = void Function(int);
typedef SignMessage = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);

View file

@ -8,7 +8,6 @@ import 'package:cw_monero/api/types.dart';
import 'package:cw_monero/api/monero_api.dart';
import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
int _boolToInt(bool value) => value ? 1 : 0;
@ -128,6 +127,10 @@ final trustedDaemonNative = moneroApi
.lookup<NativeFunction<trusted_daemon>>('trusted_daemon')
.asFunction<TrustedDaemon>();
final signMessageNative = moneroApi
.lookup<NativeFunction<sign_message>>('sign_message')
.asFunction<SignMessage>();
int getSyncingHeight() => getSyncingHeightNative();
bool isNeededToRefresh() => isNeededToRefreshNative() != 0;
@ -296,7 +299,7 @@ class SyncListener {
final bchHeight = await getNodeHeightOrUpdate(syncHeight);
if (_lastKnownBlockHeight == syncHeight || syncHeight == null) {
if (_lastKnownBlockHeight == syncHeight) {
return;
}
@ -311,7 +314,7 @@ class SyncListener {
}
// 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents;
onNewBlock?.call(syncHeight, left, ptc);
onNewBlock.call(syncHeight, left, ptc);
});
}
@ -383,3 +386,14 @@ String getSubaddressLabel(int accountIndex, int addressIndex) {
Future setTrustedDaemon(bool trusted) async => setTrustedDaemonNative(_boolToInt(trusted));
Future<bool> trustedDaemon() async => trustedDaemonNative() != 0;
String signMessage(String message, {String address = ""}) {
final messagePointer = message.toNativeUtf8();
final addressPointer = address.toNativeUtf8();
final signature = convertUTF8ToString(pointer: signMessageNative(messagePointer, addressPointer));
calloc.free(messagePointer);
calloc.free(addressPointer);
return signature;
}

View file

@ -1,14 +1,16 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:cw_monero/api/convert_utf8_to_string.dart';
import 'package:cw_monero/api/signatures.dart';
import 'package:cw_monero/api/types.dart';
import 'package:cw_monero/api/monero_api.dart';
import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart';
import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart';
import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart';
import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart';
import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart';
import 'package:cw_monero/api/monero_api.dart';
import 'package:cw_monero/api/signatures.dart';
import 'package:cw_monero/api/types.dart';
import 'package:cw_monero/api/wallet.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
final createWalletNative = moneroApi
.lookup<NativeFunction<create_wallet>>('create_wallet')
@ -175,6 +177,8 @@ void restoreWalletFromSpendKeySync(
calloc.free(languagePointer);
calloc.free(spendKeyPointer);
storeSync();
if (!isWalletRestored) {
throw WalletRestoreFromKeysException(
message: convertUTF8ToString(pointer: errorMessagePointer));

View file

@ -651,4 +651,10 @@ abstract class MoneroWalletBase
@override
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
@override
String signMessage(String message, {String? address}) {
final useAddress = address ?? "";
return monero_wallet.signMessage(message, address: useAddress);
}
}

View file

@ -13,7 +13,6 @@ import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
import 'package:cw_monero/monero_wallet.dart';
import 'package:hive/hive.dart';
import 'package:polyseed/polyseed.dart';
import 'package:polyseed/src/utils/key_utils.dart';
class MoneroNewWalletCredentials extends WalletCredentials {
MoneroNewWalletCredentials({required String name, required this.language, required this.isPolyseed, String? password})
@ -77,8 +76,12 @@ class MoneroWalletService extends WalletService<
final polyseed = Polyseed.create();
final lang = PolyseedLang.getByEnglishName(credentials.language);
final heightOverride =
getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2)));
return _restoreFromPolyseed(
path, credentials.password!, polyseed, credentials.walletInfo!, lang);
path, credentials.password!, polyseed, credentials.walletInfo!, lang,
overrideHeight: heightOverride);
}
await monero_wallet_manager.createWallet(
@ -268,18 +271,23 @@ class MoneroWalletService extends WalletService<
Future<MoneroWallet> _restoreFromPolyseed(String path, String password, Polyseed polyseed,
WalletInfo walletInfo, PolyseedLang lang,
{PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO}) async {
final height = getMoneroHeigthByDate(
{PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight}) async {
final height = overrideHeight ?? getMoneroHeigthByDate(
date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000));
final spendKey = keyToHexString(polyseed.generateKey(coin, 32));
final spendKey = polyseed.generateKey(coin, 32).toHexString();
final seed = polyseed.encode(lang, coin);
walletInfo.isRecovery = true;
walletInfo.restoreHeight = height;
await monero_wallet_manager.restoreFromSpendKey(
path: path,
password: password,
seed: polyseed.encode(lang, coin),
seed: seed,
language: lang.nameEnglish,
restoreHeight: height,
spendKey: spendKey);
final wallet = MoneroWallet(
walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource);
await wallet.init();

View file

@ -234,7 +234,6 @@ extern "C"
}
void setUnlocked(bool unlocked);
};
Monero::Coins *m_coins;
@ -375,6 +374,35 @@ extern "C"
return true;
}
bool restore_wallet_from_spend_key(char *path, char *password, char *seed, char *language, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error)
{
Monero::NetworkType _networkType = static_cast<Monero::NetworkType>(networkType);
Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createDeterministicWalletFromSpendKey(
std::string(path),
std::string(password),
std::string(language),
_networkType,
(uint64_t)restoreHeight,
std::string(spendKey));
// Cache Raw to support Polyseed
wallet->setCacheAttribute("cakewallet.seed", std::string(seed));
int status;
std::string errorString;
wallet->statusWithErrorString(status, errorString);
if (status != Monero::Wallet::Status_Ok || !errorString.empty())
{
error = strdup(errorString.c_str());
return false;
}
change_current_wallet(wallet);
return true;
}
bool load_wallet(char *path, char *password, int32_t nettype)
{
nice(19);
@ -439,6 +467,11 @@ extern "C"
const char *seed()
{
std::string _rawSeed = get_current_wallet()->getCacheAttribute("cakewallet.seed");
if (!_rawSeed.empty())
{
return strdup(_rawSeed.c_str());
}
return strdup(get_current_wallet()->seed().c_str());
}
@ -842,6 +875,12 @@ extern "C"
return m_transaction_history->count();
}
TransactionInfoRow* get_transaction(char * txId)
{
Monero::TransactionInfo *row = m_transaction_history->transaction(std::string(txId));
return new TransactionInfoRow(row);
}
int LedgerExchange(
unsigned char *command,
unsigned int cmd_len,
@ -971,6 +1010,22 @@ extern "C"
return result;
}
void freeze_coin(int index)
{
m_coins->setFrozen(index);
}
void thaw_coin(int index)
{
m_coins->thaw(index);
}
// Sign Messages //
char *sign_message(char *message, char *address = "")
{
return strdup(get_current_wallet()->signMessage(std::string(message), std::string(address)).c_str());
}
#ifdef __cplusplus
}

View file

@ -32,6 +32,7 @@ void store(char *path);
void set_trusted_daemon(bool arg);
bool trusted_daemon();
char *sign_message(char *message, char *address);
#ifdef __cplusplus
}

View file

@ -266,6 +266,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
hashlib:
dependency: transitive
description:
name: hashlib
sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
hashlib_codecs:
dependency: transitive
description:
name: hashlib_codecs
sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
hive:
dependency: transitive
description:
@ -485,12 +501,11 @@ packages:
polyseed:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "504d58a5b147fccd3bc85a25f2e72fb32771ddd7"
url: "https://github.com/cake-tech/polyseed_dart.git"
source: git
version: "0.0.1"
name: polyseed
sha256: "9b48ec535b10863f78f6354ec983b4cc0c88ca69ff48fee469d0fd1954b01d4f"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
pool:
dependency: transitive
description:
@ -632,15 +647,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tor:
dependency: transitive
description:
path: "."
ref: main
resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5"
url: "https://github.com/cake-tech/tor.git"
source: git
version: "0.0.1"
typed_data:
dependency: transitive
description:

View file

@ -19,9 +19,7 @@ dependencies:
flutter_mobx: ^2.0.6+1
intl: ^0.18.0
encrypt: ^5.0.1
polyseed:
git:
url: https://github.com/cake-tech/polyseed_dart.git
polyseed: ^0.0.2
cw_core:
path: ../cw_core

30
cw_polygon/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# 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/

10
cw_polygon/.metadata Normal file
View file

@ -0,0 +1,10 @@
# 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 and should not be manually edited.
version:
revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
channel: stable
project_type: package

3
cw_polygon/CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
cw_polygon/LICENSE Normal file
View file

@ -0,0 +1 @@
TODO: Add your license here.

39
cw_polygon/README.md Normal file
View file

@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View file

@ -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

View file

@ -0,0 +1,7 @@
library cw_polygon;
/// A Calculator.
class Calculator {
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}

View file

@ -0,0 +1,82 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
class DefaultPolygonErc20Tokens {
final List<Erc20Token> _defaultTokens = [
Erc20Token(
name: "Wrapped Ether",
symbol: "WETH",
contractAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Tether USD (PoS)",
symbol: "USDT",
contractAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
decimal: 6,
enabled: true,
),
Erc20Token(
name: "USD Coin",
symbol: "USDC",
contractAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
decimal: 6,
enabled: true,
),
Erc20Token(
name: "USD Coin (POS)",
symbol: "USDC.e",
contractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
decimal: 6,
enabled: true,
),
Erc20Token(
name: "Avalanche Token",
symbol: "AVAX",
contractAddress: "0x2C89bbc92BD86F8075d1DEcc58C7F4E0107f286b",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Wrapped BTC (PoS)",
symbol: "WBTC",
contractAddress: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6",
decimal: 8,
enabled: false,
),
Erc20Token(
name: "Dai (PoS)",
symbol: "DAI",
contractAddress: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "SHIBA INU (PoS)",
symbol: "SHIB",
contractAddress: "0x6f8a06447Ff6FcF75d803135a7de15CE88C1d4ec",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Uniswap (PoS)",
symbol: "UNI",
contractAddress: "0xb33EaAd8d922B1083446DC23f610c2567fB5180f",
decimal: 18,
enabled: false,
),
];
List<Erc20Token> get initialPolygonErc20Tokens => _defaultTokens.map((token) {
String? iconPath;
try {
iconPath = CryptoCurrency.all
.firstWhere((element) =>
element.title.toUpperCase() == token.symbol.split(".").first.toUpperCase())
.iconPath;
} catch (_) {}
return Erc20Token.copyWith(token, iconPath, 'POLY');
}).toList();
}

View file

@ -0,0 +1,19 @@
import 'dart:typed_data';
import 'package:cw_ethereum/pending_ethereum_transaction.dart';
class PendingPolygonTransaction extends PendingEthereumTransaction {
PendingPolygonTransaction({
required Function sendTransaction,
required Uint8List signedTransaction,
required BigInt fee,
required String amount,
required int exponent,
}) : super(
amount: amount,
sendTransaction: sendTransaction,
signedTransaction: signedTransaction,
fee: fee,
exponent: exponent,
);
}

View file

@ -0,0 +1,55 @@
import 'dart:convert';
import 'package:cw_ethereum/ethereum_client.dart';
import 'package:cw_polygon/polygon_transaction_model.dart';
import 'package:cw_ethereum/.secrets.g.dart' as secrets;
import 'package:flutter/foundation.dart';
import 'package:web3dart/web3dart.dart';
class PolygonClient extends EthereumClient {
@override
Transaction createTransaction({
required EthereumAddress from,
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
}) {
return Transaction(
from: from,
to: to,
value: amount,
);
}
@override
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction;
@override
int get chainId => 137;
@override
Future<List<PolygonTransactionModel>> fetchTransactions(String address,
{String? contractAddress}) async {
try {
final response = await httpClient.get(Uri.https("api.polygonscan.com", "/api", {
"module": "account",
"action": contractAddress != null ? "tokentx" : "txlist",
if (contractAddress != null) "contractaddress": contractAddress,
"address": address,
"apikey": secrets.polygonScanApiKey,
}));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (jsonResponse['result'] as List)
.map((e) => PolygonTransactionModel.fromJson(e as Map<String, dynamic>))
.toList();
}
return [];
} catch (e) {
return [];
}
}
}

View file

@ -0,0 +1,6 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_ethereum/ethereum_exceptions.dart';
class PolygonTransactionCreationException extends EthereumTransactionCreationException {
PolygonTransactionCreationException(CryptoCurrency currency) : super(currency);
}

View file

@ -0,0 +1,25 @@
import 'package:intl/intl.dart';
const polygonAmountLength = 12;
const polygonAmountDivider = 1000000000000;
final polygonAmountFormat = NumberFormat()
..maximumFractionDigits = polygonAmountLength
..minimumFractionDigits = 1;
class PolygonFormatter {
static int parsePolygonAmount(String amount) {
try {
return (double.parse(amount) * polygonAmountDivider).round();
} catch (_) {
return 0;
}
}
static double parsePolygonAmountToDouble(int amount) {
try {
return amount / polygonAmountDivider;
} catch (_) {
return 0;
}
}
}

View file

@ -0,0 +1,5 @@
class PolygonMnemonicIsIncorrectException implements Exception {
@override
String toString() =>
'Polygon mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
}

View file

@ -0,0 +1,18 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
import 'package:cw_polygon/polygon_transaction_priority.dart';
class PolygonTransactionCredentials extends EthereumTransactionCredentials {
PolygonTransactionCredentials(
List<OutputInfo> outputs, {
required PolygonTransactionPriority? priority,
required CryptoCurrency currency,
final int? feeRate,
}) : super(
outputs,
currency: currency,
priority: priority,
feeRate: feeRate,
);
}

View file

@ -0,0 +1,77 @@
import 'dart:convert';
import 'dart:core';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_ethereum/file.dart';
import 'package:cw_polygon/polygon_transaction_info.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
part 'polygon_transaction_history.g.dart';
const transactionsHistoryFileName = 'polygon_transactions.json';
class PolygonTransactionHistory = PolygonTransactionHistoryBase with _$PolygonTransactionHistory;
abstract class PolygonTransactionHistoryBase extends TransactionHistoryBase<PolygonTransactionInfo>
with Store {
PolygonTransactionHistoryBase({required this.walletInfo, required String password})
: _password = password {
transactions = ObservableMap<String, PolygonTransactionInfo>();
}
final WalletInfo walletInfo;
String _password;
Future<void> init() async => await _load();
@override
Future<void> save() async {
try {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName';
final data = json.encode({'transactions': transactions});
await writeData(path: path, password: _password, data: data);
} catch (e, s) {
print('Error while saving polygon transaction history: ${e.toString()}');
print(s);
}
}
@override
void addOne(PolygonTransactionInfo transaction) => transactions[transaction.id] = transaction;
@override
void addMany(Map<String, PolygonTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
Future<Map<String, dynamic>> _read() async {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName';
final content = await read(path: path, password: _password);
if (content.isEmpty) {
return {};
}
return json.decode(content) as Map<String, dynamic>;
}
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
txs.entries.forEach((entry) {
final val = entry.value;
if (val is Map<String, dynamic>) {
final tx = PolygonTransactionInfo.fromJson(val);
_update(tx);
}
});
} catch (e) {
print(e);
}
}
void _update(PolygonTransactionInfo transaction) => transactions[transaction.id] = transaction;
}

View file

@ -0,0 +1,49 @@
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_ethereum/ethereum_transaction_info.dart';
class PolygonTransactionInfo extends EthereumTransactionInfo {
PolygonTransactionInfo({
required String id,
required int height,
required BigInt ethAmount,
int exponent = 18,
required TransactionDirection direction,
required DateTime date,
required bool isPending,
required BigInt ethFee,
required int confirmations,
String tokenSymbol = "MATIC",
required String? to,
}) : super(
confirmations: confirmations,
id: id,
height: height,
ethAmount: ethAmount,
exponent: exponent,
direction: direction,
date: date,
isPending: isPending,
ethFee: ethFee,
to: to,
tokenSymbol: tokenSymbol,
);
factory PolygonTransactionInfo.fromJson(Map<String, dynamic> data) {
return PolygonTransactionInfo(
id: data['id'] as String,
height: data['height'] as int,
ethAmount: BigInt.parse(data['amount']),
exponent: data['exponent'] as int,
ethFee: BigInt.parse(data['fee']),
direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int,
tokenSymbol: data['tokenSymbol'] as String,
to: data['to'],
);
}
@override
String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} MATIC';
}

View file

@ -0,0 +1,49 @@
import 'package:cw_ethereum/ethereum_transaction_model.dart';
class PolygonTransactionModel extends EthereumTransactionModel {
PolygonTransactionModel({
required DateTime date,
required String hash,
required String from,
required String to,
required BigInt amount,
required int gasUsed,
required BigInt gasPrice,
required String contractAddress,
required int confirmations,
required int blockNumber,
required String? tokenSymbol,
required int? tokenDecimal,
required bool isError,
}) : super(
amount: amount,
date: date,
hash: hash,
from: from,
to: to,
gasPrice: gasPrice,
gasUsed: gasUsed,
confirmations: confirmations,
contractAddress: contractAddress,
blockNumber: blockNumber,
tokenDecimal: tokenDecimal,
tokenSymbol: tokenSymbol,
isError: isError,
);
factory PolygonTransactionModel.fromJson(Map<String, dynamic> json) => PolygonTransactionModel(
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
hash: json["hash"],
from: json["from"],
to: json["to"],
amount: BigInt.parse(json["value"]),
gasUsed: int.parse(json["gasUsed"]),
gasPrice: BigInt.parse(json["gasPrice"]),
contractAddress: json["contractAddress"],
confirmations: int.parse(json["confirmations"]),
blockNumber: int.parse(json["blockNumber"]),
tokenSymbol: json["tokenSymbol"] ?? "MATIC",
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
isError: json["isError"] == "1",
);
}

View file

@ -0,0 +1,51 @@
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
class PolygonTransactionPriority extends EthereumTransactionPriority {
const PolygonTransactionPriority({required String title, required int raw, required int tip})
: super(title: title, raw: raw, tip: tip);
static const List<PolygonTransactionPriority> all = [fast, medium, slow];
static const PolygonTransactionPriority slow =
PolygonTransactionPriority(title: 'slow', raw: 0, tip: 1);
static const PolygonTransactionPriority medium =
PolygonTransactionPriority(title: 'Medium', raw: 1, tip: 2);
static const PolygonTransactionPriority fast =
PolygonTransactionPriority(title: 'Fast', raw: 2, tip: 4);
static PolygonTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return slow;
case 1:
return medium;
case 2:
return fast;
default:
throw Exception('Unexpected token: $raw for PolygonTransactionPriority deserialize');
}
}
@override
String get units => 'gas';
@override
String toString() {
var label = '';
switch (this) {
case PolygonTransactionPriority.slow:
label = 'Slow';
break;
case PolygonTransactionPriority.medium:
label = 'Medium';
break;
case PolygonTransactionPriority.fast:
label = 'Fast';
break;
default:
break;
}
return label;
}
}

View file

@ -0,0 +1,521 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.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/transaction_priority.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_ethereum/erc20_balance.dart';
import 'package:cw_ethereum/ethereum_formatter.dart';
import 'package:cw_ethereum/file.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_polygon/default_polygon_erc20_tokens.dart';
import 'package:cw_polygon/polygon_client.dart';
import 'package:cw_polygon/polygon_exceptions.dart';
import 'package:cw_polygon/polygon_formatter.dart';
import 'package:cw_polygon/polygon_transaction_credentials.dart';
import 'package:cw_polygon/polygon_transaction_history.dart';
import 'package:cw_polygon/polygon_transaction_info.dart';
import 'package:cw_polygon/polygon_transaction_model.dart';
import 'package:cw_polygon/polygon_transaction_priority.dart';
import 'package:cw_polygon/polygon_wallet_addresses.dart';
import 'package:hive/hive.dart';
import 'package:hex/hex.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bip32/bip32.dart' as bip32;
part 'polygon_wallet.g.dart';
class PolygonWallet = PolygonWalletBase with _$PolygonWallet;
abstract class PolygonWalletBase
extends WalletBase<ERC20Balance, PolygonTransactionHistory, PolygonTransactionInfo> with Store {
PolygonWalletBase({
required WalletInfo walletInfo,
String? mnemonic,
String? privateKey,
required String password,
ERC20Balance? initialBalance,
}) : syncStatus = const NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_hexPrivateKey = privateKey,
_isTransactionUpdating = false,
_client = PolygonClient(),
walletAddresses = PolygonWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
{CryptoCurrency.maticpoly: initialBalance ?? ERC20Balance(BigInt.zero)}),
super(walletInfo) {
this.walletInfo = walletInfo;
transactionHistory = PolygonTransactionHistory(walletInfo: walletInfo, password: password);
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
CakeHive.registerAdapter(Erc20TokenAdapter());
}
_sharedPrefs.complete(SharedPreferences.getInstance());
}
final String? _mnemonic;
final String? _hexPrivateKey;
final String _password;
late final Box<Erc20Token> polygonErc20TokensBox;
late final EthPrivateKey _polygonPrivateKey;
late final PolygonClient _client;
EthPrivateKey get polygonPrivateKey => _polygonPrivateKey;
int? _gasPrice;
int? _estimatedGas;
bool _isTransactionUpdating;
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
Timer? _transactionsUpdateTimer;
@override
WalletAddresses walletAddresses;
@override
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, ERC20Balance> balance;
final Completer<SharedPreferences> _sharedPrefs = Completer();
Future<void> init() async {
polygonErc20TokensBox = await CakeHive.openBox<Erc20Token>(
"${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.polygonBoxName}");
await walletAddresses.init();
await transactionHistory.init();
_polygonPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
password: _password,
);
walletAddresses.address = _polygonPrivateKey.address.toString();
await save();
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
try {
if (priority is PolygonTransactionPriority) {
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
}
return 0;
} catch (e) {
return 0;
}
}
@override
Future<void> changePassword(String password) {
throw UnimplementedError("changePassword");
}
@override
void close() {
_client.stop();
_transactionsUpdateTimer?.cancel();
}
@action
@override
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
final isConnected = _client.connect(node);
if (!isConnected) {
throw Exception("Polygon Node connection failed");
}
_client.setListeners(_polygonPrivateKey.address, _onNewTransaction);
_setTransactionUpdateTimer();
syncStatus = ConnectedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final credentials0 = credentials as PolygonTransactionCredentials;
final outputs = credentials0.outputs;
final hasMultiDestination = outputs.length > 1;
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == credentials0.currency.title);
final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToPolygonMultiplier = pow(10, exponent);
// so far this can not be made with Polygon as Polygon does not support multiple recipients
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw PolygonTransactionCreationException(transactionCurrency);
}
final totalOriginalAmount = PolygonFormatter.parsePolygonAmountToDouble(
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToPolygonMultiplier);
if (erc20Balance.balance < totalAmount) {
throw PolygonTransactionCreationException(transactionCurrency);
}
} else {
final output = outputs.first;
// since the fees are taken from Ethereum
// then no need to subtract the fees from the amount if send all
final BigInt allAmount;
if (transactionCurrency is Erc20Token) {
allAmount = erc20Balance.balance;
} else {
allAmount =
erc20Balance.balance - BigInt.from(calculateEstimatedFee(credentials0.priority!, null));
}
final totalOriginalAmount =
EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount =
output.sendAll ? allAmount : BigInt.from(totalOriginalAmount * amountToPolygonMultiplier);
if (erc20Balance.balance < totalAmount) {
throw PolygonTransactionCreationException(transactionCurrency);
}
}
final pendingPolygonTransaction = await _client.signTransaction(
privateKey: _polygonPrivateKey,
toAddress: credentials0.outputs.first.isParsedAddress
? credentials0.outputs.first.extractedAddress!
: credentials0.outputs.first.address,
amount: totalAmount.toString(),
gas: _estimatedGas!,
priority: credentials0.priority!,
currency: transactionCurrency,
exponent: exponent,
contractAddress:
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
);
return pendingPolygonTransaction;
}
Future<void> _updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
bool isPolygonScanEnabled = (await _sharedPrefs.future).getBool("use_polygonscan") ?? true;
if (!isPolygonScanEnabled) {
return;
}
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
} catch (_) {
_isTransactionUpdating = false;
}
}
@override
Future<Map<String, PolygonTransactionInfo>> fetchTransactions() async {
final address = _polygonPrivateKey.address.hex;
final transactions = await _client.fetchTransactions(address);
final List<Future<List<PolygonTransactionModel>>> polygonErc20TokensTransactions = [];
for (var token in balance.keys) {
if (token is Erc20Token) {
polygonErc20TokensTransactions.add(
_client.fetchTransactions(
address,
contractAddress: token.contractAddress,
),
);
}
}
final tokensTransaction = await Future.wait(polygonErc20TokensTransactions);
transactions.addAll(tokensTransaction.expand((element) => element));
final Map<String, PolygonTransactionInfo> result = {};
for (var transactionModel in transactions) {
if (transactionModel.isError) {
continue;
}
result[transactionModel.hash] = PolygonTransactionInfo(
id: transactionModel.hash,
height: transactionModel.blockNumber,
ethAmount: transactionModel.amount,
direction: transactionModel.from == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
isPending: false,
date: transactionModel.date,
confirmations: transactionModel.confirmations,
ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
exponent: transactionModel.tokenDecimal ?? 18,
tokenSymbol: transactionModel.tokenSymbol ?? "MATIC",
to: transactionModel.to,
);
}
return result;
}
@override
Object get keys => throw UnimplementedError("keys");
@override
Future<void> rescan({required int height}) {
throw UnimplementedError("rescan");
}
@override
Future<void> save() async {
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@override
String? get seed => _mnemonic;
@override
String get privateKey => HEX.encode(_polygonPrivateKey.privateKey);
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
await _updateBalance();
await _updateTransactions();
_gasPrice = await _client.getGasUnitPrice();
_estimatedGas = await _client.getEstimatedGas();
Timer.periodic(
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
Timer.periodic(const Duration(seconds: 10),
(timer) async => _estimatedGas = await _client.getEstimatedGas());
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
String toJSON() => json.encode({
'mnemonic': _mnemonic,
'private_key': privateKey,
'balance': balance[currency]!.toJSON(),
});
static Future<PolygonWallet> open({
required String name,
required String password,
required WalletInfo walletInfo,
}) async {
final path = await pathForWallet(name: name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero);
return PolygonWallet(
walletInfo: walletInfo,
password: password,
mnemonic: mnemonic,
privateKey: privateKey,
initialBalance: balance,
);
}
Future<void> _updateBalance() async {
balance[currency] = await _fetchMaticBalance();
await _fetchErc20Balances();
await save();
}
Future<ERC20Balance> _fetchMaticBalance() async {
final balance = await _client.getBalance(_polygonPrivateKey.address);
return ERC20Balance(balance.getInWei);
}
Future<void> _fetchErc20Balances() async {
for (var token in polygonErc20TokensBox.values) {
try {
if (token.enabled) {
balance[token] = await _client.fetchERC20Balances(
_polygonPrivateKey.address,
token.contractAddress,
);
} else {
balance.remove(token);
}
} catch (_) {}
}
}
Future<EthPrivateKey> getPrivateKey(
{String? mnemonic, String? privateKey, required String password}) async {
assert(mnemonic != null || privateKey != null);
if (privateKey != null) {
return EthPrivateKey.fromHex(privateKey);
}
final seed = bip39.mnemonicToSeed(mnemonic!);
final root = bip32.BIP32.fromSeed(seed);
const hdPathPolygon = "m/44'/60'/0'/0";
const index = 0;
final addressAtIndex = root.derivePath("$hdPathPolygon/$index");
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
}
@override
Future<void>? updateBalance() async => await _updateBalance();
List<Erc20Token> get erc20Currencies => polygonErc20TokensBox.values.toList();
Future<void> addErc20Token(Erc20Token token) async {
String? iconPath;
try {
iconPath = CryptoCurrency.all
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
.iconPath;
} catch (_) {}
final token0 = Erc20Token(
name: token.name,
symbol: token.symbol,
contractAddress: token.contractAddress,
decimal: token.decimal,
enabled: token.enabled,
tag: token.tag ?? "POLY",
iconPath: iconPath,
);
await polygonErc20TokensBox.put(token0.contractAddress, token0);
if (token0.enabled) {
balance[token0] = await _client.fetchERC20Balances(
_polygonPrivateKey.address,
token0.contractAddress,
);
} else {
balance.remove(token0);
}
}
Future<void> deleteErc20Token(Erc20Token token) async {
await token.delete();
balance.remove(token);
_updateBalance();
}
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
await _client.getErc20Token(contractAddress);
void _onNewTransaction() {
_updateBalance();
_updateTransactions();
}
void addInitialTokens() {
final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens;
for (var token in initialErc20Tokens) {
polygonErc20TokensBox.put(token.contractAddress, token);
}
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
final currentWalletFile = File(currentWalletPath);
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
// Copies current wallet files into new wallet name's dir and files
if (currentWalletFile.existsSync()) {
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
await currentWalletFile.copy(newWalletPath);
}
if (currentTransactionsFile.existsSync()) {
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
}
// Delete old name's dir and files
await Directory(currentDirPath).delete(recursive: true);
}
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
_updateTransactions();
_updateBalance();
});
}
void updatePolygonScanUsageState(bool isEnabled) {
if (isEnabled) {
_updateTransactions();
_setTransactionUpdateTimer();
} else {
_transactionsUpdateTimer?.cancel();
}
}
@override
String signMessage(String message, {String? address}) =>
bytesToHex(_polygonPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
Web3Client? getWeb3Client() => _client.getWeb3Client();
}

View file

@ -0,0 +1,5 @@
import 'package:cw_ethereum/ethereum_wallet_addresses.dart';
class PolygonWalletAddresses extends EthereumWalletAddresses {
PolygonWalletAddresses(super.walletInfo);
}

View file

@ -0,0 +1,28 @@
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
class PolygonNewWalletCredentials extends WalletCredentials {
PolygonNewWalletCredentials({required String name, WalletInfo? walletInfo})
: super(name: name, walletInfo: walletInfo);
}
class PolygonRestoreWalletFromSeedCredentials extends WalletCredentials {
PolygonRestoreWalletFromSeedCredentials(
{required String name,
required String password,
required this.mnemonic,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String mnemonic;
}
class PolygonRestoreWalletFromPrivateKey extends WalletCredentials {
PolygonRestoreWalletFromPrivateKey(
{required String name,
required String password,
required this.privateKey,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String privateKey;
}

View file

@ -0,0 +1,119 @@
import 'dart:io';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_ethereum/ethereum_mnemonics.dart';
import 'package:cw_polygon/polygon_wallet.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:hive/hive.dart';
import 'polygon_wallet_creation_credentials.dart';
import 'package:collection/collection.dart';
class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
PolygonRestoreWalletFromSeedCredentials, PolygonRestoreWalletFromPrivateKey> {
PolygonWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource;
@override
Future<PolygonWallet> create(PolygonNewWalletCredentials credentials) async {
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final mnemonic = bip39.generateMnemonic(strength: strength);
final wallet = PolygonWallet(
walletInfo: credentials.walletInfo!,
mnemonic: mnemonic,
password: credentials.password!,
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
WalletType getType() => WalletType.polygon;
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<PolygonWallet> openWallet(String name, String password) async {
final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = await PolygonWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
await wallet.init();
await wallet.save();
return wallet;
}
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);
}
@override
Future<PolygonWallet> restoreFromKeys(PolygonRestoreWalletFromPrivateKey credentials) async {
final wallet = PolygonWallet(
password: credentials.password!,
privateKey: credentials.privateKey,
walletInfo: credentials.walletInfo!,
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<PolygonWallet> restoreFromSeed(PolygonRestoreWalletFromSeedCredentials credentials) async {
if (!bip39.validateMnemonic(credentials.mnemonic)) {
throw EthereumMnemonicIsIncorrectException();
}
final wallet = PolygonWallet(
password: credentials.password!,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
final currentWallet = await PolygonWalletBase.open(
password: password, name: currentName, walletInfo: currentWalletInfo);
await currentWallet.renameWalletFiles(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
}
}

73
cw_polygon/pubspec.yaml Normal file
View file

@ -0,0 +1,73 @@
name: cw_polygon
description: A new Flutter package project.
version: 0.0.1
publish_to: none
author: Cake Wallet
homepage: https://cakewallet.com
environment:
sdk: '>=3.0.6 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
cw_core:
path: ../cw_core
cw_ethereum:
path: ../cw_ethereum
mobx: ^2.0.7+4
intl: ^0.18.0
bip39: ^1.0.6
hive: ^2.2.3
collection: ^1.17.1
web3dart: ^2.7.1
bip32: ^2.0.0
hex: ^0.2.0
shared_preferences: ^2.0.15
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: ^2.1.11
mobx_codegen: ^2.0.7
hive_generator: ^1.1.3
# 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:
# To add assets to your 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 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

View file

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:cw_polygon/cw_polygon.dart';
void main() {
test('adds one to input values', () {
final calculator = Calculator();
expect(calculator.addOne(2), 3);
expect(calculator.addOne(-7), -6);
expect(calculator.addOne(0), 1);
});
}

View file

@ -145,8 +145,6 @@ PODS:
- SwiftProtobuf (1.22.0)
- SwiftyGif (5.4.4)
- Toast (4.0.0)
- tor (0.0.1):
- Flutter
- uni_links (0.0.1):
- Flutter
- UnstoppableDomainsResolution (4.0.0):
@ -184,7 +182,6 @@ DEPENDENCIES:
- sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- tor (from `.symlinks/plugins/tor/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
- UnstoppableDomainsResolution (~> 4.0.0)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@ -253,8 +250,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
tor:
:path: ".symlinks/plugins/tor/ios"
uni_links:
:path: ".symlinks/plugins/uni_links/ios"
url_launcher_ios:
@ -299,7 +294,6 @@ SPEC CHECKSUMS:
SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
tor: 662a9f5b980b5c86decb8ba611de9bcd4c8286eb
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b

View file

@ -160,6 +160,26 @@
<string>bitcoincash-wallet</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>polygon</string>
<key>CFBundleURLSchemes</key>
<array>
<string>polygon</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>polygon-wallet</string>
<key>CFBundleURLSchemes</key>
<array>
<string>polygon-wallet</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>

View file

@ -2,11 +2,11 @@ import 'package:flutter/foundation.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
class BuyException implements Exception {
BuyException({required this.description, required this.text});
BuyException({required this.title, required this.content});
final BuyProviderDescription description;
final String text;
final String title;
final String content;
@override
String toString() => '${description.title}: $text';
String toString() => '$title: $content';
}

View file

@ -1,27 +1,33 @@
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
abstract class BuyProvider {
BuyProvider({required this.wallet, required this.isTestEnvironment});
BuyProvider({
required this.wallet,
required this.isTestEnvironment,
});
final WalletBase wallet;
final bool isTestEnvironment;
String get title;
BuyProviderDescription get description;
String get trackUrl;
WalletType get walletType => wallet.type;
String get walletAddress => wallet.walletAddresses.address;
String get walletId => wallet.id;
String get providerDescription;
String get lightIcon;
String get darkIcon;
@override
String toString() => title;
Future<String> requestUrl(String amount, String sourceCurrency);
Future<Order> findOrderById(String id);
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency);
Future<void> launchProvider(BuildContext context, bool? isBuyAction);
Future<String> requestUrl(String amount, String sourceCurrency) => throw UnimplementedError();
Future<Order> findOrderById(String id) => throw UnimplementedError();
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError();
}

View file

@ -0,0 +1,201 @@
import 'dart:convert';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class DFXBuyProvider extends BuyProvider {
DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment);
static const _baseUrl = 'api.dfx.swiss';
static const _authPath = '/v1/auth/signMessage';
static const _signUpPath = '/v1/auth/signUp';
static const _signInPath = '/v1/auth/signIn';
static const walletName = 'CakeWallet';
@override
String get title => 'DFX Connect';
@override
String get providerDescription => S.current.dfx_option_description;
@override
String get lightIcon => 'assets/images/dfx_light.png';
@override
String get darkIcon => 'assets/images/dfx_dark.png';
String get assetOut {
switch (wallet.type) {
case WalletType.bitcoin:
return 'BTC';
case WalletType.bitcoinCash:
return 'BCH';
case WalletType.litecoin:
return 'LTC';
case WalletType.monero:
return 'XMR';
case WalletType.ethereum:
return 'ETH';
default:
throw Exception("WalletType is not available for DFX ${wallet.type}");
}
}
String get blockchain {
switch (wallet.type) {
case WalletType.bitcoin:
case WalletType.bitcoinCash:
case WalletType.litecoin:
return 'Bitcoin';
case WalletType.monero:
return 'Monero';
case WalletType.ethereum:
return 'Ethereum';
default:
throw Exception("WalletType is not available for DFX ${wallet.type}");
}
}
Future<String> getSignMessage() async {
final walletAddress = wallet.walletAddresses.address;
final uri = Uri.https(_baseUrl, _authPath, {'address': walletAddress});
var response = await http.get(uri, headers: {'accept': 'application/json'});
if (response.statusCode == 200) {
final responseBody = jsonDecode(response.body);
return responseBody['message'] as String;
} else {
throw Exception(
'Failed to get sign message. Status: ${response.statusCode} ${response.body}');
}
}
Future<String> signUp() async {
final signMessage = getSignature(await getSignMessage());
final walletAddress = wallet.walletAddresses.address;
final requestBody = jsonEncode({
'wallet': walletName,
'address': walletAddress,
'signature': signMessage,
});
final uri = Uri.https(_baseUrl, _signUpPath);
var response = await http.post(uri,
headers: {'Content-Type': 'application/json'}, body: requestBody);
if (response.statusCode == 201) {
final responseBody = jsonDecode(response.body);
return responseBody['accessToken'] as String;
} else if (response.statusCode == 403) {
final responseBody = jsonDecode(response.body);
final message = responseBody['message'] ?? 'Service unavailable in your country';
throw Exception(message);
} else {
throw Exception(
'Failed to sign up. Status: ${response.statusCode} ${response.body}');
}
}
Future<String> signIn() async {
final signMessage = getSignature(await getSignMessage());
final walletAddress = wallet.walletAddresses.address;
final requestBody = jsonEncode({
'address': walletAddress,
'signature': signMessage,
});
final uri = Uri.https(_baseUrl, _signInPath);
var response = await http.post(uri,
headers: {'Content-Type': 'application/json'}, body: requestBody);
if (response.statusCode == 201) {
final responseBody = jsonDecode(response.body);
return responseBody['accessToken'] as String;
} else if (response.statusCode == 403) {
final responseBody = jsonDecode(response.body);
final message = responseBody['message'] ?? 'Service unavailable in your country';
throw Exception(message);
} else {
throw Exception(
'Failed to sign in. Status: ${response.statusCode} ${response.body}');
}
}
String getSignature(String message) {
switch (wallet.type) {
case WalletType.ethereum:
return wallet.signMessage(message);
case WalletType.monero:
case WalletType.litecoin:
case WalletType.bitcoin:
case WalletType.bitcoinCash:
return wallet.signMessage(message,
address: wallet.walletAddresses.address);
default:
throw Exception("WalletType is not available for DFX ${wallet.type}");
}
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
try {
final assetOut = this.assetOut;
final blockchain = this.blockchain;
final actionType = isBuyAction == true ? '/buy' : '/sell';
String accessToken;
try {
accessToken = await signUp();
} on Exception catch (e) {
if (e.toString().contains('409')) {
accessToken = await signIn();
} else {
rethrow;
}
}
final uri = Uri.https('services.dfx.swiss', actionType, {
'session': accessToken,
'lang': 'en',
'asset-out': assetOut,
'blockchain': blockchain,
'asset-in': 'EUR',
});
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context)
.pushNamed(Routes.webViewPage, arguments: ["DFX Connect", uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: "DFX Connect",
alertContent: S.of(context).buy_provider_unavailable + ': $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
}
}

View file

@ -1,9 +1,14 @@
import 'dart:convert';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:crypto/crypto.dart';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
@ -14,33 +19,55 @@ import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cw_core/crypto_currency.dart';
import 'package:url_launcher/url_launcher.dart';
class MoonPaySellProvider {
MoonPaySellProvider({this.isTest = false})
: baseUrl = isTest ? _baseTestUrl : _baseProductUrl;
class MoonPaySellProvider extends BuyProvider {
MoonPaySellProvider({
required SettingsStore settingsStore,
required WalletBase wallet,
bool isTestEnvironment = false,
}) : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
this._settingsStore = settingsStore,
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
final SettingsStore _settingsStore;
static const _baseTestUrl = 'sell-sandbox.moonpay.com';
static const _baseProductUrl = 'sell.moonpay.com';
@override
String get providerDescription =>
'MoonPay offers a fast and simple way to buy and sell cryptocurrencies';
@override
String get title => 'MoonPay';
@override
String get lightIcon => 'assets/images/moonpay_light.png';
@override
String get darkIcon => 'assets/images/moonpay_dark.png';
static String themeToMoonPayTheme(ThemeBase theme) {
switch (theme.type) {
case ThemeType.bright:
return 'light';
case ThemeType.light:
return 'light';
case ThemeType.dark:
return 'dark';
}
}
static String get _apiKey => secrets.moonPayApiKey;
static String get _secretKey => secrets.moonPaySecretKey;
final bool isTest;
final String baseUrl;
Future<Uri> requestUrl(
{required CryptoCurrency currency,
Future<Uri> requestMoonPayUrl({
required CryptoCurrency currency,
required String refundWalletAddress,
required SettingsStore settingsStore}) async {
required SettingsStore settingsStore,
}) async {
final customParams = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode,
@ -50,18 +77,22 @@ class MoonPaySellProvider {
};
final originalUri = Uri.https(
baseUrl, '', <String, dynamic>{
baseUrl,
'',
<String, dynamic>{
'apiKey': _apiKey,
'defaultBaseCurrencyCode': currency.toString().toLowerCase(),
'refundWalletAddress': refundWalletAddress
}..addAll(customParams));
'refundWalletAddress': refundWalletAddress,
}..addAll(customParams),
);
final messageBytes = utf8.encode('?${originalUri.query}');
final key = utf8.encode(_secretKey);
final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
final signature = base64.encode(digest.bytes);
if (isTest) {
if (isTestEnvironment) {
return originalUri;
}
@ -70,6 +101,39 @@ class MoonPaySellProvider {
final signedUri = originalUri.replace(queryParameters: query);
return signedUri;
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
try {
final uri = await requestMoonPayUrl(
currency: wallet.currency,
refundWalletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}
}
class MoonPayBuyProvider extends BuyProvider {
@ -91,28 +155,40 @@ class MoonPayBuyProvider extends BuyProvider {
String get title => 'MoonPay';
@override
BuyProviderDescription get description => BuyProviderDescription.moonPay;
String get currencyCode =>
walletTypeToCryptoCurrency(walletType).title.toLowerCase();
String get providerDescription =>
'MoonPay offers a fast and simple way to buy and sell cryptocurrencies';
@override
String get lightIcon => 'assets/images/moonpay_light.png';
@override
String get darkIcon => 'assets/images/moonpay_dark.png';
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseUrl + '/transaction_receipt?transactionId=';
String baseUrl;
@override
Future<String> requestUrl(String amount, String sourceCurrency) async {
final enabledPaymentMethods =
'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay'
final enabledPaymentMethods = 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay'
'%2Csepa_bank_transfer%2Cgbp_bank_transfer%2Cgbp_open_banking_payment';
final suffix = '?apiKey=' + _apiKey + '&currencyCode=' +
currencyCode + '&enabledPaymentMethods=' + enabledPaymentMethods +
'&walletAddress=' + walletAddress +
'&baseCurrencyCode=' + sourceCurrency.toLowerCase() +
'&baseCurrencyAmount=' + amount + '&lockAmount=true' +
'&showAllCurrencies=false' + '&showWalletAddressForm=false';
final suffix = '?apiKey=' +
_apiKey +
'&currencyCode=' +
currencyCode +
'&enabledPaymentMethods=' +
enabledPaymentMethods +
'&walletAddress=' +
wallet.walletAddresses.address +
'&baseCurrencyCode=' +
sourceCurrency.toLowerCase() +
'&baseCurrencyAmount=' +
amount +
'&lockAmount=true' +
'&showAllCurrencies=false' +
'&showWalletAddressForm=false';
final originalUrl = baseUrl + suffix;
@ -121,25 +197,27 @@ class MoonPayBuyProvider extends BuyProvider {
final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
final signature = base64.encode(digest.bytes);
final urlWithSignature = originalUrl +
'&signature=${Uri.encodeComponent(signature)}';
final urlWithSignature = originalUrl + '&signature=${Uri.encodeComponent(signature)}';
return isTestEnvironment ? originalUrl : urlWithSignature;
}
@override
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
final url = _apiUrl + _currenciesSuffix + '/$currencyCode' +
_quoteSuffix + '/?apiKey=' + _apiKey +
'&baseCurrencyAmount=' + amount +
'&baseCurrencyCode=' + sourceCurrency.toLowerCase();
final url = _apiUrl +
_currenciesSuffix +
'/$currencyCode' +
_quoteSuffix +
'/?apiKey=' +
_apiKey +
'&baseCurrencyAmount=' +
amount +
'&baseCurrencyCode=' +
sourceCurrency.toLowerCase();
final uri = Uri.parse(url);
final response = await get(uri);
if (response.statusCode != 200) {
throw BuyException(
description: description,
text: 'Quote is not found!');
throw BuyException(title: providerDescription, content: 'Quote is not found!');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@ -148,22 +226,16 @@ class MoonPayBuyProvider extends BuyProvider {
final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int;
return BuyAmount(
sourceAmount: sourceAmount,
destAmount: destAmount,
minAmount: minSourceAmount);
sourceAmount: sourceAmount, destAmount: destAmount, minAmount: minSourceAmount);
}
@override
Future<Order> findOrderById(String id) async {
final url = _apiUrl + _transactionsSuffix + '/$id' +
'?apiKey=' + _apiKey;
final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
final uri = Uri.parse(url);
final response = await get(uri);
if (response.statusCode != 200) {
throw BuyException(
description: description,
text: 'Transaction $id is not found!');
throw BuyException(title: providerDescription, content: 'Transaction $id is not found!');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@ -175,14 +247,13 @@ class MoonPayBuyProvider extends BuyProvider {
return Order(
id: id,
provider: description,
provider: BuyProviderDescription.moonPay,
transferId: id,
state: state,
createdAt: createdAt,
amount: amount.toString(),
receiveAddress: walletAddress,
walletId: walletId
);
receiveAddress: wallet.walletAddresses.address,
walletId: wallet.id);
}
static Future<bool> onEnabled() async {
@ -201,4 +272,8 @@ class MoonPayBuyProvider extends BuyProvider {
return isBuyEnable;
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) =>
throw UnimplementedError();
}

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -9,20 +10,31 @@ import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class OnRamperBuyProvider {
OnRamperBuyProvider({required SettingsStore settingsStore, required WalletBase wallet})
: this._settingsStore = settingsStore,
this._wallet = wallet;
final SettingsStore _settingsStore;
final WalletBase _wallet;
class OnRamperBuyProvider extends BuyProvider {
OnRamperBuyProvider(this._settingsStore,
{required WalletBase wallet, bool isTestEnvironment = false})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment);
static const _baseUrl = 'buy.onramper.com';
final SettingsStore _settingsStore;
@override
String get title => 'Onramper';
@override
String get providerDescription => S.current.onramper_option_description;
@override
String get lightIcon => 'assets/images/onramper_light.png';
@override
String get darkIcon => 'assets/images/onramper_dark.png';
String get _apiKey => secrets.onramperApiKey;
String get _normalizeCryptoCurrency {
switch (_wallet.currency) {
switch (wallet.currency) {
case CryptoCurrency.ltc:
return "LTC_LITECOIN";
case CryptoCurrency.xmr:
@ -32,7 +44,7 @@ class OnRamperBuyProvider {
case CryptoCurrency.nano:
return "XNO_NANO";
default:
return _wallet.currency.title;
return wallet.currency.title;
}
}
@ -40,7 +52,7 @@ class OnRamperBuyProvider {
return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
}
Uri requestUrl(BuildContext context) {
Uri requestOnramperUrl(BuildContext context) {
String primaryColor,
secondaryColor,
primaryTextColor,
@ -50,9 +62,10 @@ class OnRamperBuyProvider {
primaryColor = getColorStr(Theme.of(context).primaryColor);
secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
primaryTextColor = getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
secondaryTextColor =
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
primaryTextColor =
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
secondaryTextColor = getColorStr(
Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
containerColor = getColorStr(Theme.of(context).colorScheme.background);
cardColor = getColorStr(Theme.of(context).cardColor);
@ -60,12 +73,13 @@ class OnRamperBuyProvider {
cardColor = getColorStr(Colors.white);
}
final networkName = _wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
final networkName =
wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
return Uri.https(_baseUrl, '', <String, dynamic>{
'apiKey': _apiKey,
'defaultCrypto': _normalizeCryptoCurrency,
'networkWallets': '${networkName}:${_wallet.walletAddresses.address}',
'networkWallets': '${networkName}:${wallet.walletAddresses.address}',
'supportSell': "false",
'supportSwap': "false",
'primaryColor': primaryColor,
@ -77,10 +91,11 @@ class OnRamperBuyProvider {
});
}
Future<void> launchProvider(BuildContext context) async {
final uri = requestUrl(context);
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
final uri = requestOnramperUrl(context);
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [S.of(context).buy, uri]);
Navigator.of(context)
.pushNamed(Routes.webViewPage, arguments: [S.of(context).buy, uri]);
} else {
await launchUrl(uri);
}

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@ -10,40 +11,44 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class RobinhoodBuyProvider {
RobinhoodBuyProvider({required WalletBase wallet}) : this._wallet = wallet;
final WalletBase _wallet;
class RobinhoodBuyProvider extends BuyProvider {
RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment);
static const _baseUrl = 'applink.robinhood.com';
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
@override
String get title => 'Robinhood Connect';
@override
String get providerDescription => S.current.robinhood_option_description;
@override
String get lightIcon => 'assets/images/robinhood_light.png';
@override
String get darkIcon => 'assets/images/robinhood_dark.png';
String get _applicationId => secrets.robinhoodApplicationId;
String get _apiSecret => secrets.robinhoodCIdApiSecret;
bool get isAvailable => [
WalletType.bitcoin,
WalletType.bitcoinCash,
WalletType.litecoin,
WalletType.ethereum
].contains(_wallet.type);
String getSignature(String message) {
switch (_wallet.type) {
switch (wallet.type) {
case WalletType.ethereum:
return _wallet.signMessage(message);
return wallet.signMessage(message);
case WalletType.litecoin:
case WalletType.bitcoin:
case WalletType.bitcoinCash:
return _wallet.signMessage(message, address: _wallet.walletAddresses.address);
return wallet.signMessage(message, address: wallet.walletAddresses.address);
default:
throw Exception("WalletType is not available for Robinhood ${_wallet.type}");
throw Exception("WalletType is not available for Robinhood ${wallet.type}");
}
}
Future<String> getConnectId() async {
final walletAddress = _wallet.walletAddresses.address;
final walletAddress = wallet.walletAddresses.address;
final valid_until = (DateTime.now().millisecondsSinceEpoch / 1000).round() + 10;
final message = "$_apiSecret:${valid_until}";
@ -64,22 +69,22 @@ class RobinhoodBuyProvider {
}
}
Future<Uri> requestUrl() async {
Future<Uri> requestProviderUrl() async {
final connectId = await getConnectId();
final networkName = _wallet.currency.fullName?.toUpperCase().replaceAll(" ", "_");
final networkName = wallet.currency.fullName?.toUpperCase().replaceAll(" ", "_");
return Uri.https(_baseUrl, '/u/connect', <String, dynamic>{
'applicationId': _applicationId,
'connectId': connectId,
'walletAddress': _wallet.walletAddresses.address,
'userIdentifier': _wallet.walletAddresses.address,
'walletAddress': wallet.walletAddresses.address,
'userIdentifier': wallet.walletAddresses.address,
'supportedNetworks': networkName
});
}
Future<void> launchProvider(BuildContext context) async {
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
try {
final uri = await requestUrl();
final uri = await requestProviderUrl();
await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (_) {
await showPopUp<void>(

View file

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
@ -12,9 +13,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets;
class WyreBuyProvider extends BuyProvider {
WyreBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
: baseApiUrl = isTestEnvironment
? _baseTestApiUrl
: _baseProductApiUrl,
: baseApiUrl = isTestEnvironment ? _baseTestApiUrl : _baseProductApiUrl,
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
static const _baseTestApiUrl = 'https://api.testwyre.com';
@ -35,26 +34,27 @@ class WyreBuyProvider extends BuyProvider {
String get title => 'Wyre';
@override
BuyProviderDescription get description => BuyProviderDescription.wyre;
String get providerDescription => '';
@override
String get trackUrl => isTestEnvironment
? _trackTestUrl
: _trackProductUrl;
String get lightIcon => 'assets/images/robinhood_light.png';
@override
String get darkIcon => 'assets/images/robinhood_dark.png';
String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl;
String baseApiUrl;
@override
Future<String> requestUrl(String amount, String sourceCurrency) async {
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final url = baseApiUrl + _ordersSuffix + _reserveSuffix +
_timeStampSuffix + timestamp;
final url = baseApiUrl + _ordersSuffix + _reserveSuffix + _timeStampSuffix + timestamp;
final uri = Uri.parse(url);
final body = {
'amount': amount,
'sourceCurrency': sourceCurrency,
'destCurrency': walletTypeToCryptoCurrency(walletType).title,
'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
'destCurrency': walletTypeToCryptoCurrency(wallet.type).title,
'dest': walletTypeToString(wallet.type).toLowerCase() + ':' + wallet.walletAddresses.address,
'referrerAccountId': _accountId,
'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest']
};
@ -67,9 +67,7 @@ class WyreBuyProvider extends BuyProvider {
body: json.encode(body));
if (response.statusCode != 200) {
throw BuyException(
description: description,
text: 'Url $url is not found!');
throw BuyException(title: providerDescription, content: 'Url $url is not found!');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@ -77,14 +75,13 @@ class WyreBuyProvider extends BuyProvider {
return urlFromResponse;
}
@override
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
final quoteUrl = _baseProductApiUrl + _ordersSuffix + _quoteSuffix;
final body = {
'amount': amount,
'sourceCurrency': sourceCurrency,
'destCurrency': walletTypeToCryptoCurrency(walletType).title,
'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
'destCurrency': walletTypeToCryptoCurrency(wallet.type).title,
'dest': walletTypeToString(wallet.type).toLowerCase() + ':' + wallet.walletAddresses.address,
'accountId': _accountId,
'country': _countryCode
};
@ -98,9 +95,7 @@ class WyreBuyProvider extends BuyProvider {
body: json.encode(body));
if (response.statusCode != 200) {
throw BuyException(
description: description,
text: 'Quote is not found!');
throw BuyException(title: providerDescription, content: 'Quote is not found!');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@ -108,58 +103,55 @@ class WyreBuyProvider extends BuyProvider {
final destAmount = responseJSON['destAmount'] as double;
final achAmount = responseJSON['sourceAmountWithoutFees'] as double;
return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount, achSourceAmount: achAmount);
return BuyAmount(
sourceAmount: sourceAmount, destAmount: destAmount, achSourceAmount: achAmount);
}
@override
Future<Order> findOrderById(String id) async {
final orderUrl = baseApiUrl + _ordersSuffix + '/$id';
final orderUri = Uri.parse(orderUrl);
final orderResponse = await get(orderUri);
if (orderResponse.statusCode != 200) {
throw BuyException(
description: description,
text: 'Order $id is not found!');
throw BuyException(title: providerDescription, content: 'Order $id is not found!');
}
final orderResponseJSON =
json.decode(orderResponse.body) as Map<String, dynamic>;
final orderResponseJSON = json.decode(orderResponse.body) as Map<String, dynamic>;
final transferId = orderResponseJSON['transferId'] as String;
final from = orderResponseJSON['sourceCurrency'] as String;
final to = orderResponseJSON['destCurrency'] as String;
final status = orderResponseJSON['status'] as String;
final state = TradeState.deserialize(raw: status.toLowerCase());
final createdAtRaw = orderResponseJSON['createdAt'] as int;
final createdAt =
DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal();
final createdAt = DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal();
final transferUrl =
baseApiUrl + _transferSuffix + transferId + _trackSuffix;
final transferUrl = baseApiUrl + _transferSuffix + transferId + _trackSuffix;
final transferUri = Uri.parse(transferUrl);
final transferResponse = await get(transferUri);
if (transferResponse.statusCode != 200) {
throw BuyException(
description: description,
text: 'Transfer $transferId is not found!');
throw BuyException(title: providerDescription, content: 'Transfer $transferId is not found!');
}
final transferResponseJSON =
json.decode(transferResponse.body) as Map<String, dynamic>;
final transferResponseJSON = json.decode(transferResponse.body) as Map<String, dynamic>;
final amount = transferResponseJSON['destAmount'] as double;
return Order(
id: id,
provider: description,
provider: BuyProviderDescription.wyre,
transferId: transferId,
from: from,
to: to,
state: state,
createdAt: createdAt,
amount: amount.toString(),
receiveAddress: walletAddress,
walletId: walletId
);
receiveAddress: wallet.walletAddresses.address,
walletId: wallet.id);
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) {
// TODO: implement launchProvider
throw UnimplementedError();
}
}

View file

@ -32,6 +32,8 @@ class AddressValidator extends TextValidator {
return '[0-9a-zA-Z_]';
case CryptoCurrency.usdc:
case CryptoCurrency.usdcpoly:
case CryptoCurrency.usdtPoly:
case CryptoCurrency.usdcEPoly:
case CryptoCurrency.ape:
case CryptoCurrency.avaxc:
case CryptoCurrency.eth:
@ -90,7 +92,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.eos:
return '[0-9a-zA-Z]';
case CryptoCurrency.bch:
return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q[0-9a-zA-Z]{42}\$|^bitcoincash:q[0-9a-zA-Z]{41}\$|^bitcoincash:q[0-9a-zA-Z]{42}\$';
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:
@ -141,6 +143,8 @@ class AddressValidator extends TextValidator {
return [42];
case CryptoCurrency.eth:
case CryptoCurrency.usdcpoly:
case CryptoCurrency.usdtPoly:
case CryptoCurrency.usdcEPoly:
case CryptoCurrency.mana:
case CryptoCurrency.matic:
case CryptoCurrency.maticpoly:
@ -271,6 +275,8 @@ class AddressValidator extends TextValidator {
'|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)';
case CryptoCurrency.eth:
return '0x[0-9a-zA-Z]{42}';
case CryptoCurrency.maticpoly:
return '0x[0-9a-zA-Z]{42}';
case CryptoCurrency.nano:
return 'nano_[0-9a-zA-Z]{60}';
case CryptoCurrency.banano:

View file

@ -16,7 +16,7 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async {
final Map<String, String> queryParams = {
'interval_count': '1',
'base': crypto,
'base': crypto.split(".").first,
'quote': fiat,
'key': secrets.fiatApiKey,
};

View file

@ -3,6 +3,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/entities/mnemonic_item.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/nano/nano.dart';
@ -34,6 +35,8 @@ class SeedValidator extends Validator<MnemonicItem> {
case WalletType.nano:
case WalletType.banano:
return nano!.getNanoWordList(language);
case WalletType.polygon:
return polygon!.getPolygonWordList(language);
default:
return [];
}

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart';
import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
@ -14,7 +15,6 @@ import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
import 'package:convert/convert.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:http/http.dart' as http;
@ -46,13 +46,12 @@ class EvmChainServiceImpl implements ChainService {
required this.wcKeyService,
required this.bottomSheetService,
required this.wallet,
Web3Client? ethClient,
}) : ethClient = ethClient ??
Web3Client? web3Client,
}) : ethClient = web3Client ??
Web3Client(
appStore.settingsStore.getCurrentNode(WalletType.ethereum).uri.toString(),
appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(),
http.Client(),
) {
for (final String event in getEvents()) {
wallet.registerEventEmitter(chainId: getChainId(), event: event);
}
@ -138,7 +137,8 @@ class EvmChainServiceImpl implements ChainService {
try {
// Load the private key
final List<ChainKeyModel> keys = wcKeyService.getKeysForChain(getChainId());
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
@ -176,13 +176,15 @@ class EvmChainServiceImpl implements ChainService {
try {
// Load the private key
final List<ChainKeyModel> keys = wcKeyService.getKeysForChain(getChainId());
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
final EthPrivateKey credentials = EthPrivateKey.fromHex(keys[0].privateKey);
final String signature = hex.encode(
credentials.signPersonalMessageToUint8List(
Uint8List.fromList(utf8.encode(message)),
chainId: getChainIdBasedOnWalletType(appStore.wallet!.type),
),
);
log(signature);
@ -212,7 +214,8 @@ class EvmChainServiceImpl implements ChainService {
}
// Load the private key
final List<ChainKeyModel> keys = wcKeyService.getKeysForChain(getChainId());
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
@ -232,7 +235,11 @@ class EvmChainServiceImpl implements ChainService {
);
try {
final result = await ethClient.sendTransaction(credentials, transaction);
final result = await ethClient.sendTransaction(
credentials,
transaction,
chainId: getChainIdBasedOnWalletType(appStore.wallet!.type),
);
log('Result: $result');
@ -267,7 +274,8 @@ class EvmChainServiceImpl implements ChainService {
return authError;
}
final List<ChainKeyModel> keys = wcKeyService.getKeysForChain(getChainId());
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
return EthSigUtil.signTypedData(
privateKey: keys[0].privateKey,
@ -277,10 +285,12 @@ class EvmChainServiceImpl implements ChainService {
}
String _convertToReadable(Map<String, dynamic> data) {
final tokenName = getTokenNameBasedOnWalletType(appStore.wallet!.type);
String gas = int.parse((data['gas'] as String).substring(2), radix: 16).toString();
String value = data['value'] != null
? (int.parse((data['value'] as String).substring(2), radix: 16) / 1e18).toString() + ' ETH'
: '0 ETH';
? (int.parse((data['value'] as String).substring(2), radix: 16) / 1e18).toString() +
' $tokenName'
: '0 $tokenName';
String from = data['from'] as String;
String to = data['to'] as String;

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