dev #10

Merged
such-gitea merged 8 commits from github-such-software/hash-wallet:dev into dev 2026-05-17 11:09:19 -04:00
170 changed files with 157 additions and 21067 deletions

View File

@@ -50,9 +50,9 @@ jobs:
# IMPORTANT: don't pre-write any of the tool/.*secrets-config.json
# files. The generator at tool/generate_secrets_config.dart has an
# early-return if tool/.secrets-config.json already exists (lines
# 57-63), which then ALSO skips creating the per-module configs (evm,
# solana, nano, tron, bitcoin) — every cw_* module then fails to
# compile with "Undefined name secrets.xxx" for dozens of keys.
# 57-63), which then ALSO skips creating the per-module configs
# (evm, nano, bitcoin) — every cw_* module then fails to compile
# with "Undefined name secrets.xxx" for dozens of keys.
#
# Instead: run the generator first so it creates all configs from the
# full SecretKey list (with empty defaults), then sed-inject Trocador
@@ -93,7 +93,8 @@ jobs:
# ---- Native crypto cores (monero_c prebuilt bundle) ------------------
# Skips the full simplybs toolchain bootstrap. The bundle has libmonero,
# libwownero, AND libzano .so files; CMake only installs the ones we
# ship (monero + wownero).
# ship (monero + wownero) — libzano is dropped at install time even
# though monero_c builds it.
- name: Fetch prebuilt monero_c .so bundle
run: |
set -x -e

4
.gitignore vendored
View File

@@ -229,3 +229,7 @@ scripts/zcash_lib
scripts/android/yttrium
scripts/bitbox_flutter
scripts/reown_flutter
# Hash Wallet: untracked notes / internal playbooks
docs/links.md
docs/UPSTREAM_SYNC.md

78
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,78 @@
# Contributing to Hash Wallet
Thanks for the interest. Before opening a PR, please read this.
## Project shape
Hash Wallet is a **friendly fork** of [Cake Wallet](https://github.com/cake-tech/cake_wallet). We periodically merge upstream changes and try to keep our divergence narrow. Every change you propose should be evaluated through that lens: will this make future upstream syncs harder, and if so, is the value worth the cost?
## Branch model
- `dev` is the integration branch — open PRs against `dev`.
- `main` is reserved for tagged releases.
- We do not use long-lived feature branches. Iterate on `dev` directly via small focused PRs.
## Build environment
See the build section of [`README.md`](README.md). Flutter version is pinned (`.tool-versions` + `Dockerfile`). Don't use a newer Flutter SDK — Cake's `hive_generator` / `build_resolvers` deps fail on Dart 3.10+ because the `_macros` pseudo-package was removed.
If you use `mise` or `asdf`, the right Flutter version will activate automatically.
## Generated code
Several layers use code generation (MobX stores, Hive type adapters, JSON serializers, configure.dart-generated wallet entry points). Regenerate with:
```bash
dart pub run build_runner build --delete-conflicting-outputs
```
For the wallet-types and pubspec generation:
```bash
dart run tool/configure.dart --monero --wownero --bitcoin --bitcoinCash \
--ethereum --polygon --base --arbitrum --bsc --nano --dogecoin
```
(That's the canonical Hash Wallet build flag set. Don't pass `--solana`, `--tron`, `--zano`, `--decred`, or `--zcash` — those packages were deleted.)
## Code style
- Match the surrounding style. We have not introduced new lints beyond Cake's.
- New observable state goes through MobX; persisted state through Hive.
- Don't add new top-level analytics. Hash Wallet ships with no telemetry by design.
- Don't import `package:cw_zano/`, `package:cw_tron/`, `package:cw_solana/`, `package:cw_decred/` — those packages are deleted. The corresponding `WalletType.*` enum values still exist in `cw_core/lib/wallet_type.dart` for backwards-compat but are never instantiated.
- Avoid adding `cake_*`-prefixed file or symbol names in new code. Use `hash_wallet`-prefixed where the symbol is ours, and leave Cake-prefixed names alone where they're inherited (so upstream merges still apply cleanly).
## What we generally won't accept
- Re-enabling Solana, Tron, Zano, Decred, or Zcash. The fork exists in part to drop these — opening that door defeats the purpose.
- New buy/sell provider integrations that require routing through Cake's `exchange-helper.cakewallet.com` proxy. Direct provider APIs are fine if we can register Hash Wallet / Such Software LLC as the partner.
- Lightning Network integration. Greenlight (and self-hosted alternatives) require running infrastructure we're not in a position to operate yet.
- Cake Pay re-enable. The infrastructure is Cake Labs's; we can't run it.
- New defaults that point at `cakewallet.com` hosts. We're moving away from depending on Cake's infrastructure — community nodes, our own proxies (`exchange-helper.such.software`, `prices.neroswap.com`), or both, are preferred.
- Anything that adds telemetry, push notifications via third-party SDKs, or remote bulletin services that the user can't audit.
## What we want
- Wownero polish (every paper cut counts — it's the reason this fork exists).
- Build reproducibility improvements (especially Docker / CI parity).
- Privacy-leaning defaults (own-node guidance, Tor/I2P integration improvements, fewer outbound calls).
- Wallet-core bug fixes — these we will happily upstream to Cake.
- Documentation, especially around the build pipeline and the Cake → Hash Wallet diff.
## Reporting bugs
Open an issue at https://github.com/Such-Software/hash-wallet/issues. Please include:
- Build artifact ID (the CI workflow names the artifact by commit SHA).
- Platform + OS version.
- Terminal output if you built and ran from a terminal — many error popups don't print to stderr; check `<appDir>/error.txt` (typically `~/.config/hash_wallet/error.txt` on Linux).
- Whether the bug also reproduces in the latest Cake Wallet build (so we know whether it's a Cake-upstream bug or a Hash Wallet regression).
## Security disclosures
See [`docs/SECURITY.md`](docs/SECURITY.md). Email `support@such.software` for anything sensitive; do not open a public issue for unpatched vulnerabilities.
## License
By contributing, you agree your contribution is licensed under the [MIT License](LICENSE.md), the same as the rest of Hash Wallet.

View File

@@ -58,10 +58,9 @@ flutter --version # confirm 3.32.0 / Dart 3.8.0
```bash
./scripts/prepare_torch.sh
./scripts/prepare_moneroc.sh
./scripts/prepare_zcash.sh # still required by build, even though Zcash is disabled
./scripts/build_bitbox_flutter.sh
# Then fetch the prebuilt reown_flutter tarball (CI does this; reproduce locally
# with the URL in .github/workflows/pr_test_build_linux.yml if you have it).
# with the URL in .github/workflows/build-linux.yml).
```
Then run the platform-specific configure script, e.g.:
@@ -72,7 +71,7 @@ APP_LINUX_TYPE=cakewallet ./configure_hash_wallet.sh linux
## Contributing
Issues and PRs welcome at https://github.com/Such-Software/hash-wallet.
Issues and PRs welcome at https://github.com/Such-Software/hash-wallet. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for ground rules.
## License

View File

@@ -1,7 +1,12 @@
-
uri: electrum.blockstream.info:50002
useSSL: true
isDefault: true
isEnabledForAutoSwitching: true
-
uri: btc-electrum.cakewallet.com:50002
useSSL: true
isDefault: true
isDefault: false
isEnabledForAutoSwitching: true
-
uri: electrs.cakewallet.com:50001
@@ -17,10 +22,6 @@
uri: bitcoin.stackwallet.com:50002
useSSL: true
isEnabledForAutoSwitching: true
-
uri: electrum.blockstream.info:50002
useSSL: true
isEnabledForAutoSwitching: true
-
uri: electrum.emzy.de:50002
useSSL: true

View File

@@ -1,15 +1,17 @@
-
uri: electrum-ltc.bysh.me:50002
useSSL: true
isDefault: true
isEnabledForAutoSwitching: true
-
uri: ltc-electrum.cakewallet.com:50002
useSSL: true
isDefault: true
isDefault: false
isEnabledForAutoSwitching: true
-
uri: litecoin.stackwallet.com:20063
useSSL: true
isEnabledForAutoSwitching: true
-
uri: electrum-ltc.bysh.me:50002
useSSL: true
-
uri: lightweight.fiatfaucet.com:50002
useSSL: true

View File

@@ -1,17 +1,17 @@
-
uri: node.sethforprivacy.com:443
is_default: true
useSSL: true
isEnabledForAutoSwitching: true
-
uri: xmr-node.cakewallet.com:18081
is_default: true
is_default: false
trusted: true
useSSL: true
isEnabledForAutoSwitching: true
-
uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081
is_default: false
-
uri: node.sethforprivacy.com:443
useSSL: true
is_default: false
isEnabledForAutoSwitching: true
-
uri: node.monerodevs.org:18089
useSSL: true

View File

@@ -1 +0,0 @@
Bug fixes

View File

@@ -1,188 +0,0 @@
Last Modified: January 3, 2022
Acceptance of the Terms of Use
==============================
These terms of use are entered into by and between You and Cake Technologies LLC ("Company," "we," or "us"). The following terms and conditions "Terms of Use") govern your access to and use of the Monero.com app, including any content, functionality, and services offered on or through the Monero.com app (the “App").
Please read the Terms of Use carefully before you start to use the App. By using the App you accept and agree to be bound and abide by these Terms of Use and our Privacy Policy, incorporated herein by reference. If you do not wish to agree to these Terms of Use or the Privacy Policy, you must not access or use the App.
Changes to the Terms of Use
===========================
We may revise and update these Terms of Use from time to time in our sole discretion. All changes are effective immediately when we post them, and apply to all access to and use of the App thereafter.
Your continued use of the App following the posting of revised Terms of Use means that you accept and agree to the changes. You are expected to check this page from time to time so you are aware of any changes, as they are binding on you.
Accessing the App and Security
==============================
We reserve the right to withdraw or amend this App, and any service or material we provide on the App, in our sole discretion without notice. We will not be liable if for any reason all or any part of the App is unavailable at any time or for any period.
You are responsible for the following:
- Making all arrangements necessary for you to have access to the App.
- Ensuring that all persons who access the App through your internet connection are aware of these Terms of Use and comply with them.
- To access the App or some of the resources it offers, you may be asked to provide certain registration details or other information. It is a condition of your use of the App that all the information you provide on the App is correct, current, and complete. You agree that all information you provide during the use of this App or otherwise, including, but not limited to, through the use of any interactive features on the App, is governed by our Privacy Policy, and you consent to all actions we take with respect to your information consistent with our Privacy Policy.
- If you choose, or are provided with, a seed or private keys for any wallet within the App, you MUST treat such information as confidential, and you MUST NOT disclose it to any other person or entity.
- You MUST keep the app up to date. Failure to update the Monero.com application means you will not be receiving the latest security fixes and features.
Intellectual Property Rights
============================
The App and its entire contents, features, and functionality (including but not limited to all information, software, text, displays, images, video, and audio, and the design, selection, and arrangement thereof) are owned by the Company, its licensors, or other providers of such material and are protected by United States and international copyright, trademark, patent, trade secret, and other intellectual property or proprietary rights laws.
These Terms of Use permit you to use the App for your personal and commercial use. You are permitted to download, store, publicly display, publicly perform, republish and transmit any of the code in our App.
You are also permitted to reproduce, distribute, modify, or create derivative works of, any of the code in our App, under the condition that any derivative work of this App remains under an open source license.
You must not:
- Modify copies of any images from the Application.
- Use any illustrations, photographs, video or audio sequences, or any graphics separately from the accompanying text.
- Delete or modify any copyright, trademark, or other rights notices from copies of materials from this site.
- Claim ownership of any code, image, text or any other information authored or created by Cake Technologies.
Trademarks
==========
The Company name, the term Monero.com, the Company logo, and all related names, logos, product and service names, designs, and slogans are trademarks of the Company or its affiliates or licensors. You must not use such marks without the prior written permission of the Company. All other names, logos, product and service names, designs, and slogans on this App are the trademarks of their respective owners.
Prohibited Uses
===============
You may use the App only for lawful purposes and in accordance with these Terms of Use. You agree not to use the App:
- In any way that violates any applicable federal, state, local, or international law or regulation (including, without limitation, any laws regarding the export of data or software to and from the US or other countries).
- For the purpose of exploiting, harming, or attempting to exploit or harm minors in any way by exposing them to inappropriate content, asking for personally identifiable information, or otherwise.
- To impersonate or attempt to impersonate the Company, a Company employee, another user, or any other person or entity (including, without limitation, by using email addresses associated with any of the foregoing).
- To engage in any other conduct that restricts or inhibits anyone's use or enjoyment of the App, or which, as determined by us, may harm the Company or users of the App, or expose them to liability.
Additionally, you agree not to:
- Use the App in any manner that could disable, overburden, damage, or impair the App or interfere with any other party's use of the App, including their ability to engage in real time activities through the App.
- Use any robot, spider, or other automatic device, process, or means to access the App for any purpose, including monitoring or copying any of the material on the App.
- Use any manual process to monitor or copy any of the material on the App, or for any other purpose not expressly authorized in these Terms of Use, without our prior written consent.
- Use any device, software, or routine that interferes with the proper working of the App or the Nodes operated by Cake Technologies.
- Introduce any viruses, Trojan horses, worms, logic bombs, or other material that is malicious or technologically harmful.
- Attempt to gain unauthorized access to, interfere with, damage, or disrupt any parts of the App, or any node, server, computer, or database connected to the App.
- Attack the App or the Nodes operated by Cake Technologies via a denial-of-service attack or a distributed denial-of-service attack.
- Otherwise attempt to interfere with the proper working of the App or the Nodes operated by Cake Technologies.
Information About You and Your Visits to the App
================================================
All information we collect on this App is subject to our Privacy Policy. By using the App, you consent to all actions taken by us with respect to your information in compliance with the Privacy Policy.
Links from the App
==================
If the App contains links to other sites and resources provided by third parties, these links are provided for your convenience only. We have no control over the contents of those sites or resources, and accept no responsibility for them or for any loss or damage that may arise from your use of them. If you decide to access any of the third-party Apps or services linked to this App, you do so entirely at your own risk and subject to the terms and conditions of use for such Apps.
Geographic Restrictions
=======================
The owner of the App is based in the State of Florida in the United States. Please consult with qualified legal counsel to assess the appropriate use of the App or any of its contents in the jurisdiction(s) you intend to use the App. The owner of the App makes no representation or warranty as to the suitability for use, compliance or other matter of law with respect to use in any jurisdiction. If you access the App from outside the United States, you do so on your own initiative and are responsible for compliance with local laws and regulations.
Translations
============
The App may contain translations of the English version of the content available on the App. These translations are provided only as a convenience. In the event of any conflict between the English language version and the translated version, the English language version shall take precedence. If you notice any inconsistencies, please report them on GitHub.
Risks Related to the use of the App
===================================
The App, the Company and the Companys owners, partners, employees, contributors, and any affiliates will not be responsible for any losses, damages or claims arising from:
- Mistakes made by the user of any Monero-related and/or Bitcoin-related and/or Litecoin-related software or service, e.g., forgotten passwords, payments sent to wrong Monero and/or Bitcoin and/or Litecoin addresses, or accidental deletion of wallets;
- Software problems of the App and/or any Monero-related or Bitcoin-related or Litecoin-related software or service, e.g., corrupted wallet file, incorrectly constructed transactions, unsafe cryptographic libraries, malware affecting the App and/or any Monero-related or Bitcoin-related or Litecoin-related software or service;
- Technical failures in the hardware of the user of any Monero-related and/or Bitcoin-related and/or Litecoin-related software or service, e.g., data loss due to a faulty or damaged storage device;
- Security problems experienced by the user of any Monero-related and/or Bitcoin-related and/or Litecoin-related software or service, e.g., unauthorized access to users' wallets and/or accounts; or
- Actions or inactions of third parties and/or events experienced by third parties, e.g., bankruptcy of service providers, information security attacks on service providers, and fraud conducted by third parties.
Investment Risks
================
All investments, including investments in Monero, Litecoin and Bitcoin, are speculative in nature and involve substantial risk of loss. We encourage investors to invest carefully. We also encourage investors to get personal advice from professional investment advisors and to make independent investigations before making any investment. We do not in any way guarantee the success of any action you take with respect to investments on the App or otherwise. Past performance is not necessarily indicative of future results. All investments, including investments in Monero, Litecoin or Bitcoin, carry risk and all investment decisions of an individual remain the responsibility of that individual. Do not enter any investment without fully understanding the worst-case scenario of that investment, including but not limited to, large market fluctuations or total loss.
Tax Matters
===========
The users of the App are solely responsible in determining what, if any, taxes apply to their Monero, Litecoin and/or Bitcoin transactions. Cake Technologies is not responsible for determining any taxes that apply to such transactions. You agree not to hold Cake Technologies responsible for any issues relating to the taxation of purchases, sale, exchanges, transfers, or any other transactions related to any cryptocurrency.
Monero, Bitcoin & Litecoin Transactions
=======================================
The App does not store Monero, Litecoin or Bitcoin. Monero, Litecoin and Bitcoin exist only by virtue of the ownership record maintained in the Monero, Litecoin and Bitcoin networks. Any transfer of title in Monero, Litecoin or Bitcoin occurs within a decentralized Monero, Litecoin or Bitcoin network, and not in the App.
Disclaimer of Warranties
========================
You understand that we cannot and do not guarantee or warrant that files available for downloading from the internet or the App will be free of viruses or other destructive code. You are responsible for implementing sufficient procedures and checkpoints to satisfy your particular requirements for anti-virus protection and accuracy of data input and output, and for maintaining a means external to our site for any reconstruction of any lost data. TO THE FULLEST EXTENT PROVIDED BY LAW, WE WILL NOT BE LIABLE FOR ANY LOSS OR DAMAGE CAUSED BY A DISTRIBUTED DENIAL-OF-SERVICE ATTACK, VIRUSES, OR OTHER TECHNOLOGICALLY HARMFUL MATERIAL THAT MAY INFECT YOUR COMPUTER EQUIPMENT, COMPUTER PROGRAMS, DATA, OR OTHER PROPRIETARY MATERIAL DUE TO YOUR USE OF THE APP OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE APP OR TO YOUR DOWNLOADING OF ANY MATERIAL POSTED ON IT, OR ON ANY APP LINKED TO IT.
YOUR USE OF THE APP, ITS CONTENT, AND ANY SERVICES OR ITEMS OBTAINED THROUGH THE APP IS AT YOUR OWN RISK. THE APP, ITS CONTENT, AND ANY SERVICES OR ITEMS OBTAINED THROUGH THE APP ARE PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS, WITHOUT ANY WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. NEITHER THE COMPANY NOR ANY PERSON ASSOCIATED WITH THE COMPANY MAKES ANY WARRANTY OR REPRESENTATION WITH RESPECT TO THE COMPLETENESS, SECURITY, RELIABILITY, QUALITY, ACCURACY, OR AVAILABILITY OF THE APP. WITHOUT LIMITING THE FOREGOING, NEITHER THE COMPANY NOR ANYONE ASSOCIATED WITH THE COMPANY REPRESENTS OR WARRANTS THAT THE APP, ITS CONTENT, OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE APP WILL BE ACCURATE, RELIABLE, ERROR-FREE, OR UNINTERRUPTED, THAT DEFECTS WILL BE CORRECTED, THAT OUR SITE OR THE SERVER THAT MAKES IT AVAILABLE ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS, OR THAT THE APP OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE APP WILL OTHERWISE MEET YOUR NEEDS OR EXPECTATIONS.
TO THE FULLEST EXTENT PROVIDED BY LAW, THE COMPANY HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND FITNESS FOR PARTICULAR PURPOSE. THE APP IS PROVIDED ON AN “AS IS” BASIS.
THE FOREGOING DOES NOT AFFECT ANY WARRANTIES THAT CANNOT BE EXCLUDED OR LIMITED UNDER APPLICABLE LAW.
Limitation on Liability
=======================
TO THE FULLEST EXTENT PROVIDED BY LAW, IN NO EVENT WILL THE COMPANY, ITS AFFILIATES, OR THEIR LICENSORS, SERVICE PROVIDERS, EMPLOYEES, AGENTS, OFFICERS, OR DIRECTORS BE LIABLE FOR DAMAGES OF ANY KIND, UNDER ANY LEGAL THEORY, ARISING OUT OF OR IN CONNECTION WITH YOUR USE, OR INABILITY TO USE, THE APP, ANY APPS LINKED TO IT, ANY CONTENT ON THE APP OR SUCH OTHER APPS, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO, PERSONAL INJURY, PAIN AND SUFFERING, EMOTIONAL DISTRESS, LOSS OF REVENUE, LOSS OF PROFITS, LOSS OF BUSINESS OR ANTICIPATED SAVINGS, LOSS OF USE, LOSS OF GOODWILL, LOSS OF DATA, AND WHETHER CAUSED BY TORT (INCLUDING NEGLIGENCE), BREACH OF CONTRACT, OR OTHERWISE, EVEN IF FORESEEABLE.
The limitation of liability set out above does not apply to liability resulting from our gross negligence or willful misconduct.
THE FOREGOING DOES NOT AFFECT ANY LIABILITY THAT CANNOT BE EXCLUDED OR LIMITED UNDER APPLICABLE LAW.
Indemnification
===============
You agree to defend, indemnify, and hold harmless the Company, its affiliates, licensors, and service providers, and its and their respective officers, directors, employees, contractors, agents, licensors, suppliers, successors, and assigns from and against any claims, liabilities, damages, judgments, awards, losses, costs, expenses, or fees (including reasonable attorneys' fees) arising out of or relating to your violation of these Terms of Use or your use of the App, including, but not limited to, your User Contributions, any use of the App's content, services, and products other than as expressly authorized in these Terms of Use, or your use of any information obtained from the App.
Governing Law and Jurisdiction
==============================
All matters relating to the App and these Terms of Use, and any dispute or claim arising therefrom or related thereto (in each case, including non-contractual disputes or claims), shall be governed by and construed in accordance with the internal laws of the State of Florida without giving effect to any choice or conflict of law provision or rule (whether of the State of Florida or any other jurisdiction).
Any legal suit, action, or proceeding arising out of, or related to, these Terms of Use or the App shall be instituted exclusively in the federal courts of the United States or the courts of the State of Florida, although we retain the right to bring any suit, action, or proceeding against you for breach of these Terms of Use in your country of residence or any other relevant country. You waive any and all objections to the exercise of jurisdiction over you by such courts and to venue in such courts.
Arbitration
===========
At Company's sole discretion, it may require You to submit any disputes arising from these Terms of Use or use of the App, including disputes arising from or concerning their interpretation, violation, invalidity, non-performance, or termination, to final and binding arbitration under the Rules of Arbitration of the American Arbitration Association applying Florida law.
Limitation on Time to File Claims
=================================
ANY CAUSE OF ACTION OR CLAIM YOU MAY HAVE ARISING OUT OF OR RELATING TO THESE TERMS OF USE OR THE APP MUST BE COMMENCED WITHIN ONE (1) YEAR AFTER THE CAUSE OF ACTION ACCRUES; OTHERWISE, SUCH CAUSE OF ACTION OR CLAIM IS PERMANENTLY BARRED.
Waiver and Severability
=======================
No waiver by the Company of any term or condition set out in these Terms of Use shall be deemed a further or continuing waiver of such term or condition or a waiver of any other term or condition, and any failure of the Company to assert a right or provision under these Terms of Use shall not constitute a waiver of such right or provision.
If any provision of these Terms of Use is held by a court or other tribunal of competent jurisdiction to be invalid, illegal, or unenforceable for any reason, such provision shall be eliminated or limited to the minimum extent such that the remaining provisions of the Terms of Use will continue in full force and effect.
Entire Agreement
================
The Terms of Use and our Privacy Policy constitute the sole and entire agreement between you and Cake Technologies Inc. regarding the App and supersede all prior and contemporaneous understandings, agreements, representations, and warranties, both written and oral, regarding the App.
Your Comments and Concerns
==========================
This App is operated by Cake Technologies Inc.
All feedback, comments, and other communications relating to the App should be directed to info@cakewallet.com. All requests for technical support should be directed to support@cakewallet.com.

View File

@@ -24,7 +24,10 @@ bool get isNonAmnesticTails {
bool showNotice = true;
void setRootDirFromEnv() =>
_rootDirPath = Platform.environment['CAKE_WALLET_DIR'];
// HASH_WALLET_DIR is the canonical override; CAKE_WALLET_DIR kept as a
// fallback so users migrating from a Cake install via env-var override
// don't lose their path.
_rootDirPath = Platform.environment['HASH_WALLET_DIR'] ?? Platform.environment['CAKE_WALLET_DIR'];
void copyDirectory(Directory source, Directory destination) {
source.listSync(recursive: false).forEach((var entity) {
@@ -42,8 +45,11 @@ void copyDirectory(Directory source, Directory destination) {
Future<void> linuxSymlinkSharedPreferences() async {
if (!Platform.isLinux) return; // nuh-uh
final dataHome = Platform.environment["XDG_DATA_HOME"] ?? p.join(Platform.environment["HOME"] ?? "", ".local", "share");
var cakeNames = ['com.example.cake_wallet', 'cake_wallet'];
for (String name in cakeNames) {
// Names to migrate from on startup. Includes the legacy Cake paths so users
// forking from a previous Cake install get their data automatically
// symlinked into Hash Wallet's new home (~/.local/share/hash_wallet).
var legacyNames = ['com.example.cake_wallet', 'cake_wallet'];
for (String name in legacyNames) {
final oldPath = p.join(dataHome, name);
final newPath = p.join((await getAppDir()).path, "_local_share");
final oldDir = Directory(oldPath);
@@ -70,7 +76,7 @@ Future<void> linuxSymlinkSharedPreferences() async {
}
Future<Directory> getAppDir() async {
const String appName = 'cake_wallet';
const String appName = 'hash_wallet';
Directory dir;
if (_rootDirPath != null && _rootDirPath!.isNotEmpty) {

39
cw_decred/.gitignore vendored
View File

@@ -1,39 +0,0 @@
# 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/
android/.externalNativeBuild/
android/.cxx/
android/libs
ios/External/
macos/External/
*libdcrwallet.h
libdcrwallet_bindings.dart

View File

@@ -1,36 +0,0 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
channel: unknown
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: android
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: ios
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: macos
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

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

View File

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

View File

@@ -1,47 +0,0 @@
# cw_decred
Decred wallet module that bridges to the native `libdcrwallet` via FFI. Provides highlevel methods to create/load wallets, sync, query balances/transactions, build and broadcast transactions, and sign/verify messages.
## Features
- FFI bindings to `libdcrwallet` with an isolatebased request/response model.
- Initialize, create, load, close wallets; watchonly creation.
- Start sync with optional peer list; query sync status and best block.
- Query balances, list transactions and unspents, rescan from height.
- Create signed transactions and broadcast raw transactions.
- Export wallet seed; change wallet password.
- Address management (new external address, default pubkey, address lists).
- Message signing and verification.
## Getting started
Ensure the platform library is available:
- Android/Linux: `libdcrwallet.so`
- Apple: embedded `cw_decred.framework/cw_decred`
Initialize and load a wallet:
```dart
final lib = await Libwallet.spawn();
await lib.initLibdcrwallet('', 'info');
await lib.loadWallet(jsonEncode({ /* libdcrwallet config */ }));
await lib.startSync('wallet.db', '');
final status = await lib.syncStatus('wallet.db');
```
## Usage
Create, sign, and broadcast a transaction:
```dart
final signed = await lib.createSignedTransaction('wallet.db', jsonEncode({
// inputs/outputs and policy for libdcrwallet
}));
final txid = await lib.sendRawTransaction('wallet.db', signed);
```
## Additional information
- See `lib/api/` for the isolate wrapper (`libdcrwallet.dart`) and lowlevel bindings.
- Errors are surfaced via the `PayloadResult` struct; some calls support `throwOnError` in higherlevel wrappers.

View File

@@ -1,4 +0,0 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -1,9 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

View File

@@ -1,59 +0,0 @@
group 'com.cakewallet.cw_decred'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '2.0.21'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 33
if (project.android.hasProperty("namespace")) {
namespace 'com.cakewallet.cw_decred'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main {
java.srcDirs += 'src/main/kotlin'
jniLibs.srcDirs 'libs' // contains libdcrwallet.so shared libraries
}
}
defaultConfig {
minSdkVersion 21
}
externalNativeBuild {
cmake {
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@@ -1 +0,0 @@
rootProject.name = 'cw_decred'

View File

@@ -1,4 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.cakewallet.cw_decred">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -1,35 +0,0 @@
package com.cakewallet.cw_decred
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** CwDecredPlugin */
class CwDecredPlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_decred")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

View File

@@ -1,3 +0,0 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -1,38 +0,0 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

View File

@@ -1,19 +0,0 @@
import Flutter
import UIKit
public class CwDecredPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "cw_decred", binaryMessenger: registrar.messenger())
let instance = CwDecredPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}

View File

@@ -1,22 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint cw_decred.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'cw_decred'
s.version = '0.0.1'
s.summary = 'Cake Wallet Decred'
s.description = 'Cake Wallet wrapper over Decred project'
s.homepage = 'http://cakewallet.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Cake Wallet' => 'support@cakewallet.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '11.0'
s.vendored_libraries = 'External/lib/libdcrwallet.a'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/External/lib/libdcrwallet.a -lstdc++" }
s.swift_version = '5.0'
end

View File

@@ -1,26 +0,0 @@
import 'package:intl/intl.dart';
import 'package:cw_core/crypto_amount_format.dart';
const decredAmountLength = 8;
const decredAmountDivider = 100000000;
final decredAmountFormat = NumberFormat()
..maximumFractionDigits = decredAmountLength
..minimumFractionDigits = 1;
String decredAmountToString({required int amount}) =>
decredAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: decredAmountDivider));
double decredAmountToDouble({required int amount}) =>
cryptoAmountToDouble(amount: amount, divider: decredAmountDivider);
int stringDoubleToDecredAmount(String amount) {
int result = 0;
try {
result = (double.parse(amount) * decredAmountDivider).round();
} catch (e) {
result = 0;
}
return result;
}

View File

@@ -1,702 +0,0 @@
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:async';
import 'dart:isolate';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_decred/api/libdcrwallet_bindings.dart';
import 'package:cw_decred/api/util.dart';
final int ErrCodeNotSynced = 1;
final String libraryName = Platform.isAndroid || Platform.isLinux // TODO: Linux.
? 'libdcrwallet.so'
: 'cw_decred.framework/cw_decred';
class Libwallet {
final SendPort _commands;
final ReceivePort _responses;
final Map<int, Completer<Object?>> _activeRequests = {};
int _idCounter = 0;
bool _closed = false;
static Future<Libwallet> spawn() async {
// Create a receive port and add its initial message handler.
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};
// Spawn the isolate.
try {
await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
} on Object {
initPort.close();
rethrow;
}
final (ReceivePort receivePort, SendPort sendPort) = await connection.future;
return Libwallet._(receivePort, sendPort);
}
Libwallet._(this._responses, this._commands) {
_responses.listen(_handleResponsesFromIsolate);
}
void _handleResponsesFromIsolate(dynamic message) {
final (int id, Object? response) = message as (int, Object?);
final completer = _activeRequests.remove(id)!;
if (response is RemoteError) {
completer.completeError(response);
} else {
completer.complete(response);
}
if (_closed && _activeRequests.isEmpty) _responses.close();
}
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
final dcrwalletApi = libdcrwallet(DynamicLibrary.open(libraryName));
receivePort.listen((message) {
if (message == 'shutdown') {
receivePort.close();
return;
}
final (int id, Map<String, String> args) = message as (int, Map<String, String>);
var res = PayloadResult("", "", 0);
final method = args["method"] ?? "";
try {
switch (method) {
case "initlibdcrwallet":
final logDir = args["logdir"] ?? "";
final level = args["level"] ?? "";
final cLogDir = logDir.toCString();
final cLevel = level.toCString();
executePayloadFn(
fn: () => dcrwalletApi.initialize(cLogDir, cLevel),
ptrsToFree: [cLogDir, cLevel],
);
break;
case "createwallet":
final config = args["config"] ?? "";
final cConfig = config.toCString();
executePayloadFn(
fn: () => dcrwalletApi.createWallet(cConfig),
ptrsToFree: [cConfig],
);
break;
case "createwatchonlywallet":
final config = args["config"] ?? "";
final cConfig = config.toCString();
executePayloadFn(
fn: () => dcrwalletApi.createWatchOnlyWallet(cConfig),
ptrsToFree: [cConfig],
);
break;
case "loadwallet":
final config = args["config"] ?? "";
final cConfig = config.toCString();
executePayloadFn(
fn: () => dcrwalletApi.loadWallet(cConfig),
ptrsToFree: [cConfig],
);
break;
case "startsync":
final name = args["name"] ?? "";
final peers = args["peers"] ?? "";
final cName = name.toCString();
final cPeers = peers.toCString();
executePayloadFn(
fn: () => dcrwalletApi.syncWallet(cName, cPeers),
ptrsToFree: [cName, cPeers],
);
break;
case "closewallet":
final name = args["name"] ?? "";
final cName = name.toCString();
executePayloadFn(
fn: () => dcrwalletApi.closeWallet(cName),
ptrsToFree: [cName],
);
break;
case "changewalletpassword":
final name = args["name"] ?? "";
final oldPass = args["oldpass"] ?? "";
final newPass = args["newpass"] ?? "";
final cName = name.toCString();
final cOldPass = oldPass.toCString();
final cNewPass = newPass.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.changePassphrase(cName, cOldPass, cNewPass),
ptrsToFree: [cName, cOldPass, cNewPass],
);
break;
case "walletseed":
final name = args["name"] ?? "";
final pass = args["pass"] ?? "";
final cName = name.toCString();
final cPass = pass.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.walletSeed(cName, cPass),
ptrsToFree: [cName, cPass],
);
break;
case "syncstatus":
final name = args["name"] ?? "";
final cName = name.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.syncWalletStatus(cName),
ptrsToFree: [cName],
);
break;
case "balance":
final name = args["name"] ?? "";
final cName = name.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.walletBalance(cName),
ptrsToFree: [cName],
);
break;
case "estimatefee":
final name = args["name"] ?? "";
final numBlocks = args["numblocks"] ?? "";
final cName = name.toCString();
final cNumBlocks = numBlocks.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.estimateFee(cName, cNumBlocks),
ptrsToFree: [cName, cNumBlocks],
);
break;
case "createsignedtransaction":
final name = args["name"] ?? "";
final signReq = args["signreq"] ?? "";
final cName = name.toCString();
final cSignReq = signReq.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq),
ptrsToFree: [cName, cSignReq],
);
break;
case "sendrawtransaction":
final name = args["name"] ?? "";
final txHex = args["txhex"] ?? "";
final cName = name.toCString();
final cTxHex = txHex.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.sendRawTransaction(cName, cTxHex),
ptrsToFree: [cName, cTxHex],
);
break;
case "listtransactions":
final name = args["name"] ?? "";
final from = args["from"] ?? "";
final count = args["count"] ?? "";
final cName = name.toCString();
final cFrom = from.toCString();
final cCount = count.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.listTransactions(cName, cFrom, cCount),
ptrsToFree: [cName, cFrom, cCount],
);
break;
case "bestblock":
final name = args["name"] ?? "";
final cName = name.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.bestBlock(cName),
ptrsToFree: [cName],
);
break;
case "listunspents":
final name = args["name"] ?? "";
final cName = name.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.listUnspents(cName),
ptrsToFree: [cName],
);
break;
case "rescanfromheight":
final name = args["name"] ?? "";
final height = args["height"] ?? "";
final cName = name.toCString();
final cHeight = height.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.rescanFromHeight(cName, cHeight),
ptrsToFree: [cName, cHeight],
);
break;
case "signmessage":
final name = args["name"] ?? "";
final message = args["message"] ?? "";
final address = args["address"] ?? "";
final pass = args["pass"] ?? "";
final cName = name.toCString();
final cMessage = message.toCString();
final cAddress = address.toCString();
final cPass = pass.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.signMessage(cName, cMessage, cAddress, cPass),
ptrsToFree: [cName, cMessage, cAddress, cPass],
);
break;
case "verifymessage":
final name = args["name"] ?? "";
final message = args["message"] ?? "";
final address = args["address"] ?? "";
final sig = args["sig"] ?? "";
final cName = name.toCString();
final cMessage = message.toCString();
final cAddress = address.toCString();
final cSig = sig.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.verifyMessage(cName, cMessage, cAddress, cSig),
ptrsToFree: [cName, cMessage, cAddress, cSig],
);
break;
case "newexternaladdress":
final name = args["name"] ?? "";
final cName = name.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.newExternalAddress(cName),
ptrsToFree: [cName],
skipErrorCheck: true,
);
break;
case "defaultpubkey":
final name = args["name"] ?? "";
final cName = name.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.defaultPubkey(cName),
ptrsToFree: [cName],
);
break;
case "addresses":
final name = args["name"] ?? "";
final nUsed = args["nused"] ?? "";
final nUnused = args["nunused"] ?? "";
final cName = name.toCString();
final cNUsed = nUsed.toCString();
final cNUnused = nUnused.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.addresses(cName, cNUsed, cNUnused),
ptrsToFree: [cName, cNUsed, cNUnused],
);
break;
case "birthstate":
final name = args["name"] ?? "";
final cName = name.toCString();
res = executePayloadFn(
fn: () => dcrwalletApi.birthState(cName),
ptrsToFree: [cName],
);
break;
case "shutdown":
final name = args["name"] ?? "";
// final cName = name.toCString();
executePayloadFn(
fn: () => dcrwalletApi.shutdown(),
ptrsToFree: [],
);
break;
default:
res = PayloadResult("", "unknown libwallet method ${method}", 0);
}
sendPort.send((id, res));
} catch (e) {
final errMsg = e.toString();
printV("decred libwallet returned an error for method ${method}: ${errMsg}");
sendPort.send((id, PayloadResult("", errMsg, 0)));
}
});
}
static void _startRemoteIsolate(SendPort sendPort) {
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
_handleCommandsToIsolate(receivePort, sendPort);
}
// initLibdcrwallet initializes libdcrwallet using the provided logDir and gets
// it ready for use. This must be done before attempting to create, load or use
// a wallet. An empty string can be used to log to stdout and create no log files.
Future<void> initLibdcrwallet(String logDir, String level) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "initlibdcrwallet",
"logdir": logDir,
"level": level,
};
_commands.send((id, req));
await completer.future;
}
Future<void> createWallet(String config) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "createwallet",
"config": config,
};
_commands.send((id, req));
await completer.future;
}
Future<void> createWatchOnlyWallet(String config) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "createwatchonlywallet",
"config": config,
};
_commands.send((id, req));
await completer.future;
}
Future<void> loadWallet(String config) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "loadwallet",
"config": config,
};
_commands.send((id, req));
await completer.future;
}
Future<void> startSync(String walletName, String peers) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "startsync",
"name": walletName,
"peers": peers,
};
_commands.send((id, req));
await completer.future;
}
Future<void> closeWallet(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "closewallet",
"name": walletName,
};
_commands.send((id, req));
await completer.future;
}
Future<String> changeWalletPassword(
String walletName, String currentPassword, String newPassword) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "changewalletpassword",
"name": walletName,
"oldpass": currentPassword,
"newpass": newPassword
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String?> walletSeed(String walletName, String walletPassword) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "walletseed",
"name": walletName,
"pass": walletPassword,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> syncStatus(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "syncstatus",
"name": walletName,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<Map> balance(String walletName, {bool throwOnError = false}) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "balance",
"name": walletName,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
try {
return jsonDecode(res.payload);
} catch (_) {
if (throwOnError) {
rethrow;
}
return {};
}
}
Future<String> estimateFee(String walletName, int numBlocks) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "estimatefee",
"name": walletName,
"numblocks": numBlocks.toString(),
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> createSignedTransaction(
String walletName, String createSignedTransactionReq) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "createsignedtransaction",
"name": walletName,
"signreq": createSignedTransactionReq,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> sendRawTransaction(String walletName, String txHex) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "sendrawtransaction",
"name": walletName,
"txhex": txHex,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> listTransactions(String walletName, String from, String count) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "listtransactions",
"name": walletName,
"from": from,
"count": count,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> bestBlock(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "bestblock",
"name": walletName,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> listUnspents(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "listunspents",
"name": walletName,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> rescanFromHeight(String walletName, String height) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "rescanfromheight",
"name": walletName,
"height": height,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> signMessage(
String walletName, String message, String address, String walletPass) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "signmessage",
"name": walletName,
"message": message,
"address": address,
"pass": walletPass,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> verifyMessage(
String walletName, String message, String address, String sig) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "verifymessage",
"name": walletName,
"message": message,
"address": address,
"sig": sig,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String?> newExternalAddress(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "newexternaladdress",
"name": walletName,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
if (res.errCode == ErrCodeNotSynced) {
// Wallet is not synced. We do not want to give out a used address so give
// nothing.
return null;
}
checkErr(res.err);
return res.payload;
}
Future<String> defaultPubkey(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "defaultpubkey",
"name": walletName,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> addresses(String walletName, String nUsed, String nUnused) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "addresses",
"name": walletName,
"nused": nUsed,
"nunused": nUnused,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<String> birthState(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "birthstate",
"name": walletName,
};
_commands.send((id, req));
final res = await completer.future as PayloadResult;
return res.payload;
}
Future<void> shutdown() async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "shutdown",
};
_commands.send((id, req));
await completer.future as PayloadResult;
}
void close() {
if (!_closed) {
_closed = true;
_commands.send('shutdown');
if (_activeRequests.isEmpty) _responses.close();
}
}
}

View File

@@ -1,64 +0,0 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'dart:convert';
class PayloadResult {
final String payload;
final String err;
final int errCode;
const PayloadResult(this.payload, this.err, this.errCode);
}
// Executes the provided fn and converts the string response to a PayloadResult.
// Returns payload, error code, and error.
PayloadResult executePayloadFn({
required Pointer<Char> fn(),
required List<Pointer> ptrsToFree,
bool skipErrorCheck = false,
}) {
final jsonStr = fn().toDartString();
freePointers(ptrsToFree);
if (jsonStr == null) throw Exception("no json return from wallet library");
final decoded = json.decode(jsonStr);
final err = decoded["error"] ?? "";
if (!skipErrorCheck) {
checkErr(err);
}
final payload = decoded["payload"] ?? "";
final errCode = decoded["errorcode"] ?? -1;
return new PayloadResult(payload, err, errCode);
}
void freePointers(List<Pointer> ptrsToFree) {
for (final ptr in ptrsToFree) {
malloc.free(ptr);
}
}
void checkErr(String err) {
if (err == "") return;
throw Exception(err);
}
extension StringUtil on String {
Pointer<Char> toCString() => toNativeUtf8().cast<Char>();
}
extension CStringUtil on Pointer<Char> {
bool get isNull => address == nullptr.address;
free() {
malloc.free(this);
}
String? toDartString() {
if (isNull) return null;
final str = cast<Utf8>().toDartString();
free();
return str;
}
}

View File

@@ -1,26 +0,0 @@
import 'package:cw_decred/amount_format.dart';
import 'package:cw_core/balance.dart';
class DecredBalance extends Balance {
DecredBalance({required this.confirmed, required this.unconfirmed, required int frozen})
: _frozen = frozen, super.fromInt(confirmed, unconfirmed);
factory DecredBalance.zero() => DecredBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
final int confirmed;
final int unconfirmed;
final int _frozen;
BigInt get frozen => BigInt.from(_frozen);
@override
String get formattedAvailableBalance => decredAmountToString(amount: confirmed - _frozen);
@override
String get formattedAdditionalBalance => decredAmountToString(amount: unconfirmed);
@override
String get formattedUnAvailableBalance {
final frozenFormatted = decredAmountToString(amount: _frozen);
return frozenFormatted == '0.0' ? '' : frozenFormatted;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_decred/amount_format.dart';
class DecredPendingTransaction with PendingTransaction {
DecredPendingTransaction(
{required this.txid,
required this.amount,
required this.fee,
required this.rawHex,
required this.send});
final int amount;
final int fee;
final String txid;
final String rawHex;
final Future<void> Function() send;
@override
String get id => txid;
@override
String get amountFormatted => decredAmountToString(amount: amount);
@override
String get feeFormatted => "$feeFormattedValue DCR";
@override
String get feeFormattedValue => decredAmountToString(amount: fee);
@override
String get hex => rawHex;
@override
Future<void> commit() async {
return send();
}
@override
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}

View File

@@ -1,10 +0,0 @@
import 'package:cw_decred/transaction_priority.dart';
import 'package:cw_core/output_info.dart';
class DecredTransactionCredentials {
DecredTransactionCredentials(this.outputs, {required this.priority, this.feeRate});
final List<OutputInfo> outputs;
final DecredTransactionPriority? priority;
final int? feeRate;
}

View File

@@ -1,31 +0,0 @@
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/transaction_history.dart';
class DecredTransactionHistory extends TransactionHistoryBase<TransactionInfo> {
DecredTransactionHistory() {
transactions = ObservableMap<String, TransactionInfo>();
}
@override
void addOne(TransactionInfo transaction) => transactions[transaction.id] = transaction;
@override
void addMany(Map<String, TransactionInfo> transactions) => this.transactions.addAll(transactions);
@override
Future<void> save() async {}
// update returns true if a known transaction that is not pending was found.
bool update(Map<String, TransactionInfo> txs) {
var foundOldTx = false;
txs.forEach((_, tx) {
if (!this.transactions.containsKey(tx.id) || this.transactions[tx.id]!.isPending) {
this.transactions[tx.id] = tx;
} else {
foundOldTx = true;
}
});
return foundOldTx;
}
}

View File

@@ -1,46 +0,0 @@
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_decred/amount_format.dart';
class DecredTransactionInfo extends TransactionInfo {
DecredTransactionInfo({
required String id,
required int amount,
required int fee,
required TransactionDirection direction,
required bool isPending,
required DateTime date,
required int height,
required int confirmations,
required String to,
}) {
this.id = id;
this.amount = amount;
this.fee = fee;
this.height = height;
this.direction = direction;
this.date = date;
this.isPending = isPending;
this.confirmations = confirmations;
this.to = to;
}
String? _fiatAmount;
@override
String amountFormatted() =>
'${formatAmount(decredAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(WalletType.decred).title}';
@override
String? feeFormatted() =>
'${formatAmount(decredAmountToString(amount: fee ?? 0))} ${walletTypeToCryptoCurrency(WalletType.decred).title}';
@override
String fiatAmount() => _fiatAmount ?? '';
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
}

View File

@@ -1,73 +0,0 @@
import 'package:cw_core/transaction_priority.dart';
import 'package:flutter/foundation.dart';
class DecredTransactionPriority extends TransactionPriority {
const DecredTransactionPriority({required String title, required int raw})
: super(title: title, raw: raw);
static const List<DecredTransactionPriority> all = [fast, medium, slow];
static const DecredTransactionPriority slow = DecredTransactionPriority(title: 'Slow', raw: 0);
static const DecredTransactionPriority medium =
DecredTransactionPriority(title: 'Medium', raw: 1);
static const DecredTransactionPriority fast = DecredTransactionPriority(title: 'Fast', raw: 2);
static DecredTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return slow;
case 1:
return medium;
case 2:
return fast;
default:
if (kDebugMode) {
throw Exception('Unexpected token: $raw for DecredTransactionPriority deserialize');
}
return medium;
}
}
String get units => 'atom';
@override
String toString() {
var label = '';
switch (this) {
case DecredTransactionPriority.slow:
label = 'Slow ~24hrs'; // '${S.current.transaction_priority_slow} ~24hrs';
break;
case DecredTransactionPriority.medium:
label = 'Medium'; // S.current.transaction_priority_medium;
break;
case DecredTransactionPriority.fast:
label = 'Fast'; // S.current.transaction_priority_fast;
break;
default:
break;
}
return label;
}
String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)';
}
class FeeCache {
int _feeRate;
DateTime stamp;
FeeCache(this._feeRate) : this.stamp = DateTime(0, 0, 0, 0, 0, 0, 0, 0);
bool isOld() {
return this.stamp.add(const Duration(minutes: 30)).isBefore(DateTime.now());
}
void update(int feeRate) {
this._feeRate = feeRate;
this.stamp = DateTime.now();
}
int feeRate() {
return this._feeRate;
}
}

View File

@@ -1,770 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_decred/amount_format.dart';
import 'package:cw_decred/pending_transaction.dart';
import 'package:cw_decred/transaction_credentials.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:hive/hive.dart';
import 'package:cw_decred/api/libdcrwallet.dart';
import 'package:cw_decred/transaction_history.dart';
import 'package:cw_decred/wallet_addresses.dart';
import 'package:cw_decred/transaction_priority.dart';
import 'package:cw_decred/wallet_service.dart';
import 'package:cw_decred/balance.dart';
import 'package:cw_decred/transaction_info.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/unspent_transaction_output.dart';
part 'wallet.g.dart';
class DecredWallet = DecredWalletBase with _$DecredWallet;
abstract class DecredWalletBase
extends WalletBase<DecredBalance, DecredTransactionHistory, DecredTransactionInfo> with Store {
DecredWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password, Box<UnspentCoinsInfo> unspentCoinsInfo,
Libwallet libwallet, Function() closeLibwallet)
: _password = password,
_libwallet = libwallet,
_closeLibwallet = closeLibwallet,
this.syncStatus = NotConnectedSyncStatus(),
this.unspentCoinsInfo = unspentCoinsInfo,
this.watchingOnly =
derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePath ||
derivationInfo.derivationPath ==
DecredWalletService.pubkeyRestorePathTestnet,
this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}),
this.isTestnet = derivationInfo.derivationPath ==
DecredWalletService.seedRestorePathTestnet ||
derivationInfo.derivationPath ==
DecredWalletService.pubkeyRestorePathTestnet,
super(walletInfo, derivationInfo) {
walletAddresses = DecredWalletAddresses(walletInfo, libwallet, isTestnet);
transactionHistory = DecredTransactionHistory();
reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = enabled;
});
}
// NOTE: Hitting this max fee would be unexpected with current on chain use
// but this may need to be updated in the future.
final maxFeeRate = 100000;
// syncIntervalSyncing is used up until synced, then transactions are checked
// every syncIntervalSynced.
final syncIntervalSyncing = 5; // seconds
final syncIntervalSynced = 30; // seconds
static final defaultFeeRate = 10000;
final String _password;
final Libwallet _libwallet;
final Function() _closeLibwallet;
final idPrefix = "decred_";
// TODO: Encrypt this.
var _seed = "";
var _pubkey = "";
var _unspents = <Unspent>[];
// synced is used to set the syncTimer interval.
bool synced = false;
bool watchingOnly;
bool connecting = false;
String persistantPeer = "default-spv-nodes";
FeeCache feeRateFast = FeeCache(defaultFeeRate);
FeeCache feeRateMedium = FeeCache(defaultFeeRate);
FeeCache feeRateSlow = FeeCache(defaultFeeRate);
Timer? syncTimer;
Box<UnspentCoinsInfo> unspentCoinsInfo;
@override
@observable
bool isEnabledAutoGenerateSubaddress = true;
@override
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, DecredBalance> balance;
@override
late DecredWalletAddresses walletAddresses;
@override
String? get seed {
if (watchingOnly) {
return null;
}
return _seed;
}
@override
Object get keys => {};
@override
bool isTestnet;
String get pubkey {
return _pubkey;
}
@override
String formatCryptoAmount(String amount) => decredAmountToString(amount: int.parse(amount));
Future<void> init() async {
final getSeed = () async {
if (!watchingOnly) {
_seed = await _libwallet.walletSeed(walletInfo.name, _password) ?? "";
}
_pubkey = await _libwallet.defaultPubkey(walletInfo.name);
};
await Future.wait([
updateBalance(),
updateTransactionHistory(),
walletAddresses.init(),
fetchTransactions(),
updateFees(),
fetchUnspents(),
getSeed(),
]);
}
Future<void> performBackgroundTasks() async {
if (!await checkSync()) {
if (synced == true) {
synced = false;
if (syncTimer != null) {
syncTimer!.cancel();
}
syncTimer = Timer.periodic(
Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks());
}
return;
}
// Set sync check interval lower since we are synced.
if (synced == false) {
synced = true;
if (syncTimer != null) {
syncTimer!.cancel();
}
syncTimer = Timer.periodic(
Duration(seconds: syncIntervalSynced), (Timer t) => performBackgroundTasks());
}
await Future.wait([
updateTransactionHistory(),
updateFees(),
fetchUnspents(),
updateBalance(),
walletAddresses.updateAddressesInBox(),
]);
}
Future<void> updateFees() async {
final feeForNb = (int nb) async {
try {
final feeStr = await _libwallet.estimateFee(walletInfo.name, nb);
var fee = int.parse(feeStr);
if (fee > maxFeeRate) {
throw "dcr fee returned from estimate fee was over max";
} else if (fee <= 0) {
throw "dcr fee returned from estimate fee was zero";
}
return fee;
} catch (e) {
printV(e);
return defaultFeeRate;
}
};
if (feeRateSlow.isOld()) {
feeRateSlow.update(await feeForNb(4));
}
if (feeRateMedium.isOld()) {
feeRateMedium.update(await feeForNb(2));
}
if (feeRateFast.isOld()) {
feeRateFast.update(await feeForNb(1));
}
}
Future<void> updateTransactionHistory() async {
// from is the number of transactions skipped from most recent, not block
// height.
var from = 0;
while (true) {
// Transactions are returned from newest to oldest. Loop fetching 5 txn
// at a time until we find a batch with txn that no longer need to be
// updated.
final txs = await this.fetchFiveTransactions(from);
if (txs.length == 0) {
return;
}
if (this.transactionHistory.update(txs)) {
return;
}
from += 5;
}
}
Future<bool> checkSync() async {
final syncStatusJSON = await _libwallet.syncStatus(walletInfo.name);
final decoded = json.decode(syncStatusJSON.isEmpty ? "{}" : syncStatusJSON);
final syncStatusCode = decoded["syncstatuscode"] ?? 0;
// final syncStatusStr = decoded["syncstatus"] ?? "";
final targetHeight = decoded["targetheight"] ?? 1;
final numPeers = decoded["numpeers"] ?? 0;
// final cFiltersHeight = decoded["cfiltersheight"] ?? 0;
final headersHeight = decoded["headersheight"] ?? 0;
final rescanHeight = decoded["rescanheight"] ?? 0;
if (numPeers == 0) {
syncStatus = NotConnectedSyncStatus();
return false;
}
// Sync codes:
// NotStarted = 0
// FetchingCFilters = 1
// FetchingHeaders = 2
// DiscoveringAddrs = 3
// Rescanning = 4
// Complete = 5
if (syncStatusCode > 4) {
syncStatus = SyncedSyncStatus();
return true;
}
if (syncStatusCode == 0) {
syncStatus = ConnectedSyncStatus();
return false;
}
if (syncStatusCode == 1) {
syncStatus = SyncingSyncStatus(targetHeight, 0.0);
return false;
}
if (syncStatusCode == 2) {
final headersProg = headersHeight / targetHeight;
// Only allow headers progress to go up half way.
syncStatus = SyncingSyncStatus(targetHeight - headersHeight, headersProg);
return false;
}
// TODO: This step takes a while so should really get more info to the UI
// that we are discovering addresses.
if (syncStatusCode == 3) {
// Hover at half.
syncStatus = ProcessingSyncStatus();
return false;
}
if (syncStatusCode == 4) {
// Start at 75%.
final rescanProg = rescanHeight / targetHeight / 4;
syncStatus = SyncingSyncStatus(targetHeight - rescanHeight, .75 + rescanProg);
return false;
}
return false;
}
@action
@override
Future<void> connectToNode({required Node node}) async {
if (connecting) {
return;
}
connecting = true;
String addr = "default-spv-nodes";
if (node.uri.host != addr) {
addr = node.uri.host;
if (node.uri.port != "") {
addr += ":" + node.uri.port.toString();
}
}
if (addr != persistantPeer) {
if (syncTimer != null) {
syncTimer!.cancel();
syncTimer = null;
}
persistantPeer = addr;
await _libwallet.closeWallet(walletInfo.name);
final network = isTestnet ? "testnet" : "mainnet";
final dirPath = await pathForWalletDir(name: walletInfo.name, type: WalletType.decred);
final config = {
"name": walletInfo.name,
"datadir": dirPath,
"net": network,
"unsyncedaddrs": true,
};
await _libwallet.loadWallet(jsonEncode(config));
}
await this._startSync();
connecting = false;
}
@action
@override
Future<void> startSync() async {
if (connecting) {
return;
}
connecting = true;
await this._startSync();
connecting = false;
}
Future<void> _startSync() async {
if (syncTimer != null) {
return;
}
try {
syncStatus = ConnectingSyncStatus();
await _libwallet.startSync(
walletInfo.name,
persistantPeer == "default-spv-nodes" ? "" : persistantPeer,
);
syncTimer = Timer.periodic(
Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks());
} catch (e) {
printV(e.toString());
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
if (watchingOnly) {
return DecredPendingTransaction(
txid: "",
amount: 0,
fee: 0,
rawHex: "",
send: () async {
throw "unable to send with watching only wallet";
});
}
var totalIn = 0;
final ignoreInputs = [];
this.unspentCoinsInfo.values.forEach((unspent) {
if (unspent.isFrozen || !unspent.isSending) {
final input = {"txid": unspent.hash, "vout": unspent.vout};
ignoreInputs.add(input);
return;
}
totalIn += unspent.value;
});
final creds = credentials as DecredTransactionCredentials;
var totalAmt = 0;
var sendAll = false;
final outputs = [];
for (final out in creds.outputs) {
var amt = 0;
if (out.sendAll) {
if (creds.outputs.length != 1) {
throw "can only send all to one output";
}
sendAll = true;
totalAmt = totalIn;
} else if (out.cryptoAmount != null) {
final coins = double.parse(out.cryptoAmount!);
amt = (coins * 1e8).round();
}
totalAmt += amt;
final o = {
"address": out.isParsedAddress ? out.extractedAddress! : out.address,
"amount": amt
};
outputs.add(o);
}
// throw exception if no selected coins under coin control
// or if the total coins selected, is less than the amount the user wants to spend
if (ignoreInputs.length == unspentCoinsInfo.values.length || totalIn < totalAmt) {
throw TransactionNoInputsException();
}
// The inputs are always used. Currently we don't have use for this
// argument. sendall ingores output value and sends everything.
final signReq = {
// "inputs": inputs,
"ignoreInputs": ignoreInputs,
"outputs": outputs,
"feerate": creds.feeRate ?? defaultFeeRate,
"password": _password,
"sendall": sendAll,
};
final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq));
final decoded = json.decode(res);
final signedHex = decoded["signedhex"];
final send = () async {
await _libwallet.sendRawTransaction(walletInfo.name, signedHex);
await updateBalance();
};
final fee = decoded["fee"] ?? 0;
if (sendAll) {
totalAmt = (totalAmt - fee).round();
}
return DecredPendingTransaction(
txid: decoded["txid"] ?? "", amount: totalAmt, fee: fee, rawHex: signedHex, send: send);
}
int feeRate(TransactionPriority priority) {
if (!(priority is DecredTransactionPriority)) {
return defaultFeeRate;
}
final p = priority;
switch (p) {
case DecredTransactionPriority.slow:
return feeRateSlow.feeRate();
case DecredTransactionPriority.medium:
return feeRateMedium.feeRate();
case DecredTransactionPriority.fast:
return feeRateFast.feeRate();
}
return defaultFeeRate;
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
if (priority is DecredTransactionPriority) {
final P2PKHOutputSize =
36; // 8 bytes value + 2 bytes version + at least 1 byte varint script size + P2PKHPkScriptSize
// MsgTxOverhead is 4 bytes version (lower 2 bytes for the real transaction
// version and upper 2 bytes for the serialization type) + 4 bytes locktime
// + 4 bytes expiry + 3 bytes of varints for the number of transaction
// inputs (x2 for witness and prefix) and outputs
final MsgTxOverhead = 15;
// TxInOverhead is the overhead for a wire.TxIn with a scriptSig length <
// 254. prefix (41 bytes) + ValueIn (8 bytes) + BlockHeight (4 bytes) +
// BlockIndex (4 bytes) + sig script var int (at least 1 byte)
final TxInOverhead = 57;
final P2PKHInputSize =
TxInOverhead + 109; // TxInOverhead (57) + var int (1) + P2PKHSigScriptSize (108)
int inputsCount = 1;
if (amount != null) {
inputsCount += _unspents.where((e) {
amount = (amount!) - e.value;
return (amount!) > 0;
}).length;
}
// Estimate using a transaction consuming inoutsCount and paying to one address with change.
return (this.feeRate(priority) / 1000).round() *
(MsgTxOverhead + P2PKHInputSize * inputsCount + P2PKHOutputSize * 2);
}
return 0;
}
@override
Future<Map<String, DecredTransactionInfo>> fetchTransactions() async {
return this.fetchFiveTransactions(0);
}
Future<Map<String, DecredTransactionInfo>> fetchFiveTransactions(int from) async {
try {
final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5");
final decoded = json.decode(res);
var txs = <String, DecredTransactionInfo>{};
for (final d in decoded) {
final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0);
var direction = TransactionDirection.outgoing;
if (d["category"] == "receive") {
direction = TransactionDirection.incoming;
}
final amountDouble = d["amount"] ?? 0.0;
final amount = (amountDouble * 1e8).round().abs();
final feeDouble = d["fee"] ?? 0.0;
final fee = (feeDouble * 1e8).round().abs();
final confs = d["confirmations"] ?? 0;
final sendTime = d["time"] ?? 0;
final height = d["height"] ?? 0;
final txInfo = DecredTransactionInfo(
id: txid,
amount: amount,
fee: fee,
direction: direction,
isPending: confs == 0,
date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false),
height: height,
confirmations: confs,
to: d["address"] ?? "",
);
txs[txid] = txInfo;
}
return txs;
} catch (e) {
printV(e);
return {};
}
}
// uniqueTxID combines the tx id and vout to create a unique id.
String uniqueTxID(String id, int vout) {
return id + ":" + vout.toString();
}
@override
Future<void> save() async {}
@override
bool get hasRescan => walletBirthdayBlockHeight() != -1;
@override
Future<void> rescan({required int height}) async {
// The required height is not used. A birthday time is recorded in the
// mnemonic. As long as not private data is imported into the wallet, we
// can always rescan from there.
var rescanHeight = 0;
if (!watchingOnly) {
rescanHeight = await walletBirthdayBlockHeight();
// Sync has not yet reached the birthday block.
if (rescanHeight == -1) {
return;
}
}
await _libwallet.rescanFromHeight(walletInfo.name, rescanHeight.toString());
}
@override
Future<void> close({bool shouldCleanup = false}) async {
if (syncTimer != null) {
syncTimer!.cancel();
syncTimer = null;
}
await _libwallet.closeWallet(walletInfo.name);
if (shouldCleanup) {
await _libwallet.shutdown();
_closeLibwallet();
}
}
@override
Future<void> changePassword(String password) async {
if (watchingOnly) {
return;
}
return () async {
await _libwallet.changeWalletPassword(walletInfo.name, _password, password);
}();
}
@override
Future<void> updateBalance() async {
final balanceMap = await _libwallet.balance(walletInfo.name);
var totalFrozen = 0;
unspentCoinsInfo.values.forEach((info) {
_unspents.forEach((element) {
if (element.hash == info.hash &&
element.vout == info.vout &&
info.isFrozen &&
element.value == info.value) {
totalFrozen += element.value;
}
});
});
balance[CryptoCurrency.dcr] = DecredBalance(
confirmed: balanceMap["confirmed"] ?? 0,
unconfirmed: balanceMap["unconfirmed"] ?? 0,
frozen: totalFrozen,
);
}
@override
Future<bool> checkNodeHealth() async => await checkSync();
@override
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => onError;
Future<void> renameWalletFiles(String newWalletName) async {
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
if (File(newDirPath).existsSync()) {
throw "wallet already exists at $newDirPath";
}
final sourceDir = Directory(currentDirPath);
final targetDir = Directory(newDirPath);
if (!targetDir.existsSync()) {
await targetDir.create(recursive: true);
}
await for (final entity in sourceDir.list(recursive: true)) {
final relativePath = entity.path.substring(sourceDir.path.length + 1);
final targetPath = p.join(targetDir.path, relativePath);
if (entity is File) {
await entity.rename(targetPath);
} else if (entity is Directory) {
await Directory(targetPath).create(recursive: true);
}
}
await sourceDir.delete(recursive: true);
}
@override
Future<String> signMessage(String message, {String? address = null}) async {
if (watchingOnly) {
throw "a watching only wallet cannot sign";
}
var addr = address;
if (addr == null) {
addr = walletAddresses.address;
}
if (addr == "") {
throw "unable to get an address from unsynced wallet";
}
return await _libwallet.signMessage(walletInfo.name, message, addr, _password);
}
Future<void> fetchUnspents() async {
try {
final res = await _libwallet.listUnspents(walletInfo.name);
final decoded = json.decode(res);
var unspents = <Unspent>[];
for (final d in decoded) {
final spendable = d["spendable"] ?? false;
if (!spendable) {
continue;
}
final amountDouble = d["amount"] ?? 0.0;
final amount = (amountDouble * 1e8).round().abs();
final utxo = Unspent(d["address"] ?? "", d["txid"] ?? "", amount, d["vout"] ?? 0, null);
utxo.isChange = d["ischange"] ?? false;
unspents.add(utxo);
}
_unspents = unspents;
} catch (e) {
printV(e);
}
}
List<Unspent> unspents() {
this.updateUnspents(_unspents);
return _unspents;
}
void updateUnspents(List<Unspent> unspentCoins) {
if (this.unspentCoinsInfo.isEmpty) {
unspentCoins.forEach((coin) => this.addCoinInfo(coin));
return;
}
if (unspentCoins.isEmpty) {
this.unspentCoinsInfo.clear();
return;
}
final walletID = idPrefix + walletInfo.name;
if (unspentCoins.isNotEmpty) {
unspentCoins.forEach((coin) {
final coinInfoList = this.unspentCoinsInfo.values.where((element) =>
element.walletId == walletID && element.hash == coin.hash && element.vout == coin.vout);
if (coinInfoList.isEmpty) {
this.addCoinInfo(coin);
} else {
final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
}
});
}
final List<dynamic> keys = <dynamic>[];
this.unspentCoinsInfo.values.forEach((element) {
final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash));
if (existUnspentCoins.isEmpty) {
keys.add(element.key);
}
});
if (keys.isNotEmpty) {
unspentCoinsInfo.deleteAll(keys);
}
}
void addCoinInfo(Unspent coin) {
final newInfo = UnspentCoinsInfo(
walletId: idPrefix + walletInfo.name,
hash: coin.hash,
isFrozen: false,
isSending: coin.isSending,
noteRaw: "",
address: coin.address,
value: coin.value,
vout: coin.vout,
isChange: coin.isChange,
keyImage: coin.keyImage,
);
unspentCoinsInfo.add(newInfo);
}
// walletBirthdayBlockHeight checks if the wallet birthday is set and returns
// it. Returns -1 if not.
Future<int> walletBirthdayBlockHeight() async {
try {
final res = await _libwallet.birthState(walletInfo.name);
final decoded = json.decode(res);
// Having these values set indicates that sync has not reached the birthday
// yet, so no birthday is set.
if (decoded["setfromheight"] == true || decoded["setfromtime"] == true) {
return -1;
}
return decoded["height"] ?? 0;
} on FormatException catch (_) {
return 0;
}
}
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
var addr = address;
if (addr == null) {
throw "an address is required to verify message";
}
return () async {
final verified = await _libwallet.verifyMessage(walletInfo.name, message, addr, signature);
if (verified == "true") {
return true;
}
return false;
}();
}
@override
String get password => _password;
@override
bool canSend() => seed != null;
}

View File

@@ -1,153 +0,0 @@
import 'dart:convert';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_decred/api/libdcrwallet.dart';
part 'wallet_addresses.g.dart';
class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses;
abstract class DecredWalletAddressesBase extends WalletAddresses with Store {
DecredWalletAddressesBase(super.walletInfo, this._libwallet, super.isTestnet);
final Libwallet _libwallet;
String _currentAddr = '';
@observable
bool isEnabledAutoGenerateSubaddress = true;
@observable
String selectedAddr = '';
@override
@computed
String get address => selectedAddr;
@override
set address(value) => selectedAddr = value;
@override
Future<void> init() async {
addressesMap = await walletInfo.getAddresses();
addressInfos = await walletInfo.getAddressInfos();
usedAddresses = await walletInfo.getUsedAddresses();
manualAddresses = await walletInfo.getManualAddresses();
hiddenAddresses = await walletInfo.getHiddenAddresses();
await updateAddressesInBox();
}
@override
Future<void> updateAddressesInBox() async {
final addrs = await _libAddresses();
final allAddrs = List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs);
// Add all addresses.
allAddrs.forEach((addr) {
if (addressesMap.containsKey(addr)) return;
addressesMap[addr] = "";
addressInfos[0] ??= [];
addressInfos[0]?.add(
WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: 0,
address: addr,
label: "",
accountIndex: 0,
),
);
});
// Add used addresses.
addrs.usedAddrs.forEach((addr) {
if (!usedAddresses.contains(addr)) usedAddresses.add(addr);
});
if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != _currentAddr) {
_currentAddr = addrs.unusedAddrs[0];
selectedAddr = _currentAddr;
}
await saveAddressesInBox();
}
List<WalletInfoAddressInfo> getAddressInfos() {
if (addressInfos.containsKey(0)) return addressInfos[0]!;
return <WalletInfoAddressInfo>[];
}
Future<void> updateAddress(String address, String label) async {
if (!addressInfos.containsKey(0)) return;
addressInfos[0]!.forEach((info) {
if (info.address == address) info.label = label;
});
await saveAddressesInBox();
}
Future<_LibAddresses> _libAddresses() async {
final nUsed = "10";
var nUnused = "1";
if (this.isEnabledAutoGenerateSubaddress) nUnused = "3";
try {
final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused);
final decoded = json.decode(res);
final usedAddrs = List<String>.from(decoded["used"] ?? []);
final unusedAddrs = List<String>.from(decoded["unused"] ?? []);
// index is the index of the first unused address.
final index = decoded["index"] ?? 0;
return _LibAddresses(usedAddrs, unusedAddrs, index);
} catch (e) {
printV(e);
return _LibAddresses([], [], 0);
}
}
Future<void> generateNewAddress(String label) async {
// NOTE: This will ignore the gap limit and may cause problems when restoring from seed if too
// many addresses are taken and not used.
final addr = await _libwallet.newExternalAddress(walletInfo.name) ?? '';
if (addr == "") return;
if (!addressesMap.containsKey(addr)) {
addressesMap[addr] = "";
addressInfos[0] ??= [];
addressInfos[0]?.add(
WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: 0,
address: addr,
label: label,
accountIndex: 0,
),
);
}
selectedAddr = addr;
await saveAddressesInBox();
}
@override
List<ReceivePageOption> get receivePageOptions => isTestnet
? [
ReceivePageOption.testnet,
...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
]
: ReceivePageOptions;
@override
PaymentURI getPaymentUri(String amount) => DecredURI(address: address, amount: amount);
}
class _LibAddresses {
final List<String> usedAddrs, unusedAddrs;
final int firstUnusedAddrIndex;
_LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex);
}

View File

@@ -1,40 +0,0 @@
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
class DecredNewWalletCredentials extends WalletCredentials {
DecredNewWalletCredentials({required String name, WalletInfo? walletInfo})
: super(name: name, walletInfo: walletInfo);
}
class DecredRestoreWalletFromSeedCredentials extends WalletCredentials {
DecredRestoreWalletFromSeedCredentials(
{required String name,
required String password,
required this.mnemonic,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String mnemonic;
}
class DecredRestoreWalletFromPubkeyCredentials extends WalletCredentials {
DecredRestoreWalletFromPubkeyCredentials(
{required String name,
required String password,
required String this.pubkey,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String pubkey;
}
class DecredRestoreWalletFromHardwareCredentials extends WalletCredentials {
DecredRestoreWalletFromHardwareCredentials(
{required String name, required this.hwAccountData, WalletInfo? walletInfo})
: t = throw UnimplementedError(),
super(name: name, walletInfo: walletInfo);
final HardwareAccountData hwAccountData;
final void t;
}

View File

@@ -1,261 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:cw_decred/api/libdcrwallet.dart';
import 'package:cw_decred/wallet_creation_credentials.dart';
import 'package:cw_decred/wallet.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:path/path.dart';
import 'package:hive/hive.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/unspent_coins_info.dart';
class DecredWalletService extends WalletService<
DecredNewWalletCredentials,
DecredRestoreWalletFromSeedCredentials,
DecredRestoreWalletFromPubkeyCredentials,
DecredRestoreWalletFromHardwareCredentials> {
DecredWalletService(this.unspentCoinsInfoSource);
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final seedRestorePath = "m/44'/42'";
static final seedRestorePathTestnet = "m/44'/1'";
static final pubkeyRestorePath = "m/44'/42'/0'";
static final pubkeyRestorePathTestnet = "m/44'/1'/0'";
final mainnet = "mainnet";
final testnet = "testnet";
static Libwallet? libwallet;
Future<void> init() async {
if (libwallet != null) {
return;
}
libwallet = await Libwallet.spawn();
// Init logging with no directory to force printing to stdout and only
// print ERROR level logs.
libwallet!.initLibdcrwallet("", "err");
}
void closeLibwallet() {
if (libwallet == null) {
return;
}
libwallet!.close();
libwallet = null;
}
@override
WalletType getType() => WalletType.decred;
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<DecredWallet> create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async {
await this.init();
final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
final network = isTestnet == true ? testnet : mainnet;
final config = {
"name": credentials.walletInfo!.name,
"datadir": dirPath,
"pass": credentials.password!,
"net": network,
"unsyncedaddrs": true,
};
await libwallet!.createWallet(jsonEncode(config));
final di = await credentials.walletInfo!.getDerivationInfo();
di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath;
await di.save();
credentials.walletInfo!.save();
credentials.walletInfo!.network = network;
// ios will move our wallet directory when updating. Since we must
// recalculate the new path every time we open the wallet, ensure this path
// is not used. An older wallet will have a directory here which is a
// condition for moving the wallet when opening, so this must be kept blank
// going forward.
credentials.walletInfo!.dirPath = "";
credentials.walletInfo!.path = "";
final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!,
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
await wallet.init();
return wallet;
}
void copyDirectorySync(Directory source, Directory destination) {
/// create destination folder if not exist
if (!destination.existsSync()) {
destination.createSync(recursive: true);
}
/// get all files from source (recursive: false is important here)
source.listSync(recursive: false).forEach((entity) {
final newPath = destination.path + Platform.pathSeparator + basename(entity.path);
if (entity is File) {
entity.rename(newPath);
} else if (entity is Directory) {
copyDirectorySync(entity, Directory(newPath));
}
});
}
Future<void> moveWallet(String fromPath, String toPath) async {
final oldWalletDir = new Directory(fromPath);
final newWalletDir = new Directory(toPath);
copyDirectorySync(oldWalletDir, newWalletDir);
// It would be ideal to delete the old directory here, but ios will error
// sometimes with "OS Error: No such file or directory, errno = 2" even
// after checking if it exists.
}
@override
Future<DecredWallet> openWallet(String name, String password) async {
final walletInfo = await WalletInfo.get(name, getType());
if (walletInfo == null) {
throw Exception('Wallet not found');
}
final di = await walletInfo.getDerivationInfo();
if (walletInfo.network == null || walletInfo.network == "") {
walletInfo.network = di.derivationPath == seedRestorePathTestnet ||
di.derivationPath == pubkeyRestorePathTestnet
? testnet
: mainnet;
walletInfo.save();
}
await this.init();
// Cake wallet version 4.27.0 and earlier gave a wallet dir that did not
// match the name. Move those to the correct place.
final dirPath = await pathForWalletDir(name: name, type: getType());
if (walletInfo.path != "") {
// On ios the stored dir no longer exists. We can only trust the basename.
// dirPath may already be updated and lost the basename, so look at path.
final randomBasename = basename(walletInfo.path);
final oldDir = await pathForWalletDir(name: randomBasename, type: getType());
if (oldDir != dirPath) {
await this.moveWallet(oldDir, dirPath);
}
// Clear the path so this does not trigger again.
walletInfo.dirPath = "";
walletInfo.path = "";
await walletInfo.save();
}
final config = {
"name": name,
"datadir": dirPath,
"net": walletInfo.network,
"unsyncedaddrs": true,
};
await libwallet!.loadWallet(jsonEncode(config));
final wallet =
DecredWallet(walletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
await wallet.init();
return wallet;
}
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = await WalletInfo.get(wallet, getType());
if (walletInfo == null) {
throw Exception('Wallet not found');
}
await WalletInfo.delete(walletInfo);
}
@override
Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = await WalletInfo.get(currentName, getType());
if (currentWalletInfo == null) {
throw Exception('Wallet not found');
}
final di = await currentWalletInfo.getDerivationInfo();
final network = di.derivationPath == seedRestorePathTestnet ||
di.derivationPath == pubkeyRestorePathTestnet
? testnet
: mainnet;
currentWalletInfo.network = network;
currentWalletInfo.save();
if (libwallet == null) {
libwallet = await Libwallet.spawn();
libwallet!.initLibdcrwallet("", "err");
}
final currentWallet = DecredWallet(
currentWalletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
await currentWallet.renameWalletFiles(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
newWalletInfo.dirPath = "";
newWalletInfo.path = "";
await newWalletInfo.save();
}
@override
Future<DecredWallet> restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
await this.init();
final network = isTestnet == true ? testnet : mainnet;
final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
final config = {
"name": credentials.walletInfo!.name,
"datadir": dirPath,
"pass": credentials.password!,
"mnemonic": credentials.mnemonic,
"net": network,
"unsyncedaddrs": true,
};
await libwallet!.createWallet(jsonEncode(config));
final di = await credentials.walletInfo!.getDerivationInfo();
di.derivationPath = isTestnet == true ? seedRestorePathTestnet : seedRestorePath;
await di.save();
credentials.walletInfo!.network = network;
credentials.walletInfo!.dirPath = "";
credentials.walletInfo!.path = "";
final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!,
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
await wallet.init();
return wallet;
}
// restoreFromKeys only supports restoring a watch only wallet from an account
// pubkey.
@override
Future<DecredWallet> restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials,
{bool? isTestnet}) async {
await this.init();
final network = isTestnet == true ? testnet : mainnet;
final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType());
final config = {
"name": credentials.walletInfo!.name,
"datadir": dirPath,
"pubkey": credentials.pubkey,
"net": network,
"unsyncedaddrs": true,
};
await libwallet!.createWatchOnlyWallet(jsonEncode(config));
final di = await credentials.walletInfo!.getDerivationInfo();
di.derivationPath = isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath;
await di.save();
credentials.walletInfo!.network = network;
credentials.walletInfo!.dirPath = "";
credentials.walletInfo!.path = "";
final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!,
this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
await wallet.init();
return wallet;
}
@override
Future<DecredWallet> restoreFromHardwareWallet(
DecredRestoreWalletFromHardwareCredentials credentials) async =>
throw UnimplementedError();
}

View File

@@ -1,19 +0,0 @@
import Cocoa
import FlutterMacOS
public class CwDecredPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "cw_decred", binaryMessenger: registrar.messenger)
let instance = CwDecredPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString)
default:
result(FlutterMethodNotImplemented)
}
}
}

View File

@@ -1,22 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint cw_decred.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'cw_decred'
s.version = '0.0.1'
s.summary = 'Cake Wallet Decred'
s.description = 'Cake Wallet wrapper over Decred project'
s.homepage = 'http://cakewallet.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Cake Wallet' => 'support@cakewallet.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'FlutterMacOS'
s.platform = :osx, '10.11'
s.vendored_libraries = 'External/lib/libdcrwallet.a'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/External/lib/libdcrwallet.a -lstdc++" }
s.swift_version = '5.0'
end

View File

@@ -1,950 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.11.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
asn1lib:
dependency: transitive
description:
name: asn1lib
sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5"
url: "https://pub.dev"
source: hosted
version: "1.5.8"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
bech32:
dependency: transitive
description:
path: "."
ref: HEAD
resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192"
url: "https://github.com/cake-tech/bech32.git"
source: git
version: "0.2.2"
blockchain_utils:
dependency: transitive
description:
path: "."
ref: cake-update-v2
resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57"
url: "https://github.com/cake-tech/blockchain_utils"
source: git
version: "3.3.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build:
dependency: transitive
description:
name: build
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
build_config:
dependency: transitive
description:
name: build_config
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
url: "https://pub.dev"
source: hosted
version: "1.1.1"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
build_resolvers:
dependency: "direct dev"
description:
name: build_resolvers
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
url: "https://pub.dev"
source: hosted
version: "2.4.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
url: "https://pub.dev"
source: hosted
version: "2.4.15"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
url: "https://pub.dev"
source: hosted
version: "8.9.5"
cake_backup:
dependency: transitive
description:
path: "."
ref: main
resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38"
url: "https://github.com/cake-tech/cake_backup.git"
source: git
version: "1.0.0+1"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
url: "https://pub.dev"
source: hosted
version: "4.10.1"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
cryptography:
dependency: transitive
description:
name: cryptography
sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05
url: "https://pub.dev"
source: hosted
version: "2.7.0"
cupertino_icons:
dependency: transitive
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
cw_core:
dependency: "direct main"
description:
path: "../cw_core"
relative: true
source: path
version: "0.0.1"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
url: "https://pub.dev"
source: hosted
version: "2.3.7"
decimal:
dependency: transitive
description:
name: decimal
sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
encrypt:
dependency: transitive
description:
name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
ffigen:
dependency: "direct dev"
description:
name: ffigen
sha256: "2119b4fe3aad0db94dc9531b90283c4640a6231070e613c400b426a4da08c704"
url: "https://pub.dev"
source: hosted
version: "16.1.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_mobx:
dependency: transitive
description:
name: flutter_mobx
sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hive:
dependency: transitive
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
hive_generator:
dependency: "direct dev"
description:
name: hive_generator
sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
http:
dependency: transitive
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
intl:
dependency: transitive
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mobx:
dependency: transitive
description:
name: mobx
sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0
url: "https://pub.dev"
source: hosted
version: "2.5.0"
mobx_codegen:
dependency: "direct dev"
description:
name: mobx_codegen
sha256: "990da80722f7d7c0017dec92040b31545d625b15d40204c36a1e63d167c73cdc"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
on_chain:
dependency: transitive
description:
path: "."
ref: "096865a8c6b89c260beadfec04f7e184c40a3273"
resolved-ref: "096865a8c6b89c260beadfec04f7e184c40a3273"
url: "https://github.com/cake-tech/on_chain.git"
source: git
version: "3.7.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
url: "https://pub.dev"
source: hosted
version: "2.2.15"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
provider:
dependency: transitive
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
quiver:
dependency: transitive
description:
name: quiver
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
source: hosted
version: "3.2.2"
rational:
dependency: transitive
description:
name: rational
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shelf:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
url: "https://pub.dev"
source: hosted
version: "1.4.1"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
socks5_proxy:
dependency: transitive
description:
path: "."
ref: "27ad7c2efae8d7460325c74b90f660085cbd0685"
resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685"
url: "https://github.com/LacticWhale/socks_dart"
source: git
version: "2.1.0"
socks_socket:
dependency: transitive
description:
path: "."
ref: e6232c53c1595469931ababa878759a067c02e94
resolved-ref: e6232c53c1595469931ababa878759a067c02e94
url: "https://github.com/sneurlax/socks_socket"
source: git
version: "1.1.1"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
source: hosted
version: "2.5.4+6"
sqflite_common_ffi:
dependency: transitive
description:
name: sqflite_common_ffi
sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36"
url: "https://pub.dev"
source: hosted
version: "2.3.4+4"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
url: "https://pub.dev"
source: hosted
version: "2.4.1+1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924
url: "https://pub.dev"
source: hosted
version: "2.9.0"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
url: "https://pub.dev"
source: hosted
version: "0.5.40"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.dev"
source: hosted
version: "3.3.0+3"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
torch_dart:
dependency: transitive
description:
path: "../scripts/torch_dart"
relative: true
source: path
version: "0.0.1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
unorm_dart:
dependency: transitive
description:
name: unorm_dart
sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
yaml_edit:
dependency: transitive
description:
name: yaml_edit
sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5
url: "https://pub.dev"
source: hosted
version: "2.2.2"
sdks:
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.24.0"

View File

@@ -1,83 +0,0 @@
name: cw_decred
description: A new Flutter plugin project.
version: 0.0.1
publish_to: none
author: Hash Wallet
homepage: https://github.com/Such-Software/hash-wallet
environment:
sdk: ">=3.2.0-0 <4.0.0"
flutter: ">=3.19.0"
dependencies:
flutter:
sdk: flutter
cw_core:
path: ../cw_core
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.15
build_resolvers: ^2.4.4
mobx_codegen: ^2.0.7
hive_generator: ^2.0.1
ffigen: ^16.1.0
ffigen:
name: libdcrwallet
description: Bindings for dcrwallet go library.
output: "lib/api/libdcrwallet_bindings.dart"
headers:
entry-points:
- "lib/api/libdcrwallet.h"
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The androidPackage and pluginClass identifiers should not ordinarily
# be modified. They are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.cakewallet.cw_decred
pluginClass: CwDecredPlugin
ios:
pluginClass: CwDecredPlugin
macos:
pluginClass: CwDecredPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

30
cw_solana/.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
## cw_solana
Solana wallet module for Cake Wallet. Provides native SOL and SPL token support built on `on_chain/solana` with high-throughput RPC usage and safe transaction parsing.
### Features
- Connect to Solana RPC (Ankr/Chainstack/custom) via `SolanaRPC` over HTTP.
- Fetch SOL balances and aggregate SPL token balances across accounts.
- Parse and stream native and SPL token transactions (filters ATA-only and spam-like micro txs).
- Estimate fees per compiled message and enforce rent-exemption checks.
- Create/sign/broadcast SOL and SPL transfers; auto-create recipient ATA when necessary.
- Manage SPL tokens; fetch on-chain metadata (symbol/name) for unknown mints.
- Sign and verify messages.
- Node health checks for SOL and a known SPL token (USDC).
### Getting started
If you use hosted RPC providers, add a secrets file for keys (optional unless using those hosts):
```dart
// cw_solana/lib/.secrets.g.dart (DO NOT COMMIT)
const String ankrApiKey = 'YOUR_ANKR_KEY';
const String chainStackApiKey = 'YOUR_CHAINSTACK_KEY';
```
Connect and sync:
```dart
final service = SolanaWalletService(walletInfoBox, true);
final wallet = await service.create(SolanaNewWalletCredentials(name: 'My SOL', password: 'secret'));
await wallet.connectToNode(node: Node(uriRaw: 'api.mainnet-beta.solana.com', isSSL: true));
await wallet.startSync();
final sol = wallet.balance[CryptoCurrency.sol]?.balance;
```
### Usage
Send SOL:
```dart
final pending = await wallet.createTransaction(
SolanaTransactionCredentials.single(
address: 'SoL...',
cryptoAmount: '0.05',
currency: CryptoCurrency.sol,
),
);
final sig = await pending.commit();
```
Add an SPL token by mint:
```dart
final token = await wallet.getSPLToken('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); // USDC
if (token != null) {
await wallet.addSPLToken(token);
}
```
### Additional information
- When using `rpc.ankr.com` or `solana-mainnet.core.chainstack.com`, the client reads API keys from `.secrets.g.dart`.
- See `lib/` for APIs: `SolanaWalletClient`, `SolanaWallet`, `SolanaWalletService`, and credential types.

View File

@@ -1,4 +0,0 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

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

View File

@@ -1,703 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/spl_token.dart';
class DefaultSPLTokens {
final List<SPLToken> _defaultTokens = [
SPLToken(
name: 'USDT Tether',
symbol: 'USDT',
mintAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
decimal: 6,
mint: 'usdtsol',
enabled: true,
),
SPLToken(
name: 'USD Coin',
symbol: 'USDC',
mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
decimal: 6,
mint: 'usdcsol',
enabled: true,
),
SPLToken(
name: 'Bonk',
symbol: 'Bonk',
mintAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
decimal: 5,
mint: 'Bonk',
iconPath: 'assets/images/bonk_icon.png',
enabled: false,
),
SPLToken(
name: 'Raydium',
symbol: 'RAY',
mintAddress: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R',
decimal: 6,
mint: 'ray',
iconPath: 'assets/images/ray_icon.png',
enabled: false,
),
SPLToken(
name: 'Wrapped Ethereum (Sollet)',
symbol: 'soETH',
mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk',
decimal: 6,
mint: 'soEth',
iconPath: 'assets/new-ui/crypto_full_icons/ethereum.svg',
enabled: false,
),
SPLToken(
name: 'Wrapped SOL',
symbol: 'WSOL',
mintAddress: 'So11111111111111111111111111111111111111112',
decimal: 9,
mint: 'WSOL',
iconPath: 'assets/new-ui/crypto_full_icons/solana.svg',
enabled: false,
),
SPLToken(
name: 'Wrapped Bitcoin (Sollet)',
symbol: 'BTC',
mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E',
decimal: 6,
mint: 'btcsol',
iconPath: 'assets/new-ui/crypto_full_icons/bitcoin.svg',
enabled: false,
),
SPLToken(
name: 'Helium Network Token',
symbol: 'HNT',
mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux',
decimal: 8,
mint: 'hnt',
iconPath: 'assets/images/hnt_icon.png',
enabled: false,
),
SPLToken(
name: 'Pyth Network',
symbol: 'PYTH',
mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3',
decimal: 6,
mint: 'pyth',
enabled: false,
),
SPLToken(
name: 'GMT',
symbol: 'GMT',
mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx',
decimal: 6,
mint: 'ray',
iconPath: 'assets/images/gmt_icon.png',
enabled: false,
),
SPLToken(
name: 'AvocadoCoin',
symbol: 'AVDO',
mintAddress: 'EE5L8cMU4itTsCSuor7NLK6RZx6JhsBe8GGV3oaAHm3P',
decimal: 8,
mint: 'avdo',
iconPath: 'assets/images/avdo_icon.png',
enabled: false,
),
SPLToken(
name: "Tether Gold",
symbol: "XAUT0",
mintAddress: "AymATz4TCL9sWNEEV9Kvyz45CHVhDZ6kUgjTJPzLpU9P",
decimal: 6,
mint: 'xaut0',
enabled: false,
iconPath: "assets/images/xau_sol.png",
),
SPLToken(
name: 'Abbott xStock',
symbol: 'ABTx',
mintAddress: 'XsHtf5RpxsQ7jeJ9ivNewouZKJHbPxhPoEy6yYvULr7',
decimal: 8,
mint: 'abtx',
enabled: false,
iconPath: 'assets/images/stocks/abtx.webp',
),
SPLToken(
name: 'AbbVie xStock',
symbol: 'ABBVx',
mintAddress: 'XswbinNKyPmzTa5CskMbCPvMW6G5CMnZXZEeQSSQoie',
decimal: 8,
mint: 'abbvx',
enabled: false,
iconPath: 'assets/images/stocks/abbv.webp',
),
SPLToken(
name: 'Accenture xStock',
symbol: 'ACNx',
mintAddress: 'Xs5UJzmCRQ8DWZjskExdSQDnbE6iLkRu2jjrRAB1JSU',
decimal: 8,
mint: 'acnx',
enabled: false,
iconPath: 'assets/images/stocks/acnx.webp',
),
SPLToken(
name: 'Alphabet xStock',
symbol: 'GOOGLx',
mintAddress: 'XsCPL9dNWBMvFtTmwcCA5v3xWPSMEBCszbQdiLLq6aN',
decimal: 8,
mint: 'googlx',
enabled: false,
iconPath: 'assets/images/stocks/googlx.webp',
),
SPLToken(
name: 'Amazon xStock',
symbol: 'AMZNx',
mintAddress: 'Xs3eBt7uRfJX8QUs4suhyU8p2M6DoUDrJyWBa8LLZsg',
decimal: 8,
mint: 'amznx',
enabled: false,
iconPath: 'assets/images/stocks/amznx.webp',
),
SPLToken(
name: 'Amber xStock',
symbol: 'AMBRx',
mintAddress: 'XsaQTCgebC2KPbf27KUhdv5JFvHhQ4GDAPURwrEhAzb',
decimal: 8,
mint: 'ambrx',
enabled: false,
iconPath: 'assets/images/stocks/ambrx.webp',
),
SPLToken(
name: 'Apple xStock',
symbol: 'AAPLx',
mintAddress: 'XsbEhLAtcf6HdfpFZ5xEMdqW8nfAvcsP5bdudRLJzJp',
decimal: 8,
mint: 'aaplx',
enabled: false,
iconPath: 'assets/images/stocks/apple.webp',
),
SPLToken(
name: 'AppLovin xStock',
symbol: 'APPx',
mintAddress: 'XsPdAVBi8Zc1xvv53k4JcMrQaEDTgkGqKYeh7AYgPHV',
decimal: 8,
mint: 'appx',
enabled: false,
iconPath: 'assets/images/stocks/appx.webp',
),
SPLToken(
name: 'AstraZeneca xStock',
symbol: 'AZNx',
mintAddress: 'Xs3ZFkPYT2BN7qBMqf1j1bfTeTm1rFzEFSsQ1z3wAKU',
decimal: 8,
mint: 'aznx',
enabled: false,
iconPath: 'assets/images/stocks/aznx.webp',
),
SPLToken(
name: 'Bank of America xStock',
symbol: 'BACx',
mintAddress: 'XswsQk4duEQmCbGzfqUUWYmi7pV7xpJ9eEmLHXCaEQP',
decimal: 8,
mint: 'bacx',
enabled: false,
iconPath: 'assets/images/stocks/bacx.webp',
),
SPLToken(
name: 'Berkshire Hathaway xStock',
symbol: 'BRK.Bx',
mintAddress: 'Xs6B6zawENwAbWVi7w92rjazLuAr5Az59qgWKcNb45x',
decimal: 8,
mint: 'brkbx',
enabled: false,
iconPath: 'assets/images/stocks/brkbx.webp',
),
SPLToken(
name: 'Broadcom xStock',
symbol: 'AVGOx',
mintAddress: 'XsgSaSvNSqLTtFuyWPBhK9196Xb9Bbdyjj4fH3cPJGo',
decimal: 8,
mint: 'avgox',
enabled: false,
iconPath: 'assets/images/stocks/avgox.webp',
),
SPLToken(
name: 'Chevron xStock',
symbol: 'CVXx',
mintAddress: 'XsNNMt7WTNA2sV3jrb1NNfNgapxRF5i4i6GcnTRRHts',
decimal: 8,
mint: 'cvxx',
enabled: false,
iconPath: 'assets/images/stocks/cvxx.webp',
),
SPLToken(
name: 'Circle xStock',
symbol: 'CRCLx',
mintAddress: 'XsueG8BtpquVJX9LVLLEGuViXUungE6WmK5YZ3p3bd1',
decimal: 8,
mint: 'crclx',
enabled: false,
iconPath: 'assets/images/stocks/crclx.webp',
),
SPLToken(
name: 'Cisco xStock',
symbol: 'CSCOx',
mintAddress: 'Xsr3pdLQyXvDJBFgpR5nexCEZwXvigb8wbPYp4YoNFf',
decimal: 8,
mint: 'cscx',
enabled: false,
iconPath: 'assets/images/stocks/cscox.webp',
),
SPLToken(
name: 'Coca-Cola xStock',
symbol: 'KOx',
mintAddress: 'XsaBXg8dU5cPM6ehmVctMkVqoiRG2ZjMo1cyBJ3AykQ',
decimal: 8,
mint: 'kox',
enabled: false,
iconPath: 'assets/images/stocks/kox.webp',
),
SPLToken(
name: 'Coinbase xStock',
symbol: 'COINx',
mintAddress: 'Xs7ZdzSHLU9ftNJsii5fCeJhoRWSC32SQGzGQtePxNu',
decimal: 8,
mint: 'coinx',
enabled: false,
iconPath: 'assets/images/stocks/coinx.webp',
),
SPLToken(
name: 'Comcast xStock',
symbol: 'CMCSAx',
mintAddress: 'XsvKCaNsxg2GN8jjUmq71qukMJr7Q1c5R2Mk9P8kcS8',
decimal: 8,
mint: 'cmcsax',
enabled: false,
iconPath: 'assets/images/stocks/cmcsax.webp',
),
SPLToken(
name: 'CrowdStrike xStock',
symbol: 'CRWDx',
mintAddress: 'Xs7xXqkcK7K8urEqGg52SECi79dRp2cEKKuYjUePYDw',
decimal: 8,
mint: 'crwdx',
enabled: false,
iconPath: 'assets/images/stocks/crwdx.webp',
),
SPLToken(
name: 'Danaher xStock',
symbol: 'DHRx',
mintAddress: 'Xseo8tgCZfkHxWS9xbFYeKFyMSbWEvZGFV1Gh53GtCV',
decimal: 8,
mint: 'dhrx',
enabled: false,
iconPath: 'assets/images/stocks/dhrx.webp',
),
SPLToken(
name: 'DFDV xStock',
symbol: 'DFDVx',
mintAddress: 'Xs2yquAgsHByNzx68WJC55WHjHBvG9JsMB7CWjTLyPy',
decimal: 8,
mint: 'dfdvx',
enabled: false,
iconPath: 'assets/images/stocks/dfdvx.webp',
),
SPLToken(
name: 'Eli Lilly xStock',
symbol: 'LLYx',
mintAddress: 'Xsnuv4omNoHozR6EEW5mXkw8Nrny5rB3jVfLqi6gKMH',
decimal: 8,
mint: 'llyx',
enabled: false,
iconPath: 'assets/images/stocks/llyx.webp',
),
SPLToken(
name: 'Exxon Mobil xStock',
symbol: 'XOMx',
mintAddress: 'XsaHND8sHyfMfsWPj6kSdd5VwvCayZvjYgKmmcNL5qh',
decimal: 8,
mint: 'xomx',
enabled: false,
iconPath: 'assets/images/stocks/xomx.webp',
),
SPLToken(
name: 'Gamestop xStock',
symbol: 'GMEx',
mintAddress: 'Xsf9mBktVB9BSU5kf4nHxPq5hCBJ2j2ui3ecFGxPRGc',
decimal: 8,
mint: 'gmex',
enabled: false,
iconPath: 'assets/images/stocks/gmex.webp',
),
SPLToken(
name: 'Gold xStock',
symbol: 'GLDx',
mintAddress: 'Xsv9hRk1z5ystj9MhnA7Lq4vjSsLwzL2nxrwmwtD3re',
decimal: 8,
mint: 'gldx',
enabled: false,
iconPath: 'assets/images/stocks/gldx.webp',
),
SPLToken(
name: 'Goldman Sachs xStock',
symbol: 'GSx',
mintAddress: 'XsgaUyp4jd1fNBCxgtTKkW64xnnhQcvgaxzsbAq5ZD1',
decimal: 8,
mint: 'gsx',
enabled: false,
iconPath: 'assets/images/stocks/gsx.webp',
),
SPLToken(
name: 'Home Depot xStock',
symbol: 'HDx',
mintAddress: 'XszjVtyhowGjSC5odCqBpW1CtXXwXjYokymrk7fGKD3',
decimal: 8,
mint: 'hdx',
enabled: false,
iconPath: 'assets/images/stocks/hdx.webp',
),
SPLToken(
name: 'Honeywell xStock',
symbol: 'HONx',
mintAddress: 'XsRbLZthfABAPAfumWNEJhPyiKDW6TvDVeAeW7oKqA2',
decimal: 8,
mint: 'honx',
enabled: false,
iconPath: 'assets/images/stocks/honx.webp',
),
SPLToken(
name: 'Intel xStock',
symbol: 'INTCx',
mintAddress: 'XshPgPdXFRWB8tP1j82rebb2Q9rPgGX37RuqzohmArM',
decimal: 8,
mint: 'intcx',
enabled: false,
iconPath: 'assets/images/stocks/intcx.webp',
),
SPLToken(
name: 'International Business Machines xStock',
symbol: 'IBMx',
mintAddress: 'XspwhyYPdWVM8XBHZnpS9hgyag9MKjLRyE3tVfmCbSr',
decimal: 8,
mint: 'ibmx',
enabled: false,
iconPath: 'assets/images/stocks/ibmx.webp',
),
SPLToken(
name: 'Johnson & Johnson xStock',
symbol: 'JNJx',
mintAddress: 'XsGVi5eo1Dh2zUpic4qACcjuWGjNv8GCt3dm5XcX6Dn',
decimal: 8,
mint: 'jnjx',
enabled: false,
iconPath: 'assets/images/stocks/jnjx.webp',
),
SPLToken(
name: 'JPMorgan Chase xStock',
symbol: 'JPMx',
mintAddress: 'XsMAqkcKsUewDrzVkait4e5u4y8REgtyS7jWgCpLV2C',
decimal: 8,
mint: 'jpmx',
enabled: false,
iconPath: 'assets/images/stocks/jpmx.webp',
),
SPLToken(
name: 'Linde xStock',
symbol: 'LINx',
mintAddress: 'XsSr8anD1hkvNMu8XQiVcmiaTP7XGvYu7Q58LdmtE8Z',
decimal: 8,
mint: 'linx',
enabled: false,
iconPath: 'assets/images/stocks/linx.webp',
),
SPLToken(
name: 'Marvell xStock',
symbol: 'MRVLx',
mintAddress: 'XsuxRGDzbLjnJ72v74b7p9VY6N66uYgTCyfwwRjVCJA',
decimal: 8,
mint: 'mrvlx',
enabled: false,
iconPath: 'assets/images/stocks/mrvlx.webp',
),
SPLToken(
name: 'Mastercard xStock',
symbol: 'MAx',
mintAddress: 'XsApJFV9MAktqnAc6jqzsHVujxkGm9xcSUffaBoYLKC',
decimal: 8,
mint: 'max',
enabled: false,
iconPath: 'assets/images/stocks/mastercard.webp',
),
SPLToken(
name: 'McDonald\'s xStock',
symbol: 'MCDx',
mintAddress: 'XsqE9cRRpzxcGKDXj1BJ7Xmg4GRhZoyY1KpmGSxAWT2',
decimal: 8,
mint: 'mcdx',
enabled: false,
iconPath: 'assets/images/stocks/mcdonalds.webp',
),
SPLToken(
name: 'Medtronic xStock',
symbol: 'MDTx',
mintAddress: 'XsDgw22qRLTv5Uwuzn6T63cW69exG41T6gwQhEK22u2',
decimal: 8,
mint: 'mdtx',
enabled: false,
iconPath: 'assets/images/stocks/mdtx.webp',
),
SPLToken(
name: 'Merck xStock',
symbol: 'MRKx',
mintAddress: 'XsnQnU7AdbRZYe2akqqpibDdXjkieGFfSkbkjX1Sd1X',
decimal: 8,
mint: 'mrkx',
enabled: false,
iconPath: 'assets/images/stocks/mrkx.webp',
),
SPLToken(
name: 'Meta xStock',
symbol: 'METAx',
mintAddress: 'Xsa62P5mvPszXL1krVUnU5ar38bBSVcWAB6fmPCo5Zu',
decimal: 8,
mint: 'metax',
enabled: false,
iconPath: 'assets/images/stocks/metax.webp',
),
SPLToken(
name: 'Microsoft xStock',
symbol: 'MSFTx',
mintAddress: 'XspzcW1PRtgf6Wj92HCiZdjzKCyFekVD8P5Ueh3dRMX',
decimal: 8,
mint: 'msftx',
enabled: false,
iconPath: 'assets/images/stocks/msftx.webp',
),
SPLToken(
name: 'MicroStrategy xStock',
symbol: 'MSTRx',
mintAddress: 'XsP7xzNPvEHS1m6qfanPUGjNmdnmsLKEoNAnHjdxxyZ',
decimal: 8,
mint: 'mstrx',
enabled: false,
iconPath: 'assets/images/stocks/mstrx.webp',
),
SPLToken(
name: 'Nasdaq xStock',
symbol: 'QQQx',
mintAddress: 'Xs8S1uUs1zvS2p7iwtsG3b6fkhpvmwz4GYU3gWAmWHZ',
decimal: 8,
mint: 'qqqx',
enabled: false,
iconPath: 'assets/images/stocks/qqqx.webp',
),
SPLToken(
name: 'Netflix xStock',
symbol: 'NFLXx',
mintAddress: 'XsEH7wWfJJu2ZT3UCFeVfALnVA6CP5ur7Ee11KmzVpL',
decimal: 8,
mint: 'nflxx',
enabled: false,
iconPath: 'assets/images/stocks/nflxx.webp',
),
SPLToken(
name: 'Novo Nordisk xStock',
symbol: 'NVOx',
mintAddress: 'XsfAzPzYrYjd4Dpa9BU3cusBsvWfVB9gBcyGC87S57n',
decimal: 8,
mint: 'nvox',
enabled: false,
iconPath: 'assets/images/stocks/nvox.webp',
),
SPLToken(
name: 'NVIDIA xStock',
symbol: 'NVDAx',
mintAddress: 'Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh',
decimal: 8,
mint: 'nvdax',
enabled: false,
iconPath: 'assets/images/stocks/nvdax.webp',
),
SPLToken(
name: 'OPEN xStock',
symbol: 'OPENx',
mintAddress: 'XsGtpmjhmC8kyjVSWL4VicGu36ceq9u55PTgF8bhGv6',
decimal: 8,
mint: 'openx',
enabled: false,
iconPath: 'assets/images/stocks/openx.webp',
),
SPLToken(
name: 'Oracle xStock',
symbol: 'ORCLx',
mintAddress: 'XsjFwUPiLofddX5cWFHW35GCbXcSu1BCUGfxoQAQjeL',
decimal: 8,
mint: 'orclx',
enabled: false,
iconPath: 'assets/images/stocks/orclx.webp',
),
SPLToken(
name: 'Palantir xStock',
symbol: 'PLTRx',
mintAddress: 'XsoBhf2ufR8fTyNSjqfU71DYGaE6Z3SUGAidpzriAA4',
decimal: 8,
mint: 'pltrx',
enabled: false,
iconPath: 'assets/images/stocks/pltrx.webp',
),
SPLToken(
name: 'PepsiCo xStock',
symbol: 'PEPx',
mintAddress: 'Xsv99frTRUeornyvCfvhnDesQDWuvns1M852Pez91vF',
decimal: 8,
mint: 'pepx',
enabled: false,
iconPath: 'assets/images/stocks/pepx.webp',
),
SPLToken(
name: 'Pfizer xStock',
symbol: 'PFEx',
mintAddress: 'XsAtbqkAP1HJxy7hFDeq7ok6yM43DQ9mQ1Rh861X8rw',
decimal: 8,
mint: 'pfex',
enabled: false,
iconPath: 'assets/images/stocks/pfex.webp',
),
SPLToken(
name: 'Philip Morris xStock',
symbol: 'PMx',
mintAddress: 'Xsba6tUnSjDae2VcopDB6FGGDaxRrewFCDa5hKn5vT3',
decimal: 8,
mint: 'pmx',
enabled: false,
iconPath: 'assets/images/stocks/pmx.webp',
),
SPLToken(
name: 'Procter & Gamble xStock',
symbol: 'PGx',
mintAddress: 'XsYdjDjNUygZ7yGKfQaB6TxLh2gC6RRjzLtLAGJrhzV',
decimal: 8,
mint: 'pgx',
enabled: false,
iconPath: 'assets/images/stocks/pgx.webp',
),
SPLToken(
name: 'Robinhood xStock',
symbol: 'HOODx',
mintAddress: 'XsvNBAYkrDRNhA7wPHQfX3ZUXZyZLdnCQDfHZ56bzpg',
decimal: 8,
mint: 'hoodx',
enabled: false,
iconPath: 'assets/images/stocks/hoodx.webp',
),
SPLToken(
name: 'Salesforce xStock',
symbol: 'CRMx',
mintAddress: 'XsczbcQ3zfcgAEt9qHQES8pxKAVG5rujPSHQEXi4kaN',
decimal: 8,
mint: 'crmx',
enabled: false,
iconPath: 'assets/images/stocks/crmx.webp',
),
SPLToken(
name: 'SP500 xStock',
symbol: 'SPYx',
mintAddress: 'XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W',
decimal: 8,
mint: 'spyx',
enabled: false,
iconPath: 'assets/images/stocks/spyx.webp',
),
SPLToken(
name: 'TBLL xStock',
symbol: 'TBLLx',
mintAddress: 'XsqBC5tcVQLYt8wqGCHRnAUUecbRYXoJCReD6w7QEKp',
decimal: 8,
mint: 'tbllx',
enabled: false,
iconPath: 'assets/images/stocks/tbllx.webp',
),
SPLToken(
name: 'Tesla xStock',
symbol: 'TSLAx',
mintAddress: 'XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB',
decimal: 8,
mint: 'tslax',
enabled: false,
iconPath: 'assets/images/stocks/tslax.webp',
),
SPLToken(
name: 'Thermo Fisher xStock',
symbol: 'TMOx',
mintAddress: 'Xs8drBWy3Sd5QY3aifG9kt9KFs2K3PGZmx7jWrsrk57',
decimal: 8,
mint: 'tmox',
enabled: false,
iconPath: 'assets/images/stocks/tmox.webp',
),
SPLToken(
name: 'TON xStock',
symbol: 'TONXx',
mintAddress: 'XscE4GUcsYhcyZu5ATiGUMmhxYa1D5fwbpJw4K6K4dp',
decimal: 8,
mint: 'tonxx',
enabled: false,
iconPath: 'assets/images/stocks/tonxx.webp',
),
SPLToken(
name: 'TQQQ xStock',
symbol: 'TQQQx',
mintAddress: 'XsjQP3iMAaQ3kQScQKthQpx9ALRbjKAjQtHg6TFomoc',
decimal: 8,
mint: 'tqqqx',
enabled: false,
iconPath: 'assets/images/stocks/tqqqx.webp',
),
SPLToken(
name: 'UnitedHealth xStock',
symbol: 'UNHx',
mintAddress: 'XszvaiXGPwvk2nwb3o9C1CX4K6zH8sez11E6uyup6fe',
decimal: 8,
mint: 'unhx',
enabled: false,
iconPath: 'assets/images/stocks/unhx.webp',
),
SPLToken(
name: 'Vanguard xStock',
symbol: 'VTIx',
mintAddress: 'XsssYEQjzxBCFgvYFFNuhJFBeHNdLWYeUSP8F45cDr9',
decimal: 8,
mint: 'vtix',
enabled: false,
iconPath: 'assets/images/stocks/vtix.webp',
),
SPLToken(
name: 'Visa xStock',
symbol: 'Vx',
mintAddress: 'XsqgsbXwWogGJsNcVZ3TyVouy2MbTkfCFhCGGGcQZ2p',
decimal: 8,
mint: 'vx',
enabled: false,
iconPath: 'assets/images/stocks/vx.webp',
),
SPLToken(
name: 'Walmart xStock',
symbol: 'WMTx',
mintAddress: 'Xs151QeqTCiuKtinzfRATnUESM2xTU6V9Wy8Vy538ci',
decimal: 8,
mint: 'wmtx',
enabled: false,
iconPath: 'assets/images/stocks/wmtx.webp',
),
];
List<SPLToken> get initialSPLTokens => _defaultTokens.map((token) {
String? iconPath;
if (token.iconPath?.isEmpty ?? true) {
try {
iconPath = CryptoCurrency.all
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
.iconPath;
} catch (_) {}
} else {
iconPath = token.iconPath;
}
return SPLToken.copyWith(token, icon: iconPath, tag: 'SOL');
}).toList();
}

View File

@@ -1,51 +0,0 @@
import 'package:cw_core/pending_transaction.dart';
class PendingSolanaTransaction with PendingTransaction {
final double amount;
final String serializedTransaction;
final String destinationAddress;
final Function sendTransaction;
final double fee;
String? _sig;
PendingSolanaTransaction({
required this.fee,
required this.amount,
required this.serializedTransaction,
required this.destinationAddress,
required this.sendTransaction,
});
@override
String get amountFormatted {
String stringifiedAmount = amount.toString();
if (stringifiedAmount.toString().length >= 6) {
stringifiedAmount = stringifiedAmount.substring(0, 6);
}
return stringifiedAmount;
}
@override
Future<void> commit() async {
_sig = await sendTransaction();
}
@override
String get feeFormatted => "$feeFormattedValue SOL";
@override
String get feeFormattedValue => fee.toString();
@override
String get hex => serializedTransaction;
@override
String get id => _sig ?? '';
@override
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}

View File

@@ -1,44 +0,0 @@
import 'dart:convert';
import 'package:cw_core/balance.dart';
class SolanaBalance extends Balance {
SolanaBalance(this.balance, bool isToken) : super(
BigInt.from(int.tryParse(balance.toStringAsFixed(isToken ? 6 : 9).replaceFirst(".", "")) ?? 0),
BigInt.from(int.tryParse(balance.toStringAsFixed(isToken ? 6 : 9).replaceFirst(".", "")) ?? 0));
// Using raw amount from RPC to avoid decimals mismatch for SPL tokens.
SolanaBalance.forToken(BigInt rawAmount, double uiAmount)
: balance = uiAmount,
super(rawAmount, rawAmount);
final double balance;
String get formattedAdditionalBalance => _balanceFormatted();
String get formattedAvailableBalance => _balanceFormatted();
String _balanceFormatted() {
String stringBalance = balance.toString();
if (stringBalance.toString().length >= 12) {
stringBalance = stringBalance.substring(0, 12);
}
return stringBalance;
}
static SolanaBalance? fromJSON(String? jsonSource, bool isToken) {
if (jsonSource == null) {
return null;
}
final decoded = json.decode(jsonSource) as Map;
try {
return SolanaBalance(decoded['balance'], isToken);
} catch (e) {
return SolanaBalance(0.0, isToken);
}
}
String toJSON() => json.encode({'balance': balance.toString()});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/exceptions.dart';
class SolanaTransactionCreationException implements Exception {
final String exceptionMessage;
SolanaTransactionCreationException(CryptoCurrency currency)
: exceptionMessage = 'Error creating ${currency.title} transaction.';
@override
String toString() => exceptionMessage;
}
class SolanaTransactionWrongBalanceException implements Exception {
final String exceptionMessage;
SolanaTransactionWrongBalanceException(CryptoCurrency currency)
: exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
@override
String toString() => exceptionMessage;
}
class SolanaSignNativeTokenTransactionRentException
extends SignNativeTokenTransactionRentException {}
class SolanaCreateAssociatedTokenAccountException extends CreateAssociatedTokenAccountException {
SolanaCreateAssociatedTokenAccountException(super.errorMessage);
}
class SolanaSignSPLTokenTransactionRentException extends SignSPLTokenTransactionRentException {}
class SolanaNoAssociatedTokenAccountException extends NoAssociatedTokenAccountException {
SolanaNoAssociatedTokenAccountException(this.account, this.mint);
final String account;
final String mint;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart';
class SolanaTransactionCredentials {
SolanaTransactionCredentials(
this.outputs, {
required this.currency,
});
final List<OutputInfo> outputs;
final CryptoCurrency currency;
}

View File

@@ -1,84 +0,0 @@
import 'dart:convert';
import 'dart:core';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_solana/solana_transaction_info.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
part 'solana_transaction_history.g.dart';
const transactionsHistoryFileName = 'solana_transactions.json';
class SolanaTransactionHistory = SolanaTransactionHistoryBase with _$SolanaTransactionHistory;
abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase<SolanaTransactionInfo>
with Store {
SolanaTransactionHistoryBase(
{required this.walletInfo, required String password, required this.encryptionFileUtils})
: _password = password {
transactions = ObservableMap<String, SolanaTransactionInfo>();
}
final WalletInfo walletInfo;
final EncryptionFileUtils encryptionFileUtils;
String _password;
Future<void> init() async {
clear();
await _load();
}
@override
Future<void> save() async {
try {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName';
final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
final data = json.encode({'transactions': transactionMaps});
await encryptionFileUtils.write(path: path, password: _password, data: data);
} catch (e, s) {
printV('Error while saving solana transaction history: ${e.toString()}');
printV(s);
}
}
@override
void addOne(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction;
@override
void addMany(Map<String, SolanaTransactionInfo> 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 encryptionFileUtils.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 = SolanaTransactionInfo.fromJson(val);
_update(tx);
}
});
} catch (e) {
printV(e);
}
}
void _update(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction;
}

View File

@@ -1,77 +0,0 @@
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
class SolanaTransactionInfo extends TransactionInfo {
SolanaTransactionInfo({
required this.id,
required this.blockTime,
required this.to,
required this.from,
required this.direction,
required this.solAmount,
this.tokenSymbol = "SOL",
required this.isPending,
required this.txFee,
}) : amount = solAmount.toInt();
final String id;
final String? to;
final String? from;
final int amount;
final bool isPending;
final double solAmount;
final String tokenSymbol;
final DateTime blockTime;
final double txFee;
final TransactionDirection direction;
String? _fiatAmount;
@override
DateTime get date => blockTime;
@override
String amountFormatted() {
String stringBalance = solAmount.toString();
if (stringBalance.toString().length >= 12) {
stringBalance = stringBalance.substring(0, 12);
}
return '$stringBalance $tokenSymbol';
}
@override
String fiatAmount() => _fiatAmount ?? '';
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() => '${txFee.toString()} SOL';
factory SolanaTransactionInfo.fromJson(Map<String, dynamic> data) {
return SolanaTransactionInfo(
id: data['id'] as String,
solAmount: data['solAmount'],
direction: parseTransactionDirectionFromInt(data['direction'] as int),
blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
isPending: data['isPending'] as bool,
tokenSymbol: data['tokenSymbol'] as String,
to: data['to'],
from: data['from'],
txFee: data['txFee'],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'solAmount': solAmount,
'direction': direction.index,
'blockTime': blockTime.millisecondsSinceEpoch,
'isPending': isPending,
'tokenSymbol': tokenSymbol,
'to': to,
'from': from,
'txFee': txFee,
};
}

View File

@@ -1,47 +0,0 @@
class SolanaTransactionModel {
final String id;
final String from;
final String to;
final double amount;
// If this is an outgoing transaction
final bool isOutgoingTx;
// The Program ID of this transaction, e.g, System Program, Token Program...
final String programId;
// The DateTime from the UNIX timestamp of the block where the transaction was included
final DateTime blockTime;
// The Transaction fee
final double fee;
// The token symbol
final String tokenSymbol;
SolanaTransactionModel({
required this.id,
required this.to,
required this.from,
required this.amount,
required this.programId,
required int blockTimeInInt,
this.isOutgoingTx = false,
required this.tokenSymbol,
required this.fee,
}) : blockTime = DateTime.fromMillisecondsSinceEpoch(blockTimeInInt * 1000);
factory SolanaTransactionModel.fromJson(Map<String, dynamic> json) => SolanaTransactionModel(
id: json['id'],
blockTimeInInt: int.parse(json["timeStamp"]) * 1000,
from: json["from"],
to: json["to"],
amount: double.parse(json["value"]),
programId: json["programId"],
fee: json['fee'],
tokenSymbol: json['tokenSymbol'],
);
}

View File

@@ -1,845 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.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/utils/print_verbose.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_core/wallet_keys_file.dart';
import 'package:cw_solana/default_spl_tokens.dart';
import 'package:cw_solana/solana_balance.dart';
import 'package:cw_solana/solana_client.dart';
import 'package:cw_solana/solana_exceptions.dart';
import 'package:cw_solana/solana_transaction_credentials.dart';
import 'package:cw_solana/solana_transaction_history.dart';
import 'package:cw_solana/solana_transaction_info.dart';
import 'package:cw_solana/solana_transaction_model.dart';
import 'package:cw_solana/solana_wallet_addresses.dart';
import 'package:cw_core/spl_token.dart';
import 'package:hex/hex.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:on_chain/solana/solana.dart' hide Store;
import 'package:bip39/bip39.dart' as bip39;
import 'package:blockchain_utils/blockchain_utils.dart';
part 'solana_wallet.g.dart';
class SolanaWallet = SolanaWalletBase with _$SolanaWallet;
abstract class SolanaWalletBase
extends WalletBase<SolanaBalance, SolanaTransactionHistory, SolanaTransactionInfo>
with Store, WalletKeysFile {
SolanaWalletBase({
required WalletInfo walletInfo,
required DerivationInfo derivationInfo,
String? mnemonic,
String? privateKey,
required String password,
SolanaBalance? initialBalance,
required this.encryptionFileUtils,
this.passphrase,
}) : syncStatus = const NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_hexPrivateKey = privateKey,
_client = SolanaWalletClient(),
walletAddresses = SolanaWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, SolanaBalance>.of(
{CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble(), false)}),
super(walletInfo, derivationInfo) {
this.walletInfo = walletInfo;
transactionHistory = SolanaTransactionHistory(
walletInfo: walletInfo,
password: password,
encryptionFileUtils: encryptionFileUtils,
);
if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
CakeHive.registerAdapter(SPLTokenAdapter());
}
_sharedPrefs.complete(SharedPreferences.getInstance());
}
final String _password;
final String? _mnemonic;
final String? _hexPrivateKey;
final EncryptionFileUtils encryptionFileUtils;
late final SolanaWalletClient _client;
SolanaWalletClient get client => _client;
@observable
double? estimatedFee;
Timer? _transactionsUpdateTimer;
late final Box<SPLToken> splTokensBox;
@override
WalletAddresses walletAddresses;
@override
@observable
SyncStatus syncStatus;
@override
@observable
ObservableMap<CryptoCurrency, SolanaBalance> balance =
ObservableMap<CryptoCurrency, SolanaBalance>();
final Completer<SharedPreferences> _sharedPrefs = Completer();
@override
Object get keys => throw UnimplementedError("keys");
late final SolanaPrivateKey _solanaPrivateKey;
late final SolanaPublicKey _solanaPublicKey;
SolanaPublicKey get solanaPublicKey => _solanaPublicKey;
SolanaPrivateKey get solanaPrivateKey => _solanaPrivateKey;
String get solanaAddress => _solanaPublicKey.toAddress().address;
@override
String? get seed => _mnemonic;
@override
String get privateKey => _solanaPrivateKey.seedHex();
@override
WalletKeysData get walletKeysData => WalletKeysData(
mnemonic: _mnemonic,
privateKey: privateKey,
passphrase: passphrase,
);
Future<void> init() async {
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}";
splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
// Create the privatekey using either the mnemonic or the privateKey
_solanaPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
passphrase: passphrase,
);
// Extract the public key and wallet address
_solanaPublicKey = _solanaPrivateKey.publicKey();
walletInfo.address = _solanaPublicKey.toAddress().address;
await walletAddresses.init();
await transactionHistory.init();
await save();
}
Future<SolanaPrivateKey> getPrivateKey({
String? mnemonic,
String? privateKey,
String? passphrase,
}) async {
assert(mnemonic != null || privateKey != null);
if (mnemonic != null) {
final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
// Derive a Solana private key from the seed
final bip44 = Bip44.fromSeed(seed, Bip44Coins.solana);
final childKey = bip44.deriveDefaultPath.change(Bip44Changes.chainExt);
return SolanaPrivateKey.fromSeed(childKey.privateKey.raw);
}
try {
final keypairBytes = Base58Decoder.decode(privateKey!);
return SolanaPrivateKey.fromBytes(keypairBytes);
} catch (_) {
final privateKeyBytes = HEX.decode(privateKey!);
return SolanaPrivateKey.fromSeed(privateKeyBytes);
}
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
@override
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
@override
Future<void> close({bool shouldCleanup = false}) async {
_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("Solana Node connection failed");
}
_setTransactionUpdateTimer();
syncStatus = ConnectedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
Future<void> _getEstimatedFees() async {
try {
estimatedFee = await _client.getEstimatedFee(_solanaPublicKey, Commitment.confirmed);
printV(estimatedFee.toString());
} catch (e) {
estimatedFee = 0.0;
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final solCredentials = credentials as SolanaTransactionCredentials;
final outputs = solCredentials.outputs;
final hasMultiDestination = outputs.length > 1;
await updateTokenBalance();
final transactionCurrency = balance.keys.firstWhere(
(currency) =>
currency.title == credentials.currency.title &&
currency.tag == credentials.currency.tag,
orElse: () => throw Exception(
'Currency ${credentials.currency.title} ${credentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
final solBalance = balance[CryptoCurrency.sol]!.balance;
double totalAmount = 0.0;
bool isSendAll = false;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
}
final totalAmountFromCredentials =
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
totalAmount = totalAmountFromCredentials.toDouble();
if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
}
} else {
final output = outputs.first;
isSendAll = output.sendAll;
if (isSendAll) {
totalAmount = walletBalanceForCurrency;
} else {
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
totalAmount = totalOriginalAmount;
}
if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
}
}
String? tokenMint;
// Token Mint is only needed for transactions that are not native tokens(non-SOL transactions)
if (transactionCurrency.title != CryptoCurrency.sol.title) {
tokenMint = (transactionCurrency as SPLToken).mintAddress;
}
final pendingSolanaTransaction = await _client.signSolanaTransaction(
tokenMint: tokenMint,
tokenTitle: transactionCurrency.title,
inputAmount: totalAmount,
ownerPrivateKey: _solanaPrivateKey,
tokenDecimals: transactionCurrency.decimals,
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
isSendAll: isSendAll,
solBalance: solBalance,
);
return pendingSolanaTransaction;
}
@override
Future<Map<String, SolanaTransactionInfo>> fetchTransactions() async => {};
@override
Future<void> updateTransactionsHistory({List<String>? specificTokenMints}) async {
await Future.wait([
_updateNativeSOLTransactions(),
updateSPLTokenTransactions(specificMints: specificTokenMints),
]);
}
/// Polls for a specific transaction by signature with exponential backoff
/// I'm using this in case we make the call to fetch the transaction and it has not finished its confirmations on the solana network and been indexed by the node networks we use.
Future<void> pollForTransaction({
required String signature,
Duration initialDelay = const Duration(seconds: 1),
int maxRetries = 5,
}) async {
final walletAddress = _solanaPublicKey.toAddress().address;
for (int i = 0; i < maxRetries; i++) {
await Future.delayed(initialDelay * (i + 1));
try {
final result = await _client.fetchTransactionBySignature(
signature: signature,
walletAddress: walletAddress,
);
if (result != null && result.transactions.isNotEmpty) {
await addTransactionsToTransactionHistory(result.transactions);
// Update only the tokens involved in this transaction
if (result.tokenMints.isNotEmpty) {
await Future.wait([
updateSPLTokenTransactions(specificMints: result.tokenMints),
updateTokenBalance(tokenMints: result.tokenMints),
]);
} else {
// If no token mints, still update SOL balance
await updateTokenBalance(tokenMints: []);
}
return;
}
} catch (e) {
printV('Error polling for transaction (attempt ${i + 1}/$maxRetries): $e');
}
}
// Fallback to full refresh if not found after max retries
printV('Transaction not found after $maxRetries attempts, falling back to full refresh');
await updateTransactionsHistory();
}
void updateTransactions(List<SolanaTransactionModel> updatedTx) {
addTransactionsToTransactionHistory(updatedTx);
}
/// Fetches the native SOL transactions linked to the wallet Public Key
Future<void> _updateNativeSOLTransactions() async {
final transactions =
await _client.fetchTransactions(_solanaPublicKey.toAddress(), onUpdate: updateTransactions);
await addTransactionsToTransactionHistory(transactions);
}
Future<void> updateSPLTokenTransactions({List<String>? specificMints}) async {
final allTokens = balance.keys.whereType<SPLToken>().toList(growable: false);
// Filter to specific mints if provided
final tokens = specificMints != null
? allTokens.where((t) => specificMints.contains(t.mintAddress)).toList(growable: false)
: allTokens;
if (tokens.isEmpty) return;
const int batchSize = 5;
for (var i = 0; i < tokens.length; i += batchSize) {
final batch = tokens.sublist(
i,
i + batchSize > tokens.length ? tokens.length : i + batchSize,
);
final results = await Future.wait(
batch.map((token) async {
try {
return await _client.getSPLTokenTransfers(
mintAddress: token.mintAddress,
splTokenSymbol: token.symbol,
splTokenDecimal: token.decimal,
privateKey: _solanaPrivateKey,
onUpdate: updateTransactions,
);
} catch (_) {
return <SolanaTransactionModel>[];
}
}),
);
for (final list in results) {
await addTransactionsToTransactionHistory(list);
}
}
}
Future<void> addTransactionsToTransactionHistory(
List<SolanaTransactionModel> transactions,
) async {
final Map<String, SolanaTransactionInfo> result = {};
for (var transactionModel in transactions) {
result[transactionModel.id] = SolanaTransactionInfo(
id: transactionModel.id,
to: transactionModel.to,
from: transactionModel.from,
blockTime: transactionModel.blockTime,
direction: transactionModel.isOutgoingTx
? TransactionDirection.outgoing
: TransactionDirection.incoming,
solAmount: transactionModel.amount,
isPending: false,
txFee: transactionModel.fee,
tokenSymbol: transactionModel.tokenSymbol,
);
}
transactionHistory.addMany(result);
await transactionHistory.save();
}
@override
Future<void> rescan({required int height}) => throw UnimplementedError("rescan");
@override
Future<void> save() async {
if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
await saveKeysFile(_password, encryptionFileUtils);
saveKeysFile(_password, encryptionFileUtils, true);
}
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
// Verify node health before attempting to sync
final isHealthy = await checkNodeHealth();
if (!isHealthy) {
syncStatus = FailedSyncStatus();
return;
}
await Future.wait([
updateTokenBalance(),
_updateNativeSOLTransactions(),
updateSPLTokenTransactions(),
_getEstimatedFees(),
]);
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
String toJSON() => json.encode({
'mnemonic': _mnemonic,
'private_key': _hexPrivateKey,
'balance': balance[currency]!.toJSON(),
'passphrase': passphrase,
});
static Future<SolanaWallet> open({
required String name,
required String password,
required WalletInfo walletInfo,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
final path = await pathForWallet(name: name, type: walletInfo.type);
Map<String, dynamic>? data;
try {
final jsonSource = await encryptionFileUtils.read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} catch (e) {
if (!hasKeysFile) rethrow;
}
final balance = SolanaBalance.fromJSON(data?['balance'] as String?, false) ?? SolanaBalance(0.0, false);
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
final mnemonic = data!['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final passphrase = data['passphrase'] as String?;
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
walletInfo.type,
password,
encryptionFileUtils,
);
}
final derivationInfo = await walletInfo.getDerivationInfo();
return SolanaWallet(
walletInfo: walletInfo,
derivationInfo: derivationInfo,
password: password,
passphrase: keysData.passphrase,
mnemonic: keysData.mnemonic,
privateKey: keysData.privateKey,
initialBalance: balance,
encryptionFileUtils: encryptionFileUtils,
);
}
Future<void> updateTokenBalance({List<String>? tokenMints}) async {
// Fetch SOL and SPL token balances in parallel for better performance
await Future.wait([
_fetchSOLBalance().then((solBalance) {
balance[CryptoCurrency.sol] = solBalance;
}),
_updateSplTokenBalancesInternal(tokenMints: tokenMints),
]);
await save();
}
Future<SolanaBalance> _fetchSOLBalance() async {
final balance = await _client.getBalance(solanaAddress);
return SolanaBalance(balance, false);
}
/// Internal helper to update SPL token balances.
/// When [tokenMints] is null or empty, updates all enabled tokens.
Future<void> _updateSplTokenBalancesInternal({
List<String>? tokenMints,
}) async {
// Remove disabled tokens first to keep state clean
for (var token in splTokensBox.values.where((t) => !t.enabled)) {
balance.remove(token);
}
final enabledTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false);
if (enabledTokens.isEmpty) return;
final tokens = tokenMints == null || tokenMints.isEmpty
? enabledTokens
: enabledTokens.where((t) => tokenMints.contains(t.mintAddress)).toList(growable: false);
if (tokens.isEmpty) return;
const int batchSize = 5;
for (var i = 0; i < tokens.length; i += batchSize) {
final batch = tokens.sublist(
i,
i + batchSize > tokens.length ? tokens.length : i + batchSize,
);
final results = await Future.wait(batch.map((token) async {
try {
final fetched = await _client.getSplTokenBalance(token.mintAddress, solanaAddress);
return MapEntry(token, fetched);
} catch (e) {
printV('Error fetching spl token (${token.symbol}) balance ${e.toString()}');
return MapEntry<SPLToken, SolanaBalance?>(token, null);
}
}));
for (final entry in results) {
final token = entry.key;
final fetchedBalance = entry.value;
final currentBalance = balance[token] ??
SolanaBalance.forToken(BigInt.zero, 0.0);
balance[token] = fetchedBalance ?? currentBalance;
}
}
}
@override
Future<void>? updateBalance() async => await updateTokenBalance();
@override
Future<bool> checkNodeHealth() async {
try {
// Check native balance
await _client.getBalance(solanaAddress, throwOnError: true);
// Check USDC token balance
const usdcMintAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
await _client.getSplTokenBalance(usdcMintAddress, solanaAddress, throwOnError: true);
return true;
} catch (e) {
return false;
}
}
List<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
void addInitialTokens() {
final initialSPLTokens = DefaultSPLTokens().initialSPLTokens;
for (var token in initialSPLTokens) {
if (!splTokensBox.containsKey(token.mintAddress)) {
splTokensBox.put(token.mintAddress, token);
} else {
// update existing token
final existingToken = splTokensBox.get(token.mintAddress);
splTokensBox.put(
token.mintAddress, SPLToken.copyWith(token, enabled: existingToken!.enabled));
}
}
}
Future<SolanaMoralisDiscoveryResult> discoverTokensFromMoralis() async {
try {
if (!splTokensBox.isOpen) return SolanaMoralisDiscoveryResult.empty;
final address = walletAddresses.address;
if (address.isEmpty) return SolanaMoralisDiscoveryResult.empty;
final walletTokens = await _client.fetchWalletTokensFromMoralis(address);
if (walletTokens.isEmpty) return SolanaMoralisDiscoveryResult.empty;
final existingMints = {
for (final token in splTokensBox.values) token.mintAddress: token,
};
final defaultMints = DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
final newTokens = <DiscoveredSPLToken>[];
for (final moralisToken in walletTokens) {
final mint = moralisToken.mint;
final existingToken = existingMints[mint];
if (existingToken != null) {
if (defaultMints.contains(mint) && !existingToken.enabled) {
existingToken.enabled = true;
await existingToken.save();
await addSPLToken(existingToken);
}
continue;
}
final tokenInfo = await _client.fetchSPLTokenInfo(mint);
if (tokenInfo == null) continue;
final discoveredToken = SPLToken(
name: tokenInfo.name,
symbol: tokenInfo.symbol,
mintAddress: mint,
decimal: moralisToken.decimals,
mint: tokenInfo.mint,
iconPath: tokenInfo.iconPath,
tag: 'SOL',
);
newTokens.add(
DiscoveredSPLToken(
token: discoveredToken,
balance: moralisToken.amount,
),
);
}
return SolanaMoralisDiscoveryResult(newTokens: newTokens);
} catch (e) {
printV('Error discovering SPL tokens from Moralis: ${e.toString()}');
return SolanaMoralisDiscoveryResult.empty;
}
}
Future<void> addSPLToken(SPLToken token) async {
await splTokensBox.put(token.mintAddress, token);
if (token.enabled) {
final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ??
balance[token] ??
SolanaBalance.forToken(BigInt.zero, 0.0);
balance[token] = tokenBalance;
} else {
balance.remove(token);
}
}
Future<void> deleteSPLToken(SPLToken token) async {
if (splTokensBox.isOpen) {
await splTokensBox.delete(token.mintAddress);
}
balance.remove(token);
await _removeTokenTransactionsInHistory(token);
updateTokenBalance();
}
Future<void> _removeTokenTransactionsInHistory(SPLToken token) async {
transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title);
await transactionHistory.save();
}
Future<SPLToken?> getSPLToken(String mintAddress) async {
try {
return await _client.fetchSPLTokenInfo(mintAddress);
} catch (e, s) {
printV('Error fetching token: ${e.toString()}, ${s.toString()}');
return null;
}
}
@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: 30), (_) {
updateTokenBalance();
_updateNativeSOLTransactions();
updateSPLTokenTransactions();
_getEstimatedFees();
});
}
@override
Future<String> signMessage(String message, {String? address}) async {
// Convert the message to bytes
final messageBytes = utf8.encode(message);
// Sign the message bytes with the wallet's private key
final signature = (_solanaPrivateKey.sign(messageBytes));
return Base58Encoder.encode(signature);
}
List<List<int>> bytesFromSigString(String signatureString) {
final regex = RegExp(r'Signature\(\[(.+)\], publicKey: (.+)\)');
final match = regex.firstMatch(signatureString);
if (match != null) {
final bytesString = match.group(1)!;
final base58EncodedPublicKeyString = match.group(2)!;
final sigBytes = bytesString.split(', ').map(int.parse).toList();
List<int> pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString);
return [sigBytes, pubKeyBytes];
} else {
throw const FormatException('Invalid Signature string format');
}
}
@override
Future<bool> verifyMessage(String message, String signature, {String? address}) async {
String signatureString = utf8.decode(HEX.decode(signature));
List<List<int>> bytes = bytesFromSigString(signatureString);
final messageBytes = utf8.encode(message);
final sigBytes = bytes[0];
final pubKeyBytes = bytes[1];
if (address == null) {
return false;
}
// make sure the address derived from the public key provided matches the one we expect
final pub = SolanaPublicKey.fromBytes(pubKeyBytes);
if (address != pub.toAddress().address) {
return false;
}
return pub.verify(
message: messageBytes,
signature: sigBytes,
);
}
SolanaRPC? get solanaProvider => _client.getSolanaProvider;
@override
String get password => _password;
@override
final String? passphrase;
}
class DiscoveredSPLToken {
final SPLToken token;
final double balance;
const DiscoveredSPLToken({
required this.token,
required this.balance,
});
}
class SolanaMoralisDiscoveryResult {
final List<DiscoveredSPLToken> newTokens;
const SolanaMoralisDiscoveryResult({required this.newTokens});
static const SolanaMoralisDiscoveryResult empty = SolanaMoralisDiscoveryResult(newTokens: []);
}

View File

@@ -1,41 +0,0 @@
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
part 'solana_wallet_addresses.g.dart';
class SolanaWalletAddresses = SolanaWalletAddressesBase with _$SolanaWalletAddresses;
abstract class SolanaWalletAddressesBase extends WalletAddresses with Store {
SolanaWalletAddressesBase(WalletInfo walletInfo)
: address = '',
super(walletInfo);
@override
String address;
@override
String get primaryAddress => address;
@override
Future<void> init() async {
address = walletInfo.address;
await updateAddressesInBox();
}
@override
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = '';
await saveAddressesInBox();
} catch (e) {
printV(e.toString());
}
}
@override
PaymentURI getPaymentUri(String amount) => SolanaURI(address: address, amount: amount);
}

View File

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

View File

@@ -1,169 +0,0 @@
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:collection/collection.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_solana/solana_mnemonics.dart';
import 'package:cw_solana/solana_wallet.dart';
import 'package:cw_solana/solana_wallet_creation_credentials.dart';
import 'package:hive/hive.dart';
class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey, SolanaNewWalletCredentials> {
SolanaWalletService(this.isDirect);
final bool isDirect;
@override
Future<SolanaWallet> create(SolanaNewWalletCredentials credentials, {bool? isTestnet}) async {
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength);
final wallet = SolanaWallet(
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
mnemonic: mnemonic,
password: credentials.password!,
passphrase: credentials.passphrase,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
WalletType getType() => WalletType.solana;
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<SolanaWallet> openWallet(String name, String password) async {
final walletInfo = await WalletInfo.get(name, getType());
if (walletInfo == null) {
throw Exception('Wallet not found');
}
try {
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
saveBackup(name);
return wallet;
} catch (_) {
await restoreWalletFilesFromBackup(name);
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
}
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = await WalletInfo.get(wallet, getType());
if (walletInfo == null) {
throw Exception('Wallet not found');
}
await WalletInfo.delete(walletInfo);
}
@override
Future<SolanaWallet> restoreFromKeys(SolanaRestoreWalletFromPrivateKey credentials,
{bool? isTestnet}) async {
final wallet = SolanaWallet(
password: credentials.password!,
privateKey: credentials.privateKey,
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<SolanaWallet> restoreFromSeed(SolanaRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
if (!bip39.validateMnemonic(credentials.mnemonic)) {
throw SolanaMnemonicIsIncorrectException();
}
final wallet = SolanaWallet(
password: credentials.password!,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
passphrase: credentials.passphrase,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = await WalletInfo.get(currentName, getType());
if (currentWalletInfo == null) {
throw Exception('Wallet not found');
}
final currentWallet = await SolanaWalletBase.open(
password: password,
name: currentName,
walletInfo: currentWalletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
await newWalletInfo.save();
}
@override
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>> restoreFromHardwareWallet(SolanaNewWalletCredentials credentials) {
// TODO: implement restoreFromHardwareWallet
throw UnimplementedError();
}
}

View File

@@ -1,46 +0,0 @@
name: cw_solana
description: A new Flutter package project.
version: 0.0.1
publish_to: none
homepage: https://github.com/Such-Software/hash-wallet
environment:
sdk: '>=3.0.6 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
cw_core:
path: ../cw_core
http: ^1.1.0
hive: ^2.2.3
bip39: ^1.0.6
mobx: ^2.3.0+1
shared_preferences: ^2.0.15
bip32: ^2.0.0
hex: ^0.2.0
on_chain:
git:
url: https://github.com/cake-tech/on_chain.git
ref: 096865a8c6b89c260beadfec04f7e184c40a3273
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: ^2.4.15
mobx_codegen: ^2.0.7
hive_generator: ^2.0.1
dependency_overrides:
watcher: ^1.1.0
flutter:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg

View File

@@ -1,12 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:cw_solana/cw_solana.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);
});
}

30
cw_tron/.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
## cw_tron
TRON wallet module for Cake Wallet. Implements TRX and TRC-20 token support on top of `on_chain` (Tron), with transaction history and fee estimation via TronGrid.
### Features
- Connect to TRON nodes over HTTP(S) via `TronProvider`/`TronHTTPProvider`.
- Fetch TRX balance and TRC-20 token balances.
- Estimate fees using bandwidth/energy and memo fee; accounts for available account resources.
- Create and sign TRX and TRC-20 transfers (supports send-all and optional memo).
- Broadcast signed transactions.
- Load account history (TRX and TRC-20), filtering spam and TRC10-only events.
- Manage TRC-20 tokens: add/remove tokens and fetch token metadata.
- Sign/verify messages.
- Node health checks for both native and token balance endpoints.
### Getting started
Provide a TRON RPC endpoint and a TronGrid API key. Add a secrets file:
```dart
// cw_tron/lib/.secrets.g.dart (DO NOT COMMIT)
const String tronGridApiKey = 'YOUR_TRONGRID_API_KEY';
```
Basic connect and sync:
```dart
final service = TronWalletService(walletInfoBox, client: TronClient(), isDirect: true);
final wallet = await service.create(TronNewWalletCredentials(name: 'My TRON', password: 'secret'));
await wallet.connectToNode(node: Node(uriRaw: 'api.trongrid.io', isSSL: true));
await wallet.startSync();
final trxBalance = wallet.balance[CryptoCurrency.trx];
```
### Usage
Send TRX:
```dart
final pending = await wallet.createTransaction(
TronTransactionCredentials.single(
address: 'T...',
cryptoAmount: '1.5',
currency: CryptoCurrency.trx,
),
);
final txHash = await pending.commit();
```
Add USDT (TRC-20):
```dart
final usdt = await wallet.getTronToken('TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t');
if (usdt != null) {
await wallet.addTronToken(usdt);
}
```
### Additional information
- History and token queries use TronGrid; set `tronGridApiKey`.
- See `lib/` for APIs: `TronClient`, `TronWallet`, `TronWalletService`, and credential types.

View File

@@ -1,4 +0,0 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

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

View File

@@ -1,107 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/tron_token.dart';
class DefaultTronTokens {
final List<TronToken> _defaultTokens = [
TronToken(
name: "Tether USD",
symbol: "USDT",
contractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
decimal: 6,
enabled: true,
),
TronToken(
name: "USD Coin",
symbol: "USDC",
contractAddress: "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8",
decimal: 6,
enabled: true,
),
TronToken(
name: "Bitcoin",
symbol: "BTC",
contractAddress: "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
decimal: 8,
enabled: false,
),
TronToken(
name: "Ethereum",
symbol: "ETH",
contractAddress: "TRFe3hT5oYhjSZ6f3ji5FJ7YCfrkWnHRvh",
decimal: 18,
enabled: false,
),
TronToken(
name: "Wrapped BTC",
symbol: "WBTC",
contractAddress: "TXpw8XeWYeTUd4quDskoUqeQPowRh4jY65",
decimal: 8,
enabled: true,
),
TronToken(
name: "Dogecoin",
symbol: "DOGE",
contractAddress: "THbVQp8kMjStKNnf2iCY6NEzThKMK5aBHg",
decimal: 8,
enabled: true,
),
TronToken(
name: "JUST Stablecoin",
symbol: "USDJ",
contractAddress: "TMwFHYXLJaRUPeW6421aqXL4ZEzPRFGkGT",
decimal: 18,
enabled: false,
),
TronToken(
name: "SUN",
symbol: "SUN",
contractAddress: "TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S",
decimal: 18,
enabled: false,
),
TronToken(
name: "Wrapped TRX",
symbol: "WTRX",
contractAddress: "TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR",
decimal: 6,
enabled: false,
),
TronToken(
name: "BitTorent",
symbol: "BTT",
contractAddress: "TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4",
decimal: 18,
enabled: false,
),
TronToken(
name: "BUSD Token",
symbol: "BUSD",
contractAddress: "TMz2SWatiAtZVVcH2ebpsbVtYwUPT9EdjH",
decimal: 18,
enabled: false,
),
TronToken(
name: "HTX",
symbol: "HTX",
contractAddress: "TUPM7K8REVzD2UdV4R5fe5M8XbnR2DdoJ6",
decimal: 18,
enabled: false,
),
];
List<TronToken> get initialTronTokens => _defaultTokens.map((token) {
String? iconPath;
if (token.iconPath?.isEmpty ?? true) {
try {
iconPath = CryptoCurrency.all
.firstWhere((element) =>
element.title.toUpperCase() == token.symbol.split(".").first.toUpperCase())
.iconPath;
} catch (_) {}
} else {
iconPath = token.iconPath;
}
return TronToken.copyWith(token, icon: iconPath, tag: 'TRX');
}).toList();
}

View File

@@ -1,41 +0,0 @@
import 'package:cw_core/pending_transaction.dart';
import 'package:web3dart/crypto.dart';
class PendingTronTransaction with PendingTransaction {
final Function sendTransaction;
final List<int> signedTransaction;
final String fee;
final String amount;
PendingTronTransaction({
required this.sendTransaction,
required this.signedTransaction,
required this.fee,
required this.amount,
});
@override
String get amountFormatted => amount;
@override
Future<void> commit() async => await sendTransaction();
@override
String get feeFormatted => "$feeFormattedValue TRX";
@override
String get feeFormattedValue => fee;
@override
String get hex => bytesToHex(signedTransaction);
@override
String get id => '';
@override
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}

View File

@@ -1,436 +0,0 @@
final trc20Abi = [
{"inputs": [], "stateMutability": "nonpayable", "type": "constructor"},
{
"anonymous": false,
"inputs": [
{"indexed": true, "internalType": "address", "name": "owner", "type": "address"},
{"indexed": true, "internalType": "address", "name": "spender", "type": "address"},
{"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{"indexed": false, "internalType": "uint256", "name": "total", "type": "uint256"},
{"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"},
{"indexed": true, "internalType": "address", "name": "buyer", "type": "address"},
{"indexed": true, "internalType": "address", "name": "seller", "type": "address"},
{"indexed": false, "internalType": "address", "name": "contract_address", "type": "address"}
],
"name": "OrderPaid",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "internalType": "address", "name": "previousOwner", "type": "address"},
{"indexed": true, "internalType": "address", "name": "newOwner", "type": "address"}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{"indexed": false, "internalType": "address", "name": "token", "type": "address"},
{"indexed": false, "internalType": "bool", "name": "active", "type": "bool"}
],
"name": "TokenUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "internalType": "address", "name": "from", "type": "address"},
{"indexed": true, "internalType": "address", "name": "to", "type": "address"},
{"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{"indexed": false, "internalType": "string", "name": "username", "type": "string"},
{"indexed": true, "internalType": "address", "name": "seller", "type": "address"}
],
"name": "UserRegistred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"},
{"indexed": true, "internalType": "address", "name": "buyer", "type": "address"},
{"indexed": false, "internalType": "address", "name": "seller", "type": "address"}
],
"name": "WBuyer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"},
{"indexed": true, "internalType": "address", "name": "seller", "type": "address"},
{"indexed": false, "internalType": "address", "name": "buyer", "type": "address"}
],
"name": "WSeller",
"type": "event"
},
{
"inputs": [],
"name": "CONTRACTPERCENTAGE",
"outputs": [
{"internalType": "uint8", "name": "", "type": "uint8"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "uint16", "name": "order_id", "type": "uint16"},
{"internalType": "uint256", "name": "order_total", "type": "uint256"},
{"internalType": "address", "name": "contractAddress", "type": "address"},
{"internalType": "address", "name": "seller", "type": "address"}
],
"name": "PayWithTokens",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "TOKENINCREAMENT",
"outputs": [
{"internalType": "uint16", "name": "", "type": "uint16"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "", "type": "address"}
],
"name": "_signer",
"outputs": [
{"internalType": "bool", "name": "", "type": "bool"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "", "type": "address"}
],
"name": "_tokens",
"outputs": [
{"internalType": "bool", "name": "active", "type": "bool"},
{"internalType": "uint16", "name": "token", "type": "uint16"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "", "type": "address"}
],
"name": "_users",
"outputs": [
{"internalType": "bool", "name": "active", "type": "bool"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "owner", "type": "address"},
{"internalType": "address", "name": "spender", "type": "address"}
],
"name": "allowance",
"outputs": [
{"internalType": "uint256", "name": "", "type": "uint256"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "spender", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "approve",
"outputs": [
{"internalType": "bool", "name": "", "type": "bool"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "account", "type": "address"}
],
"name": "balanceOf",
"outputs": [
{"internalType": "uint256", "name": "", "type": "uint256"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "token", "type": "address"}
],
"name": "balanceOfContract",
"outputs": [
{"internalType": "uint256", "name": "", "type": "uint256"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "burn",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "account", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "burnFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "uint256", "name": "value", "type": "uint256"},
{"internalType": "address", "name": "_contractAddress", "type": "address"}
],
"name": "contractWithdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{"internalType": "uint8", "name": "", "type": "uint8"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "spender", "type": "address"},
{"internalType": "uint256", "name": "subtractedValue", "type": "uint256"}
],
"name": "decreaseAllowance",
"outputs": [
{"internalType": "bool", "name": "", "type": "bool"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "spender", "type": "address"},
{"internalType": "uint256", "name": "addedValue", "type": "uint256"}
],
"name": "increaseAllowance",
"outputs": [
{"internalType": "bool", "name": "", "type": "bool"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "to", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "mint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{"internalType": "string", "name": "", "type": "string"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{"internalType": "address", "name": "", "type": "address"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "token", "type": "address"},
{"internalType": "uint256", "name": "value", "type": "uint256"}
],
"name": "payToContract",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{"internalType": "uint16", "name": "order_id", "type": "uint16"},
{"internalType": "address", "name": "seller", "type": "address"}
],
"name": "payWithNativeToken",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{"internalType": "string", "name": "username", "type": "string"}
],
"name": "regiserUser",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "uint16", "name": "id", "type": "uint16"},
{"internalType": "address", "name": "buyer", "type": "address"},
{"internalType": "address", "name": "seller", "type": "address"}
],
"name": "selectOrder",
"outputs": [
{"internalType": "uint232", "name": "", "type": "uint232"},
{"internalType": "uint16", "name": "", "type": "uint16"},
{"internalType": "uint8", "name": "", "type": "uint8"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{"internalType": "string", "name": "", "type": "string"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "signer", "type": "address"}
],
"name": "toggleSigner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "tokenAddress", "type": "address"}
],
"name": "toggleToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{"internalType": "uint256", "name": "", "type": "uint256"}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "to", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "transfer",
"outputs": [
{"internalType": "bool", "name": "", "type": "bool"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "from", "type": "address"},
{"internalType": "address", "name": "to", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"}
],
"name": "transferFrom",
"outputs": [
{"internalType": "bool", "name": "", "type": "bool"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "newOwner", "type": "address"}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "uint8", "name": "newPercentage", "type": "uint8"}
],
"name": "updateContractPercentage",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address[]", "name": "buyer", "type": "address[]"},
{"internalType": "bytes[]", "name": "signature", "type": "bytes[]"},
{"internalType": "uint16[]", "name": "order_id", "type": "uint16[]"},
{"internalType": "address", "name": "contractAddress", "type": "address"}
],
"name": "widthrawForSellers",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{"internalType": "address", "name": "seller", "type": "address"},
{"internalType": "bytes", "name": "signature", "type": "bytes"},
{"internalType": "uint16", "name": "order_id", "type": "uint16"},
{"internalType": "address", "name": "contractAddress", "type": "address"}
],
"name": "widthrowForBuyers",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];

View File

@@ -1,25 +0,0 @@
import 'dart:convert';
import 'package:cw_core/balance.dart';
class TronBalance extends Balance {
TronBalance(this.balance) : super(balance, balance);
final BigInt balance;
String toJSON() => json.encode({ 'balance': balance.toString() });
static TronBalance? fromJSON(String? jsonSource) {
if (jsonSource == null) {
return null;
}
final decoded = json.decode(jsonSource) as Map;
try {
return TronBalance(BigInt.parse(decoded['balance']));
} catch (e) {
return TronBalance(BigInt.zero);
}
}
}

View File

@@ -1,568 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_tron/pending_tron_transaction.dart';
import 'package:cw_tron/tron_abi.dart';
import 'package:cw_tron/tron_balance.dart';
import 'package:cw_tron/tron_http_provider.dart';
import 'package:cw_core/tron_token.dart';
import 'package:cw_tron/tron_transaction_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '.secrets.g.dart' as secrets;
import 'package:on_chain/on_chain.dart';
class TronClient {
late final client = ProxyWrapper().getHttpIOClient();
TronProvider? _provider;
// This is an internal tracker, so we don't have to "refetch".
int _nativeTxEstimatedFee = 0;
Future<List<TronTransactionModel>> fetchTransactions(String address,
{String? contractAddress}) async {
try {
final response = await client.get(
Uri.https(
"api.trongrid.io",
"/v1/accounts/$address/transactions",
{
"only_confirmed": "true",
"limit": "200",
},
),
headers: {
'Content-Type': 'application/json',
'TRON-PRO-API-KEY': secrets.tronGridApiKey,
},
);
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 &&
response.statusCode < 300 &&
jsonResponse['status'] != false) {
return (jsonResponse['data'] as List).map((e) {
return TronTransactionModel.fromJson(e as Map<String, dynamic>);
}).toList();
}
return [];
} catch (e, s) {
log('Error getting tx: ${e.toString()}\n ${s.toString()}');
return [];
}
}
Future<List<TronTRC20TransactionModel>> fetchTrc20ExcludedTransactions(String address) async {
try {
final response = await client.get(
Uri.https(
"api.trongrid.io",
"/v1/accounts/$address/transactions/trc20",
{
"only_confirmed": "true",
"limit": "200",
},
),
headers: {
'Content-Type': 'application/json',
'TRON-PRO-API-KEY': secrets.tronGridApiKey,
},
);
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 &&
response.statusCode < 300 &&
jsonResponse['status'] != false) {
return (jsonResponse['data'] as List).map((e) {
return TronTRC20TransactionModel.fromJson(e as Map<String, dynamic>);
}).toList();
}
return [];
} catch (e, s) {
log('Error getting trc20 tx: ${e.toString()}\n ${s.toString()}');
return [];
}
}
bool connect(Node node) {
try {
final formattedUrl = '${node.isSSL ? 'https' : 'http'}://${node.uriRaw}';
_provider = TronProvider(TronHTTPProvider(url: formattedUrl));
return true;
} catch (e) {
return false;
}
}
Future<BigInt> getBalance(TronAddress address, {bool throwOnError = false}) async {
try {
final accountDetails = await _provider!.request(TronRequestGetAccount(address: address));
return accountDetails?.balance ?? BigInt.zero;
} catch (_) {
if (throwOnError) {
rethrow;
}
return BigInt.zero;
}
}
Future<int> getFeeLimit(
TransactionRaw rawTransaction,
TronAddress address,
TronAddress receiverAddress, {
int energyUsed = 0,
bool isEstimatedFeeFlow = false,
}) async {
try {
// Get the tron chain parameters.
final chainParams = await _provider!.request(TronRequestGetChainParameters());
final bandWidthInSun = chainParams.getTransactionFee!;
log('BandWidth In Sun: $bandWidthInSun');
final energyInSun = chainParams.getEnergyFee!;
log('Energy In Sun: $energyInSun');
final fakeTransaction = Transaction(
rawData: rawTransaction,
signature: [Uint8List(65)],
);
// Calculate the total size of the fake transaction, considering the required network overhead.
final transactionSize = fakeTransaction.length + 64;
// Assign the calculated size to the variable representing the required bandwidth.
int neededBandWidth = transactionSize;
log('Initial Needed Bandwidth: $neededBandWidth');
int neededEnergy = energyUsed;
log('Initial Needed Energy: $neededEnergy');
// Fetch account resources to assess the available bandwidth and energy
final accountResource =
await _provider!.request(TronRequestGetAccountResource(address: address));
neededEnergy -= accountResource.howManyEnergy.toInt();
log('Account resource energy: ${accountResource.howManyEnergy.toInt()}');
log('Needed Energy after deducting from account resource energy: $neededEnergy');
// Deduct the bandwidth from the account's available bandwidth.
final BigInt accountBandWidth = accountResource.howManyBandwIth;
log('Account resource bandwidth: ${accountResource.howManyBandwIth.toInt()}');
if (accountBandWidth >= BigInt.from(neededBandWidth) && !isEstimatedFeeFlow) {
log('Account has more bandwidth than required');
neededBandWidth = 0;
}
if (neededEnergy < 0) {
neededEnergy = 0;
}
final energyBurn = neededEnergy * energyInSun.toInt();
log('Energy Burn: $energyBurn');
final bandWidthBurn = neededBandWidth * bandWidthInSun;
log('Bandwidth Burn: $bandWidthBurn');
int totalBurn = energyBurn + bandWidthBurn;
log('Total Burn: $totalBurn');
/// If there is a note (memo), calculate the memo fee.
if (rawTransaction.data != null) {
totalBurn += chainParams.getMemoFee!;
}
log('Final total burn: $totalBurn');
return totalBurn;
} catch (_) {
return 0;
}
}
Future<int> getEstimatedFee(TronAddress ownerAddress) async {
const constantAmount = '1000';
// Fetch the latest Tron block
final block = await _provider!.request(TronRequestGetNowBlock());
// Create the transfer contract
final contract = TransferContract(
amount: TronHelper.toSun(constantAmount),
ownerAddress: ownerAddress,
toAddress: ownerAddress,
);
// Prepare the contract parameter for the transaction.
final parameter = Any(typeUrl: contract.typeURL, value: contract);
// Create a TransactionContract object with the contract type and parameter.
final transactionContract =
TransactionContract(type: contract.contractType, parameter: parameter);
// Set the transaction expiration time (maximum 24 hours)
final expireTime = DateTime.now().add(const Duration(minutes: 30));
// Create a raw transaction
TransactionRaw rawTransaction = TransactionRaw(
refBlockBytes: block.blockHeader.rawData.refBlockBytes,
refBlockHash: block.blockHeader.rawData.refBlockHash,
expiration: BigInt.from(expireTime.millisecondsSinceEpoch),
contract: [transactionContract],
timestamp: block.blockHeader.rawData.timestamp,
);
final estimatedFee = await getFeeLimit(
rawTransaction,
ownerAddress,
ownerAddress,
isEstimatedFeeFlow: true,
);
_nativeTxEstimatedFee = estimatedFee;
return estimatedFee;
}
Future<int> getTRCEstimatedFee(TronAddress ownerAddress) async {
String contractAddress = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
String constantAmount =
'0'; // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets.
final contract = ContractABI.fromJson(trc20Abi, isTron: true);
final function = contract.functionFromName("transfer");
/// address /// amount
final transferparams = [
ownerAddress,
TronHelper.toSun(constantAmount),
];
final contractAddr = TronAddress(contractAddress);
final request = await _provider!.request(
TronRequestTriggerConstantContract(
ownerAddress: ownerAddress,
contractAddress: contractAddr,
data: function.encodeHex(transferparams),
),
);
if (!request.isSuccess) {
log("Tron TRC20 error: ${request.error} \n ${request.respose}");
}
final feeLimit = await getFeeLimit(
request.transactionRaw!,
ownerAddress,
ownerAddress,
energyUsed: request.energyUsed ?? 0,
isEstimatedFeeFlow: true,
);
return feeLimit;
}
Future<PendingTronTransaction> signTransaction({
required TronPrivateKey ownerPrivKey,
required String toAddress,
required String amount,
required CryptoCurrency currency,
required BigInt tronBalance,
required bool sendAll,
}) async {
// Get the owner tron address from the key
final ownerAddress = ownerPrivKey.publicKey().toAddress();
// Define the receiving Tron address for the transaction.
final receiverAddress = TronAddress(toAddress);
bool isNativeTransaction = currency == CryptoCurrency.trx;
String totalAmount;
TransactionRaw rawTransaction;
if (isNativeTransaction) {
if (sendAll) {
final accountResource =
await _provider!.request(TronRequestGetAccountResource(address: ownerAddress));
final availableBandWidth = accountResource.howManyBandwIth.toInt();
// 269 is the current middle ground for bandwidth per transaction
if (availableBandWidth >= 269) {
totalAmount = amount;
} else {
final amountInSun = TronHelper.toSun(amount).toInt();
// 5000 added here is a buffer since we're working with "estimated" value of the fee.
final result = amountInSun - (_nativeTxEstimatedFee + 5000);
totalAmount = TronHelper.fromSun(BigInt.from(result));
}
} else {
totalAmount = amount;
}
rawTransaction = await _signNativeTransaction(
ownerAddress,
receiverAddress,
totalAmount,
tronBalance,
sendAll,
);
} else {
final tokenAddress = (currency as TronToken).contractAddress;
totalAmount = amount;
rawTransaction = await _signTrcTokenTransaction(
ownerAddress,
receiverAddress,
totalAmount,
tokenAddress,
tronBalance,
);
}
final signature = ownerPrivKey.sign(rawTransaction.toBuffer());
sendTx() async => await sendTransaction(
rawTransaction: rawTransaction,
signature: signature,
);
return PendingTronTransaction(
signedTransaction: signature,
amount: totalAmount,
fee: TronHelper.fromSun(rawTransaction.feeLimit ?? BigInt.zero),
sendTransaction: sendTx,
);
}
Future<TransactionRaw> _signNativeTransaction(
TronAddress ownerAddress,
TronAddress receiverAddress,
String amount,
BigInt tronBalance,
bool sendAll,
) async {
// This is introduce to server as a limit in cases where feeLimit is 0
// The transaction signing will fail if the feeLimit is explicitly 0.
int defaultFeeLimit = 269000;
final block = await _provider!.request(TronRequestGetNowBlock());
// Create the transfer contract
final contract = TransferContract(
amount: TronHelper.toSun(amount),
ownerAddress: ownerAddress,
toAddress: receiverAddress,
);
// Prepare the contract parameter for the transaction.
final parameter = Any(typeUrl: contract.typeURL, value: contract);
// Create a TransactionContract object with the contract type and parameter.
final transactionContract =
TransactionContract(type: contract.contractType, parameter: parameter);
// Set the transaction expiration time (maximum 24 hours)
final expireTime = DateTime.now().add(const Duration(minutes: 30));
// Create a raw transaction
TransactionRaw rawTransaction = TransactionRaw(
refBlockBytes: block.blockHeader.rawData.refBlockBytes,
refBlockHash: block.blockHeader.rawData.refBlockHash,
expiration: BigInt.from(expireTime.millisecondsSinceEpoch),
contract: [transactionContract],
timestamp: block.blockHeader.rawData.timestamp,
);
final feeLimit = await getFeeLimit(rawTransaction, ownerAddress, receiverAddress);
final feeLimitToUse = feeLimit != 0 ? feeLimit : defaultFeeLimit;
final tronBalanceInt = tronBalance.toInt();
if (feeLimit > tronBalanceInt) {
final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
throw Exception(
'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up.\nTransaction fee: $feeInTrx TRX',
);
}
rawTransaction = rawTransaction.copyWith(
feeLimit: BigInt.from(feeLimitToUse),
);
return rawTransaction;
}
Future<TransactionRaw> _signTrcTokenTransaction(
TronAddress ownerAddress,
TronAddress receiverAddress,
String amount,
String contractAddress,
BigInt tronBalance,
) async {
final contract = ContractABI.fromJson(trc20Abi, isTron: true);
final function = contract.functionFromName("transfer");
/// address /// amount
final transferparams = [
receiverAddress,
TronHelper.toSun(amount),
];
final contractAddr = TronAddress(contractAddress);
final request = await _provider!.request(
TronRequestTriggerConstantContract(
ownerAddress: ownerAddress,
contractAddress: contractAddr,
data: function.encodeHex(transferparams),
),
);
if (!request.isSuccess) {
log("Tron TRC20 error: ${request.error} \n ${request.respose}");
throw Exception(
'An error occurred while creating the transfer request. Please try again.',
);
}
final feeLimit = await getFeeLimit(
request.transactionRaw!,
ownerAddress,
receiverAddress,
energyUsed: request.energyUsed ?? 0,
);
final tronBalanceInt = tronBalance.toInt();
if (feeLimit > tronBalanceInt) {
final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
throw Exception(
'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up. Transaction fee: $feeInTrx TRX',
);
}
final rawTransaction = request.transactionRaw!.copyWith(
feeLimit: BigInt.from(feeLimit),
);
return rawTransaction;
}
Future<String> sendTransaction({
required TransactionRaw rawTransaction,
required List<int> signature,
}) async {
try {
final transaction = Transaction(rawData: rawTransaction, signature: [signature]);
final raw = BytesUtils.toHexString(transaction.toBuffer());
final txBroadcastResult = await _provider!.request(TronRequestBroadcastHex(transaction: raw));
if (txBroadcastResult.isSuccess) {
return txBroadcastResult.txId!;
} else {
throw Exception(txBroadcastResult.error);
}
} catch (e) {
log('Send block Exception: ${e.toString()}');
throw Exception(e);
}
}
Future<TronBalance> fetchTronTokenBalances(String userAddress, String contractAddress, {bool throwOnError = false}) async {
try {
final ownerAddress = TronAddress(userAddress);
final tokenAddress = TronAddress(contractAddress);
final contract = ContractABI.fromJson(trc20Abi, isTron: true);
final function = contract.functionFromName("balanceOf");
final request = await _provider!.request(
TronRequestTriggerConstantContract.fromMethod(
ownerAddress: ownerAddress,
contractAddress: tokenAddress,
function: function,
params: [ownerAddress],
),
);
final outputResult = request.outputResult?.first ?? BigInt.zero;
return TronBalance(outputResult);
} catch (_) {
if (throwOnError) {
rethrow;
}
return TronBalance(BigInt.zero);
}
}
Future<TronToken?> getTronToken(String contractAddress, String userAddress) async {
try {
final tokenAddress = TronAddress(contractAddress);
final ownerAddress = TronAddress(userAddress);
final contract = ContractABI.fromJson(trc20Abi, isTron: true);
final name =
(await getTokenDetail(contract, "name", ownerAddress, tokenAddress) as String?) ?? '';
final symbol =
(await getTokenDetail(contract, "symbol", ownerAddress, tokenAddress) as String?) ?? '';
final decimal =
(await getTokenDetail(contract, "decimals", ownerAddress, tokenAddress) as BigInt?) ??
BigInt.zero;
return TronToken(
name: name,
symbol: symbol,
contractAddress: contractAddress,
decimal: decimal.toInt(),
);
} catch (e) {
return null;
}
}
Future<dynamic> getTokenDetail(
ContractABI contract,
String functionName,
TronAddress ownerAddress,
TronAddress tokenAddress,
) async {
final function = contract.functionFromName(functionName);
try {
final request = await _provider!.request(
TronRequestTriggerConstantContract.fromMethod(
ownerAddress: ownerAddress,
contractAddress: tokenAddress,
function: function,
params: [],
),
);
final outputResult = request.outputResult?.first;
return outputResult;
} catch (_) {
log('Erorr fetching detail: ${_.toString()}');
return null;
}
}
}

View File

@@ -1,16 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
class TronMnemonicIsIncorrectException implements Exception {
@override
String toString() =>
'Tron mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
}
class TronTransactionCreationException implements Exception {
final String exceptionMessage;
TronTransactionCreationException(CryptoCurrency currency)
: exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
@override
String toString() => exceptionMessage;
}

View File

@@ -1,46 +0,0 @@
import 'dart:convert';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:on_chain/tron/tron.dart';
import '.secrets.g.dart' as secrets;
class TronHTTPProvider implements TronServiceProvider {
TronHTTPProvider(
{required this.url,
this.defaultRequestTimeout = const Duration(seconds: 30)});
@override
final String url;
late final client = ProxyWrapper().getHttpIOClient();
final Duration defaultRequestTimeout;
@override
Future<Map<String, dynamic>> get(TronRequestDetails params,
[Duration? timeout]) async {
final response = await client.get(Uri.parse(params.url(url)), headers: {
'Content-Type': 'application/json',
if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey,
if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey,
}).timeout(timeout ?? defaultRequestTimeout);
final data = json.decode(response.body) as Map<String, dynamic>;
return data;
}
@override
Future<Map<String, dynamic>> post(TronRequestDetails params,
[Duration? timeout]) async {
final response = await client
.post(Uri.parse(params.url(url)),
headers: {
'Content-Type': 'application/json',
if (url.contains("trongrid"))
'TRON-PRO-API-KEY': secrets.tronGridApiKey,
if (url.contains("nownodes"))
'api-key': secrets.tronNowNodesApiKey,
},
body: params.toRequestBody())
.timeout(timeout ?? defaultRequestTimeout);
final data = json.decode(response.body) as Map<String, dynamic>;
return data;
}
}

View File

@@ -1,12 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart';
class TronTransactionCredentials {
TronTransactionCredentials(
this.outputs, {
required this.currency,
});
final List<OutputInfo> outputs;
final CryptoCurrency currency;
}

View File

@@ -1,85 +0,0 @@
import 'dart:convert';
import 'dart:core';
import 'dart:developer';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_tron/tron_transaction_info.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
part 'tron_transaction_history.g.dart';
class TronTransactionHistory = TronTransactionHistoryBase with _$TronTransactionHistory;
abstract class TronTransactionHistoryBase extends TransactionHistoryBase<TronTransactionInfo>
with Store {
TronTransactionHistoryBase(
{required this.walletInfo, required String password, required this.encryptionFileUtils})
: _password = password {
transactions = ObservableMap<String, TronTransactionInfo>();
}
String _password;
final WalletInfo walletInfo;
final EncryptionFileUtils encryptionFileUtils;
Future<void> init() async {
clear();
await _load();
}
@override
Future<void> save() async {
String transactionsHistoryFileNameForWallet = 'tron_transactions.json';
try {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
String path = '$dirPath/$transactionsHistoryFileNameForWallet';
final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
final data = json.encode({'transactions': transactionMaps});
await encryptionFileUtils.write(path: path, password: _password, data: data);
} catch (e, s) {
log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}');
log(s.toString());
}
}
@override
void addOne(TronTransactionInfo transaction) => transactions[transaction.id] = transaction;
@override
void addMany(Map<String, TronTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
Future<Map<String, dynamic>> _read() async {
String transactionsHistoryFileNameForWallet = 'tron_transactions.json';
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
String path = '$dirPath/$transactionsHistoryFileNameForWallet';
final content = await encryptionFileUtils.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>? ?? {};
for (var entry in txs.entries) {
final val = entry.value;
if (val is Map<String, dynamic>) {
final tx = TronTransactionInfo.fromJson(val);
_update(tx);
}
}
} catch (e) {
log(e.toString());
}
}
void _update(TronTransactionInfo transaction) => transactions[transaction.id] = transaction;
}

View File

@@ -1,93 +0,0 @@
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:on_chain/on_chain.dart' as onchain;
import 'package:on_chain/tron/tron.dart';
class TronTransactionInfo extends TransactionInfo {
TronTransactionInfo({
required this.id,
required this.tronAmount,
required this.txFee,
required this.direction,
required this.blockTime,
required this.to,
required this.from,
required this.isPending,
this.tokenSymbol = 'TRX',
}) : amount = tronAmount.toInt();
final String id;
final String? to;
final String? from;
final int amount;
final BigInt tronAmount;
final String tokenSymbol;
final DateTime blockTime;
final bool isPending;
final int? txFee;
final TransactionDirection direction;
factory TronTransactionInfo.fromJson(Map<String, dynamic> data) {
return TronTransactionInfo(
id: data['id'] as String,
tronAmount: BigInt.parse(data['tronAmount']),
txFee: data['txFee'],
direction: parseTransactionDirectionFromInt(data['direction'] as int),
blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
tokenSymbol: data['tokenSymbol'] as String,
to: data['to'],
from: data['from'],
isPending: data['isPending'],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'tronAmount': tronAmount.toString(),
'txFee': txFee,
'direction': direction.index,
'blockTime': blockTime.millisecondsSinceEpoch,
'tokenSymbol': tokenSymbol,
'to': to,
'from': from,
'isPending': isPending,
};
@override
DateTime get date => blockTime;
String? _fiatAmount;
@override
String amountFormatted() {
String formattedAmount = _rawAmountAsString(tronAmount);
return '$formattedAmount $tokenSymbol';
}
@override
String fiatAmount() => _fiatAmount ?? '';
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() {
final formattedFee = onchain.TronHelper.fromSun(BigInt.from(txFee ?? 0));
return '$formattedFee TRX';
}
String _rawAmountAsString(BigInt amount) {
String formattedAmount = TronHelper.fromSun(amount);
if (formattedAmount.length >= 8) {
formattedAmount = formattedAmount.substring(0, 8);
}
return formattedAmount;
}
String rawTronAmount() => _rawAmountAsString(tronAmount);
}

View File

@@ -1,205 +0,0 @@
import 'package:blockchain_utils/hex/hex.dart';
import 'package:on_chain/on_chain.dart';
class TronTRC20TransactionModel extends TronTransactionModel {
String? transactionId;
String? tokenSymbol;
int? timestamp;
@override
String? from;
@override
String? to;
String? value;
@override
String get hash => transactionId!;
@override
DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp ?? 0);
@override
BigInt? get amount => BigInt.parse(value ?? '0');
@override
int? get fee => 0;
TronTRC20TransactionModel({
this.transactionId,
this.tokenSymbol,
this.timestamp,
this.from,
this.to,
this.value,
});
TronTRC20TransactionModel.fromJson(Map<String, dynamic> json) {
transactionId = json['transaction_id'];
tokenSymbol = json['token_info'] != null ? json['token_info']['symbol'] : null;
timestamp = json['block_timestamp'];
from = json['from'];
to = json['to'];
value = json['value'];
}
}
class TronTransactionModel {
List<Ret>? ret;
String? txID;
int? blockTimestamp;
List<Contract>? contracts;
/// Getters to extract out the needed/useful information directly from the model params
/// Without having to go through extra steps in the methods that use this model.
bool get isError {
if (ret?.first.contractRet == null) return true;
return ret?.first.contractRet != "SUCCESS";
}
String get hash => txID!;
DateTime get date => DateTime.fromMillisecondsSinceEpoch(blockTimestamp ?? 0);
String? get from => contracts?.first.parameter?.value?.ownerAddress;
String? get to => contracts?.first.parameter?.value?.receiverAddress;
BigInt? get amount => contracts?.first.parameter?.value?.txAmount;
int? get fee => ret?.first.fee;
String? get contractAddress => contracts?.first.parameter?.value?.contractAddress;
TronTransactionModel({
this.ret,
this.txID,
this.blockTimestamp,
this.contracts,
});
TronTransactionModel.fromJson(Map<String, dynamic> json) {
if (json['ret'] != null) {
ret = <Ret>[];
json['ret'].forEach((v) {
ret!.add(Ret.fromJson(v));
});
}
txID = json['txID'];
blockTimestamp = json['block_timestamp'];
contracts = json['raw_data'] != null
? (json['raw_data']['contract'] as List)
.map((e) => Contract.fromJson(e as Map<String, dynamic>))
.toList()
: null;
}
}
class Ret {
String? contractRet;
int? fee;
Ret({this.contractRet, this.fee});
Ret.fromJson(Map<String, dynamic> json) {
contractRet = json['contractRet'];
fee = json['fee'];
}
}
class Contract {
Parameter? parameter;
String? type;
Contract({this.parameter, this.type});
Contract.fromJson(Map<String, dynamic> json) {
parameter = json['parameter'] != null ? Parameter.fromJson(json['parameter']) : null;
type = json['type'];
}
}
class Parameter {
Value? value;
String? typeUrl;
Parameter({this.value, this.typeUrl});
Parameter.fromJson(Map<String, dynamic> json) {
value = json['value'] != null ? Value.fromJson(json['value']) : null;
typeUrl = json['type_url'];
}
}
class Value {
String? data;
String? ownerAddress;
String? contractAddress;
int? amount;
String? toAddress;
String? assetName;
//Getters to extract address for tron transactions
/// If the contract address is null, it returns the toAddress
/// If it's not null, it decodes the data field and gets the receiver address.
String? get receiverAddress {
if (contractAddress == null) return toAddress;
if (data == null) return null;
return _decodeAddressFromEncodedDataField(data!);
}
//Getters to extract amount for tron transactions
/// If the contract address is null, it returns the amount
/// If it's not null, it decodes the data field and gets the tx amount.
BigInt? get txAmount {
if (contractAddress == null) return BigInt.from(amount ?? 0);
if (data == null) return null;
return _decodeAmountInvolvedFromEncodedDataField(data!);
}
Value(
{this.data,
this.ownerAddress,
this.contractAddress,
this.amount,
this.toAddress,
this.assetName});
Value.fromJson(Map<String, dynamic> json) {
data = json['data'];
ownerAddress = json['owner_address'];
contractAddress = json['contract_address'];
amount = json['amount'];
toAddress = json['to_address'];
assetName = json['asset_name'];
}
/// To get the address from the encoded data field
String _decodeAddressFromEncodedDataField(String output) {
// To get the receiver address from the encoded params
output = output.replaceFirst('0x', '').substring(8);
final abiCoder = ABICoder.fromType('address');
final decoded = abiCoder.decode(AbiParameter.bytes, hex.decode(output));
final tronAddress = TronAddress.fromEthAddress((decoded.result as ETHAddress).toBytes());
return tronAddress.toString();
}
/// To get the amount from the encoded data field
BigInt _decodeAmountInvolvedFromEncodedDataField(String output) {
output = output.replaceFirst('0x', '').substring(72);
final amountAbiCoder = ABICoder.fromType('uint256');
final decodedA = amountAbiCoder.decode(AbiParameter.uint256, hex.decode(output));
final amount = decodedA.result as BigInt;
return amount;
}
}

View File

@@ -1,676 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.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_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_tron/default_tron_tokens.dart';
import 'package:cw_tron/tron_abi.dart';
import 'package:cw_tron/tron_balance.dart';
import 'package:cw_tron/tron_client.dart';
import 'package:cw_tron/tron_exception.dart';
import 'package:cw_core/tron_token.dart';
import 'package:cw_tron/tron_transaction_credentials.dart';
import 'package:cw_tron/tron_transaction_history.dart';
import 'package:cw_tron/tron_transaction_info.dart';
import 'package:cw_tron/tron_wallet_addresses.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:on_chain/on_chain.dart';
part 'tron_wallet.g.dart';
class TronWallet = TronWalletBase with _$TronWallet;
abstract class TronWalletBase
extends WalletBase<TronBalance, TronTransactionHistory, TronTransactionInfo>
with Store, WalletKeysFile {
TronWalletBase({
required WalletInfo walletInfo,
required DerivationInfo derivationInfo,
String? mnemonic,
String? privateKey,
required String password,
TronBalance? initialBalance,
required this.encryptionFileUtils,
this.passphrase,
}) : syncStatus = const NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_hexPrivateKey = privateKey,
_client = TronClient(),
walletAddresses = TronWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, TronBalance>.of(
{CryptoCurrency.trx: initialBalance ?? TronBalance(BigInt.zero)},
),
super(walletInfo, derivationInfo) {
this.walletInfo = walletInfo;
transactionHistory = TronTransactionHistory(
walletInfo: walletInfo, password: password, encryptionFileUtils: encryptionFileUtils);
if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
CakeHive.registerAdapter(TronTokenAdapter());
}
}
final String? _mnemonic;
final String? _hexPrivateKey;
final String _password;
final EncryptionFileUtils encryptionFileUtils;
late final Box<TronToken> tronTokensBox;
late final TronPrivateKey _tronPrivateKey;
late final TronPublicKey _tronPublicKey;
TronPublicKey get tronPublicKey => _tronPublicKey;
TronPrivateKey get tronPrivateKey => _tronPrivateKey;
late String _tronAddress;
late final TronClient _client;
Timer? _transactionsUpdateTimer;
@override
WalletAddresses walletAddresses;
@observable
String? nativeTxEstimatedFee;
@observable
String? trc20EstimatedFee;
@override
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, TronBalance> balance;
Future<void> init() async {
await initTronTokensBox();
await walletAddresses.init();
await transactionHistory.init();
_tronPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
password: _password,
passphrase: passphrase,
);
_tronPublicKey = _tronPrivateKey.publicKey();
_tronAddress = _tronPublicKey.toAddress().toString();
walletAddresses.address = _tronAddress;
await save();
}
static Future<TronWallet> open({
required String name,
required String password,
required WalletInfo walletInfo,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
final path = await pathForWallet(name: name, type: walletInfo.type);
Map<String, dynamic>? data;
try {
final jsonSource = await encryptionFileUtils.read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} catch (e) {
if (!hasKeysFile) rethrow;
}
final balance = TronBalance.fromJSON(data?['balance'] as String?) ?? TronBalance(BigInt.zero);
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
final mnemonic = data!['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final passphrase = data['passphrase'] as String?;
keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
walletInfo.type,
password,
encryptionFileUtils,
);
}
final derivationInfo = await walletInfo.getDerivationInfo();
return TronWallet(
walletInfo: walletInfo,
derivationInfo: derivationInfo,
password: password,
mnemonic: keysData.mnemonic,
privateKey: keysData.privateKey,
passphrase: keysData.passphrase,
initialBalance: balance,
encryptionFileUtils: encryptionFileUtils,
);
}
void addInitialTokens() {
final initialTronTokens = DefaultTronTokens().initialTronTokens;
for (var token in initialTronTokens) {
if (!tronTokensBox.containsKey(token.contractAddress)) {
tronTokensBox.put(token.contractAddress, token);
} else {
// update existing token
final existingToken = tronTokensBox.get(token.contractAddress);
tronTokensBox.put(
token.contractAddress, TronToken.copyWith(token, enabled: existingToken!.enabled));
}
}
}
Future<void> initTronTokensBox() async {
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}";
tronTokensBox = await CakeHive.openBox<TronToken>(boxName);
}
String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
Future<TronPrivateKey> getPrivateKey({
String? mnemonic,
String? privateKey,
required String password,
String? passphrase,
}) async {
assert(mnemonic != null || privateKey != null);
if (privateKey != null) return TronPrivateKey(privateKey);
final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? '');
// Derive a TRON private key from the seed
final bip44 = Bip44.fromSeed(seed, Bip44Coins.tron);
final childKey = bip44.deriveDefaultPath;
return TronPrivateKey.fromBytes(childKey.privateKey.raw);
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
@override
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
@override
Future<void> close({bool shouldCleanup = false}) async => _transactionsUpdateTimer?.cancel();
@action
@override
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
final isConnected = _client.connect(node);
if (!isConnected) {
throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed");
}
_getEstimatedFees();
_setTransactionUpdateTimer();
syncStatus = ConnectedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
Future<void> _getEstimatedFees() async {
final nativeFee = await _getNativeTxFee();
nativeTxEstimatedFee = TronHelper.fromSun(BigInt.from(nativeFee));
final trc20Fee = await _getTrc20TxFee();
trc20EstimatedFee = TronHelper.fromSun(BigInt.from(trc20Fee));
log('Native Estimated Fee: $nativeTxEstimatedFee');
log('TRC20 Estimated Fee: $trc20EstimatedFee');
}
Future<int> _getNativeTxFee() async {
try {
final fee = await _client.getEstimatedFee(_tronPublicKey.toAddress());
return fee;
} catch (e) {
log(e.toString());
return 0;
}
}
Future<int> _getTrc20TxFee() async {
try {
final trc20fee = await _client.getTRCEstimatedFee(_tronPublicKey.toAddress());
return trc20fee;
} catch (e) {
log(e.toString());
return 0;
}
}
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
// Verify node health before attempting to sync
final isHealthy = await checkNodeHealth();
if (!isHealthy) {
syncStatus = FailedSyncStatus();
return;
}
await _updateBalance();
await fetchTransactions();
fetchTrc20ExcludedTransactions();
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final tronCredentials = credentials as TronTransactionCredentials;
final outputs = tronCredentials.outputs;
final hasMultiDestination = outputs.length > 1;
final transactionCurrency = balance.keys.firstWhere(
(currency) =>
currency.title == tronCredentials.currency.title &&
currency.tag == tronCredentials.currency.tag,
orElse: () => throw Exception(
'Currency ${tronCredentials.currency.title} ${tronCredentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
BigInt totalAmount = BigInt.zero;
bool shouldSendAll = false;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw TronTransactionCreationException(transactionCurrency);
}
final totalAmountFromCredentials =
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
totalAmount = BigInt.from(totalAmountFromCredentials);
if (walletBalanceForCurrency < totalAmount) {
throw TronTransactionCreationException(transactionCurrency);
}
} else {
final output = outputs.first;
shouldSendAll = output.sendAll;
if (shouldSendAll) {
totalAmount = walletBalanceForCurrency;
} else {
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
totalAmount = TronHelper.toSun(totalOriginalAmount.toString());
}
if (walletBalanceForCurrency < totalAmount || totalAmount < BigInt.zero) {
throw TronTransactionCreationException(transactionCurrency);
}
}
final tronBalance = balance[CryptoCurrency.trx]?.balance ?? BigInt.zero;
final pendingTransaction = await _client.signTransaction(
ownerPrivKey: _tronPrivateKey,
toAddress: tronCredentials.outputs.first.isParsedAddress
? tronCredentials.outputs.first.extractedAddress!
: tronCredentials.outputs.first.address,
amount: TronHelper.fromSun(totalAmount),
currency: transactionCurrency,
tronBalance: tronBalance,
sendAll: shouldSendAll,
);
return pendingTransaction;
}
@override
Future<void> updateTransactionsHistory() async {
await Future.wait([
fetchTransactions(),
fetchTrc20ExcludedTransactions(),
]);
}
@override
Future<Map<String, TronTransactionInfo>> fetchTransactions() async {
final address = _tronAddress;
final transactions = await _client.fetchTransactions(address);
final Map<String, TronTransactionInfo> result = {};
final contract = ContractABI.fromJson(trc20Abi, isTron: true);
final ownerAddress = TronAddress(_tronAddress);
for (var transactionModel in transactions) {
if (transactionModel.isError) {
continue;
}
// Filter out spam transaactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions
if (transactionModel.contracts?.first.type == "TransferAssetContract") {
continue;
}
String? tokenSymbol;
if (transactionModel.contractAddress != null) {
final tokenAddress = TronAddress(transactionModel.contractAddress!);
tokenSymbol = (await _client.getTokenDetail(
contract,
"symbol",
ownerAddress,
tokenAddress,
) as String?) ??
'';
}
result[transactionModel.hash] = TronTransactionInfo(
id: transactionModel.hash,
tronAmount: transactionModel.amount ?? BigInt.zero,
direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
blockTime: transactionModel.date,
txFee: transactionModel.fee,
tokenSymbol: tokenSymbol ?? "TRX",
to: transactionModel.to,
from: transactionModel.from,
isPending: false,
);
}
transactionHistory.addMany(result);
await transactionHistory.save();
return transactionHistory.transactions;
}
Future<void> fetchTrc20ExcludedTransactions() async {
final address = _tronAddress;
final transactions = await _client.fetchTrc20ExcludedTransactions(address);
final Map<String, TronTransactionInfo> result = {};
for (var transactionModel in transactions) {
if (transactionHistory.transactions.containsKey(transactionModel.hash)) {
continue;
}
result[transactionModel.hash] = TronTransactionInfo(
id: transactionModel.hash,
tronAmount: transactionModel.amount ?? BigInt.zero,
direction: transactionModel.from! == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
blockTime: transactionModel.date,
txFee: transactionModel.fee,
tokenSymbol: transactionModel.tokenSymbol ?? "TRX",
to: transactionModel.to,
from: transactionModel.from,
isPending: false,
);
}
transactionHistory.addMany(result);
await transactionHistory.save();
}
@override
Object get keys => throw UnimplementedError("keys");
@override
Future<void> rescan({required int height}) => throw UnimplementedError("rescan");
@override
Future<void> save() async {
if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
await saveKeysFile(_password, encryptionFileUtils);
saveKeysFile(_password, encryptionFileUtils, true);
}
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@override
String? get seed => _mnemonic;
@override
String get privateKey => _tronPrivateKey.toHex();
@override
WalletKeysData get walletKeysData => WalletKeysData(
mnemonic: _mnemonic,
privateKey: privateKey,
passphrase: passphrase,
);
String toJSON() => json.encode({
'mnemonic': _mnemonic,
'private_key': privateKey,
'balance': balance[currency]!.toJSON(),
'passphrase': passphrase,
});
Future<void> _updateBalance() async {
balance[currency] = await _fetchTronBalance();
await _fetchTronTokenBalances();
await save();
}
Future<TronBalance> _fetchTronBalance() async {
final balance = await _client.getBalance(_tronPublicKey.toAddress());
return TronBalance(balance);
}
Future<void> _fetchTronTokenBalances() async {
for (var token in tronTokensBox.values) {
try {
if (token.enabled) {
balance[token] = await _client.fetchTronTokenBalances(
_tronAddress,
token.contractAddress,
);
} else {
balance.remove(token);
}
} catch (_) {}
}
}
@override
Future<void>? updateBalance() async => await _updateBalance();
@override
Future<bool> checkNodeHealth() async {
try {
// Check native balance
await _client.getBalance(_tronPublicKey.toAddress(), throwOnError: true);
// Check USDT token balance
const usdtContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress, throwOnError: true);
return true;
} catch (e) {
return false;
}
}
List<TronToken> get tronTokenCurrencies => tronTokensBox.values.toList();
Future<void> addTronToken(TronToken token) async {
String? iconPath;
if ((token.iconPath == null || token.iconPath!.isEmpty) && !token.isPotentialScam) {
try {
iconPath = CryptoCurrency.all
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
.iconPath;
} catch (_) {}
} else if (!token.isPotentialScam) {
iconPath = token.iconPath;
}
final newToken = TronToken(
name: token.name,
symbol: token.symbol,
contractAddress: token.contractAddress,
decimal: token.decimal,
enabled: token.enabled,
tag: token.tag ?? "TRX",
iconPath: iconPath,
isPotentialScam: token.isPotentialScam,
);
await tronTokensBox.put(newToken.contractAddress, newToken);
if (newToken.enabled) {
balance[newToken] = await _client.fetchTronTokenBalances(
_tronAddress,
newToken.contractAddress,
);
} else {
balance.remove(newToken);
}
}
Future<void> deleteTronToken(TronToken token) async {
if (tronTokensBox.isOpen) {
await tronTokensBox.delete(token.contractAddress);
}
balance.remove(token);
await _removeTokenTransactionsInHistory(token);
_updateBalance();
}
Future<void> _removeTokenTransactionsInHistory(TronToken token) async {
transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title);
await transactionHistory.save();
}
Future<TronToken?> getTronToken(String contractAddress) async =>
await _client.getTronToken(contractAddress, _tronAddress);
@override
Future<void> renameWalletFiles(String newWalletName) async {
const transactionHistoryFileNameForWallet = 'tron_transactions.json';
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/$transactionHistoryFileNameForWallet');
// 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/$transactionHistoryFileNameForWallet');
}
// 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: 30), (_) async {
_updateBalance();
await fetchTransactions();
fetchTrc20ExcludedTransactions();
});
}
@override
Future<String> signMessage(String message, {String? address}) async {
return _tronPrivateKey.signPersonalMessage(ascii.encode(message));
}
@override
Future<bool> verifyMessage(String message, String signature, {String? address}) async {
if (address == null) {
return false;
}
TronPublicKey pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!;
return pubKey.toAddress().toString() == address;
}
String getTronBase58AddressFromHex(String hexAddress) => TronAddress(hexAddress).toAddress();
void updateScanProviderUsageState(bool isEnabled) {
if (isEnabled) {
fetchTransactions();
fetchTrc20ExcludedTransactions();
_setTransactionUpdateTimer();
} else {
_transactionsUpdateTimer?.cancel();
}
}
@override
String get password => _password;
@override
final String? passphrase;
}

View File

@@ -1,43 +0,0 @@
import 'dart:developer';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
part 'tron_wallet_addresses.g.dart';
class TronWalletAddresses = TronWalletAddressesBase with _$TronWalletAddresses;
abstract class TronWalletAddressesBase extends WalletAddresses with Store {
TronWalletAddressesBase(WalletInfo walletInfo)
: address = '',
super(walletInfo);
@override
@observable
String address;
@override
String get primaryAddress => address;
@override
Future<void> init() async {
address = walletInfo.address;
await updateAddressesInBox();
}
@override
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = '';
await saveAddressesInBox();
} catch (e) {
log(e.toString());
}
}
@override
PaymentURI getPaymentUri(String amount) => TronURI(amount: amount, address: address);
}

View File

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

View File

@@ -1,180 +0,0 @@
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/balance.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_tron/tron_client.dart';
import 'package:cw_tron/tron_exception.dart';
import 'package:cw_tron/tron_wallet.dart';
import 'package:cw_tron/tron_wallet_creation_credentials.dart';
import 'package:hive/hive.dart';
class TronWalletService extends WalletService<
TronNewWalletCredentials,
TronRestoreWalletFromSeedCredentials,
TronRestoreWalletFromPrivateKey,
TronNewWalletCredentials> {
TronWalletService({required this.client, required this.isDirect});
late TronClient client;
final bool isDirect;
@override
WalletType getType() => WalletType.tron;
@override
Future<TronWallet> create(TronNewWalletCredentials credentials, {bool? isTestnet}) async {
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength);
final wallet = TronWallet(
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
mnemonic: mnemonic,
password: credentials.password!,
passphrase: credentials.passphrase,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<TronWallet> openWallet(String name, String password) async {
final walletInfo = await WalletInfo.get(name, getType());
if (walletInfo == null) {
throw Exception('Wallet not found');
}
try {
final wallet = await TronWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
saveBackup(name);
return wallet;
} catch (_) {
await restoreWalletFilesFromBackup(name);
final wallet = await TronWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
}
@override
Future<TronWallet> restoreFromKeys(
TronRestoreWalletFromPrivateKey credentials, {
bool? isTestnet,
}) async {
final wallet = TronWallet(
password: credentials.password!,
privateKey: credentials.privateKey,
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<TronWallet> restoreFromSeed(
TronRestoreWalletFromSeedCredentials credentials, {
bool? isTestnet,
}) async {
if (!bip39.validateMnemonic(credentials.mnemonic)) {
throw TronMnemonicIsIncorrectException();
}
final wallet = TronWallet(
password: credentials.password!,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
passphrase: credentials.passphrase,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = await WalletInfo.get(currentName, getType());
if (currentWalletInfo == null) {
throw Exception('Wallet not found');
}
final currentWallet = await TronWalletBase.open(
password: password,
name: currentName,
walletInfo: currentWalletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
await newWalletInfo.save();
}
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = await WalletInfo.get(wallet, getType());
if (walletInfo == null) {
throw Exception('Wallet not found');
}
await WalletInfo.delete(walletInfo);
}
@override
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>>
restoreFromHardwareWallet(TronNewWalletCredentials credentials) {
// TODO: implement restoreFromHardwareWallet
throw UnimplementedError();
}
}

View File

@@ -1,39 +0,0 @@
name: cw_tron
description: A new Flutter package project.
version: 0.0.1
publish_to: none
homepage: https://github.com/Such-Software/hash-wallet
environment:
sdk: '>=3.0.6 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
cw_core:
path: ../cw_core
cw_evm:
path: ../cw_evm
on_chain:
git:
url: https://github.com/cake-tech/on_chain.git
ref: 096865a8c6b89c260beadfec04f7e184c40a3273
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v2
mobx: ^2.3.0+1
bip39: ^1.0.6
hive: ^2.2.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: ^2.4.15
mobx_codegen: ^2.1.1
hive_generator: ^2.0.1
flutter:
# assets:
# - images/a_dot_burr.jpeg

View File

@@ -1,12 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:cw_tron/cw_tron.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);
});
}

7
cw_zano/.gitignore vendored
View File

@@ -1,7 +0,0 @@
.DS_Store
.dart_tool/
.packages
.pub/
build/

View File

@@ -1,10 +0,0 @@
# 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: 4d7946a68d26794349189cf21b3f68cc6fe61dcb
channel: stable
project_type: plugin

View File

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

View File

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

View File

@@ -1,23 +0,0 @@
# cw_zano
Zano wallet module for Cake Wallet. Provides a Dart wrapper around the Zano wallet API with typed models and transaction helpers.
## Features
- Wallet lifecycle and status queries via `ZanoWalletApi`.
- Typed models for balances, transfers, recent history, and wallet info.
- Build and submit transfers; pending transaction modeling.
- Address and asset utilities, formatter helpers.
## Usage
See `lib/zano_wallet_api.dart` and `lib/zano_wallet.dart` for the high-level API. Typical flow:
```dart
final api = ZanoWalletApi();
final info = await api.getWalletInfo();
final balance = await api.getBalance();
// create transfer params and broadcast
```
Consult `lib/api/model/` for full set of supported request/response models.

View File

@@ -1,6 +0,0 @@
class Consts {
static const errorWrongSeed = 'WRONG_SEED';
static const errorAlreadyExists = 'ALREADY_EXISTS';
static const errorWalletWrongId = 'WALLET_WRONG_ID';
static const errorBusy = 'BUSY';
}

View File

@@ -1,9 +0,0 @@
class AssetIdParams {
final String assetId;
AssetIdParams({required this.assetId});
Map<String, dynamic> toJson() => {
'asset_id': assetId,
};
}

View File

@@ -1,32 +0,0 @@
import 'package:cw_core/zano_asset.dart';
import 'package:cw_zano/model/zano_asset.dart';
import 'package:cw_zano/zano_formatter.dart';
class Balance {
final ZanoAsset assetInfo;
final BigInt awaitingIn;
final BigInt awaitingOut;
final BigInt total;
final BigInt unlocked;
Balance(
{required this.assetInfo,
required this.awaitingIn,
required this.awaitingOut,
required this.total,
required this.unlocked});
String get assetId => assetInfo.assetId;
@override
String toString() => '$assetInfo: $total/$unlocked';
factory Balance.fromJson(Map<String, dynamic> json) => Balance(
assetInfo:
ZanoAsset.fromJson(json['asset_info'] as Map<String, dynamic>? ?? {}),
awaitingIn: ZanoFormatter.bigIntFromDynamic(json['awaiting_in']),
awaitingOut: ZanoFormatter.bigIntFromDynamic(json['awaiting_out']),
total: ZanoFormatter.bigIntFromDynamic(json['total']),
unlocked: ZanoFormatter.bigIntFromDynamic(json['unlocked']),
);
}

View File

@@ -1,52 +0,0 @@
import 'package:cw_zano/api/model/recent_history.dart';
import 'package:cw_zano/api/model/wi.dart';
import 'package:cw_zano/zano_wallet.dart';
class CreateWalletResult {
final String name;
final String pass;
final RecentHistory recentHistory;
final bool recovered;
final int walletFileSize;
final int walletId;
final int walletLocalBcSize;
final Wi wi;
final String privateSpendKey;
final String privateViewKey;
final String publicSpendKey;
final String publicViewKey;
CreateWalletResult(
{required this.name,
required this.pass,
required this.recentHistory,
required this.recovered,
required this.walletFileSize,
required this.walletId,
required this.walletLocalBcSize,
required this.wi,
required this.privateSpendKey,
required this.privateViewKey,
required this.publicSpendKey,
required this.publicViewKey});
factory CreateWalletResult.fromJson(Map<String, dynamic> json) =>
CreateWalletResult(
name: json['name'] as String? ?? '',
pass: json['pass'] as String? ?? '',
recentHistory: RecentHistory.fromJson(
json['recent_history'] as Map<String, dynamic>? ?? {}),
recovered: json['recovered'] as bool? ?? false,
walletFileSize: json['wallet_file_size'] as int? ?? 0,
walletId: json['wallet_id'] as int? ?? 0,
walletLocalBcSize: json['wallet_local_bc_size'] as int? ?? 0,
wi: Wi.fromJson(json['wi'] as Map<String, dynamic>? ?? {}),
privateSpendKey: json['private_spend_key'] as String? ?? '',
privateViewKey: json['private_view_key'] as String? ?? '',
publicSpendKey: json['public_spend_key'] as String? ?? '',
publicViewKey: json['public_view_key'] as String? ?? '',
);
Future<String> seed(ZanoWalletBase api) {
return api.getSeed();
}
}

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