Merge branch 'master' into ldk

This commit is contained in:
Marcos Rodriguez Velez 2024-07-28 16:32:50 -04:00
commit 9bdd0787a7
No known key found for this signature in database
GPG Key ID: 6030B2F48CCE86D7
611 changed files with 37066 additions and 56006 deletions

View File

@ -3,7 +3,7 @@ jobs:
lint:
docker:
- image: cimg/node:16.20.2
- image: cimg/node:20.16.0
working_directory: ~/repo
@ -11,22 +11,22 @@ jobs:
- checkout
- restore_cache:
key: node_modules-{{ checksum "package-lock.json" }}
key: node_modules-{{ checksum "yarn.lock" }}
- run: test -d node_modules || npm i
- run: test -d node_modules || yarn
- save_cache:
key: node_modules-{{ checksum "package-lock.json" }}
key: node_modules-{{ checksum "yarn.lock" }}
paths:
- node_modules
# run tests!
- run:
command: npm run tslint && npm run lint
command: yarn tslint && yarn lint
unit:
docker:
- image: cimg/node:16.20.2
- image: cimg/node:20.16.0
working_directory: ~/repo
@ -34,23 +34,26 @@ jobs:
- checkout
- restore_cache:
key: node_modules-{{ checksum "package-lock.json" }}
key: node_modules-{{ checksum "yarn.lock" }}
- run: test -d node_modules || npm i
- run: test -d node_modules || yarn
- save_cache:
key: node_modules-{{ checksum "package-lock.json" }}
key: node_modules-{{ checksum "yarn.lock" }}
paths:
- node_modules
# run tests!
- run:
command: npm run unit
command: yarn unit
integration:
docker:
- image: cimg/node:16.20.2
- image: cimg/node:20.16.0
environment:
RETRY: "1"
working_directory: ~/repo
@ -60,18 +63,18 @@ jobs:
- checkout
- restore_cache:
key: node_modules-{{ checksum "package-lock.json" }}
key: node_modules-{{ checksum "yarn.lock" }}
- run: test -d node_modules || npm i
- run: test -d node_modules || yarn
- save_cache:
key: node_modules-{{ checksum "package-lock.json" }}
key: node_modules-{{ checksum "yarn.lock" }}
paths:
- node_modules
# run tests!
- run:
command: npm run jest || npm run jest || npm run jest
command: yarn jest || yarn jest || yarn jest || yarn jest
# Orchestrate our job run sequence
workflows:

View File

@ -2,7 +2,7 @@
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"react-native" // for no-inline-styles rule
"react-native", // for no-inline-styles rule
],
"extends": [
"standard",
@ -49,7 +49,7 @@
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-use-before-define": "off"
"@typescript-eslint/no-use-before-define": "off",
},
"overrides": [
{

View File

@ -1,183 +0,0 @@
name: Build Release PR and upload to BrowserStack (iOS)
on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled]
branches:
- master
workflow_dispatch:
jobs:
build:
if: contains(github.event.pull_request.labels.*.name, 'browserstack')
runs-on: macos-14
timeout-minutes: 180
outputs:
new_build_number: ${{ steps.generate_build_number.outputs.build_number }}
project_version: ${{ steps.determine_marketing_version.outputs.project_version }}
latest_commit_message: ${{ steps.get_latest_commit_message.outputs.commit_message }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }} # Setting the environment variable
steps:
- name: Checkout project
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetches all history
- name: Specify node version
uses: actions/setup-node@v2-beta
with:
node-version: 18
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 15.2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install dependencies with Bundler
run: bundle install
- name: Install Fastlane 2.217.0
run: gem install fastlane -v 2.217.0
- name: Install CocoaPods
run: sudo gem install cocoapods
- name: Clear Derived Data
run: bundle exec fastlane ios clear_derived_data_lane
working-directory: ./ios
- name: Install node_modules
run: npm install
- name: Display release-notes.txt
run: cat release-notes.txt
- name: Get Latest Commit Message
id: get_latest_commit_message
run: |
LATEST_COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s")
echo "LATEST_COMMIT_MESSAGE=${LATEST_COMMIT_MESSAGE}" >> $GITHUB_ENV
echo "::set-output name=commit_message::$LATEST_COMMIT_MESSAGE"
- name: Set up Git Authentication
env:
ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
run: |
git config --global credential.helper 'cache --timeout=3600'
git config --global http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n x-access-token:${ACCESS_TOKEN} | base64)"
- name: Create Temporary Keychain
run: bundle exec fastlane ios create_temp_keychain
working-directory: ./ios
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Setup Provisioning Profiles
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_URL: ${{ secrets.GIT_URL }}
ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }}
ITC_TEAM_NAME: ${{ secrets.ITC_TEAM_NAME }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: bundle exec fastlane ios setup_provisioning_profiles
working-directory: ./ios
- name: Generate Build Number based on timestamp
id: generate_build_number
run: |
NEW_BUILD_NUMBER=$(date +%s)
echo "NEW_BUILD_NUMBER=$NEW_BUILD_NUMBER" >> $GITHUB_ENV
echo "::set-output name=build_number::$NEW_BUILD_NUMBER"
- name: Set Build Number
run: bundle exec fastlane ios increment_build_number_lane
working-directory: ./ios
- name: Determine Marketing Version
id: determine_marketing_version
run: |
MARKETING_VERSION=$(grep MARKETING_VERSION ios/BlueWallet.xcodeproj/project.pbxproj | awk -F '= ' '{print $2}' | tr -d ' ;' | head -1)
echo "PROJECT_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "::set-output name=project_version::$MARKETING_VERSION"
- name: Expected IPA file name
run: |
echo "IPA file name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa"
- name: Install CocoaPods Dependencies
run: bundle exec fastlane ios install_pods
working-directory: ./ios
- name: Build App
run: bundle exec fastlane ios build_app_lane
working-directory: ./ios
- name: Upload IPA as Artifact
uses: actions/upload-artifact@v2
with:
name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
path: ./ios/build/BlueWallet.${{env.PROJECT_VERSION}}(${{ env.NEW_BUILD_NUMBER }}).ipa
upload-to-browserstack:
if: contains(github.event.pull_request.labels.*.name, 'browserstack')
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v3
- name: List files in the build directory
run: ls -l ./build
- name: Download IPA from Artifact
uses: actions/download-artifact@v2
with:
name: BlueWallet.${{needs.build.outputs.project_version}}(${{needs.build.outputs.new_build_number}}).ipa
path: ./build
- name: Set permissions for the IPA file
run: chmod 644 ./build/BlueWallet.${{needs.build.outputs.project_version}}(${{needs.build.outputs.new_build_number}}).ipa
- name: Upload IPA to BrowserStack
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
NEW_BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
run: |
curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \
-X POST "https://api-cloud.browserstack.com/app-automate/upload" \
-F "file=@./ios/build/BlueWallet.${{env.PROJECT_VERSION}}(${{ env.NEW_BUILD_NUMBER }}).ipa" \
-o browserstack_response.json
- name: Extract BrowserStack App URL
id: extract_url
run: |
APP_URL=$(jq -r '.app_url' browserstack_response.json)
echo "APP_URL=$APP_URL"
echo "::set-output name=app_url::$APP_URL"
- name: Post comment with BrowserStack link
uses: actions/github-script@v6
if: github.event_name == 'pull_request'
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const prNumber = context.issue.number;
const browserStackLink = '${{ steps.extract_url.outputs.app_url }}'; // Use the output from the previous step.
const message = `🚀 App is now available for testing on BrowserStack: [Test Here](${browserStackLink})`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: message
});

View File

@ -1,14 +1,17 @@
name: Build Release Pull Request (iOS)
name: Build Release and Upload to TestFlight (iOS)
on:
push:
branches:
- master
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled]
types: [opened, reopened, synchronize, labeled]
branches:
- master
workflow_dispatch:
jobs:
build:
if: contains(github.event.pull_request.labels.*.name, 'testflight')
runs-on: macos-14
timeout-minutes: 180
outputs:
@ -16,119 +19,98 @@ jobs:
project_version: ${{ steps.determine_marketing_version.outputs.project_version }}
latest_commit_message: ${{ steps.get_latest_commit_message.outputs.commit_message }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }} # Setting the environment variable
APPLE_ID: ${{ secrets.APPLE_ID }}
steps:
- name: Checkout project
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetches all history
- name: Specify node version
uses: actions/setup-node@v2-beta
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 15.2
xcode-version: 15.4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.6
bundler-cache: true
- name: Install dependencies with Bundler
run: bundle install
- name: Install Fastlane 2.217.0
run: gem install fastlane -v 2.217.0
- name: Install CocoaPods
run: sudo gem install cocoapods
- name: Clear Derived Data
run: bundle exec fastlane ios clear_derived_data_lane
working-directory: ./ios
- name: Install node_modules
run: npm install
run: yarn install
- name: Install CocoaPods Dependencies
run: |
gem install cocoapods
bundle exec pod install
working-directory: ./ios
- name: Cache CocoaPods Pods
uses: actions/cache@v2
with:
path: ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
- name: Display release-notes.txt
run: cat release-notes.txt
- name: Get Latest Commit Message
id: get_latest_commit_message
run: |
LATEST_COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s")
echo "LATEST_COMMIT_MESSAGE=${LATEST_COMMIT_MESSAGE}" >> $GITHUB_ENV
echo "::set-output name=commit_message::$LATEST_COMMIT_MESSAGE"
- name: Set up Git Authentication
env:
ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
run: |
git config --global credential.helper 'cache --timeout=3600'
git config --global http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n x-access-token:${ACCESS_TOKEN} | base64)"
- name: Create Temporary Keychain
run: bundle exec fastlane ios create_temp_keychain
working-directory: ./ios
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Setup Provisioning Profiles
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_URL: ${{ secrets.GIT_URL }}
ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }}
ITC_TEAM_NAME: ${{ secrets.ITC_TEAM_NAME }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: bundle exec fastlane ios setup_provisioning_profiles
working-directory: ./ios
- name: Generate Build Number based on timestamp
id: generate_build_number
run: |
NEW_BUILD_NUMBER=$(date +%s)
echo "NEW_BUILD_NUMBER=$NEW_BUILD_NUMBER" >> $GITHUB_ENV
echo "::set-output name=build_number::$NEW_BUILD_NUMBER"
- name: Set Build Number
run: bundle exec fastlane ios increment_build_number_lane
working-directory: ./ios
- name: Determine Marketing Version
id: determine_marketing_version
run: |
MARKETING_VERSION=$(grep MARKETING_VERSION ios/BlueWallet.xcodeproj/project.pbxproj | awk -F '= ' '{print $2}' | tr -d ' ;' | head -1)
echo "PROJECT_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "::set-output name=project_version::$MARKETING_VERSION"
- name: Expected IPA file name
run: |
echo "IPA file name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa"
- name: Install CocoaPods Dependencies
run: bundle exec fastlane ios install_pods
working-directory: ./ios
- name: Build App
run: bundle exec fastlane ios build_app_lane
working-directory: ./ios
- name: Upload IPA as Artifact
uses: actions/upload-artifact@v2
with:
name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
path: ./ios/build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
testflight-upload:
if: contains(github.event.pull_request.labels.*.name, 'testflight')
needs: build
runs-on: macos-14
if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'testflight')
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
NEW_BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
@ -136,8 +118,12 @@ jobs:
LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }}
steps:
- name: Checkout project
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.6
bundler-cache: true
- name: Cache Ruby Gems
uses: actions/cache@v2
with:
@ -145,26 +131,22 @@ jobs:
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: Install dependencies with Bundler
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
bundle install --jobs 4 --retry 3 --quiet
- name: Download IPA from Artifact
uses: actions/download-artifact@v2
with:
name: BlueWallet.${{ needs.build.outputs.project_version }}(${{ needs.build.outputs.new_build_number }}).ipa
path: ./ios/build
- name: Create App Store Connect API Key JSON
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./ios/appstore_api_key.json
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/ios/appstore_api_key.p8
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_URL: ${{ secrets.GIT_URL }}
ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }}
ITC_TEAM_NAME: ${{ secrets.ITC_TEAM_NAME }}
@ -174,8 +156,8 @@ jobs:
run: bundle exec fastlane ios upload_to_testflight_lane
working-directory: ./ios
- name: Post PR Comment
if: success() && github.event_name == 'pull_request'
uses: actions/github-script@v6
if: success() # Ensures the message is only posted if previous steps succeed
env:
BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }}

View File

@ -1,180 +0,0 @@
name: Build Release and Upload to TestFlight (iOS)
on:
push:
branches:
- master
jobs:
build:
runs-on: macos-14
timeout-minutes: 180
outputs:
new_build_number: ${{ steps.generate_build_number.outputs.build_number }}
project_version: ${{ steps.determine_marketing_version.outputs.project_version }}
latest_commit_message: ${{ steps.get_latest_commit_message.outputs.commit_message }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }} # Setting the environment variable
steps:
- name: Checkout project
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetches all history
- name: Specify node version
uses: actions/setup-node@v2-beta
with:
node-version: 18
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 15.2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install dependencies with Bundler
run: bundle install
- name: Install Fastlane
run: gem install fastlane
- name: Clear Derived Data
run: bundle exec fastlane ios clear_derived_data_lane
working-directory: ./ios
- name: Install node_modules
run: npm install
- name: Install CocoaPods Dependencies
run: bundle exec pod install
working-directory: ./ios
- name: Cache CocoaPods Pods
uses: actions/cache@v2
with:
path: ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
- name: Display release-notes.txt
run: cat release-notes.txt
- name: Get Latest Commit Message
id: get_latest_commit_message
run: |
LATEST_COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s")
echo "LATEST_COMMIT_MESSAGE=${LATEST_COMMIT_MESSAGE}" >> $GITHUB_ENV
echo "::set-output name=commit_message::$LATEST_COMMIT_MESSAGE"
- name: Set up Git Authentication
env:
ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
run: |
git config --global credential.helper 'cache --timeout=3600'
git config --global http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n x-access-token:${ACCESS_TOKEN} | base64)"
- name: Create Temporary Keychain
run: bundle exec fastlane ios create_temp_keychain
working-directory: ./ios
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Setup Provisioning Profiles
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_URL: ${{ secrets.GIT_URL }}
ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }}
ITC_TEAM_NAME: ${{ secrets.ITC_TEAM_NAME }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: bundle exec fastlane ios setup_provisioning_profiles
working-directory: ./ios
- name: Generate Build Number based on timestamp
id: generate_build_number
run: |
NEW_BUILD_NUMBER=$(date +%s)
echo "NEW_BUILD_NUMBER=$NEW_BUILD_NUMBER" >> $GITHUB_ENV
echo "::set-output name=build_number::$NEW_BUILD_NUMBER"
- name: Set Build Number
run: bundle exec fastlane ios increment_build_number_lane
working-directory: ./ios
- name: Determine Marketing Version
id: determine_marketing_version
run: |
MARKETING_VERSION=$(grep MARKETING_VERSION ios/BlueWallet.xcodeproj/project.pbxproj | awk -F '= ' '{print $2}' | tr -d ' ;' | head -1)
echo "PROJECT_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "::set-output name=project_version::$MARKETING_VERSION"
- name: Expected IPA file name
run: |
echo "IPA file name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa"
- name: Build App
run: bundle exec fastlane ios build_app_lane
working-directory: ./ios
- name: Upload IPA as Artifact
uses: actions/upload-artifact@v2
with:
name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
path: ./ios/build/BlueWallet.${{env.PROJECT_VERSION}}(${{ env.NEW_BUILD_NUMBER }}).ipa
testflight-upload:
needs: build
runs-on: macos-14
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
NEW_BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }}
steps:
- name: Checkout project
uses: actions/checkout@v3
- name: Cache CocoaPods Pods
uses: actions/cache@v2
with:
path: ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
- name: Cache Ruby Gems
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: Install dependencies with Bundler
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Download IPA from Artifact
uses: actions/download-artifact@v2
with:
name: BlueWallet.${{needs.build.outputs.project_version}}(${{needs.build.outputs.new_build_number}}).ipa
path: ./ios/build
- name: Create App Store Connect API Key JSON
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./ios/appstore_api_key.json
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/ios/appstore_api_key.p8
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_URL: ${{ secrets.GIT_URL }}
ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }}
ITC_TEAM_NAME: ${{ secrets.ITC_TEAM_NAME }}
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: bundle exec fastlane ios upload_to_testflight_lane
working-directory: ./ios

View File

@ -18,27 +18,27 @@ jobs:
fetch-depth: "0"
- name: Specify node version
uses: actions/setup-node@v2-beta
uses: actions/setup-node@v4
with:
node-version: 16
node-version: 20
- name: Use npm caches
- name: Use yarn caches
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
path: ~/.yarn/berry/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-npm-
${{ runner.os }}-yarn-
- name: Use specific Java version for sdkmanager to work
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
java-version: '17'
cache: 'gradle'
- name: Install node_modules
run: npm install --production
run: yarn install
- name: Extract Version Name
id: version_name

View File

@ -12,37 +12,37 @@ on: [pull_request]
jobs:
test:
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Specify node version
uses: actions/setup-node@v2-beta
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Use npm caches
uses: actions/cache@v3
- name: Use yarn caches
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
path: ~/.yarn/berry/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-npm-
${{ runner.os }}-yarn-
- name: Use node_modules caches
id: cache-nm
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-nm-${{ hashFiles('package-lock.json') }}
key: ${{ runner.os }}-nm-${{ hashFiles('yarn.lock') }}
- name: Install node_modules
if: steps.cache-nm.outputs.cache-hit != 'true'
run: npm install
run: yarn
- name: Run tests
run: npm test || npm test || npm test
run: yarn test || yarn test || yarn test || yarn test
env:
BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
@ -53,20 +53,50 @@ jobs:
FAULTY_ZPUB: ${{ secrets.FAULTY_ZPUB }}
MNEMONICS_COBO: ${{ secrets.MNEMONICS_COBO }}
MNEMONICS_COLDCARD: ${{ secrets.MNEMONICS_COLDCARD }}
RETRY: 1
e2e:
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: yarn and gradle caches in /mnt
run: |
rm -rf ~/.yarn
rm -rf ~/.gradle
sudo mkdir -p /mnt/.yarn
sudo mkdir -p /mnt/.gradle
sudo chown -R runner /mnt/.yarn
sudo chown -R runner /mnt/.gradle
ln -s /mnt/.yarn /home/runner/
ln -s /mnt/.gradle /home/runner/
- name: Create artifacts directory on /mnt
run: |
sudo mkdir -p /mnt/artifacts
sudo chown -R runner /mnt/artifacts
- name: Specify node version
uses: actions/setup-node@v2-beta
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Use gradle caches
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@ -75,77 +105,44 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Use npm caches
uses: actions/cache@v3
- name: Use yarn caches
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
path: ~/.yarn/berry/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-npm-
${{ runner.os }}-yarn-
- name: Install node_modules
run: npm install
run: yarn || yarn
- name: Use specific Java version for sdkmanager to work
uses: actions/setup-java@v2
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '11'
java-version: '17'
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Build
run: npm run e2e:release-build
run: yarn e2e:release-build
- name: Test attempt 1
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true
id: test1
with:
api-level: 31
avd-name: Pixel_API_29_AOSP
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
arch: x86_64
script: npm run e2e:release-test
script: yarn e2e:release-test --record-videos all --record-logs all --take-screenshots all --headless -d 200000 -R 5 --artifacts-location /mnt/artifacts
- name: Test attempt 2
uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true
id: test2
if: steps.test1.outcome != 'success'
with:
api-level: 31
avd-name: Pixel_API_29_AOSP
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
arch: x86_64
script: npm run e2e:release-test
- name: Test attempt 3
uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true
id: test3
if: steps.test1.outcome != 'success' && steps.test2.outcome != 'success'
with:
api-level: 31
avd-name: Pixel_API_29_AOSP
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
arch: x86_64
script: npm run e2e:release-test
- name: Test attempt 4
uses: reactivecircus/android-emulator-runner@v2
if: steps.test1.outcome != 'success' && steps.test2.outcome != 'success' && steps.test3.outcome != 'success'
with:
api-level: 31
avd-name: Pixel_API_29_AOSP
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
arch: x86_64
script: npm run e2e:release-test
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-test-videos
path: ./artifacts/
path: /mnt/artifacts/

12
.gitignore vendored
View File

@ -85,3 +85,15 @@ artifacts/
*.mx
*.realm
*.realm.lock
android/app/.project
android/app/.classpath
android/.settings/org.eclipse.buildship.core.prefs
android/.project
# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

1
.npmrc
View File

@ -1 +0,0 @@
legacy-peer-deps=true

View File

@ -1 +1 @@
2.7.6
3.1.6

874
.yarn/releases/yarn-3.6.4.cjs vendored Executable file

File diff suppressed because one or more lines are too long

5
.yarnrc.yml Normal file
View File

@ -0,0 +1,5 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.6.4.cjs
# checksumBehavior: 'update'

307
App.js
View File

@ -1,307 +0,0 @@
import 'react-native-gesture-handler'; // should be on top
import React, { useContext, useEffect, useRef } from 'react';
import {
AppState,
NativeModules,
NativeEventEmitter,
Linking,
Platform,
StyleSheet,
UIManager,
useColorScheme,
View,
LogBox,
} from 'react-native';
import { NavigationContainer, CommonActions } from '@react-navigation/native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { navigationRef } from './NavigationService';
import * as NavigationService from './NavigationService';
import { Chain } from './models/bitcoinUnits';
import DeeplinkSchemaMatch from './class/deeplink-schema-match';
import loc from './loc';
import { BlueDefaultTheme, BlueDarkTheme } from './components/themes';
import InitRoot from './Navigation';
import BlueClipboard from './blue_modules/clipboard';
import { BlueStorageContext } from './blue_modules/storage-context';
import WatchConnectivity from './WatchConnectivity';
import DeviceQuickActions from './class/quick-actions';
import Notifications from './blue_modules/notifications';
import Biometric from './class/biometrics';
import WidgetCommunication from './blue_modules/WidgetCommunication';
import ActionSheet from './screen/ActionSheet';
import HandoffComponent from './components/handoff';
import triggerHapticFeedback, { HapticFeedbackTypes } from './blue_modules/hapticFeedback';
import MenuElements from './components/MenuElements';
import { updateExchangeRate } from './blue_modules/currency';
const A = require('./blue_modules/analytics');
const eventEmitter = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.EventEmitter) : undefined;
const { EventEmitter, SplashScreen } = NativeModules;
LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']);
const ClipboardContentType = Object.freeze({
BITCOIN: 'BITCOIN',
LIGHTNING: 'LIGHTNING',
});
if (Platform.OS === 'android') {
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
const App = () => {
const {
walletsInitialized,
wallets,
addWallet,
saveToDisk,
fetchAndSaveWalletTransactions,
refreshAllWalletTransactions,
setSharedCosigner,
} = useContext(BlueStorageContext);
const appState = useRef(AppState.currentState);
const clipboardContent = useRef();
const colorScheme = useColorScheme();
const onNotificationReceived = async notification => {
const payload = Object.assign({}, notification, notification.data);
if (notification.data && notification.data.data) Object.assign(payload, notification.data.data);
payload.foreground = true;
await Notifications.addNotification(payload);
// if user is staring at the app when he receives the notification we process it instantly
// so app refetches related wallet
if (payload.foreground) await processPushNotifications();
};
const onUserActivityOpen = data => {
switch (data.activityType) {
case HandoffComponent.activityTypes.ReceiveOnchain:
NavigationService.navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
address: data.userInfo.address,
},
});
break;
case HandoffComponent.activityTypes.Xpub:
NavigationService.navigate('WalletXpubRoot', {
screen: 'WalletXpub',
params: {
xpub: data.userInfo.xpub,
},
});
break;
default:
break;
}
};
useEffect(() => {
if (walletsInitialized) {
addListeners();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized]);
const addListeners = () => {
Linking.addEventListener('url', handleOpenURL);
AppState.addEventListener('change', handleAppStateChange);
EventEmitter?.getMostRecentUserActivity()
.then(onUserActivityOpen)
.catch(() => console.log('No userActivity object sent'));
handleAppStateChange(undefined);
/*
When a notification on iOS is shown while the app is on foreground;
On willPresent on AppDelegate.m
*/
eventEmitter?.addListener('onNotificationReceived', onNotificationReceived);
eventEmitter?.addListener('onUserActivityOpen', onUserActivityOpen);
};
/**
* Processes push notifications stored in AsyncStorage. Might navigate to some screen.
*
* @returns {Promise<boolean>} returns TRUE if notification was processed _and acted_ upon, i.e. navigation happened
* @private
*/
const processPushNotifications = async () => {
if (!walletsInitialized) {
console.log('not processing push notifications because wallets are not initialized');
return;
}
await new Promise(resolve => setTimeout(resolve, 200));
// sleep needed as sometimes unsuspend is faster than notification module actually saves notifications to async storage
const notifications2process = await Notifications.getStoredNotifications();
await Notifications.clearStoredNotifications();
Notifications.setApplicationIconBadgeNumber(0);
const deliveredNotifications = await Notifications.getDeliveredNotifications();
setTimeout(() => Notifications.removeAllDeliveredNotifications(), 5000); // so notification bubble wont disappear too fast
for (const payload of notifications2process) {
const wasTapped = payload.foreground === false || (payload.foreground === true && payload.userInteraction);
console.log('processing push notification:', payload);
let wallet;
switch (+payload.type) {
case 2:
case 3:
wallet = wallets.find(w => w.weOwnAddress(payload.address));
break;
case 1:
case 4:
wallet = wallets.find(w => w.weOwnTransaction(payload.txid || payload.hash));
break;
}
if (wallet) {
const walletID = wallet.getID();
fetchAndSaveWalletTransactions(walletID);
if (wasTapped) {
if (payload.type !== 3 || wallet.chain === Chain.OFFCHAIN) {
NavigationService.dispatch(
CommonActions.navigate({
name: 'WalletTransactions',
params: {
walletID,
walletType: wallet.type,
},
}),
);
} else {
NavigationService.navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
walletID,
address: payload.address,
},
});
}
return true;
}
} else {
console.log('could not find wallet while processing push notification, NOP');
}
} // end foreach notifications loop
if (deliveredNotifications.length > 0) {
// notification object is missing userInfo. We know we received a notification but don't have sufficient
// data to refresh 1 wallet. let's refresh all.
refreshAllWalletTransactions();
}
// if we are here - we did not act upon any push
return false;
};
const handleAppStateChange = async nextAppState => {
if (wallets.length === 0) return;
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
updateExchangeRate();
const processed = await processPushNotifications();
if (processed) return;
const clipboard = await BlueClipboard().getClipboardContent();
const isAddressFromStoredWallet = wallets.some(wallet => {
if (wallet.chain === Chain.ONCHAIN) {
// checking address validity is faster than unwrapping hierarchy only to compare it to garbage
return wallet.isAddressValid && wallet.isAddressValid(clipboard) && wallet.weOwnAddress(clipboard);
} else {
return wallet.isInvoiceGeneratedByWallet(clipboard) || wallet.weOwnAddress(clipboard);
}
});
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(clipboard);
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(clipboard);
const isLNURL = DeeplinkSchemaMatch.isLnUrl(clipboard);
const isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(clipboard);
if (
!isAddressFromStoredWallet &&
clipboardContent.current !== clipboard &&
(isBitcoinAddress || isLightningInvoice || isLNURL || isBothBitcoinAndLightning)
) {
let contentType;
if (isBitcoinAddress) {
contentType = ClipboardContentType.BITCOIN;
} else if (isLightningInvoice || isLNURL) {
contentType = ClipboardContentType.LIGHTNING;
} else if (isBothBitcoinAndLightning) {
contentType = ClipboardContentType.BITCOIN;
}
showClipboardAlert({ contentType });
}
clipboardContent.current = clipboard;
}
if (nextAppState) {
appState.current = nextAppState;
}
};
const handleOpenURL = event => {
DeeplinkSchemaMatch.navigationRouteFor(event, value => NavigationService.navigate(...value), {
wallets,
addWallet,
saveToDisk,
setSharedCosigner,
});
};
const showClipboardAlert = ({ contentType }) => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
BlueClipboard()
.getClipboardContent()
.then(clipboard => {
ActionSheet.showActionSheetWithOptions(
{
title: loc._.clipboard,
message: contentType === ClipboardContentType.BITCOIN ? loc.wallets.clipboard_bitcoin : loc.wallets.clipboard_lightning,
options: [loc._.cancel, loc._.continue],
cancelButtonIndex: 0,
},
buttonIndex => {
switch (buttonIndex) {
case 0: // Cancel
break;
case 1:
handleOpenURL({ url: clipboard });
break;
}
},
);
});
};
useEffect(() => {
if (Platform.OS === 'ios') {
// Call hide to setup the listener on the native side
SplashScreen?.addObserver();
}
}, []);
return (
<SafeAreaProvider>
<View style={styles.root}>
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<InitRoot />
<Notifications onProcessNotifications={processPushNotifications} />
<MenuElements />
<DeviceQuickActions />
</NavigationContainer>
</View>
<WatchConnectivity />
<Biometric />
<WidgetCommunication />
</SafeAreaProvider>
);
};
const styles = StyleSheet.create({
root: {
flex: 1,
},
});
export default App;

32
App.tsx Normal file
View File

@ -0,0 +1,32 @@
import 'react-native-gesture-handler'; // should be on top
import { NavigationContainer } from '@react-navigation/native';
import React from 'react';
import { useColorScheme } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { LargeScreenProvider } from './components/Context/LargeScreenProvider';
import { SettingsProvider } from './components/Context/SettingsProvider';
import { BlueDarkTheme, BlueDefaultTheme } from './components/themes';
import MasterView from './navigation/MasterView';
import { navigationRef } from './NavigationService';
import { StorageProvider } from './components/Context/StorageProvider';
const App = () => {
const colorScheme = useColorScheme();
return (
<LargeScreenProvider>
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<SafeAreaProvider>
<StorageProvider>
<SettingsProvider>
<MasterView />
</SettingsProvider>
</StorageProvider>
</SafeAreaProvider>
</NavigationContainer>
</LargeScreenProvider>
);
};
export default App;

View File

@ -1,13 +1,10 @@
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
import React, { Component, forwardRef } from 'react';
import PropTypes from 'prop-types';
import { Icon, Text, Header } from 'react-native-elements';
import Clipboard from '@react-native-clipboard/clipboard';
import React, { forwardRef } from 'react';
import {
ActivityIndicator,
Alert,
Animated,
Dimensions,
Image,
I18nManager,
InputAccessoryView,
Keyboard,
KeyboardAvoidingView,
@ -16,16 +13,11 @@ import {
TextInput,
TouchableOpacity,
View,
I18nManager,
ImageBackground,
} from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { BlueCurrentTheme, useTheme } from './components/themes';
import PlusIcon from './components/icons/PlusIcon';
import loc, { formatStringAddTwoWhiteSpaces } from './loc';
import SafeArea from './components/SafeArea';
import { Icon, Text } from '@rneui/themed';
import { useTheme } from './components/themes';
import loc from './loc';
const { height, width } = Dimensions.get('window');
const aspectRatio = height / width;
@ -36,140 +28,6 @@ if (aspectRatio > 1.6) {
isIpad = true;
}
export const BitcoinButton = props => {
const { colors } = useTheme();
return (
<TouchableOpacity accessibilityRole="button" testID={props.testID} onPress={props.onPress}>
<View
style={{
borderColor: (props.active && colors.newBlue) || colors.buttonDisabledBackgroundColor,
borderWidth: 1.5,
borderRadius: 8,
backgroundColor: colors.buttonDisabledBackgroundColor,
minWidth: props.style.width,
minHeight: props.style.height,
height: props.style.height,
flex: 1,
marginBottom: 8,
}}
>
<View style={{ marginHorizontal: 16, marginVertical: 10, flexDirection: 'row', alignItems: 'center' }}>
<View>
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/bitcoin.png')} />
</View>
<View style={{ flex: 1 }}>
<Text style={{ color: colors.newBlue, fontWeight: 'bold', fontSize: 18, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr' }}>
{loc.wallets.add_bitcoin}
</Text>
<Text
style={{
color: colors.alternativeTextColor,
fontSize: 13,
fontWeight: '500',
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
}}
>
{loc.wallets.add_bitcoin_explain}
</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
};
export const VaultButton = props => {
const { colors } = useTheme();
return (
<TouchableOpacity accessibilityRole="button" testID={props.testID} onPress={props.onPress}>
<View
style={{
borderColor: (props.active && colors.foregroundColor) || colors.buttonDisabledBackgroundColor,
borderWidth: 1.5,
borderRadius: 8,
backgroundColor: colors.buttonDisabledBackgroundColor,
minWidth: props.style.width,
minHeight: props.style.height,
height: props.style.height,
flex: 1,
}}
>
<View style={{ marginHorizontal: 16, marginVertical: 10, flexDirection: 'row', alignItems: 'center' }}>
<View>
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/vault.png')} />
</View>
<View style={{ flex: 1 }}>
<Text
style={{
color: colors.foregroundColor,
fontWeight: 'bold',
fontSize: 18,
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
}}
>
{loc.multisig.multisig_vault}
</Text>
<Text
style={{
color: colors.alternativeTextColor,
fontSize: 13,
fontWeight: '500',
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
}}
>
{loc.multisig.multisig_vault_explain}
</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
};
export const LightningButton = props => {
const { colors } = useTheme();
return (
<TouchableOpacity accessibilityRole="button" onPress={props.onPress}>
<View
style={{
borderColor: (props.active && colors.lnborderColor) || colors.buttonDisabledBackgroundColor,
borderWidth: 1.5,
borderRadius: 8,
backgroundColor: colors.buttonDisabledBackgroundColor,
minWidth: props.style.width,
minHeight: props.style.height,
height: props.style.height,
flex: 1,
marginBottom: 8,
}}
>
<View style={{ marginHorizontal: 16, marginVertical: 10, flexDirection: 'row', alignItems: 'center' }}>
<View>
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/lightning.png')} />
</View>
<View style={{ flex: 1 }}>
<Text
style={{ color: colors.lnborderColor, fontWeight: 'bold', fontSize: 18, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr' }}
>
{loc.wallets.add_lightning}
</Text>
<Text
style={{
color: colors.alternativeTextColor,
fontSize: 13,
fontWeight: '500',
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
}}
>
{loc.wallets.add_lightning_explain}
</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
};
/**
* TODO: remove this comment once this file gets properly converted to typescript.
*
@ -193,104 +51,6 @@ export const BlueButtonLink = forwardRef((props, ref) => {
);
});
export const BlueAlertWalletExportReminder = ({ onSuccess = () => {}, onFailure }) => {
Alert.alert(
loc.wallets.details_title,
loc.pleasebackup.ask,
[
{ text: loc.pleasebackup.ask_yes, onPress: onSuccess, style: 'cancel' },
{ text: loc.pleasebackup.ask_no, onPress: onFailure },
],
{ cancelable: false },
);
};
export const BluePrivateBalance = () => {
return (
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 13, borderRadius: 9 }}>
<ImageBackground
blurRadius={6}
style={{ backgroundColor: '#FFFFFF', opacity: 0.5, height: 30, width: 110, marginRight: 8, borderRadius: 9 }}
/>
<Icon name="eye-slash" type="font-awesome" color="#FFFFFF" />
</View>
);
};
export const BlueCopyToClipboardButton = ({ stringToCopy, displayText = false }) => {
return (
<TouchableOpacity accessibilityRole="button" onPress={() => Clipboard.setString(stringToCopy)}>
<Text style={{ fontSize: 13, fontWeight: '400', color: '#68bbe1' }}>{displayText || loc.transactions.details_copy}</Text>
</TouchableOpacity>
);
};
export class BlueCopyTextToClipboard extends Component {
static propTypes = {
text: PropTypes.string,
truncated: PropTypes.bool,
};
static defaultProps = {
text: '',
truncated: false,
};
constructor(props) {
super(props);
this.state = { hasTappedText: false, address: props.text };
}
static getDerivedStateFromProps(props, state) {
if (state.hasTappedText) {
return { hasTappedText: state.hasTappedText, address: state.address, truncated: props.truncated };
} else {
return { hasTappedText: state.hasTappedText, address: props.text, truncated: props.truncated };
}
}
copyToClipboard = () => {
this.setState({ hasTappedText: true }, () => {
Clipboard.setString(this.props.text);
this.setState({ address: loc.wallets.xpub_copiedToClipboard }, () => {
setTimeout(() => {
this.setState({ hasTappedText: false, address: this.props.text });
}, 1000);
});
});
};
render() {
return (
<View style={{ justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16 }}>
<TouchableOpacity
accessibilityRole="button"
onPress={this.copyToClipboard}
disabled={this.state.hasTappedText}
testID="BlueCopyTextToClipboard"
>
<Animated.Text
style={styleCopyTextToClipboard.address}
{...(this.props.truncated ? { numberOfLines: 1, ellipsizeMode: 'middle' } : { numberOfLines: 0 })}
testID="AddressValue"
>
{this.state.address}
</Animated.Text>
</TouchableOpacity>
</View>
);
}
}
const styleCopyTextToClipboard = StyleSheet.create({
address: {
marginVertical: 32,
fontSize: 15,
color: '#9aa0aa',
textAlign: 'center',
},
});
export const BlueCard = props => {
return <View {...props} style={{ padding: 20 }} />;
};
@ -355,67 +115,6 @@ export const BlueFormMultiInput = props => {
);
};
export const BlueHeaderDefaultSub = props => {
const { colors } = useTheme();
return (
<SafeArea>
<Header
backgroundColor={colors.background}
leftContainerStyle={{ minWidth: '100%' }}
outerContainerStyles={{
borderBottomColor: 'transparent',
borderBottomWidth: 0,
}}
leftComponent={
<Text
adjustsFontSizeToFit
style={{
fontWeight: 'bold',
fontSize: 30,
color: colors.foregroundColor,
}}
>
{props.leftText}
</Text>
}
{...props}
/>
</SafeArea>
);
};
export const BlueHeaderDefaultMain = props => {
const { colors } = useTheme();
const { isDrawerList } = props;
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: isDrawerList ? colors.elevated : colors.background,
paddingHorizontal: 16,
borderTopColor: isDrawerList ? colors.elevated : colors.background,
borderBottomColor: isDrawerList ? colors.elevated : colors.background,
marginBottom: 8,
}}
>
<Text
style={{
textAlign: 'left',
fontWeight: 'bold',
fontSize: 34,
color: colors.foregroundColor,
}}
>
{props.leftText}
</Text>
<PlusIcon accessibilityRole="button" accessibilityLabel={loc.wallets.add_title} onPress={props.onNewWalletPress} />
</View>
);
};
export const BlueSpacing = props => {
return <View {...props} style={{ height: 60 }} />;
};
@ -501,166 +200,7 @@ export const BlueLoading = props => {
);
};
export class BlueReplaceFeeSuggestions extends Component {
static propTypes = {
onFeeSelected: PropTypes.func.isRequired,
transactionMinimum: PropTypes.number.isRequired,
};
static defaultProps = {
transactionMinimum: 1,
};
state = {
customFeeValue: '1',
};
async componentDidMount() {
try {
const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey));
if (cachedNetworkTransactionFees && 'fastestFee' in cachedNetworkTransactionFees) {
this.setState({ networkFees: cachedNetworkTransactionFees }, () => this.onFeeSelected(NetworkTransactionFeeType.FAST));
}
} catch (_) {}
const networkFees = await NetworkTransactionFees.recommendedFees();
this.setState({ networkFees }, () => this.onFeeSelected(NetworkTransactionFeeType.FAST));
}
onFeeSelected = selectedFeeType => {
if (selectedFeeType !== NetworkTransactionFeeType.CUSTOM) {
Keyboard.dismiss();
}
if (selectedFeeType === NetworkTransactionFeeType.FAST) {
this.props.onFeeSelected(this.state.networkFees.fastestFee);
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.fastestFee));
} else if (selectedFeeType === NetworkTransactionFeeType.MEDIUM) {
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.mediumFee));
} else if (selectedFeeType === NetworkTransactionFeeType.SLOW) {
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.slowFee));
} else if (selectedFeeType === NetworkTransactionFeeType.CUSTOM) {
this.props.onFeeSelected(Number(this.state.customFeeValue));
}
};
onCustomFeeTextChange = customFee => {
const customFeeValue = customFee.replace(/[^0-9]/g, '');
this.setState({ customFeeValue, selectedFeeType: NetworkTransactionFeeType.CUSTOM }, () => {
this.onFeeSelected(NetworkTransactionFeeType.CUSTOM);
});
};
render() {
const { networkFees, selectedFeeType } = this.state;
return (
<View>
{networkFees &&
[
{
label: loc.send.fee_fast,
time: loc.send.fee_10m,
type: NetworkTransactionFeeType.FAST,
rate: networkFees.fastestFee,
active: selectedFeeType === NetworkTransactionFeeType.FAST,
},
{
label: formatStringAddTwoWhiteSpaces(loc.send.fee_medium),
time: loc.send.fee_3h,
type: NetworkTransactionFeeType.MEDIUM,
rate: networkFees.mediumFee,
active: selectedFeeType === NetworkTransactionFeeType.MEDIUM,
},
{
label: loc.send.fee_slow,
time: loc.send.fee_1d,
type: NetworkTransactionFeeType.SLOW,
rate: networkFees.slowFee,
active: selectedFeeType === NetworkTransactionFeeType.SLOW,
},
].map(({ label, type, time, rate, active }, index) => (
<TouchableOpacity
accessibilityRole="button"
key={label}
onPress={() => this.onFeeSelected(type)}
style={[
{ paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 },
active && { borderRadius: 8, backgroundColor: BlueCurrentTheme.colors.incomingBackgroundColor },
]}
>
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ fontSize: 22, color: BlueCurrentTheme.colors.successColor, fontWeight: '600' }}>{label}</Text>
<View
style={{
backgroundColor: BlueCurrentTheme.colors.successColor,
borderRadius: 5,
paddingHorizontal: 6,
paddingVertical: 3,
}}
>
<Text style={{ color: BlueCurrentTheme.colors.background }}>~{time}</Text>
</View>
</View>
<View style={{ justifyContent: 'flex-end', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ color: BlueCurrentTheme.colors.successColor }}>{rate} sat/byte</Text>
</View>
</TouchableOpacity>
))}
<TouchableOpacity
accessibilityRole="button"
onPress={() => this.customTextInput.focus()}
style={[
{ paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 },
selectedFeeType === NetworkTransactionFeeType.CUSTOM && {
borderRadius: 8,
backgroundColor: BlueCurrentTheme.colors.incomingBackgroundColor,
},
]}
>
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ fontSize: 22, color: BlueCurrentTheme.colors.successColor, fontWeight: '600' }}>
{formatStringAddTwoWhiteSpaces(loc.send.fee_custom)}
</Text>
</View>
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center', marginTop: 5 }}>
<TextInput
onChangeText={this.onCustomFeeTextChange}
keyboardType="numeric"
value={this.state.customFeeValue}
ref={ref => (this.customTextInput = ref)}
maxLength={9}
style={{
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderBottomWidth: 0.5,
borderColor: BlueCurrentTheme.colors.formBorder,
borderRadius: 4,
borderWidth: 1.0,
color: '#81868e',
flex: 1,
marginRight: 10,
minHeight: 33,
paddingRight: 5,
paddingLeft: 5,
}}
onFocus={() => this.onCustomFeeTextChange(this.state.customFeeValue)}
defaultValue={this.props.transactionMinimum}
placeholder={loc.send.fee_satvbyte}
placeholderTextColor="#81868e"
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<Text style={{ color: BlueCurrentTheme.colors.successColor }}>sat/byte</Text>
</View>
</TouchableOpacity>
<BlueText style={{ color: BlueCurrentTheme.colors.alternativeTextColor }}>
{loc.formatString(loc.send.fee_replace_minvb, { min: this.props.transactionMinimum })}
</BlueText>
</View>
);
}
}
export function BlueBigCheckmark({ style }) {
export function BlueBigCheckmark({ style = {} }) {
const defaultStyles = {
backgroundColor: '#ccddf9',
width: 120,
@ -678,40 +218,3 @@ export function BlueBigCheckmark({ style }) {
</View>
);
}
const tabsStyles = StyleSheet.create({
root: {
flexDirection: 'row',
height: 50,
borderColor: '#e3e3e3',
borderBottomWidth: 1,
},
tabRoot: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
borderColor: 'white',
borderBottomWidth: 2,
},
});
export const BlueTabs = ({ active, onSwitch, tabs }) => (
<View style={[tabsStyles.root, isIpad && { marginBottom: 30 }]}>
{tabs.map((Tab, i) => (
<TouchableOpacity
key={i}
accessibilityRole="button"
onPress={() => onSwitch(i)}
style={[
tabsStyles.tabRoot,
active === i && {
borderColor: BlueCurrentTheme.colors.buttonAlternativeTextColor,
borderBottomWidth: 2,
},
]}
>
<Tab active={active === i} />
</TouchableOpacity>
))}
</View>
);

10
Gemfile
View File

@ -1,8 +1,8 @@
source 'https://rubygems.org'
source "https://rubygems.org"
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby '>= 2.6.10'
gem 'cocoapods', '>= 1.13', '< 1.15'
gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'
ruby "3.1.6"
gem 'rubyzip', '2.3.0'
gem "cocoapods", "1.15.2"
gem "activesupport", ">= 6.1.7.3", "< 7.1.0"
gem "fastlane"

View File

@ -5,42 +5,41 @@ GEM
base64
nkf
rexml
activesupport (6.1.7.7)
activesupport (7.0.8.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.895.0)
aws-sdk-core (3.191.3)
aws-partitions (1.950.0)
aws-sdk-core (3.201.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.156.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
cocoapods (1.14.3)
cocoapods (1.15.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.14.3)
cocoapods-core (= 1.15.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
@ -55,7 +54,7 @@ GEM
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.23.0, < 2.0)
cocoapods-core (1.14.3)
cocoapods-core (1.15.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
@ -78,18 +77,17 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.2.3)
concurrent-ruby (1.3.3)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
excon (0.109.0)
excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@ -118,15 +116,15 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.0)
fastlane (2.219.0)
fastimage (2.3.1)
fastlane (2.221.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@ -147,10 +145,10 @@ GEM
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
@ -159,8 +157,8 @@ GEM
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
ffi (1.16.3)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
ffi (1.17.0)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
@ -178,19 +176,19 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.29.0)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.1)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.45.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.29.0)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
@ -201,43 +199,44 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
http-cookie (1.0.6)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.14.1)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.7.1)
jwt (2.8.1)
json (2.7.2)
jwt (2.8.2)
base64
mini_magick (4.12.0)
mini_magick (4.13.1)
mini_mime (1.1.5)
minitest (5.22.2)
minitest (5.24.1)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.0)
multipart-post (2.4.1)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
nkf (0.2.0)
optparse (0.4.0)
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (4.0.7)
rake (13.1.0)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rexml (3.2.9)
strscan
rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.18.0)
rubyzip (2.3.0)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@ -245,6 +244,7 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
@ -258,9 +258,6 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.9.1)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
xcodeproj (1.24.0)
@ -274,18 +271,18 @@ GEM
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
zeitwerk (2.6.13)
PLATFORMS
ruby
DEPENDENCIES
activesupport (>= 6.1.7.3, < 7.1.0)
cocoapods (>= 1.13, < 1.15)
cocoapods (= 1.15.2)
fastlane
rubyzip (= 2.3.0)
RUBY VERSION
ruby 2.7.4p191
ruby 3.1.6p260
BUNDLED WITH
2.4.22
2.5.13

23
MasterView.tsx Normal file
View File

@ -0,0 +1,23 @@
import 'react-native-gesture-handler'; // should be on top
import React, { lazy, Suspense } from 'react';
import MainRoot from './navigation';
import { useStorage } from './hooks/context/useStorage';
const CompanionDelegates = lazy(() => import('./components/CompanionDelegates'));
const MasterView = () => {
const { walletsInitialized } = useStorage();
return (
<>
<MainRoot />
{walletsInitialized && (
<Suspense>
<CompanionDelegates />
</Suspense>
)}
</>
);
};
export default MasterView;

View File

@ -1,614 +0,0 @@
import { DrawerNavigationOptions, createDrawerNavigator } from '@react-navigation/drawer';
import { NativeStackNavigationOptions, createNativeStackNavigator } from '@react-navigation/native-stack';
import React, { useContext, useMemo } from 'react';
import { Dimensions, I18nManager, Platform, useWindowDimensions } from 'react-native';
import PlausibleDeniability from './screen/PlausibleDeniability';
import Selftest from './screen/selftest';
import Currency from './screen/settings/Currency';
import GeneralSettings from './screen/settings/GeneralSettings';
import Licensing from './screen/settings/Licensing';
import NetworkSettings from './screen/settings/NetworkSettings';
import Settings from './screen/settings/Settings';
import About from './screen/settings/about';
import DefaultView from './screen/settings/defaultView';
import ElectrumSettings from './screen/settings/electrumSettings';
import EncryptStorage from './screen/settings/encryptStorage';
import Language from './screen/settings/language';
import LightningSettings from './screen/settings/lightningSettings';
import NotificationSettings from './screen/settings/notificationSettings';
import ReleaseNotes from './screen/settings/releasenotes';
import Tools from './screen/settings/tools';
import AddWallet from './screen/wallets/add';
import WalletsAddMultisig from './screen/wallets/addMultisig';
import WalletsAddMultisigHelp, { WalletAddMultisigHelpNavigationOptions } from './screen/wallets/addMultisigHelp';
import WalletsAddMultisigStep2 from './screen/wallets/addMultisigStep2';
import WalletAddresses from './screen/wallets/addresses';
import WalletDetails from './screen/wallets/details';
import WalletExport from './screen/wallets/export';
import ExportMultisigCoordinationSetup from './screen/wallets/exportMultisigCoordinationSetup';
import GenerateWord from './screen/wallets/generateWord';
import ImportWallet from './screen/wallets/import';
import ImportCustomDerivationPath from './screen/wallets/importCustomDerivationPath';
import ImportWalletDiscovery from './screen/wallets/importDiscovery';
import ImportSpeed from './screen/wallets/importSpeed';
import WalletsList from './screen/wallets/list';
import PleaseBackup from './screen/wallets/pleaseBackup';
import PleaseBackupLNDHub from './screen/wallets/pleaseBackupLNDHub';
import ProvideEntropy from './screen/wallets/provideEntropy';
import ReorderWallets from './screen/wallets/reorderWallets';
import SelectWallet from './screen/wallets/selectWallet';
import SignVerify from './screen/wallets/signVerify';
import WalletTransactions from './screen/wallets/transactions';
import ViewEditMultisigCosigners from './screen/wallets/viewEditMultisigCosigners';
import WalletXpub from './screen/wallets/xpub';
import CPFP from './screen/transactions/CPFP';
import RBFBumpFee from './screen/transactions/RBFBumpFee';
import RBFCancel from './screen/transactions/RBFCancel';
import TransactionDetails from './screen/transactions/details';
import TransactionStatus from './screen/transactions/transactionStatus';
import AztecoRedeem from './screen/receive/aztecoRedeem';
import ReceiveDetails from './screen/receive/details';
import ScanQRCode from './screen/send/ScanQRCode';
import Broadcast from './screen/send/broadcast';
import CoinControl from './screen/send/coinControl';
import Confirm from './screen/send/confirm';
import SendCreate from './screen/send/create';
import SendDetails from './screen/send/details';
import IsItMyAddress from './screen/send/isItMyAddress';
import PsbtMultisig from './screen/send/psbtMultisig';
import PsbtMultisigQRCode from './screen/send/psbtMultisigQRCode';
import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet';
import Success from './screen/send/success';
import UnlockWith from './screen/UnlockWith';
import { isDesktop, isHandset, isTablet } from './blue_modules/environment';
import navigationStyle from './components/navigationStyle';
import { useTheme } from './components/themes';
import loc from './loc';
import LappBrowser from './screen/lnd/browser';
import LNDCreateInvoice from './screen/lnd/lndCreateInvoice';
import LNDViewAdditionalInvoiceInformation from './screen/lnd/lndViewAdditionalInvoiceInformation';
import LNDViewAdditionalInvoicePreImage from './screen/lnd/lndViewAdditionalInvoicePreImage';
import LNDViewInvoice from './screen/lnd/lndViewInvoice';
import LnurlAuth from './screen/lnd/lnurlAuth';
import LnurlPay from './screen/lnd/lnurlPay';
import LnurlPaySuccess from './screen/lnd/lnurlPaySuccess';
import ScanLndInvoice from './screen/lnd/scanLndInvoice';
import SettingsPrivacy from './screen/settings/SettingsPrivacy';
import DrawerList from './screen/wallets/drawerList';
import PaymentCode from './screen/wallets/paymentCode';
import PaymentCodesList from './screen/wallets/paymentCodesList';
import { BlueStorageContext } from './blue_modules/storage-context';
const WalletsStack = createNativeStackNavigator();
const WalletsRoot = () => {
const theme = useTheme();
return (
<WalletsStack.Navigator screenOptions={{ headerShadowVisible: false }}>
<WalletsStack.Screen name="WalletsList" component={WalletsList} options={WalletsList.navigationOptions(theme)} />
<WalletsStack.Screen name="WalletTransactions" component={WalletTransactions} options={WalletTransactions.navigationOptions(theme)} />
<WalletsStack.Screen name="WalletDetails" component={WalletDetails} options={WalletDetails.navigationOptions(theme)} />
<WalletsStack.Screen name="TransactionDetails" component={TransactionDetails} options={TransactionDetails.navigationOptions(theme)} />
<WalletsStack.Screen name="TransactionStatus" component={TransactionStatus} options={TransactionStatus.navigationOptions(theme)} />
<WalletsStack.Screen name="CPFP" component={CPFP} options={CPFP.navigationOptions(theme)} />
<WalletsStack.Screen name="RBFBumpFee" component={RBFBumpFee} options={RBFBumpFee.navigationOptions(theme)} />
<WalletsStack.Screen name="RBFCancel" component={RBFCancel} options={RBFCancel.navigationOptions(theme)} />
<WalletsStack.Screen name="Settings" component={Settings} options={Settings.navigationOptions(theme)} />
<WalletsStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions(theme)} />
<WalletsStack.Screen name="Currency" component={Currency} options={navigationStyle({ title: loc.settings.currency })(theme)} />
<WalletsStack.Screen name="About" component={About} options={About.navigationOptions(theme)} />
<WalletsStack.Screen name="ReleaseNotes" component={ReleaseNotes} options={ReleaseNotes.navigationOptions(theme)} />
<WalletsStack.Screen name="Selftest" component={Selftest} options={Selftest.navigationOptions(theme)} />
<WalletsStack.Screen name="Licensing" component={Licensing} options={Licensing.navigationOptions(theme)} />
<WalletsStack.Screen name="DefaultView" component={DefaultView} options={DefaultView.navigationOptions(theme)} />
<WalletsStack.Screen name="Language" component={Language} options={navigationStyle({ title: loc.settings.language })(theme)} />
<WalletsStack.Screen name="EncryptStorage" component={EncryptStorage} options={EncryptStorage.navigationOptions(theme)} />
<WalletsStack.Screen
name="GeneralSettings"
component={GeneralSettings}
options={navigationStyle({ title: loc.settings.general })(theme)}
/>
<WalletsStack.Screen name="NetworkSettings" component={NetworkSettings} options={NetworkSettings.navigationOptions(theme)} />
<WalletsStack.Screen
name="NotificationSettings"
component={NotificationSettings}
options={NotificationSettings.navigationOptions(theme)}
/>
<WalletsStack.Screen
name="PlausibleDeniability"
component={PlausibleDeniability}
options={navigationStyle({ title: loc.plausibledeniability.title })(theme)}
/>
<WalletsStack.Screen name="LightningSettings" component={LightningSettings} options={LightningSettings.navigationOptions(theme)} />
<WalletsStack.Screen name="ElectrumSettings" component={ElectrumSettings} options={ElectrumSettings.navigationOptions(theme)} />
<WalletsStack.Screen name="SettingsPrivacy" component={SettingsPrivacy} options={SettingsPrivacy.navigationOptions(theme)} />
<WalletsStack.Screen name="Tools" component={Tools} options={Tools.navigationOptions(theme)} />
<WalletsStack.Screen name="LNDViewInvoice" component={LNDViewInvoice} options={LNDViewInvoice.navigationOptions(theme)} />
<WalletsStack.Screen
name="LNDViewAdditionalInvoiceInformation"
component={LNDViewAdditionalInvoiceInformation}
options={LNDViewAdditionalInvoiceInformation.navigationOptions(theme)}
/>
<WalletsStack.Screen
name="LNDViewAdditionalInvoicePreImage"
component={LNDViewAdditionalInvoicePreImage}
options={LNDViewAdditionalInvoicePreImage.navigationOptions(theme)}
/>
<WalletsStack.Screen name="Broadcast" component={Broadcast} options={Broadcast.navigationOptions(theme)} />
<WalletsStack.Screen name="IsItMyAddress" component={IsItMyAddress} options={IsItMyAddress.navigationOptions(theme)} />
<WalletsStack.Screen name="GenerateWord" component={GenerateWord} options={GenerateWord.navigationOptions(theme)} />
<WalletsStack.Screen name="LnurlPay" component={LnurlPay} options={LnurlPay.navigationOptions(theme)} />
<WalletsStack.Screen name="LnurlPaySuccess" component={LnurlPaySuccess} options={LnurlPaySuccess.navigationOptions(theme)} />
<WalletsStack.Screen name="LnurlAuth" component={LnurlAuth} options={LnurlAuth.navigationOptions(theme)} />
<WalletsStack.Screen
name="Success"
component={Success}
options={{
headerShown: false,
gestureEnabled: false,
}}
/>
<WalletsStack.Screen name="WalletAddresses" component={WalletAddresses} options={WalletAddresses.navigationOptions(theme)} />
</WalletsStack.Navigator>
);
};
const AddWalletStack = createNativeStackNavigator();
const AddWalletRoot = () => {
const theme = useTheme();
return (
<AddWalletStack.Navigator screenOptions={{ headerShadowVisible: false }}>
<AddWalletStack.Screen
name="AddWallet"
component={AddWallet}
options={navigationStyle({
closeButton: true,
headerBackVisible: false,
title: loc.wallets.add_title,
})(theme)}
/>
<AddWalletStack.Screen name="ImportWallet" component={ImportWallet} options={ImportWallet.navigationOptions(theme)} />
<AddWalletStack.Screen
name="ImportWalletDiscovery"
component={ImportWalletDiscovery}
options={ImportWalletDiscovery.navigationOptions(theme)}
/>
<AddWalletStack.Screen
name="ImportCustomDerivationPath"
component={ImportCustomDerivationPath}
options={ImportCustomDerivationPath.navigationOptions(theme)}
/>
<AddWalletStack.Screen name="ImportSpeed" component={ImportSpeed} options={ImportSpeed.navigationOptions(theme)} />
<AddWalletStack.Screen
name="PleaseBackup"
component={PleaseBackup}
options={navigationStyle({
gestureEnabled: false,
headerBackVisible: false,
title: loc.pleasebackup.title,
})(theme)}
/>
<AddWalletStack.Screen
name="PleaseBackupLNDHub"
component={PleaseBackupLNDHub}
options={PleaseBackupLNDHub.navigationOptions(theme)}
/>
<AddWalletStack.Screen name="ProvideEntropy" component={ProvideEntropy} options={ProvideEntropy.navigationOptions(theme)} />
<AddWalletStack.Screen
name="WalletsAddMultisig"
component={WalletsAddMultisig}
options={WalletsAddMultisig.navigationOptions(theme)}
initialParams={WalletsAddMultisig.initialParams}
/>
<AddWalletStack.Screen
name="WalletsAddMultisigStep2"
component={WalletsAddMultisigStep2}
options={WalletsAddMultisigStep2.navigationOptions(theme)}
/>
<AddWalletStack.Screen
name="WalletsAddMultisigHelp"
component={WalletsAddMultisigHelp}
options={WalletAddMultisigHelpNavigationOptions}
/>
</AddWalletStack.Navigator>
);
};
// CreateTransactionStackNavigator === SendDetailsStack
const SendDetailsStack = createNativeStackNavigator();
const SendDetailsRoot = () => {
const theme = useTheme();
return (
<SendDetailsStack.Navigator screenOptions={{ headerShadowVisible: false }}>
<SendDetailsStack.Screen
name="SendDetails"
component={SendDetails}
options={SendDetails.navigationOptions(theme)}
initialParams={SendDetails.initialParams}
/>
<SendDetailsStack.Screen name="Confirm" component={Confirm} options={Confirm.navigationOptions(theme)} />
<SendDetailsStack.Screen
name="PsbtWithHardwareWallet"
component={PsbtWithHardwareWallet}
options={PsbtWithHardwareWallet.navigationOptions(theme)}
/>
<SendDetailsStack.Screen name="CreateTransaction" component={SendCreate} options={SendCreate.navigationOptions(theme)} />
<SendDetailsStack.Screen name="PsbtMultisig" component={PsbtMultisig} options={PsbtMultisig.navigationOptions(theme)} />
<SendDetailsStack.Screen
name="PsbtMultisigQRCode"
component={PsbtMultisigQRCode}
options={PsbtMultisigQRCode.navigationOptions(theme)}
/>
<SendDetailsStack.Screen
name="Success"
component={Success}
options={{
headerShown: false,
gestureEnabled: false,
}}
/>
<SendDetailsStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions(theme)} />
<SendDetailsStack.Screen name="CoinControl" component={CoinControl} options={CoinControl.navigationOptions(theme)} />
</SendDetailsStack.Navigator>
);
};
const LNDCreateInvoiceStack = createNativeStackNavigator();
const LNDCreateInvoiceRoot = () => {
const theme = useTheme();
return (
<LNDCreateInvoiceStack.Navigator screenOptions={{ headerShadowVisible: false }}>
<LNDCreateInvoiceStack.Screen
name="LNDCreateInvoice"
component={LNDCreateInvoice}
options={LNDCreateInvoice.navigationOptions(theme)}
/>
<LNDCreateInvoiceStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions(theme)} />
<LNDCreateInvoiceStack.Screen name="LNDViewInvoice" component={LNDViewInvoice} options={LNDViewInvoice.navigationOptions(theme)} />
<LNDCreateInvoiceStack.Screen
name="LNDViewAdditionalInvoiceInformation"
component={LNDViewAdditionalInvoiceInformation}
options={LNDViewAdditionalInvoiceInformation.navigationOptions(theme)}
/>
<LNDCreateInvoiceStack.Screen
name="LNDViewAdditionalInvoicePreImage"
component={LNDViewAdditionalInvoicePreImage}
options={LNDViewAdditionalInvoicePreImage.navigationOptions(theme)}
/>
</LNDCreateInvoiceStack.Navigator>
);
};
// LightningScanInvoiceStackNavigator === ScanLndInvoiceStack
const ScanLndInvoiceStack = createNativeStackNavigator();
const ScanLndInvoiceRoot = () => {
const theme = useTheme();
return (
<ScanLndInvoiceStack.Navigator screenOptions={{ headerShadowVisible: false }}>
<ScanLndInvoiceStack.Screen
name="ScanLndInvoice"
component={ScanLndInvoice}
options={ScanLndInvoice.navigationOptions(theme)}
initialParams={ScanLndInvoice.initialParams}
/>
<ScanLndInvoiceStack.Screen name="SelectWallet" component={SelectWallet} options={SelectWallet.navigationOptions(theme)} />
<ScanLndInvoiceStack.Screen name="Success" component={Success} options={{ headerShown: false, gestureEnabled: false }} />
<ScanLndInvoiceStack.Screen name="LnurlPay" component={LnurlPay} options={LnurlPay.navigationOptions(theme)} />
<ScanLndInvoiceStack.Screen name="LnurlPaySuccess" component={LnurlPaySuccess} options={LnurlPaySuccess.navigationOptions(theme)} />
</ScanLndInvoiceStack.Navigator>
);
};
const AztecoRedeemStack = createNativeStackNavigator();
const AztecoRedeemRoot = () => {
const theme = useTheme();
return (
<AztecoRedeemStack.Navigator screenOptions={{ headerShadowVisible: false }}>
<AztecoRedeemStack.Screen name="AztecoRedeem" component={AztecoRedeem} options={AztecoRedeem.navigationOptions(theme)} />
<AztecoRedeemStack.Screen name="SelectWallet" component={SelectWallet} />
</AztecoRedeemStack.Navigator>
);
};
const ScanQRCodeStack = createNativeStackNavigator();
const ScanQRCodeRoot = () => (
<ScanQRCodeStack.Navigator
initialRouteName="ScanQRCode"
id="ScanQRCodeRoot"
screenOptions={{ headerShown: false, presentation: 'fullScreenModal' }}
>
<ScanQRCodeStack.Screen name="ScanQRCode" component={ScanQRCode} initialParams={ScanQRCode.initialParams} />
</ScanQRCodeStack.Navigator>
);
const UnlockWithScreenStack = createNativeStackNavigator();
const UnlockWithScreenRoot = () => (
<UnlockWithScreenStack.Navigator
id="UnlockWithScreenRoot"
screenOptions={{ headerShown: false, statusBarStyle: 'auto', autoHideHomeIndicator: true }}
>
<UnlockWithScreenStack.Screen name="UnlockWithScreen" component={UnlockWith} />
</UnlockWithScreenStack.Navigator>
);
const ReorderWalletsStack = createNativeStackNavigator();
const ReorderWalletsStackRoot = () => {
const theme = useTheme();
return (
<ReorderWalletsStack.Navigator id="ReorderWalletsRoot" screenOptions={{ headerShadowVisible: false }}>
<ReorderWalletsStack.Screen
name="ReorderWalletsScreen"
component={ReorderWallets}
options={ReorderWallets.navigationOptions(theme)}
/>
</ReorderWalletsStack.Navigator>
);
};
const Drawer = createDrawerNavigator();
const DrawerRoot = () => {
const dimensions = useWindowDimensions();
const isLargeScreen = useMemo(() => {
return Platform.OS === 'android' ? isTablet() : (dimensions.width >= Dimensions.get('screen').width / 2 && isTablet()) || isDesktop;
}, [dimensions.width]);
const drawerStyle: DrawerNavigationOptions = useMemo(
() => ({
drawerPosition: I18nManager.isRTL ? 'right' : 'left',
drawerStyle: { width: isLargeScreen ? 320 : '0%' },
drawerType: isLargeScreen ? 'permanent' : 'back',
}),
[isLargeScreen],
);
return (
<Drawer.Navigator screenOptions={drawerStyle} drawerContent={DrawerList}>
<Drawer.Screen
name="Navigation"
component={Navigation}
options={{ headerShown: false, gestureHandlerProps: { enableTrackpadTwoFingerGesture: false } }}
/>
</Drawer.Navigator>
);
};
const ReceiveDetailsStack = createNativeStackNavigator();
const ReceiveDetailsStackRoot = () => {
const theme = useTheme();
return (
<ReceiveDetailsStack.Navigator id="ReceiveDetailsRoot" screenOptions={{ headerShadowVisible: false }} initialRouteName="ReceiveDetails">
<ReceiveDetailsStack.Screen name="ReceiveDetails" component={ReceiveDetails} options={ReceiveDetails.navigationOptions(theme)} />
</ReceiveDetailsStack.Navigator>
);
};
const WalletXpubStack = createNativeStackNavigator();
const WalletXpubStackRoot = () => {
const theme = useTheme();
return (
<WalletXpubStack.Navigator
id="WalletXpubRoot"
screenOptions={{ headerShadowVisible: false, statusBarStyle: 'light' }}
initialRouteName="WalletXpub"
>
<WalletXpubStack.Screen
name="WalletXpub"
component={WalletXpub}
options={navigationStyle({
closeButton: true,
headerBackVisible: false,
headerTitle: loc.wallets.xpub_title,
})(theme)}
/>
</WalletXpubStack.Navigator>
);
};
const SignVerifyStack = createNativeStackNavigator();
const SignVerifyStackRoot = () => {
const theme = useTheme();
return (
<SignVerifyStack.Navigator
id="SignVerifyRoot"
screenOptions={{ headerShadowVisible: false, statusBarStyle: 'light' }}
initialRouteName="SignVerify"
>
<SignVerifyStack.Screen name="SignVerify" component={SignVerify} options={SignVerify.navigationOptions(theme)} />
</SignVerifyStack.Navigator>
);
};
const WalletExportStack = createNativeStackNavigator();
const WalletExportStackRoot = () => {
const theme = useTheme();
return (
<WalletExportStack.Navigator
id="WalletExportRoot"
screenOptions={{ headerShadowVisible: false, statusBarStyle: 'light' }}
initialRouteName="WalletExport"
>
<WalletExportStack.Screen name="WalletExport" component={WalletExport} options={WalletExport.navigationOptions(theme)} />
</WalletExportStack.Navigator>
);
};
const LappBrowserStack = createNativeStackNavigator();
const LappBrowserStackRoot = () => {
const theme = useTheme();
return (
<LappBrowserStack.Navigator id="LappBrowserRoot" screenOptions={{ headerShadowVisible: false }} initialRouteName="LappBrowser">
<LappBrowserStack.Screen name="LappBrowser" component={LappBrowser} options={LappBrowser.navigationOptions(theme)} />
</LappBrowserStack.Navigator>
);
};
const InitStack = createNativeStackNavigator();
const InitRoot = () => {
const { walletsInitialized } = useContext(BlueStorageContext);
return (
<InitStack.Navigator initialRouteName="UnlockWithScreenRoot" screenOptions={{ animationTypeForReplace: 'push' }}>
{!walletsInitialized ? (
<InitStack.Screen name="UnlockWithScreenRoot" component={UnlockWithScreenRoot} options={{ headerShown: false }} />
) : (
<InitStack.Screen
name={isHandset ? 'Navigation' : 'DrawerRoot'}
component={isHandset ? Navigation : DrawerRoot}
options={{ headerShown: false }}
/>
)}
</InitStack.Navigator>
);
};
export type ViewEditMultisigCosignersStackParamsList = {
ViewEditMultisigCosigners: { walletId: string };
};
const ViewEditMultisigCosignersStack = createNativeStackNavigator<ViewEditMultisigCosignersStackParamsList>();
const ViewEditMultisigCosignersRoot = () => {
const theme = useTheme();
return (
<ViewEditMultisigCosignersStack.Navigator
id="ViewEditMultisigCosignersRoot"
initialRouteName="ViewEditMultisigCosigners"
screenOptions={{ headerShadowVisible: false, statusBarStyle: 'light' }}
>
<ViewEditMultisigCosignersStack.Screen
name="ViewEditMultisigCosigners"
component={ViewEditMultisigCosigners}
options={ViewEditMultisigCosigners.navigationOptions(theme)}
/>
</ViewEditMultisigCosignersStack.Navigator>
);
};
const ExportMultisigCoordinationSetupStack = createNativeStackNavigator();
const ExportMultisigCoordinationSetupRoot = () => {
const theme = useTheme();
return (
<ExportMultisigCoordinationSetupStack.Navigator
id="ExportMultisigCoordinationSetupRoot"
initialRouteName="ExportMultisigCoordinationSetup"
screenOptions={{ headerShadowVisible: false, statusBarStyle: 'light' }}
>
<ExportMultisigCoordinationSetupStack.Screen
name="ExportMultisigCoordinationSetup"
component={ExportMultisigCoordinationSetup}
options={ExportMultisigCoordinationSetup.navigationOptions(theme)}
/>
</ExportMultisigCoordinationSetupStack.Navigator>
);
};
export type PaymentCodeStackParamList = {
PaymentCode: { paymentCode: string };
PaymentCodesList: { walletID: string };
};
const PaymentCodeStack = createNativeStackNavigator<PaymentCodeStackParamList>();
const PaymentCodeStackRoot = () => {
return (
<PaymentCodeStack.Navigator id="PaymentCodeRoot" screenOptions={{ headerShadowVisible: false }} initialRouteName="PaymentCode">
<PaymentCodeStack.Screen name="PaymentCode" component={PaymentCode} options={{ headerTitle: loc.bip47.payment_code }} />
<PaymentCodeStack.Screen
name="PaymentCodesList"
component={PaymentCodesList}
options={{ headerTitle: loc.bip47.payment_codes_list }}
/>
</PaymentCodeStack.Navigator>
);
};
const RootStack = createNativeStackNavigator();
const NavigationDefaultOptions: NativeStackNavigationOptions = { headerShown: false, presentation: 'modal' };
const NavigationFormModalOptions: NativeStackNavigationOptions = {
headerShown: false,
presentation: 'formSheet',
};
const StatusBarLightOptions: NativeStackNavigationOptions = { statusBarStyle: 'light' };
const Navigation = () => {
return (
<RootStack.Navigator initialRouteName="UnlockWithScreenRoot" screenOptions={{ headerShadowVisible: false, statusBarStyle: 'auto' }}>
{/* stacks */}
<RootStack.Screen name="WalletsRoot" component={WalletsRoot} options={{ headerShown: false, statusBarTranslucent: false }} />
<RootStack.Screen name="AddWalletRoot" component={AddWalletRoot} options={NavigationFormModalOptions} />
<RootStack.Screen name="SendDetailsRoot" component={SendDetailsRoot} options={NavigationDefaultOptions} />
<RootStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
<RootStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} />
<RootStack.Screen name="AztecoRedeemRoot" component={AztecoRedeemRoot} options={NavigationDefaultOptions} />
{/* screens */}
<RootStack.Screen
name="WalletExportRoot"
component={WalletExportStackRoot}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }}
/>
<RootStack.Screen
name="ExportMultisigCoordinationSetupRoot"
component={ExportMultisigCoordinationSetupRoot}
options={NavigationDefaultOptions}
/>
<RootStack.Screen
name="ViewEditMultisigCosignersRoot"
component={ViewEditMultisigCosignersRoot}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }}
/>
<RootStack.Screen
name="WalletXpubRoot"
component={WalletXpubStackRoot}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }}
/>
<RootStack.Screen
name="SignVerifyRoot"
component={SignVerifyStackRoot}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }}
/>
<RootStack.Screen name="SelectWallet" component={SelectWallet} />
<RootStack.Screen name="ReceiveDetailsRoot" component={ReceiveDetailsStackRoot} options={NavigationDefaultOptions} />
<RootStack.Screen name="LappBrowserRoot" component={LappBrowserStackRoot} options={NavigationDefaultOptions} />
<RootStack.Screen
name="ScanQRCodeRoot"
component={ScanQRCodeRoot}
options={{
headerShown: false,
presentation: 'fullScreenModal',
statusBarHidden: true,
}}
initialParams={ScanQRCode.initialParams}
/>
<RootStack.Screen name="PaymentCodeRoot" component={PaymentCodeStackRoot} options={NavigationDefaultOptions} />
<InitStack.Screen
name="ReorderWallets"
component={ReorderWalletsStackRoot}
options={{
headerShown: false,
gestureEnabled: false,
presentation: 'modal',
}}
/>
</RootStack.Navigator>
);
};
export default InitRoot;

View File

@ -1,10 +1,10 @@
import { createNavigationContainerRef, ParamListBase, NavigationAction } from '@react-navigation/native';
import { createNavigationContainerRef, NavigationAction, ParamListBase, StackActions } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef<ParamListBase>();
export function navigate(name: string, params?: ParamListBase) {
export function navigate(name: string, params?: ParamListBase, options?: { merge: boolean }) {
if (navigationRef.isReady()) {
navigationRef.current?.navigate(name, params);
navigationRef.current?.navigate({ name, params, merge: options?.merge });
}
}
@ -14,11 +14,27 @@ export function dispatch(action: NavigationAction) {
}
}
export function navigateToWalletsList() {
navigate('WalletsList');
}
export function reset() {
if (navigationRef.isReady()) {
navigationRef.current?.reset({
index: 0,
routes: [{ name: 'UnlockWithScreenRoot' }],
routes: [{ name: 'UnlockWithScreen' }],
});
}
}
export function popToTop() {
if (navigationRef.isReady()) {
navigationRef.current?.dispatch(StackActions.popToTop());
}
}
export function pop() {
if (navigationRef.isReady()) {
navigationRef.current?.dispatch(StackActions.pop());
}
}

View File

@ -27,12 +27,12 @@ Community: [telegram group](https://t.me/bluewallet)
## BUILD & RUN IT
Please refer to the engines field in package.json file for the minimum required versions of Node and npm. It is preferred that you use an even-numbered version of Node as these are LTS versions.
Please refer to the engines field in package.json file for the minimum required versions of Node and yarn. It is preferred that you use an even-numbered version of Node as these are LTS versions.
To view the version of Node and npm in your environment, run the following in your console:
To view the version of Node and yarn in your environment, run the following in your console:
```
node --version && npm --version
node --version && yarn --version
```
* In your console:
@ -40,10 +40,10 @@ node --version && npm --version
```
git clone https://github.com/BlueWallet/BlueWallet.git
cd BlueWallet
npm install
yarn
```
Please make sure that your console is running the most stable versions of npm and node (even-numbered versions).
Please make sure that your console is running the most stable versions of node (even-numbered versions).
* To run on Android:
@ -69,7 +69,7 @@ The above command will build the app and install it. Once you launch the app it
```
npx pod-install
npm start
yarn start
```
In another terminal window within the BlueWallet folder:
@ -83,6 +83,7 @@ npx react-native run-ios
```
npx pod-install
yarn start
```
Open ios/BlueWallet.xcworkspace. Once the project loads, select the scheme/target BlueWallet. Click Run.
@ -90,7 +91,7 @@ Open ios/BlueWallet.xcworkspace. Once the project loads, select the scheme/targe
## TESTS
```bash
npm run test
yarn test
```

View File

@ -79,11 +79,22 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "6.6.0"
versionName "6.6.9"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
lintOptions {
abortOnError false
checkReleaseBuilds false
}
sourceSets {
main {
assets.srcDirs = ['src/main/assets', 'src/main/res/assets']
}
}
buildTypes {
release {
// Caution! In production, you need to generate your own keystore file.
@ -93,11 +104,21 @@ android {
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
}
}
}
task copyFiatUnits(type: Copy) {
from '../../models/fiatUnits.json'
into 'src/main/assets'
}
preBuild.dependsOn(copyFiatUnits)
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation 'androidx.core:core-ktx'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
@ -107,7 +128,9 @@ dependencies {
androidTestImplementation('com.wix:detox:+')
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
apply plugin: 'com.google.gms.google-services' // Google Services plugin
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle")
apply plugin: 'org.jetbrains.kotlin.android'; applyNativeModulesAppBuildGradle(project)

View File

@ -10,7 +10,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
@ -19,6 +19,10 @@
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.ACTION_GET_CONTENT" />
<uses-permission android:name="android.permission.ACTION_CREATE_DOCUMENT" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACTION_SCREEN_OFF"/>
<uses-permission android:name="android.permission.ACTION_SCREEN_ON"/>
<application
android:name=".MainApplication"
@ -33,30 +37,43 @@
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config">
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
android:value="BlueWallet notifications"/> <!-- YOUR NOTIFICATION CHANNEL NAME -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
android:value="Notifications about incoming payments"/> <!-- YOUR NOTIFICATION CHANNEL DESCRIPTION -->
<!-- Change the value to true to enable pop-up for in foreground (remote-only, for local use ignoreInForeground) -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_channel_name"
android:value="BlueWallet notifications" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_channel_description"
android:value="Notifications about incoming payments" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="true" />
<!-- Change the value to false if you don't want the creation of the default channel -->
<meta-data android:name="com.dieam.reactnativepushnotification.channel_create_default"
<meta-data
android:name="com.dieam.reactnativepushnotification.channel_create_default"
android:value="true" />
<!-- Change the resource name to your App's accent color - or any other color you want -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/white"/> <!-- or @android:color/{name} to use a standard color -->
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/white" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"
<receiver
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".BitcoinPriceWidget" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<action android:name="android.intent.action.SCREEN_ON"/>
<action android:name="android.intent.action.SCREEN_OFF"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/bitcoin_price_widget_info"/>
</receiver>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false">
@ -64,15 +81,6 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity android:name=".SplashActivity"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
@ -81,6 +89,10 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@ -109,7 +121,6 @@
android:mimeType="text/plain"
android:pathPattern=".*\\.psbt" />
</intent-filter>
</activity>
<meta-data android:name="com.bugsnag.android.API_KEY" android:value="17ba9059f676f1cc4f45d98182388b01" />

View File

@ -0,0 +1,408 @@
{
"USD": {
"endPointKey": "USD",
"locale": "en-US",
"source": "Kraken",
"symbol": "$",
"country": "United States (US Dollar)"
},
"AED": {
"endPointKey": "AED",
"locale": "ar-AE",
"source": "CoinGecko",
"symbol": "د.إ.",
"country": "United Arab Emirates (UAE Dirham)"
},
"ANG": {
"endPointKey": "ANG",
"locale": "en-SX",
"source": "CoinDesk",
"symbol": "ƒ",
"country": "Sint Maarten (Netherlands Antillean Guilder)"
},
"ARS": {
"endPointKey": "ARS",
"locale": "es-AR",
"source": "Yadio",
"symbol": "$",
"country": "Argentina (Argentine Peso)"
},
"AUD": {
"endPointKey": "AUD",
"locale": "en-AU",
"source": "CoinGecko",
"symbol": "$",
"country": "Australia (Australian Dollar)"
},
"AWG": {
"endPointKey": "AWG",
"locale": "nl-AW",
"source": "CoinDesk",
"symbol": "ƒ",
"country": "Aruba (Aruban Florin)"
},
"BHD": {
"endPointKey": "BHD",
"locale": "ar-BH",
"source": "CoinGecko",
"symbol": "د.ب.",
"country": "Bahrain (Bahraini Dinar)"
},
"BRL": {
"endPointKey": "BRL",
"locale": "pt-BR",
"source": "CoinGecko",
"symbol": "R$",
"country": "Brazil (Brazilian Real)"
},
"CAD": {
"endPointKey": "CAD",
"locale": "en-CA",
"source": "CoinGecko",
"symbol": "$",
"country": "Canada (Canadian Dollar)"
},
"CHF": {
"endPointKey": "CHF",
"locale": "de-CH",
"source": "CoinGecko",
"symbol": "CHF",
"country": "Switzerland (Swiss Franc)"
},
"CLP": {
"endPointKey": "CLP",
"locale": "es-CL",
"source": "Yadio",
"symbol": "$",
"country": "Chile (Chilean Peso)"
},
"CNY": {
"endPointKey": "CNY",
"locale": "zh-CN",
"source": "Coinbase",
"symbol": "¥",
"country": "China (Chinese Yuan)"
},
"COP": {
"endPointKey": "COP",
"locale": "es-CO",
"source": "CoinDesk",
"symbol": "$",
"country": "Colombia (Colombian Peso)"
},
"CZK": {
"endPointKey": "CZK",
"locale": "cs-CZ",
"source": "CoinGecko",
"symbol": "Kč",
"country": "Czech Republic (Czech Koruna)"
},
"DKK": {
"endPointKey": "DKK",
"locale": "da-DK",
"source": "CoinGecko",
"symbol": "kr",
"country": "Denmark (Danish Krone)"
},
"EUR": {
"endPointKey": "EUR",
"locale": "en-IE",
"source": "Kraken",
"symbol": "€",
"country": "European Union (Euro)"
},
"GBP": {
"endPointKey": "GBP",
"locale": "en-GB",
"source": "Kraken",
"symbol": "£",
"country": "United Kingdom (British Pound)"
},
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",
"source": "CoinDesk",
"symbol": "HRK",
"country": "Croatia (Croatian Kuna)"
},
"HUF": {
"endPointKey": "HUF",
"locale": "hu-HU",
"source": "CoinGecko",
"symbol": "Ft",
"country": "Hungary (Hungarian Forint)"
},
"IDR": {
"endPointKey": "IDR",
"locale": "id-ID",
"source": "CoinGecko",
"symbol": "Rp",
"country": "Indonesia (Indonesian Rupiah)"
},
"ILS": {
"endPointKey": "ILS",
"locale": "he-IL",
"source": "CoinGecko",
"symbol": "₪",
"country": "Israel (Israeli New Shekel)"
},
"INR": {
"endPointKey": "INR",
"locale": "hi-IN",
"source": "wazirx",
"symbol": "₹",
"country": "India (Indian Rupee)"
},
"IRR": {
"endPointKey": "IRR",
"locale": "fa-IR",
"source": "Exir",
"symbol": "﷼",
"country": "Iran (Iranian Rial)"
},
"IRT": {
"endPointKey": "IRT",
"locale": "fa-IR",
"source": "Exir",
"symbol": "تومان",
"country": "Iran (Iranian Toman)"
},
"ISK": {
"endPointKey": "ISK",
"locale": "is-IS",
"source": "CoinDesk",
"symbol": "kr",
"country": "Iceland (Icelandic Króna)"
},
"JPY": {
"endPointKey": "JPY",
"locale": "ja-JP",
"source": "CoinGecko",
"symbol": "¥",
"country": "Japan (Japanese Yen)"
},
"KES": {
"endPointKey": "KES",
"locale": "en-KE",
"source": "CoinDesk",
"symbol": "Ksh",
"country": "Kenya (Kenyan Shilling)"
},
"KRW": {
"endPointKey": "KRW",
"locale": "ko-KR",
"source": "CoinGecko",
"symbol": "₩",
"country": "South Korea (South Korean Won)"
},
"KWD": {
"endPointKey": "KWD",
"locale": "ar-KW",
"source": "CoinGecko",
"symbol": "د.ك.",
"country": "Kuwait (Kuwaiti Dinar)"
},
"LBP": {
"endPointKey": "LBP",
"locale": "ar-LB",
"source": "YadioConvert",
"symbol": "ل.ل.",
"country": "Lebanon (Lebanese Pound)"
},
"LKR": {
"endPointKey": "LKR",
"locale": "si-LK",
"source": "CoinGecko",
"symbol": "රු.",
"country": "Sri Lanka (Sri Lankan Rupee)"
},
"MXN": {
"endPointKey": "MXN",
"locale": "es-MX",
"source": "CoinGecko",
"symbol": "$",
"country": "Mexico (Mexican Peso)"
},
"MYR": {
"endPointKey": "MYR",
"locale": "ms-MY",
"source": "CoinGecko",
"symbol": "RM",
"country": "Malaysia (Malaysian Ringgit)"
},
"MZN": {
"endPointKey": "MZN",
"locale": "seh-MZ",
"source": "CoinDesk",
"symbol": "MTn",
"country": "Mozambique (Mozambican Metical)"
},
"NGN": {
"endPointKey": "NGN",
"locale": "en-NG",
"source": "CoinGecko",
"symbol": "₦",
"country": "Nigeria (Nigerian Naira)"
},
"NOK": {
"endPointKey": "NOK",
"locale": "nb-NO",
"source": "CoinGecko",
"symbol": "kr",
"country": "Norway (Norwegian Krone)"
},
"NZD": {
"endPointKey": "NZD",
"locale": "en-NZ",
"source": "CoinGecko",
"symbol": "$",
"country": "New Zealand (New Zealand Dollar)"
},
"OMR": {
"endPointKey": "OMR",
"locale": "ar-OM",
"source": "CoinDesk",
"symbol": "ر.ع.",
"country": "Oman (Omani Rial)"
},
"PHP": {
"endPointKey": "PHP",
"locale": "en-PH",
"source": "CoinGecko",
"symbol": "₱",
"country": "Philippines (Philippine Peso)"
},
"PLN": {
"endPointKey": "PLN",
"locale": "pl-PL",
"source": "CoinGecko",
"symbol": "zł",
"country": "Poland (Polish Zloty)"
},
"QAR": {
"endPointKey": "QAR",
"locale": "ar-QA",
"source": "CoinDesk",
"symbol": "ر.ق.",
"country": "Qatar (Qatari Riyal)"
},
"RON": {
"endPointKey": "RON",
"locale": "ro-RO",
"source": "BNR",
"symbol": "lei",
"country": "Romania (Romanian Leu)"
},
"RUB": {
"endPointKey": "RUB",
"locale": "ru-RU",
"source": "CoinGecko",
"symbol": "₽",
"country": "Russia (Russian Ruble)"
},
"SAR": {
"endPointKey": "SAR",
"locale": "ar-SA",
"source": "CoinGecko",
"symbol": "ر.س.",
"country": "Saudi Arabia (Saudi Riyal)"
},
"SEK": {
"endPointKey": "SEK",
"locale": "sv-SE",
"source": "CoinGecko",
"symbol": "kr",
"country": "Sweden (Swedish Krona)"
},
"SGD": {
"endPointKey": "SGD",
"locale": "zh-SG",
"source": "CoinGecko",
"symbol": "S$",
"country": "Singapore (Singapore Dollar)"
},
"THB": {
"endPointKey": "THB",
"locale": "th-TH",
"source": "CoinGecko",
"symbol": "฿",
"country": "Thailand (Thai Baht)"
},
"TRY": {
"endPointKey": "TRY",
"locale": "tr-TR",
"source": "CoinGecko",
"symbol": "₺",
"country": "Turkey (Turkish Lira)"
},
"TWD": {
"endPointKey": "TWD",
"locale": "zh-Hant-TW",
"source": "CoinGecko",
"symbol": "NT$",
"country": "Taiwan (New Taiwan Dollar)"
},
"TZS": {
"endPointKey": "TZS",
"locale": "en-TZ",
"source": "CoinDesk",
"symbol": "TSh",
"country": "Tanzania (Tanzanian Shilling)"
},
"UAH": {
"endPointKey": "UAH",
"locale": "uk-UA",
"source": "CoinGecko",
"symbol": "₴",
"country": "Ukraine (Ukrainian Hryvnia)"
},
"UGX": {
"endPointKey": "UGX",
"locale": "en-UG",
"source": "CoinDesk",
"symbol": "USh",
"country": "Uganda (Ugandan Shilling)"
},
"UYU": {
"endPointKey": "UYU",
"locale": "es-UY",
"source": "CoinDesk",
"symbol": "$",
"country": "Uruguay (Uruguayan Peso)"
},
"VEF": {
"endPointKey": "VEF",
"locale": "es-VE",
"source": "CoinGecko",
"symbol": "Bs.",
"country": "Venezuela (Venezuelan Bolívar Fuerte)"
},
"VES": {
"endPointKey": "VES",
"locale": "es-VE",
"source": "Yadio",
"symbol": "Bs.",
"country": "Venezuela (Venezuelan Bolívar Soberano)"
},
"XAF": {
"endPointKey": "XAF",
"locale": "fr-CF",
"source": "CoinDesk",
"symbol": "Fr",
"country": "Central African Republic (Central African Franc)"
},
"ZAR": {
"endPointKey": "ZAR",
"locale": "en-ZA",
"source": "CoinGecko",
"symbol": "R",
"country": "South Africa (South African Rand)"
},
"GHS": {
"endPointKey": "GHS",
"locale": "en-GH",
"source": "CoinDesk",
"symbol": "₵",
"country": "Ghana (Ghanaian Cedi)"
}
}

View File

@ -0,0 +1,34 @@
package io.bluewallet.bluewallet
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.WorkManager
class BitcoinPriceWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d("BitcoinPriceWidget", "onUpdate called")
WidgetUpdateWorker.scheduleWork(context)
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
Log.d("BitcoinPriceWidget", "onEnabled called")
WidgetUpdateWorker.scheduleWork(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
Log.d("BitcoinPriceWidget", "onDisabled called")
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
}
override fun onReceive(context: Context, intent:Intent) {
super.onReceive(context, intent)
Log.d("BitcoinPriceWidget", "onReceive called with action: ${intent.action}")
}
}

View File

@ -1,42 +0,0 @@
package io.bluewallet.bluewallet;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "BlueWallet";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
if (getResources().getBoolean(R.bool.portrait_only)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
/**
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
* (aka React 18) with two boolean flags.
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new DefaultReactActivityDelegate(
this,
getMainComponentName(),
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled());
}
}

View File

@ -0,0 +1,39 @@
package io.bluewallet.bluewallet
import android.content.pm.ActivityInfo
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
override fun getMainComponentName(): String {
return "BlueWallet"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
if (resources.getBoolean(R.bool.portrait_only)) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
/**
* Returns the instance of the [ReactActivityDelegate]. Here we use a util class [DefaultReactActivityDelegate]
* which allows you to easily enable Fabric and Concurrent React (aka React 18) with two boolean flags.
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return DefaultReactActivityDelegate(
this,
mainComponentName,
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.fabricEnabled
)
}
}

View File

@ -1,69 +0,0 @@
package io.bluewallet.bluewallet;
import android.app.Application;
import android.content.Context;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import com.facebook.react.modules.i18nmanager.I18nUtil;
import java.util.List;
import com.bugsnag.android.Bugsnag;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new DefaultReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return packages;
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
Bugsnag.start(this);
I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
sharedI18nUtilInstance.allowRTL(getApplicationContext(), true);
SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
}
}

View File

@ -0,0 +1,57 @@
package io.bluewallet.bluewallet
import android.app.Application
import android.content.Context
import com.bugsnag.android.Bugsnag
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader
import com.facebook.react.modules.i18nmanager.I18nUtil
class MainApplication : Application(), ReactApplication {
private val mReactNativeHost = object : DefaultReactNativeHost(this) {
override fun getUseDeveloperSupport() = BuildConfig.DEBUG
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
// Packages that cannot be autolinked yet can be added manually here, for example:
return packages
}
override fun getJSMainModuleName() = "index"
override val isNewArchEnabled = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled = BuildConfig.IS_HERMES_ENABLED
}
override fun getReactNativeHost() = mReactNativeHost
override fun onCreate() {
super.onCreate()
val sharedI18nUtilInstance = I18nUtil.getInstance()
sharedI18nUtilInstance.allowRTL(applicationContext, true)
SoLoader.init(this, /* native exopackage */ false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load()
}
val sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
// Retrieve the "donottrack" value. Default to "0" if not found.
val isDoNotTrackEnabled = sharedPref.getString("donottrack", "0")
// Check if do not track is not enabled and initialize Bugsnag if so
if (isDoNotTrackEnabled != "1") {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this)
}
}
}

View File

@ -0,0 +1,99 @@
package io.bluewallet.bluewallet
import android.content.Context
import android.util.Log
import org.json.JSONObject
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
object MarketAPI {
private const val TAG = "MarketAPI"
private const val HARD_CODED_JSON = "{\n" +
" \"USD\": {\n" +
" \"endPointKey\": \"USD\",\n" +
" \"locale\": \"en-US\",\n" +
" \"source\": \"Kraken\",\n" +
" \"symbol\": \"$\",\n" +
" \"country\": \"United States (US Dollar)\"\n" +
" }\n" +
"}"
var baseUrl: String? = null
fun fetchPrice(context: Context, currency: String): String? {
return try {
val json = JSONObject(HARD_CODED_JSON)
val currencyInfo = json.getJSONObject(currency)
val source = currencyInfo.getString("source")
val endPointKey = currencyInfo.getString("endPointKey")
val urlString = buildURLString(source, endPointKey)
Log.d(TAG, "Fetching price from URL: $urlString")
val url = URL(urlString)
val urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "GET"
urlConnection.connect()
val responseCode = urlConnection.responseCode
if (responseCode != 200) {
Log.e(TAG, "Failed to fetch price. Response code: $responseCode")
return null
}
val reader = InputStreamReader(urlConnection.inputStream)
val jsonResponse = StringBuilder()
val buffer = CharArray(1024)
var read: Int
while (reader.read(buffer).also { read = it } != -1) {
jsonResponse.append(buffer, 0, read)
}
parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey)
} catch (e: Exception) {
Log.e(TAG, "Error fetching price", e)
null
}
}
private fun buildURLString(source: String, endPointKey: String): String {
return if (baseUrl != null) {
baseUrl + endPointKey
} else {
when (source) {
"Yadio" -> "https://api.yadio.io/json/$endPointKey"
"YadioConvert" -> "https://api.yadio.io/convert/1/BTC/$endPointKey"
"Exir" -> "https://api.exir.io/v1/ticker?symbol=btc-irt"
"wazirx" -> "https://api.wazirx.com/api/v2/tickers/btcinr"
"Bitstamp" -> "https://www.bitstamp.net/api/v2/ticker/btc${endPointKey.lowercase()}"
"Coinbase" -> "https://api.coinbase.com/v2/prices/BTC-${endPointKey.uppercase()}/buy"
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
}
}
}
private fun parseJSONBasedOnSource(jsonString: String, source: String, endPointKey: String): String? {
return try {
val json = JSONObject(jsonString)
when (source) {
"Yadio" -> json.getJSONObject(endPointKey).getString("price")
"YadioConvert" -> json.getString("rate")
"CoinGecko" -> json.getJSONObject("bitcoin").getString(endPointKey.lowercase())
"Exir" -> json.getString("last")
"Bitstamp" -> json.getString("last")
"wazirx" -> json.getJSONObject("ticker").getString("buy")
"Coinbase" -> json.getJSONObject("data").getString("amount")
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
else -> null
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing price", e)
null
}
}
}

View File

@ -1,26 +0,0 @@
package io.bluewallet.bluewallet; // Replace with your package name
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import androidx.appcompat.app.AppCompatActivity;
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.splash_screen); // Replace with your layout name
int SPLASH_DISPLAY_LENGTH = 1000; // Splash screen duration in milliseconds
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent mainIntent = new Intent(SplashActivity.this, MainActivity.class);
SplashActivity.this.startActivity(mainIntent);
SplashActivity.this.finish();
}
}, SPLASH_DISPLAY_LENGTH);
}
}

View File

@ -0,0 +1,168 @@
package io.bluewallet.bluewallet
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.work.*
import org.json.JSONObject
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
companion object {
const val TAG = "WidgetUpdateWorker"
const val WORK_NAME = "widget_update_work"
const val REPEAT_INTERVAL_MINUTES = 15L
fun scheduleWork(context: Context) {
val workRequest = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(
REPEAT_INTERVAL_MINUTES, TimeUnit.MINUTES
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
)
Log.d(TAG, "Scheduling work for widget updates, will run every $REPEAT_INTERVAL_MINUTES minutes")
}
}
override fun doWork(): Result {
Log.d(TAG, "Widget update worker running")
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
val sharedPref = applicationContext.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)
val preferredCurrency = sharedPref.getString("preferredCurrency", "USD")
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", "en-US")
// Show loading indicator
views.setViewVisibility(R.id.loading_indicator, View.VISIBLE)
views.setViewVisibility(R.id.price_value, View.GONE)
views.setViewVisibility(R.id.last_updated_label, View.GONE)
views.setViewVisibility(R.id.last_updated_time, View.GONE)
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
appWidgetManager.updateAppWidget(appWidgetIds, views)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
fetchPrice(preferredCurrency) { fetchedPrice, error ->
if (error != null) {
Log.e(TAG, "Error fetching price: $error")
views.setViewVisibility(R.id.loading_indicator, View.GONE)
views.setTextViewText(R.id.price_value, "Error fetching data")
views.setViewVisibility(R.id.price_value, View.VISIBLE)
} else {
val previousPrice = sharedPref.getString("previous_price", null)
val currentPrice = fetchedPrice?.toDouble()?.let { it.toInt() } // Remove cents
if (currentPrice == previousPrice?.toDouble()?.let { it.toInt() }) {
views.setTextViewText(R.id.last_updated_time, currentTime)
} else {
Log.d(TAG, "Fetch completed with price: $fetchedPrice at $currentTime. Previous price: $previousPrice")
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.forLanguageTag(preferredCurrencyLocale!!)).apply {
maximumFractionDigits = 0
}
views.setViewVisibility(R.id.loading_indicator, View.GONE)
views.setTextViewText(R.id.price_value, currencyFormat.format(currentPrice))
views.setTextViewText(R.id.last_updated_time, currentTime)
views.setViewVisibility(R.id.price_value, View.VISIBLE)
views.setViewVisibility(R.id.last_updated_label, View.VISIBLE)
views.setViewVisibility(R.id.last_updated_time, View.VISIBLE)
if (previousPrice != null) {
views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE)
views.setTextViewText(R.id.previous_price, currencyFormat.format(previousPrice.toDouble().toInt()))
if (currentPrice!! > previousPrice.toDouble().toInt()) {
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_up_float)
} else {
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_down_float)
}
} else {
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
}
savePrice(sharedPref, fetchedPrice!!)
}
}
appWidgetManager.updateAppWidget(appWidgetIds, views)
}
return Result.success()
}
private fun fetchPrice(currency: String?, callback: (String?, String?) -> Unit) {
val fiatUnitsJson = applicationContext.assets.open("fiatUnits.json").bufferedReader().use { it.readText() }
val json = JSONObject(fiatUnitsJson)
val currencyInfo = json.getJSONObject(currency ?: "USD")
val source = currencyInfo.getString("source")
val endPointKey = currencyInfo.getString("endPointKey")
val urlString = buildURLString(source, endPointKey)
Log.d(TAG, "Fetching price from URL: $urlString")
val url = URL(urlString)
val urlConnection = url.openConnection() as HttpURLConnection
try {
val reader = InputStreamReader(urlConnection.inputStream)
val jsonResponse = StringBuilder()
val buffer = CharArray(1024)
var read: Int
while (reader.read(buffer).also { read = it } != -1) {
jsonResponse.append(buffer, 0, read)
}
val responseJson = JSONObject(jsonResponse.toString())
val price = parseJSONBasedOnSource(responseJson, source, endPointKey)
callback(price, null)
} catch (e: Exception) {
Log.e(TAG, "Error fetching price", e)
callback(null, e.message)
} finally {
urlConnection.disconnect()
}
}
private fun buildURLString(source: String, endPointKey: String): String {
return when (source) {
"Yadio" -> "https://api.yadio.io/json/$endPointKey"
"YadioConvert" -> "https://api.yadio.io/convert/1/BTC/$endPointKey"
"Exir" -> "https://api.exir.io/v1/ticker?symbol=btc-irt"
"wazirx" -> "https://api.wazirx.com/api/v2/tickers/btcinr"
"Bitstamp" -> "https://www.bitstamp.net/api/v2/ticker/btc${endPointKey.lowercase()}"
"Coinbase" -> "https://api.coinbase.com/v2/prices/BTC-${endPointKey.uppercase()}/buy"
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
}
}
private fun parseJSONBasedOnSource(json: JSONObject, source: String, endPointKey: String): String {
return when (source) {
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
"CoinGecko" -> json.getJSONObject("bitcoin").getString(endPointKey.lowercase())
"Coinbase" -> json.getJSONObject("data").getString("amount")
"Bitstamp" -> json.getString("last")
"wazirx" -> json.getJSONObject("ticker").getString("buy")
"Exir" -> json.getString("last")
"Yadio", "YadioConvert" -> json.getJSONObject(endPointKey).getString("price")
else -> throw IllegalArgumentException("Unsupported source: $source")
}
}
private fun savePrice(sharedPref: SharedPreferences, price: String) {
sharedPref.edit().putString("previous_price", price).apply()
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/widget_background" />
<corners android:radius="16dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,13 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_color">
<ImageView
android:layout_width="100dp"
android:layout_height="75dp"
android:scaleType="fitCenter"
android:layout_centerInParent="true"
android:src="@drawable/splash_icon" />
</RelativeLayout>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:background="@drawable/widget_background"
android:gravity="end">
<ProgressBar
android:id="@+id/loading_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
<TextView
android:id="@+id/last_updated_label"
style="@style/WidgetTextSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Last Updated"
android:textSize="12sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:visibility="gone"/>
<TextView
android:id="@+id/last_updated_time"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginTop="2dp"
android:visibility="gone"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
<TextView
android:id="@+id/price_value"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:visibility="gone">
<ImageView
android:id="@+id/price_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price_label"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="From:"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,4 +1,7 @@
<resources>
<color name="background_color">#000000</color> <!-- Dark background -->
<color name="background_color">#000000</color>
<color name="widget_background">#1B1B1F</color>
<color name="text_primary">#FFFFFF</color>
<color name="text_secondary">#D3D3D3</color>
</resources>

View File

@ -0,0 +1,9 @@
<resources>
<style name="WidgetTextPrimary">
<item name="android:textColor">@color/text_primary</item>
</style>
<style name="WidgetTextSecondary">
<item name="android:textColor">@color/text_secondary</item>
</style>
</resources>

View File

@ -1,4 +1,7 @@
<resources>
<color name="white">#FFF</color>
<color name="background_color">#FFFFFF</color> <!-- Light background -->
<color name="background_color">#FFFFFF</color>
<color name="widget_background">#FFFFFF</color>
<color name="text_primary">#0C234F</color>
<color name="text_secondary">#949494</color>
</resources>

View File

@ -0,0 +1 @@
<dimen name="widget_corner_radius">16dp</dimen>

View File

@ -1,3 +1,6 @@
<resources>
<string name="app_name">BlueWallet</string>
<string name="loading">Loading...</string>
<string name="last_updated">Last Updated</string>
<string name="from">From</string>
</resources>

View File

@ -1,9 +1,14 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style>
<style name="WidgetTextPrimary">
<item name="android:textColor">@color/text_primary</item>
</style>
<style name="WidgetTextSecondary">
<item name="android:textColor">@color/text_secondary</item>
</style>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minWidth="160dp"
android:minHeight="80dp"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen"
android:previewImage="@drawable/widget_preview"
android:resizeMode="none"
/>

View File

@ -4,9 +4,9 @@ buildscript {
ext {
minSdkVersion = 24
supportLibVersion = "28.0.0"
buildToolsVersion = "34.0.0"
compileSdkVersion = 33
targetSdkVersion = 33
buildToolsVersion = "33.0.0"
compileSdkVersion = 34
targetSdkVersion = 34
googlePlayServicesVersion = "16.+"
googlePlayServicesIidVersion = "16.0.1"
firebaseVersion = "17.3.4"
@ -14,8 +14,8 @@ buildscript {
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
kotlin_version = '1.9.22'
kotlinVersion = '1.9.20'
kotlin_version = '1.9.25'
kotlinVersion = '1.9.25'
}
repositories {
google()
@ -23,10 +23,10 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.bugsnag:bugsnag-android-gradle-plugin:5.+")
classpath 'com.google.gms:google-services:4.4.1' // Google Services plugin
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
classpath("com.bugsnag:bugsnag-android-gradle-plugin:5.+")
classpath 'com.google.gms:google-services:4.4.2' // Google Services plugin
}
}
@ -36,11 +36,7 @@ allprojects {
maven {
url("$rootDir/../node_modules/detox/Detox-android")
}
jcenter() {
content {
includeModule("com.wei.android.lib", "fingerprintidentify")
}
}
mavenCentral {
// We don't want to fetch react-native from Maven Central as there are
// older versions over there.
@ -60,14 +56,19 @@ allprojects {
google()
maven { url 'https://www.jitpack.io' }
}
configurations.all {
resolutionStrategy {
force 'androidx.activity:activity:1.5.1'
}
}
}
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
buildToolsVersion "34.0.0"
compileSdkVersion 33
buildToolsVersion "33.0.0"
compileSdkVersion 34
defaultConfig {
minSdkVersion 24
}
@ -83,9 +84,3 @@ subprojects {
}
}
subprojects { subproject ->
if(project['name'] == 'react-native-widget-center') {
project.configurations { compile { } }
}
}

View File

@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit

View File

@ -1,7 +1,7 @@
#Tue Jul 21 23:04:55 CDT 2020
#Wed Jun 26 20:30:20 AST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
networkTimeout=10000

View File

@ -1,98 +0,0 @@
type Utxo = {
height: number;
value: number;
address: string;
txId: string;
vout: number;
wif?: string;
};
export type ElectrumTransaction = {
txid: string;
hash: string;
version: number;
size: number;
vsize: number;
weight: number;
locktime: number;
vin: {
txid: string;
vout: number;
scriptSig: { asm: string; hex: string };
txinwitness: string[];
sequence: number;
addresses?: string[];
value?: number;
}[];
vout: {
value: number;
n: number;
scriptPubKey: {
asm: string;
hex: string;
reqSigs: number;
type: string;
addresses: string[];
};
}[];
blockhash: string;
confirmations?: number;
time: number;
blocktime: number;
};
type MempoolTransaction = {
height: 0;
tx_hash: string;
fee: number;
};
export async function connectMain(): Promise<void>;
export async function waitTillConnected(): Promise<boolean>;
export function forceDisconnect(): void;
export function getBalanceByAddress(address: string): Promise<{ confirmed: number; unconfirmed: number }>;
export function multiGetUtxoByAddress(addresses: string[]): Promise<Record<string, Utxo[]>>;
// TODO: this function returns different results based on the value of `verbose`, consider splitting it into two
export function multiGetTransactionByTxid(
txIds: string[],
batchsize: number = 45,
verbose: true = true,
): Promise<Record<string, ElectrumTransaction>>;
export function multiGetTransactionByTxid(txIds: string[], batchsize: number, verbose: false): Promise<Record<string, string>>;
export type MultiGetBalanceResponse = {
balance: number;
unconfirmed_balance: number;
addresses: Record<string, { confirmed: number; unconfirmed: number }>;
};
export function multiGetBalanceByAddress(addresses: string[], batchsize?: number): Promise<MultiGetBalanceResponse>;
export function getTransactionsByAddress(address: string): ElectrumTransaction[];
export function getMempoolTransactionsByAddress(address: string): Promise<MempoolTransaction[]>;
export function estimateCurrentBlockheight(): number;
export type ElectrumHistory = {
tx_hash: string;
height: number;
address: string;
};
export function multiGetHistoryByAddress(addresses: string[]): Promise<Record<string, ElectrumHistory[]>>;
export function estimateFees(): Promise<{ fast: number; medium: number; slow: number }>;
export function broadcastV2(txhex: string): Promise<string>;
export function getTransactionsFullByAddress(address: string): Promise<ElectrumTransaction[]>;
export function txhexToElectrumTransaction(txhes: string): ElectrumTransaction;
export function isDisabled(): Promise<boolean>;

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +0,0 @@
import { useContext, useEffect } from 'react';
import { BlueStorageContext } from './storage-context';
import DefaultPreference from 'react-native-default-preference';
import RNWidgetCenter from 'react-native-widget-center';
import AsyncStorage from '@react-native-async-storage/async-storage';
function WidgetCommunication() {
WidgetCommunication.WidgetCommunicationAllWalletsSatoshiBalance = 'WidgetCommunicationAllWalletsSatoshiBalance';
WidgetCommunication.WidgetCommunicationAllWalletsLatestTransactionTime = 'WidgetCommunicationAllWalletsLatestTransactionTime';
WidgetCommunication.WidgetCommunicationDisplayBalanceAllowed = 'WidgetCommunicationDisplayBalanceAllowed';
WidgetCommunication.LatestTransactionIsUnconfirmed = 'WidgetCommunicationLatestTransactionIsUnconfirmed';
const { wallets, walletsInitialized, isStorageEncrypted } = useContext(BlueStorageContext);
WidgetCommunication.isBalanceDisplayAllowed = async () => {
try {
const displayBalance = JSON.parse(await AsyncStorage.getItem(WidgetCommunication.WidgetCommunicationDisplayBalanceAllowed));
if (displayBalance !== null) {
return displayBalance;
} else {
return true;
}
} catch (e) {
return true;
}
};
WidgetCommunication.setBalanceDisplayAllowed = async value => {
await AsyncStorage.setItem(WidgetCommunication.WidgetCommunicationDisplayBalanceAllowed, JSON.stringify(value));
setValues();
};
WidgetCommunication.reloadAllTimelines = () => {
RNWidgetCenter.reloadAllTimelines();
};
const allWalletsBalanceAndTransactionTime = async () => {
if ((await isStorageEncrypted()) || !(await WidgetCommunication.isBalanceDisplayAllowed())) {
return { allWalletsBalance: 0, latestTransactionTime: 0 };
} else {
let balance = 0;
let latestTransactionTime = 0;
for (const wallet of wallets) {
if (wallet.hideBalance) {
continue;
}
balance += wallet.getBalance();
if (wallet.getLatestTransactionTimeEpoch() > latestTransactionTime) {
if (wallet.getTransactions()[0].confirmations === 0) {
latestTransactionTime = WidgetCommunication.LatestTransactionIsUnconfirmed;
} else {
latestTransactionTime = wallet.getLatestTransactionTimeEpoch();
}
}
}
return { allWalletsBalance: balance, latestTransactionTime };
}
};
const setValues = async () => {
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
const { allWalletsBalance, latestTransactionTime } = await allWalletsBalanceAndTransactionTime();
await DefaultPreference.set(WidgetCommunication.WidgetCommunicationAllWalletsSatoshiBalance, JSON.stringify(allWalletsBalance));
await DefaultPreference.set(
WidgetCommunication.WidgetCommunicationAllWalletsLatestTransactionTime,
JSON.stringify(latestTransactionTime),
);
RNWidgetCenter.reloadAllTimelines();
};
useEffect(() => {
if (walletsInitialized) {
setValues();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallets, walletsInitialized]);
return null;
}
export default WidgetCommunication;

View File

@ -1,8 +0,0 @@
function WidgetCommunication(props) {
WidgetCommunication.isBalanceDisplayAllowed = () => {};
WidgetCommunication.setBalanceDisplayAllowed = () => {};
WidgetCommunication.reloadAllTimelines = () => {};
return null;
}
export default WidgetCommunication;

View File

@ -1,13 +1,16 @@
import { getUniqueId } from 'react-native-device-info';
import Bugsnag from '@bugsnag/react-native';
const BlueApp = require('../BlueApp');
import { getUniqueId } from 'react-native-device-info';
import { BlueApp as BlueAppClass } from '../class';
const BlueApp = BlueAppClass.getInstance();
/**
* in case Bugsnag was started, but user decided to opt out while using the app, we have this
* flag `userHasOptedOut` and we forbid logging in `onError` handler
* @type {boolean}
*/
let userHasOptedOut = false;
let userHasOptedOut: boolean = false;
if (process.env.NODE_ENV !== 'development') {
(async () => {
@ -20,7 +23,6 @@ if (process.env.NODE_ENV !== 'development') {
}
Bugsnag.start({
collectUserIp: false,
user: {
id: uniqueID,
},
@ -31,7 +33,7 @@ if (process.env.NODE_ENV !== 'development') {
})();
}
const A = async event => {};
const A = async (event: string) => {};
A.ENUM = {
INIT: 'INIT',
@ -42,13 +44,13 @@ A.ENUM = {
APP_UNSUSPENDED: 'APP_UNSUSPENDED',
};
A.setOptOut = value => {
A.setOptOut = (value: boolean) => {
if (value) userHasOptedOut = true;
};
A.logError = errorString => {
A.logError = (errorString: string) => {
console.error(errorString);
Bugsnag.notify(new Error(String(errorString)));
};
module.exports = A;
export default A;

View File

@ -1,14 +0,0 @@
const base = require('base-x');
const Base43 = {
encode: function () {
throw new Error('not implemented');
},
decode: function (input) {
const x = base('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:');
return x.decode(input).toString('hex');
},
};
module.exports = Base43;

15
blue_modules/base43.ts Normal file
View File

@ -0,0 +1,15 @@
import base from 'base-x';
const Base43 = {
encode: function () {
throw new Error('not implemented');
},
decode: function (input: string): string {
const x = base('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:');
const uint8 = x.decode(input);
return Buffer.from(uint8).toString('hex');
},
};
export default Base43;

View File

@ -1,15 +1,15 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import BigNumber from 'bignumber.js';
import DefaultPreference from 'react-native-default-preference';
import * as RNLocalize from 'react-native-localize';
import BigNumber from 'bignumber.js';
import { FiatUnit, FiatUnitType, getFiatRate } from '../models/fiatUnit';
import WidgetCommunication from './WidgetCommunication';
const PREFERRED_CURRENCY_STORAGE_KEY = 'preferredCurrency';
const PREFERRED_CURRENCY_LOCALE_STORAGE_KEY = 'preferredCurrencyLocale';
const EXCHANGE_RATES_STORAGE_KEY = 'exchangeRates';
const LAST_UPDATED = 'LAST_UPDATED';
const GROUP_IO_BLUEWALLET = 'group.io.bluewallet.bluewallet';
export const GROUP_IO_BLUEWALLET = 'group.io.bluewallet.bluewallet';
const BTC_PREFIX = 'BTC_';
export interface CurrencyRate {
@ -32,16 +32,21 @@ async function setPreferredCurrency(item: FiatUnitType): Promise<void> {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(PREFERRED_CURRENCY_STORAGE_KEY, item.endPointKey);
await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, item.locale.replace('-', '_'));
// @ts-ignore: Convert to TSX later
WidgetCommunication.reloadAllTimelines();
}
async function getPreferredCurrency(): Promise<FiatUnitType> {
const preferredCurrency = JSON.parse((await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY)) || '{}');
const preferredCurrency = await AsyncStorage.getItem(PREFERRED_CURRENCY_STORAGE_KEY);
if (preferredCurrency) {
const parsedPreferredCurrency = JSON.parse(preferredCurrency);
preferredFiatCurrency = FiatUnit[parsedPreferredCurrency.endPointKey];
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.set(PREFERRED_CURRENCY_STORAGE_KEY, preferredCurrency.endPointKey);
await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, preferredCurrency.locale.replace('-', '_'));
return preferredCurrency;
await DefaultPreference.set(PREFERRED_CURRENCY_STORAGE_KEY, preferredFiatCurrency.endPointKey);
await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, preferredFiatCurrency.locale.replace('-', '_'));
return preferredFiatCurrency;
}
return FiatUnit.USD;
}
async function _restoreSavedExchangeRatesFromStorage(): Promise<void> {
@ -106,6 +111,11 @@ async function isRateOutdated(): Promise<boolean> {
}
}
async function restoreSavedPreferredFiatCurrencyAndExchangeFromStorage(): Promise<void> {
await _restoreSavedExchangeRatesFromStorage();
await _restoreSavedPreferredFiatCurrencyFromStorage();
}
async function initCurrencyDaemon(clearLastUpdatedTime: boolean = false): Promise<void> {
await _restoreSavedExchangeRatesFromStorage();
await _restoreSavedPreferredFiatCurrencyFromStorage();
@ -210,22 +220,23 @@ function _setSkipUpdateExchangeRate(): void {
}
export {
updateExchangeRate,
initCurrencyDaemon,
satoshiToLocalCurrency,
fiatToBTC,
satoshiToBTC,
BTCToLocalCurrency,
setPreferredCurrency,
getPreferredCurrency,
btcToSatoshi,
getCurrencySymbol,
_setPreferredFiatCurrency,
_setExchangeRate,
_setPreferredFiatCurrency,
_setSkipUpdateExchangeRate,
PREFERRED_CURRENCY_STORAGE_KEY,
BTCToLocalCurrency,
btcToSatoshi,
EXCHANGE_RATES_STORAGE_KEY,
fiatToBTC,
getCurrencySymbol,
getPreferredCurrency,
initCurrencyDaemon,
isRateOutdated,
LAST_UPDATED,
mostRecentFetchedRate,
isRateOutdated,
PREFERRED_CURRENCY_STORAGE_KEY,
restoreSavedPreferredFiatCurrencyAndExchangeFromStorage,
satoshiToBTC,
satoshiToLocalCurrency,
setPreferredCurrency,
updateExchangeRate,
};

View File

@ -1,7 +1,13 @@
// https://levelup.gitconnected.com/debounce-in-javascript-improve-your-applications-performance-5b01855e086
const debounce = <T extends (...args: any[]) => void>(func: T, wait: number) => {
// blue_modules/debounce.ts
type DebouncedFunction<T extends (...args: any[]) => void> = {
(this: ThisParameterType<T>, ...args: Parameters<T>): void;
cancel(): void;
};
const debounce = <T extends (...args: any[]) => void>(func: T, wait: number): DebouncedFunction<T> => {
let timeout: NodeJS.Timeout | null;
return function executedFunction(this: ThisParameterType<T>, ...args: Parameters<T>) {
const debouncedFunction = function (this: ThisParameterType<T>, ...args: Parameters<T>) {
const later = () => {
timeout = null;
func.apply(this, args);
@ -11,6 +17,15 @@ const debounce = <T extends (...args: any[]) => void>(func: T, wait: number) =>
}
timeout = setTimeout(later, wait);
};
debouncedFunction.cancel = () => {
if (timeout) {
clearTimeout(timeout);
}
timeout = null;
};
return debouncedFunction as DebouncedFunction<T>;
};
export default debounce;

View File

@ -1,17 +1,17 @@
// @ts-ignore: Ignore import errors
import CryptoJS from 'crypto-js';
import AES from 'crypto-js/aes';
import Utf8 from 'crypto-js/enc-utf8';
export function encrypt(data: string, password: string): string {
if (data.length < 10) throw new Error('data length cant be < 10');
const ciphertext = CryptoJS.AES.encrypt(data, password);
const ciphertext = AES.encrypt(data, password);
return ciphertext.toString();
}
export function decrypt(data: string, password: string): string | false {
const bytes = CryptoJS.AES.decrypt(data, password);
const bytes = AES.decrypt(data, password);
let str: string | false = false;
try {
str = bytes.toString(CryptoJS.enc.Utf8);
str = bytes.toString(Utf8);
} catch (e) {}
// For some reason, sometimes decrypt would succeed with an incorrect password and return random characters.

View File

@ -1,42 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native';
import { isTablet, getDeviceType } from 'react-native-device-info';
import { getDeviceType, isTablet as checkIsTablet } from 'react-native-device-info';
const isTablet: boolean = checkIsTablet();
const isDesktop: boolean = getDeviceType() === 'Desktop';
const isHandset: boolean = getDeviceType() === 'Handset';
const getIsTorCapable = (): boolean => {
let capable = true;
if (Platform.OS === 'android' && Platform.Version < 26) {
capable = false;
} else if (isDesktop) {
capable = false;
}
console.log('getIsTorCapable', capable);
return capable;
};
const IS_TOR_DAEMON_DISABLED: string = 'is_tor_daemon_disabled';
export async function setIsTorDaemonDisabled(disabled: boolean = true): Promise<void> {
return AsyncStorage.setItem(IS_TOR_DAEMON_DISABLED, disabled ? '1' : '');
}
export async function isTorDaemonDisabled(): Promise<boolean> {
let result: boolean;
try {
const savedValue = await AsyncStorage.getItem(IS_TOR_DAEMON_DISABLED);
if (savedValue === null) {
result = false;
} else {
result = savedValue === '1';
}
} catch {
result = true;
}
return result;
}
export const isHandset: boolean = getDeviceType() === 'Handset';
export const isTorCapable: boolean = getIsTorCapable();
export { isDesktop, isTablet };
export { isDesktop, isHandset, isTablet };

View File

@ -1,23 +1,34 @@
import LocalQRCode from '@remobile/react-native-qrcode-local-image';
import { Alert, Linking, PermissionsAndroid, Platform } from 'react-native';
import RNFS from 'react-native-fs';
import Share from 'react-native-share';
import loc from '../loc';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import { launchImageLibrary } from 'react-native-image-picker';
import { isDesktop } from './environment';
import Share from 'react-native-share';
import presentAlert from '../components/Alert';
import loc from '../loc';
import { isDesktop } from './environment';
import { readFile } from './react-native-bw-file-access';
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
const _sanitizeFileName = (fileName: string) => {
// Remove any path delimiters and non-alphanumeric characters except for -, _, and .
return fileName.replace(/[^a-zA-Z0-9\-_.]/g, '');
};
const _shareOpen = async (filePath: string) => {
const _shareOpen = async (filePath: string, showShareDialog: boolean = false) => {
return await Share.open({
url: 'file://' + filePath,
saveToFiles: isDesktop,
saveToFiles: isDesktop || !showShareDialog,
// @ts-ignore: Website claims this propertie exists, but TS cant find it. Send anyways.
useInternalStorage: Platform.OS === 'android',
failOnCancel: false,
})
.catch(error => {
presentAlert({ message: error.message });
console.log(error);
// If user cancels sharing, we dont want to show an error. for some reason we get 'CANCELLED' string as error
if (error.message !== 'CANCELLED') {
presentAlert({ message: error.message });
}
})
.finally(() => {
RNFS.unlink(filePath);
@ -28,11 +39,12 @@ const _shareOpen = async (filePath: string) => {
* Writes a file to fs, and triggers an OS sharing dialog, so user can decide where to put this file (share to cloud
* or perhabs messaging app). Provided filename should be just a file name, NOT a path
*/
export const writeFileAndExport = async function (filename: string, contents: string) {
export const writeFileAndExport = async function (fileName: string, contents: string, showShareDialog: boolean = true) {
const sanitizedFileName = _sanitizeFileName(fileName);
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`;
const filePath = RNFS.TemporaryDirectoryPath + `/${sanitizedFileName}`;
await RNFS.writeFile(filePath, contents);
await _shareOpen(filePath);
await _shareOpen(filePath, showShareDialog);
} else if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
title: loc.send.permission_storage_title,
@ -44,15 +56,18 @@ export const writeFileAndExport = async function (filename: string, contents: st
// In Android 13 no WRITE_EXTERNAL_STORAGE permission is needed
// @see https://stackoverflow.com/questions/76311685/permissionandroid-request-always-returns-never-ask-again-without-any-prompt-r
if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.Version >= 33) {
const filePath = RNFS.DocumentDirectoryPath + `/${filename}`;
if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.Version >= 30) {
const filePath = RNFS.DownloadDirectoryPath + `/${sanitizedFileName}`;
try {
await RNFS.writeFile(filePath, contents);
console.log(`file saved to ${filePath}`);
if (showShareDialog) {
await _shareOpen(filePath);
} else {
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { fileName: sanitizedFileName }) });
}
} catch (e: any) {
console.log(e);
presentAlert({ message: e.message });
}
} else {
console.log('Storage Permission: Denied');
@ -75,7 +90,7 @@ export const writeFileAndExport = async function (filename: string, contents: st
/**
* Opens & reads *.psbt files, and returns base64 psbt. FALSE if something went wrong (wont throw).
*/
export const openSignedTransaction = async function (): Promise<string | boolean> {
export const openSignedTransaction = async function (): Promise<string | false> {
try {
const res = await DocumentPicker.pickSingle({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles],
@ -106,7 +121,7 @@ const _readPsbtFileIntoBase64 = async function (uri: string): Promise<string> {
}
};
export const showImagePickerAndReadImage = () => {
export const showImagePickerAndReadImage = (): Promise<string | undefined> => {
return new Promise((resolve, reject) =>
launchImageLibrary(
{

View File

@ -1,5 +1,5 @@
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import DeviceInfo, { PowerState } from 'react-native-device-info';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
// Define a const enum for HapticFeedbackTypes
export const enum HapticFeedbackTypes {

View File

@ -5,12 +5,12 @@
* @see https://github.com/bitcoinjs/tiny-secp256k1/issues/84#issuecomment-1185682315
* @see https://github.com/bitcoinjs/bitcoinjs-lib/issues/1781
*/
import createHash from 'create-hash';
import { createHmac } from 'crypto';
import * as necc from '@noble/secp256k1';
import { TinySecp256k1Interface } from 'ecpair/src/ecpair';
import { TinySecp256k1Interface as TinySecp256k1InterfaceBIP32 } from 'bip32/types/bip32';
import { XOnlyPointAddTweakResult } from 'bitcoinjs-lib/src/types';
import createHash from 'create-hash';
import { createHmac } from 'crypto';
import { TinySecp256k1Interface } from 'ecpair/src/ecpair';
export interface TinySecp256k1InterfaceExtended {
pointMultiply(p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null;

View File

@ -1,19 +1,21 @@
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import { Alert, Platform } from 'react-native';
import Frisbee from 'frisbee';
import { getApplicationName, getVersion, getSystemName, getSystemVersion, hasGmsSync, hasHmsSync } from 'react-native-device-info';
import AsyncStorage from '@react-native-async-storage/async-storage';
import loc from '../loc';
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import Frisbee from 'frisbee';
import { findNodeHandle, Platform } from 'react-native';
import { getApplicationName, getSystemName, getSystemVersion, getVersion, hasGmsSync, hasHmsSync } from 'react-native-device-info';
import { requestNotifications } from 'react-native-permissions';
import PushNotification from 'react-native-push-notification';
const constants = require('./constants');
import loc from '../loc';
import ActionSheet from '../screen/ActionSheet';
import { groundControlUri } from './constants';
const PUSH_TOKEN = 'PUSH_TOKEN';
const GROUNDCONTROL_BASE_URI = 'GROUNDCONTROL_BASE_URI';
const NOTIFICATIONS_STORAGE = 'NOTIFICATIONS_STORAGE';
const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK_FLAG';
let alreadyConfigured = false;
let baseURI = constants.groundControlUri;
let baseURI = groundControlUri;
function Notifications(props) {
async function _setPushToken(token) {
@ -128,7 +130,7 @@ function Notifications(props) {
*
* @returns {Promise<boolean>} TRUE if permissions were obtained, FALSE otherwise
*/
Notifications.tryToObtainPermissions = async function () {
Notifications.tryToObtainPermissions = async function (anchor) {
if (!Notifications.isNotificationsCapable) return false;
if (await Notifications.getPushToken()) {
// we already have a token, no sense asking again, just configure pushes to register callbacks and we are done
@ -142,34 +144,29 @@ function Notifications(props) {
}
return new Promise(function (resolve) {
Alert.alert(
loc.settings.notifications,
loc.notifications.would_you_like_to_receive_notifications,
[
const options = [loc.notifications.no_and_dont_ask, loc.notifications.ask_me_later, loc._.ok];
ActionSheet.showActionSheetWithOptions(
{
text: loc.notifications.no_and_dont_ask,
onPress: () => {
AsyncStorage.setItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG, '1');
title: loc.settings.notifications,
message: loc.notifications.would_you_like_to_receive_notifications,
options,
cancelButtonIndex: 0, // Assuming 'no and don't ask' is still treated as the cancel action
anchor: anchor ? findNodeHandle(anchor.current) : undefined,
},
buttonIndex => {
switch (buttonIndex) {
case 0:
AsyncStorage.setItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG, '1').then(() => resolve(false));
break;
case 1:
resolve(false);
break;
case 2:
configureNotifications().then(resolve);
break;
}
},
style: 'cancel',
},
{
text: loc.notifications.ask_me_later,
onPress: () => {
resolve(false);
},
style: 'cancel',
},
{
text: loc._.ok,
onPress: async () => {
resolve(await configureNotifications());
},
style: 'default',
},
],
{ cancelable: false },
);
});
};
@ -258,11 +255,11 @@ function Notifications(props) {
};
Notifications.getDefaultUri = function () {
return constants.groundControlUri;
return groundControlUri;
};
Notifications.saveUri = async function (uri) {
baseURI = uri || constants.groundControlUri; // settign the url to use currently. if not set - use default
baseURI = uri || groundControlUri; // setting the url to use currently. if not set - use default
return AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, uri);
};

View File

@ -1,32 +0,0 @@
// @ts-ignore: Ignore
import type { Element } from 'react';
import { Text, TouchableNativeFeedback, TouchableWithoutFeedback, View, findNodeHandle, UIManager } from 'react-native';
type PopupMenuItem = { id?: any; label: string };
type OnPopupMenuItemSelect = (selectedPopupMenuItem: PopupMenuItem) => void;
type PopupAnchor = Element<typeof Text | typeof TouchableNativeFeedback | typeof TouchableWithoutFeedback | typeof View>;
type PopupMenuOptions = { onCancel?: () => void };
function showPopupMenu(
items: PopupMenuItem[],
onSelect: OnPopupMenuItemSelect,
anchor: PopupAnchor,
{ onCancel }: PopupMenuOptions = {},
): void {
UIManager.showPopupMenu(
// @ts-ignore: Ignore
findNodeHandle(anchor),
items.map(item => item.label),
function () {
if (onCancel) onCancel();
},
function (eventName: 'dismissed' | 'itemSelected', selectedIndex?: number) {
// @ts-ignore: Ignore
if (eventName === 'itemSelected') onSelect(items[selectedIndex]);
else onCancel && onCancel();
},
);
}
export type { PopupMenuItem, OnPopupMenuItemSelect, PopupMenuOptions };
export default showPopupMenu;

View File

@ -0,0 +1,70 @@
import { Platform } from 'react-native';
import { BlueApp as BlueAppClass } from '../class/';
import prompt from '../helpers/prompt';
import { showKeychainWipeAlert } from '../hooks/useBiometrics';
import loc from '../loc';
const BlueApp = BlueAppClass.getInstance();
// If attempt reaches 10, a wipe keychain option will be provided to the user.
let unlockAttempt = 0;
export const startAndDecrypt = async (retry?: boolean): Promise<boolean> => {
console.log('startAndDecrypt');
if (BlueApp.getWallets().length > 0) {
console.log('App already has some wallets, so we are in already started state, exiting startAndDecrypt');
return true;
}
await BlueApp.migrateKeys();
let password: undefined | string;
if (await BlueApp.storageIsEncrypted()) {
do {
password = await prompt((retry && loc._.bad_password) || loc._.enter_password, loc._.storage_is_encrypted, false);
} while (!password);
}
let success = false;
let wasException = false;
try {
success = await BlueApp.loadFromDisk(password);
} catch (error) {
// in case of exception reading from keystore, lets retry instead of assuming there is no storage and
// proceeding with no wallets
console.warn('exception loading from disk:', error);
wasException = true;
}
if (wasException) {
// retrying, but only once
try {
await new Promise(resolve => setTimeout(resolve, 3000)); // sleep
success = await BlueApp.loadFromDisk(password);
} catch (error) {
console.warn('second exception loading from disk:', error);
}
}
if (success) {
console.log('loaded from disk');
// We want to return true to let the UnlockWith screen that its ok to proceed.
return true;
}
if (password) {
// we had password and yet could not load/decrypt
unlockAttempt++;
if (unlockAttempt < 10 || Platform.OS !== 'ios') {
return startAndDecrypt(true);
} else {
unlockAttempt = 0;
showKeychainWipeAlert();
// We want to return false to let the UnlockWith screen that it is NOT ok to proceed.
return false;
}
} else {
unlockAttempt = 0;
// Return true because there was no wallet data in keychain. Proceed.
return true;
}
};
export default BlueApp;

View File

@ -1,286 +0,0 @@
import React, { createContext, useEffect, useState } from 'react';
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
import { FiatUnit } from '../models/fiatUnit';
import Notifications from '../blue_modules/notifications';
import loc, { STORAGE_KEY as LOC_STORAGE_KEY } from '../loc';
import { LegacyWallet, WatchOnlyWallet } from '../class';
import presentAlert from '../components/Alert';
import triggerHapticFeedback, { HapticFeedbackTypes } from './hapticFeedback';
import { PREFERRED_CURRENCY_STORAGE_KEY } from './currency';
const BlueApp = require('../BlueApp');
const BlueElectrum = require('./BlueElectrum');
const A = require('../blue_modules/analytics');
const _lastTimeTriedToRefetchWallet = {}; // hashmap of timestamps we _started_ refetching some wallet
export const WalletTransactionsStatus = { NONE: false, ALL: true };
export const BlueStorageContext = createContext();
export const BlueStorageProvider = ({ children }) => {
const [wallets, setWallets] = useState([]);
const [selectedWalletID, setSelectedWalletID] = useState();
const [walletTransactionUpdateStatus, setWalletTransactionUpdateStatus] = useState(WalletTransactionsStatus.NONE);
const [walletsInitialized, setWalletsInitialized] = useState(false);
const [preferredFiatCurrency, _setPreferredFiatCurrency] = useState(FiatUnit.USD);
const [language, _setLanguage] = useState();
const getPreferredCurrencyAsyncStorage = useAsyncStorage(PREFERRED_CURRENCY_STORAGE_KEY).getItem;
const getLanguageAsyncStorage = useAsyncStorage(LOC_STORAGE_KEY).getItem;
const [isHandOffUseEnabled, setIsHandOffUseEnabled] = useState(false);
const [isElectrumDisabled, setIsElectrumDisabled] = useState(true);
const [isPrivacyBlurEnabled, setIsPrivacyBlurEnabled] = useState(true);
const [currentSharedCosigner, setCurrentSharedCosigner] = useState('');
const [reloadTransactionsMenuActionFunction, setReloadTransactionsMenuActionFunction] = useState(() => {});
useEffect(() => {
BlueElectrum.isDisabled().then(setIsElectrumDisabled);
}, []);
useEffect(() => {
if (walletsInitialized) {
BlueElectrum.connectMain();
}
}, [walletsInitialized]);
useEffect(() => {
console.log(`Privacy blur: ${isPrivacyBlurEnabled}`);
if (!isPrivacyBlurEnabled) {
presentAlert({ message: 'Privacy blur has been disabled.' });
}
}, [isPrivacyBlurEnabled]);
const setIsHandOffUseEnabledAsyncStorage = value => {
setIsHandOffUseEnabled(value);
return BlueApp.setIsHandoffEnabled(value);
};
const saveToDisk = async (force = false) => {
if (BlueApp.getWallets().length === 0 && !force) {
console.log('not saving empty wallets array');
return;
}
BlueApp.tx_metadata = txMetadata;
await BlueApp.saveToDisk();
setWallets([...BlueApp.getWallets()]);
txMetadata = BlueApp.tx_metadata;
};
useEffect(() => {
setWallets(BlueApp.getWallets());
}, []);
useEffect(() => {
(async () => {
try {
const enabledHandoff = await BlueApp.isHandoffEnabled();
setIsHandOffUseEnabled(!!enabledHandoff);
} catch (_e) {
setIsHandOffUseEnabledAsyncStorage(false);
setIsHandOffUseEnabled(false);
}
})();
}, []);
const getPreferredCurrency = async () => {
const item = JSON.parse(await getPreferredCurrencyAsyncStorage()) ?? FiatUnit.USD;
_setPreferredFiatCurrency(item);
return item;
};
const setPreferredFiatCurrency = () => {
getPreferredCurrency();
};
const getLanguage = async () => {
const item = await getLanguageAsyncStorage();
_setLanguage(item);
};
const setLanguage = () => {
getLanguage();
};
useEffect(() => {
getPreferredCurrency();
getLanguageAsyncStorage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resetWallets = () => {
setWallets(BlueApp.getWallets());
};
const setWalletsWithNewOrder = wlts => {
BlueApp.wallets = wlts;
saveToDisk();
};
const refreshAllWalletTransactions = async (lastSnappedTo, showUpdateStatusIndicator = true) => {
let noErr = true;
try {
await BlueElectrum.waitTillConnected();
if (showUpdateStatusIndicator) {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
const paymentCodesStart = Date.now();
await fetchSenderPaymentCodes(lastSnappedTo);
const paymentCodesEnd = Date.now();
console.log('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
const balanceStart = +new Date();
await fetchWalletBalances(lastSnappedTo);
const balanceEnd = +new Date();
console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = +new Date();
await fetchWalletTransactions(lastSnappedTo);
const end = +new Date();
console.log('fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
console.warn(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
if (noErr) await saveToDisk(); // caching
};
const fetchAndSaveWalletTransactions = async walletID => {
const index = wallets.findIndex(wallet => wallet.getID() === walletID);
let noErr = true;
try {
// 5sec debounce:
if (+new Date() - _lastTimeTriedToRefetchWallet[walletID] < 5000) {
console.log('re-fetch wallet happens too fast; NOP');
return;
}
_lastTimeTriedToRefetchWallet[walletID] = +new Date();
await BlueElectrum.waitTillConnected();
setWalletTransactionUpdateStatus(walletID);
const balanceStart = +new Date();
await fetchWalletBalances(index);
const balanceEnd = +new Date();
console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = +new Date();
await fetchWalletTransactions(index);
const end = +new Date();
console.log('fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
console.warn(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
if (noErr) await saveToDisk(); // caching
};
const addWallet = wallet => {
BlueApp.wallets.push(wallet);
setWallets([...BlueApp.getWallets()]);
};
const deleteWallet = wallet => {
BlueApp.deleteWallet(wallet);
setWallets([...BlueApp.getWallets()]);
};
const addAndSaveWallet = async w => {
if (wallets.some(i => i.getID() === w.getID())) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: 'This wallet has been previously imported.' });
return;
}
const emptyWalletLabel = new LegacyWallet().getLabel();
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable);
w.setUserHasSavedExport(true);
addWallet(w);
await saveToDisk();
A(A.ENUM.CREATED_WALLET);
presentAlert({ message: w.type === WatchOnlyWallet.type ? loc.wallets.import_success_watchonly : loc.wallets.import_success });
Notifications.majorTomToGroundControl(w.getAllExternalAddresses(), [], []);
// start balance fetching at the background
await w.fetchBalance();
setWallets([...BlueApp.getWallets()]);
};
const setSharedCosigner = cosigner => {
setCurrentSharedCosigner(cosigner);
};
let txMetadata = BlueApp.tx_metadata || {};
const getTransactions = BlueApp.getTransactions;
const isAdvancedModeEnabled = BlueApp.isAdvancedModeEnabled;
const fetchSenderPaymentCodes = BlueApp.fetchSenderPaymentCodes;
const fetchWalletBalances = BlueApp.fetchWalletBalances;
const fetchWalletTransactions = BlueApp.fetchWalletTransactions;
const getBalance = BlueApp.getBalance;
const isStorageEncrypted = BlueApp.storageIsEncrypted;
const startAndDecrypt = BlueApp.startAndDecrypt;
const encryptStorage = BlueApp.encryptStorage;
const sleep = BlueApp.sleep;
const createFakeStorage = BlueApp.createFakeStorage;
const decryptStorage = BlueApp.decryptStorage;
const isPasswordInUse = BlueApp.isPasswordInUse;
const cachedPassword = BlueApp.cachedPassword;
const setIsAdvancedModeEnabled = BlueApp.setIsAdvancedModeEnabled;
const setDoNotTrack = BlueApp.setDoNotTrack;
const isDoNotTrackEnabled = BlueApp.isDoNotTrackEnabled;
const getItem = BlueApp.getItem;
const setItem = BlueApp.setItem;
return (
<BlueStorageContext.Provider
value={{
wallets,
setWalletsWithNewOrder,
txMetadata,
saveToDisk,
getTransactions,
selectedWalletID,
setSelectedWalletID,
addWallet,
deleteWallet,
currentSharedCosigner,
setSharedCosigner,
addAndSaveWallet,
setItem,
getItem,
isAdvancedModeEnabled,
fetchWalletBalances,
fetchWalletTransactions,
fetchAndSaveWalletTransactions,
isStorageEncrypted,
encryptStorage,
startAndDecrypt,
cachedPassword,
getBalance,
walletsInitialized,
setWalletsInitialized,
refreshAllWalletTransactions,
sleep,
createFakeStorage,
resetWallets,
decryptStorage,
isPasswordInUse,
setIsAdvancedModeEnabled,
setPreferredFiatCurrency,
preferredFiatCurrency,
setLanguage,
language,
isHandOffUseEnabled,
setIsHandOffUseEnabledAsyncStorage,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
setDoNotTrack,
isDoNotTrackEnabled,
isElectrumDisabled,
setIsElectrumDisabled,
isPrivacyBlurEnabled,
setIsPrivacyBlurEnabled,
reloadTransactionsMenuActionFunction,
setReloadTransactionsMenuActionFunction,
}}
>
{children}
</BlueStorageContext.Provider>
);
};

View File

@ -60,7 +60,7 @@ function encodeURv1(arg1, arg2) {
/**
*
* @param str {string} For PSBT, or coordination setup (translates to `bytes`) it expects hex string. For ms cosigner it expects plain json string
* @param len {number} lenght of each fragment
* @param len {number} length of each fragment
* @return {string[]} txt fragments ready to be displayed in dynamic QR
*/
function encodeURv2(str, len) {

View File

@ -1,186 +0,0 @@
import { useContext } from 'react';
import { Alert, Platform } from 'react-native';
import ReactNativeBiometrics, { BiometryTypes as RNBiometryTypes } from 'react-native-biometrics';
import PasscodeAuth from 'react-native-passcode-auth';
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
import loc from '../loc';
import * as NavigationService from '../NavigationService';
import { BlueStorageContext } from '../blue_modules/storage-context';
import presentAlert from '../components/Alert';
const STORAGEKEY = 'Biometrics';
const rnBiometrics = new ReactNativeBiometrics({ allowDeviceCredentials: true });
// Define a function type with properties
type DescribableFunction = {
(): null; // Call signature
FaceID: 'Face ID';
TouchID: 'Touch ID';
Biometrics: 'Biometrics';
isBiometricUseCapableAndEnabled: () => Promise<boolean>;
isDeviceBiometricCapable: () => Promise<boolean>;
setBiometricUseEnabled: (arg: boolean) => Promise<void>;
biometricType: () => Promise<keyof typeof RNBiometryTypes | undefined>;
isBiometricUseEnabled: () => Promise<boolean>;
unlockWithBiometrics: () => Promise<boolean>;
showKeychainWipeAlert: () => void;
};
// Bastard component/module. All properties are added in runtime
const Biometric = function () {
const { getItem, setItem } = useContext(BlueStorageContext);
Biometric.FaceID = 'Face ID';
Biometric.TouchID = 'Touch ID';
Biometric.Biometrics = 'Biometrics';
Biometric.isDeviceBiometricCapable = async () => {
try {
const { available } = await rnBiometrics.isSensorAvailable();
return available;
} catch (e) {
console.log('Biometrics isDeviceBiometricCapable failed');
console.log(e);
Biometric.setBiometricUseEnabled(false);
}
return false;
};
Biometric.biometricType = async () => {
try {
const { available, biometryType } = await rnBiometrics.isSensorAvailable();
if (!available) {
return undefined;
}
return biometryType;
} catch (e) {
console.log('Biometrics biometricType failed');
console.log(e);
return undefined; // Explicitly return false in case of an error
}
};
Biometric.isBiometricUseEnabled = async () => {
try {
const enabledBiometrics = await getItem(STORAGEKEY);
return !!enabledBiometrics;
} catch (_) {}
return false;
};
Biometric.isBiometricUseCapableAndEnabled = async () => {
const isBiometricUseEnabled = await Biometric.isBiometricUseEnabled();
const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable();
return isBiometricUseEnabled && isDeviceBiometricCapable;
};
Biometric.setBiometricUseEnabled = async value => {
await setItem(STORAGEKEY, value === true ? '1' : '');
};
Biometric.unlockWithBiometrics = async () => {
const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable();
if (isDeviceBiometricCapable) {
return new Promise(resolve => {
rnBiometrics
.simplePrompt({ promptMessage: loc.settings.biom_conf_identity })
.then((result: { success: any }) => {
if (result.success) {
resolve(true);
} else {
console.log('Biometrics authentication failed');
resolve(false);
}
})
.catch((error: Error) => {
console.log('Biometrics authentication error');
presentAlert({ message: error.message });
resolve(false);
});
});
}
return false;
};
const clearKeychain = async () => {
try {
console.log('Wiping keychain');
console.log('Wiping key: data');
await RNSecureKeyStore.set('data', JSON.stringify({ data: { wallets: [] } }), {
accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
console.log('Wiped key: data');
console.log('Wiping key: data_encrypted');
await RNSecureKeyStore.set('data_encrypted', '', { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY });
console.log('Wiped key: data_encrypted');
console.log('Wiping key: STORAGEKEY');
await RNSecureKeyStore.set(STORAGEKEY, '', { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY });
console.log('Wiped key: STORAGEKEY');
NavigationService.reset();
} catch (error: any) {
console.warn(error);
presentAlert({ message: error.message });
}
};
const requestDevicePasscode = async () => {
let isDevicePasscodeSupported: boolean | undefined = false;
try {
isDevicePasscodeSupported = await PasscodeAuth.isSupported();
if (isDevicePasscodeSupported) {
const isAuthenticated = await PasscodeAuth.authenticate();
if (isAuthenticated) {
Alert.alert(
loc.settings.encrypt_tstorage,
loc.settings.biom_remove_decrypt,
[
{ text: loc._.cancel, style: 'cancel' },
{
text: loc._.ok,
style: 'destructive',
onPress: async () => await clearKeychain(),
},
],
{ cancelable: false },
);
}
}
} catch {
isDevicePasscodeSupported = undefined;
}
if (isDevicePasscodeSupported === false) {
presentAlert({ message: loc.settings.biom_no_passcode });
}
};
Biometric.showKeychainWipeAlert = () => {
if (Platform.OS === 'ios') {
Alert.alert(
loc.settings.encrypt_tstorage,
loc.settings.biom_10times,
[
{
text: loc._.cancel,
onPress: () => {
console.log('Cancel Pressed');
},
style: 'cancel',
},
{
text: loc._.ok,
onPress: () => requestDevicePasscode(),
style: 'default',
},
],
{ cancelable: false },
);
}
};
return null;
} as DescribableFunction;
export default Biometric;
export { RNBiometryTypes as BiometricType };

View File

@ -35,6 +35,30 @@
"script_type": "p2wpkh",
"iterate_accounts": true
},
{
"description": "Non-standard legacy on BIP84 path",
"derivation_path": "m/84'/0'/0'",
"script_type": "p2pkh",
"iterate_accounts": true
},
{
"description": "Non-standard compatibility segwit on BIP84 path",
"derivation_path": "m/84'/0'/0'",
"script_type": "p2wpkh-p2sh",
"iterate_accounts": true
},
{
"description": "Non-standard legacy on BIP49 path",
"derivation_path": "m/49'/0'/0'",
"script_type": "p2pkh",
"iterate_accounts": true
},
{
"description": "Non-standard native segwit on BIP49 path",
"derivation_path": "m/49'/0'/0'",
"script_type": "p2wpkh",
"iterate_accounts": true
},
{
"description": "Copay native segwit",
"derivation_path": "m/44'/0'/0'",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
import { Linking, Alert } from 'react-native';
import loc from '../loc';
import { Alert, Linking } from 'react-native';
import { isDesktop } from '../blue_modules/environment';
import loc from '../loc';
export const openPrivacyDesktopSettings = () => {
if (isDesktop) {

42
class/contact-list.ts Normal file
View File

@ -0,0 +1,42 @@
import BIP47Factory from '@spsina/bip47';
import { SilentPayment } from 'silent-payments';
import ecc from '../blue_modules/noble_ecc';
import * as bitcoin from 'bitcoinjs-lib';
export class ContactList {
isBip47PaymentCodeValid(pc: string) {
try {
BIP47Factory(ecc).fromPaymentCode(pc);
return true;
} catch (_) {
return false;
}
}
isBip352PaymentCodeValid(pc: string) {
return SilentPayment.isPaymentCodeValid(pc);
}
isPaymentCodeValid(pc: string): boolean {
return this.isBip47PaymentCodeValid(pc) || this.isBip352PaymentCodeValid(pc);
}
isAddressValid(address: string): boolean {
try {
bitcoin.address.toOutputScript(address); // throws, no?
if (!address.toLowerCase().startsWith('bc1')) return true;
const decoded = bitcoin.address.fromBech32(address);
if (decoded.version === 0) return true;
if (decoded.version === 1 && decoded.data.length !== 32) return false;
if (decoded.version === 1 && !ecc.isPoint(Buffer.concat([Buffer.from([2]), decoded.data]))) return false;
if (decoded.version > 1) return false;
// ^^^ some day, when versions above 1 will be actually utilized, we would need to unhardcode this
return true;
} catch (e) {
return false;
}
}
}

View File

@ -1,18 +1,14 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import bip21, { TOptions } from 'bip21';
import * as bitcoin from 'bitcoinjs-lib';
import URL from 'url';
import { readFileOutsideSandbox } from '../blue_modules/fs';
import { Chain } from '../models/bitcoinUnits';
import { LightningCustodianWallet, WatchOnlyWallet } from './';
import { WatchOnlyWallet } from './';
import Azteco from './azteco';
import Lnurl from './lnurl';
import type { TWallet } from './wallets/types';
const BlueApp = require('../BlueApp');
const AppStorage = BlueApp.AppStorage;
type TCompletionHandlerParams = [string, object];
type TContext = {
wallets: TWallet[];
@ -183,7 +179,7 @@ class DeeplinkSchemaMatch {
},
]);
} else if (Lnurl.isLightningAddress(event.url)) {
// this might be not just an email but a lightning addres
// this might be not just an email but a lightning address
// @see https://lightningaddress.com
completionHandler([
'ScanLndInvoiceRoot',
@ -218,64 +214,6 @@ class DeeplinkSchemaMatch {
(async () => {
if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
switch (urlObject.host) {
case 'openlappbrowser': {
console.log('opening LAPP', urlObject.query.url);
// searching for LN wallet:
let haveLnWallet = false;
for (const w of context.wallets) {
if (w.type === LightningCustodianWallet.type) {
haveLnWallet = true;
}
}
if (!haveLnWallet) {
// need to create one
const w = new LightningCustodianWallet();
w.setLabel(w.typeReadable);
try {
const lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
if (lndhub) {
w.setBaseURI(lndhub);
w.init();
}
await w.createAccount();
await w.authorize();
} catch (Err) {
// giving up, not doing anything
return;
}
context.addWallet(w);
context.saveToDisk();
}
// now, opening lapp browser and navigating it to URL.
// looking for a LN wallet:
let lnWallet;
for (const w of context.wallets) {
if (w.type === LightningCustodianWallet.type) {
lnWallet = w;
break;
}
}
if (!lnWallet) {
// something went wrong
return;
}
completionHandler([
'LappBrowserRoot',
{
screen: 'LappBrowser',
params: {
walletID: lnWallet.getID(),
url: urlObject.query.url,
},
},
]);
break;
}
case 'setelectrumserver':
completionHandler([
'ElectrumSettings',

View File

@ -1,8 +1,9 @@
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import * as BlueElectrum from '../blue_modules/BlueElectrum';
import { HDSegwitBech32Wallet } from './wallets/hd-segwit-bech32-wallet';
import { SegwitBech32Wallet } from './wallets/segwit-bech32-wallet';
const bitcoin = require('bitcoinjs-lib');
const BlueElectrum = require('../blue_modules/BlueElectrum');
const BigNumber = require('bignumber.js');
/**
* Represents transaction of a BIP84 wallet.
@ -39,7 +40,7 @@ export class HDSegwitBech32Transaction {
* @private
*/
async _fetchTxhexAndDecode() {
const hexes = await BlueElectrum.multiGetTransactionByTxid([this._txid], 10, false);
const hexes = await BlueElectrum.multiGetTransactionByTxid([this._txid], false, 10);
this._txhex = hexes[this._txid];
if (!this._txhex) throw new Error("Transaction can't be found in mempool");
this._txDecoded = bitcoin.Transaction.fromHex(this._txhex);
@ -80,7 +81,7 @@ export class HDSegwitBech32Transaction {
* @private
*/
async _fetchRemoteTx() {
const result = await BlueElectrum.multiGetTransactionByTxid([this._txid || this._txDecoded.getId()]);
const result = await BlueElectrum.multiGetTransactionByTxid([this._txid || this._txDecoded.getId()], true);
this._remoteTx = Object.values(result)[0];
}
@ -154,7 +155,7 @@ export class HDSegwitBech32Transaction {
prevInputs.push(reversedHash);
}
const prevTransactions = await BlueElectrum.multiGetTransactionByTxid(prevInputs);
const prevTransactions = await BlueElectrum.multiGetTransactionByTxid(prevInputs, true);
// fetched, now lets count how much satoshis went in
let wentIn = 0;
@ -167,7 +168,7 @@ export class HDSegwitBech32Transaction {
value = new BigNumber(value).multipliedBy(100000000).toNumber();
wentIn += value;
const address = SegwitBech32Wallet.witnessToAddress(inp.witness[inp.witness.length - 1]);
utxos.push({ vout: inp.index, value, txId: reversedHash, address });
utxos.push({ vout: inp.index, value, txid: reversedHash, address });
}
}
@ -205,7 +206,7 @@ export class HDSegwitBech32Transaction {
unconfirmedUtxos.push({
vout: outp.n,
value,
txId: this._txid || this._txDecoded.getId(),
txid: this._txid || this._txDecoded.getId(),
address,
});
}

View File

@ -1,19 +1,20 @@
export * from './wallets/abstract-wallet';
export * from './wallets/legacy-wallet';
export * from './wallets/segwit-bech32-wallet';
export * from './wallets/taproot-wallet';
export * from './wallets/segwit-p2sh-wallet';
export * from './wallets/hd-segwit-p2sh-wallet';
export * from './wallets/hd-legacy-breadwallet-wallet';
export * from './wallets/hd-legacy-p2pkh-wallet';
export * from './wallets/watch-only-wallet';
export * from './wallets/lightning-custodian-wallet';
export * from './wallets/abstract-hd-wallet';
export * from './wallets/hd-segwit-bech32-wallet';
export * from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
export * from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
export * from './wallets/hd-aezeed-wallet';
export * from './wallets/multisig-hd-wallet';
export * from './wallets/slip39-wallets';
export * from './blue-app';
export * from './hd-segwit-bech32-transaction';
export * from './multisig-cosigner';
export * from './wallets/abstract-hd-wallet';
export * from './wallets/abstract-wallet';
export * from './wallets/hd-aezeed-wallet';
export * from './wallets/hd-legacy-breadwallet-wallet';
export * from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
export * from './wallets/hd-legacy-p2pkh-wallet';
export * from './wallets/hd-segwit-bech32-wallet';
export * from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
export * from './wallets/hd-segwit-p2sh-wallet';
export * from './wallets/legacy-wallet';
export * from './wallets/lightning-custodian-wallet';
export * from './wallets/multisig-hd-wallet';
export * from './wallets/segwit-bech32-wallet';
export * from './wallets/segwit-p2sh-wallet';
export * from './wallets/slip39-wallets';
export * from './wallets/taproot-wallet';
export * from './wallets/watch-only-wallet';

View File

@ -1,10 +1,10 @@
import { bech32 } from 'bech32';
import bolt11 from 'bolt11';
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
import createHash from 'create-hash';
import { createHmac } from 'crypto';
import CryptoJS from 'crypto-js';
import secp256k1 from 'secp256k1';
const CryptoJS = require('crypto-js');
const createHash = require('create-hash');
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
const ONION_REGEX = /^(http:\/\/[^/:@]+\.onion(?::\d{1,5})?)(\/.*)?$/; // regex for onion URL

View File

@ -1,7 +1,8 @@
import b58 from 'bs58check';
import { MultisigHDWallet } from './wallets/multisig-hd-wallet';
import BIP32Factory from 'bip32';
import b58 from 'bs58check';
import ecc from '../blue_modules/noble_ecc';
import { MultisigHDWallet } from './wallets/multisig-hd-wallet';
const bip32 = BIP32Factory(ecc);
export class MultisigCosigner {

View File

@ -1,48 +0,0 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AbstractWallet } from './wallets/abstract-wallet';
const BlueApp = require('../BlueApp');
export default class OnAppLaunch {
static STORAGE_KEY = 'ONAPP_LAUNCH_SELECTED_DEFAULT_WALLET_KEY';
static async isViewAllWalletsEnabled(): Promise<boolean> {
try {
const selectedDefaultWallet = await AsyncStorage.getItem(OnAppLaunch.STORAGE_KEY);
return selectedDefaultWallet === '' || selectedDefaultWallet === null;
} catch (_e) {
return true;
}
}
static async setViewAllWalletsEnabled(value: boolean) {
if (!value) {
const selectedDefaultWallet = await OnAppLaunch.getSelectedDefaultWallet();
if (!selectedDefaultWallet) {
const firstWallet = BlueApp.getWallets()[0];
await OnAppLaunch.setSelectedDefaultWallet(firstWallet.getID());
}
} else {
await AsyncStorage.setItem(OnAppLaunch.STORAGE_KEY, '');
}
}
static async getSelectedDefaultWallet(): Promise<AbstractWallet | boolean> {
let selectedWallet: AbstractWallet | false = false;
try {
const selectedWalletID = JSON.parse((await AsyncStorage.getItem(OnAppLaunch.STORAGE_KEY)) || 'null');
if (selectedWalletID) {
selectedWallet = BlueApp.getWallets().find((wallet: AbstractWallet) => wallet.getID() === selectedWalletID);
if (!selectedWallet) {
await AsyncStorage.setItem(OnAppLaunch.STORAGE_KEY, '');
}
}
} catch (_e) {
return false;
}
return selectedWallet;
}
static async setSelectedDefaultWallet(value: string) {
await AsyncStorage.setItem(OnAppLaunch.STORAGE_KEY, JSON.stringify(value));
}
}

View File

@ -1,8 +1,9 @@
import * as bitcoin from 'bitcoinjs-lib';
import presentAlert from '../components/Alert';
import { ECPairFactory } from 'ecpair';
import ecc from '../blue_modules/noble_ecc';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import ecc from '../blue_modules/noble_ecc';
import presentAlert from '../components/Alert';
const ECPair = ECPairFactory(ecc);
const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));

View File

@ -1,9 +1,8 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
const SHA256 = require('crypto-js/sha256');
const ENCHEX = require('crypto-js/enc-hex');
const ENCUTF8 = require('crypto-js/enc-utf8');
const AES = require('crypto-js/aes');
import AES from 'crypto-js/aes';
import ENCHEX from 'crypto-js/enc-hex';
import ENCUTF8 from 'crypto-js/enc-utf8';
import SHA256 from 'crypto-js/sha256';
export default class SyncedAsyncStorage {
defaultBaseUrl = 'https://bytes-store.herokuapp.com';

View File

@ -1,17 +1,17 @@
import { LegacyWallet } from './wallets/legacy-wallet';
import { HDSegwitP2SHWallet } from './wallets/hd-segwit-p2sh-wallet';
import { LightningCustodianWallet } from './wallets/lightning-custodian-wallet'; // Missing import
import { HDLegacyBreadwalletWallet } from './wallets/hd-legacy-breadwallet-wallet';
import { HDLegacyP2PKHWallet } from './wallets/hd-legacy-p2pkh-wallet';
import { WatchOnlyWallet } from './wallets/watch-only-wallet';
import { HDSegwitBech32Wallet } from './wallets/hd-segwit-bech32-wallet';
import { SegwitBech32Wallet } from './wallets/segwit-bech32-wallet';
import { HDLegacyElectrumSeedP2PKHWallet } from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
import { HDSegwitElectrumSeedP2WPKHWallet } from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
import { MultisigHDWallet } from './wallets/multisig-hd-wallet';
import { HDAezeedWallet } from './wallets/hd-aezeed-wallet';
import { SLIP39LegacyP2PKHWallet, SLIP39SegwitP2SHWallet, SLIP39SegwitBech32Wallet } from './wallets/slip39-wallets';
import { useTheme } from '../components/themes';
import { HDAezeedWallet } from './wallets/hd-aezeed-wallet';
import { HDLegacyBreadwalletWallet } from './wallets/hd-legacy-breadwallet-wallet';
import { HDLegacyElectrumSeedP2PKHWallet } from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
import { HDLegacyP2PKHWallet } from './wallets/hd-legacy-p2pkh-wallet';
import { HDSegwitBech32Wallet } from './wallets/hd-segwit-bech32-wallet';
import { HDSegwitElectrumSeedP2WPKHWallet } from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
import { HDSegwitP2SHWallet } from './wallets/hd-segwit-p2sh-wallet';
import { LegacyWallet } from './wallets/legacy-wallet';
import { LightningCustodianWallet } from './wallets/lightning-custodian-wallet'; // Missing import
import { MultisigHDWallet } from './wallets/multisig-hd-wallet';
import { SegwitBech32Wallet } from './wallets/segwit-bech32-wallet';
import { SLIP39LegacyP2PKHWallet, SLIP39SegwitBech32Wallet, SLIP39SegwitP2SHWallet } from './wallets/slip39-wallets';
import { WatchOnlyWallet } from './wallets/watch-only-wallet';
export default class WalletGradient {
static hdSegwitP2SHWallet: string[] = ['#007AFF', '#0040FF'];

View File

@ -1,6 +1,7 @@
import wif from 'wif';
import bip38 from 'bip38';
import wif from 'wif';
import loc from '../loc';
import {
HDAezeedWallet,
HDLegacyBreadwalletWallet,
@ -12,17 +13,16 @@ import {
LegacyWallet,
LightningCustodianWallet,
MultisigHDWallet,
SegwitBech32Wallet,
SegwitP2SHWallet,
SLIP39LegacyP2PKHWallet,
SLIP39SegwitBech32Wallet,
SLIP39SegwitP2SHWallet,
SegwitBech32Wallet,
SegwitP2SHWallet,
WatchOnlyWallet,
} from '.';
import type { TWallet } from './wallets/types';
import loc from '../loc';
import bip39WalletFormats from './bip39_wallet_formats.json'; // https://github.com/spesmilo/electrum/blob/master/electrum/bip39_wallet_formats.json
import bip39WalletFormatsBlueWallet from './bip39_wallet_formats_bluewallet.json';
import type { TWallet } from './wallets/types';
// https://github.com/bitcoinjs/bip32/blob/master/ts-src/bip32.ts#L43
export const validateBip32 = (path: string) => path.match(/^(m\/)?(\d+'?\/)*\d+'?$/) !== null;
@ -369,10 +369,8 @@ const startImport = (
yield { wallet: aezeed2 }; // not fetching txs or balances, fuck it, yolo, life is too short
}
// if it is multi-line string, then it is probably SLIP39 wallet
// each line - one share
// Let's try SLIP39
yield { progress: 'SLIP39' };
if (text.includes('\n')) {
const s1 = new SLIP39SegwitP2SHWallet();
s1.setSecret(text);
@ -403,7 +401,6 @@ const startImport = (
}
yield { wallet: s3 };
}
}
// is it BC-UR payload with multiple accounts?
yield { progress: 'BC-UR' };

View File

@ -1,24 +1,25 @@
/* eslint react/prop-types: "off", @typescript-eslint/ban-ts-comment: "off", camelcase: "off" */
import BIP47Factory, { BIP47Interface } from '@spsina/bip47';
import assert from 'assert';
import BigNumber from 'bignumber.js';
import BIP32Factory, { BIP32Interface } from 'bip32';
import * as bip39 from 'bip39';
import * as bitcoin from 'bitcoinjs-lib';
import { Transaction as BTransaction, Psbt } from 'bitcoinjs-lib';
import { Psbt, Transaction as BTransaction } from 'bitcoinjs-lib';
import b58 from 'bs58check';
import { CoinSelectReturnInput, CoinSelectTarget } from 'coinselect';
import { CoinSelectOutput, CoinSelectReturnInput } from 'coinselect';
import { ECPairFactory } from 'ecpair';
import { ECPairInterface } from 'ecpair/src/ecpair';
import type BlueElectrumNs from '../../blue_modules/BlueElectrum';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import { ElectrumHistory } from '../../blue_modules/BlueElectrum';
import ecc from '../../blue_modules/noble_ecc';
import { randomBytes } from '../rng';
import { AbstractHDWallet } from './abstract-hd-wallet';
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
import { CreateTransactionResult, CreateTransactionTarget, CreateTransactionUtxo, Transaction, Utxo } from './types';
import { SilentPayment, UTXOType as SPUTXOType, UTXO as SPUTXO } from 'silent-payments';
const ECPair = ECPairFactory(ecc);
const BlueElectrum: typeof BlueElectrumNs = require('../../blue_modules/BlueElectrum');
const bip32 = BIP32Factory(ecc);
const bip47 = BIP47Factory(ecc);
@ -31,10 +32,14 @@ type BalanceByIndex = {
* Electrum - means that it utilizes Electrum protocol for blockchain data
*/
export class AbstractHDElectrumWallet extends AbstractHDWallet {
static type = 'abstract';
static typeReadable = 'abstract';
static readonly type = 'abstract';
static readonly typeReadable = 'abstract';
static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68
static finalRBFSequence = 4294967295; // 0xFFFFFFFF
// @ts-ignore: override
public readonly type = AbstractHDElectrumWallet.type;
// @ts-ignore: override
public readonly typeReadable = AbstractHDElectrumWallet.typeReadable;
_balances_by_external_index: Record<number, BalanceByIndex>;
_balances_by_internal_index: Record<number, BalanceByIndex>;
@ -49,10 +54,44 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// BIP47
_enable_BIP47: boolean;
_payment_code: string;
_sender_payment_codes: string[];
_addresses_by_payment_code: Record<string, string[]>;
_next_free_payment_code_address_index: Record<string, number>;
/**
* payment codes of people who can pay us
*/
_receive_payment_codes: string[];
/**
* payment codes of people whom we can pay
*/
_send_payment_codes: string[];
/**
* joint addresses with remote counterparties, to receive funds
*/
_addresses_by_payment_code_receive: Record<string, string[]>;
/**
* receive index
*/
_next_free_payment_code_address_index_receive: Record<string, number>;
/**
* joint addresses with remote counterparties, whom we can send funds
*/
_addresses_by_payment_code_send: Record<string, string[]>;
/**
* send index
*/
_next_free_payment_code_address_index_send: Record<string, number>;
/**
* this is where we put transactions related to our PC receive addresses. this is both
* incoming transactions AND outgoing transactions (when we spend those funds)
*
*/
_txs_by_payment_code_index: Record<string, Transaction[][]>;
_balances_by_payment_code_index: Record<string, BalanceByIndex>;
_bip47_instance?: BIP47Interface;
@ -69,11 +108,14 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// BIP47
this._enable_BIP47 = false;
this._payment_code = '';
this._sender_payment_codes = [];
this._next_free_payment_code_address_index = {};
this._receive_payment_codes = [];
this._send_payment_codes = [];
this._next_free_payment_code_address_index_receive = {};
this._txs_by_payment_code_index = {};
this._addresses_by_payment_code_send = {};
this._next_free_payment_code_address_index_send = {};
this._balances_by_payment_code_index = {};
this._addresses_by_payment_code = {};
this._addresses_by_payment_code_receive = {};
}
/**
@ -87,7 +129,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (const bal of Object.values(this._balances_by_internal_index)) {
ret += bal.c;
}
for (const pc of this._sender_payment_codes) {
for (const pc of this._receive_payment_codes) {
ret += this._getBalancesByPaymentCodeIndex(pc).c;
}
return ret + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
@ -105,7 +147,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (const bal of Object.values(this._balances_by_internal_index)) {
ret += bal.u;
}
for (const pc of this._sender_payment_codes) {
for (const pc of this._receive_payment_codes) {
ret += this._getBalancesByPaymentCodeIndex(pc).u;
}
return ret;
@ -117,9 +159,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
async generateFromEntropy(user: Buffer) {
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0);
const buf = Buffer.concat([user, random], 32);
this.secret = bip39.entropyToMnemonic(buf.toString('hex'));
if (user.length !== 32 && user.length !== 16) {
throw new Error('Entropy has to be 16 or 32 bytes long');
}
this.secret = bip39.entropyToMnemonic(user.toString('hex'));
}
_getExternalWIFByIndex(index: number): string | false {
@ -291,8 +334,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
// next, bip47 addresses
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
let hasUnconfirmed = false;
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
@ -300,7 +343,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7;
if (hasUnconfirmed || this._txs_by_payment_code_index[pc][c].length === 0 || this._balances_by_payment_code_index[pc].u !== 0) {
addresses2fetch.push(this._getBIP47Address(pc, c));
addresses2fetch.push(this._getBIP47AddressReceive(pc, c));
}
}
}
@ -315,7 +358,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
// next, batch fetching each txid we got
const txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs));
const txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs), true);
// now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too.
// then we combine all this data (we need inputs to see source addresses and amounts)
@ -326,7 +369,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// ^^^^ not all inputs have txid, some of them are Coinbase (newly-created coins)
}
}
const vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids);
const vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids, true);
// fetched all transactions from our inputs. now we need to combine it.
// iterating all _our_ transactions:
@ -352,13 +395,14 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c].filter(tx => !!tx.confirmations);
}
}
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index
// now, we need to put transactions in all relevant `cells` of internal hashmaps:
// this._txs_by_internal_index, this._txs_by_external_index & this._txs_by_payment_code_index
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
for (const tx of Object.values(txdatas)) {
@ -442,11 +486,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
for (const tx of Object.values(txdatas)) {
for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this._getBIP47Address(pc, c)) !== -1) {
// since we are iterating PCs who can pay us, we can completely ignore `tx.vin` and only iterate `tx.vout`
for (const vout of tx.vout) {
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getBIP47AddressReceive(pc, c)) !== -1) {
// this TX is related to our address
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
@ -464,25 +509,6 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (!replaced) this._txs_by_payment_code_index[pc][c].push(clonedTx);
}
}
for (const vout of tx.vout) {
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getBIP47Address(pc, c)) !== -1) {
// this TX is related to our address
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
const { vin: txVin, vout: txVout, ...txRest } = tx;
const clonedTx = { ...txRest, inputs: txVin.slice(0), outputs: txVout.slice(0) };
// trying to replace tx if it exists already (because it has lower confirmations, for example)
let replaced = false;
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
replaced = true;
this._txs_by_internal_index[c][cc] = clonedTx;
}
}
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
}
}
}
}
}
@ -499,8 +525,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (const addressTxs of Object.values(this._txs_by_internal_index)) {
txs = txs.concat(addressTxs);
}
if (this._sender_payment_codes) {
for (const pc of this._sender_payment_codes) {
if (this._receive_payment_codes) {
for (const pc of this._receive_payment_codes) {
if (this._txs_by_payment_code_index[pc])
for (const addressTxs of Object.values(this._txs_by_payment_code_index[pc])) {
txs = txs.concat(addressTxs);
@ -519,10 +545,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
}
if (this._sender_payment_codes)
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + 1; c++) {
ownedAddressesHashmap[this._getBIP47Address(pc, c)] = true;
if (this._receive_payment_codes)
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + 1; c++) {
ownedAddressesHashmap[this._getBIP47AddressReceive(pc, c)] = true;
}
}
// hack: in case this code is called from LegacyWallet:
@ -549,6 +575,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber();
}
}
if (this.allowBIP47() && this.isBIP47Enabled()) {
tx.counterparty = this.getBip47CounterpartyByTx(tx);
}
ret.push(tx);
}
@ -655,7 +685,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
const generateChunkAddresses = (chunkNum: number) => {
const ret = [];
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
ret.push(this._getBIP47Address(paymentCode, c));
ret.push(this._getBIP47AddressReceive(paymentCode, c));
}
return ret;
};
@ -684,7 +714,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
c < Number(lastChunkWithUsedAddressesNum) * this.gap_limit + this.gap_limit;
c++
) {
const address = this._getBIP47Address(paymentCode, c);
const address = this._getBIP47AddressReceive(paymentCode, c);
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) {
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unused
}
@ -700,9 +730,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// doing binary search for last used address:
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000);
this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000);
if (this._sender_payment_codes) {
for (const pc of this._sender_payment_codes) {
this._next_free_payment_code_address_index[pc] = await this._binarySearchIterationForBIP47Address(pc, 1000);
if (this._receive_payment_codes) {
for (const pc of this._receive_payment_codes) {
this._next_free_payment_code_address_index_receive[pc] = await this._binarySearchIterationForBIP47Address(pc, 1000);
}
}
} // end rescanning fresh wallet
@ -726,13 +756,13 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = this.next_free_change_address_index; c < this.next_free_change_address_index + this.gap_limit; c++) {
lagAddressesToFetch.push(this._getInternalAddressByIndex(c));
}
for (const pc of this._sender_payment_codes) {
for (const pc of this._receive_payment_codes) {
for (
let c = this._next_free_payment_code_address_index[pc];
c < this._next_free_payment_code_address_index[pc] + this.gap_limit;
let c = this._next_free_payment_code_address_index_receive[pc];
c < this._next_free_payment_code_address_index_receive[pc] + this.gap_limit;
c++
) {
lagAddressesToFetch.push(this._getBIP47Address(pc, c));
lagAddressesToFetch.push(this._getBIP47AddressReceive(pc, c));
}
}
@ -754,16 +784,16 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
for (const pc of this._sender_payment_codes) {
for (const pc of this._receive_payment_codes) {
for (
let c = this._next_free_payment_code_address_index[pc];
c < this._next_free_payment_code_address_index[pc] + this.gap_limit;
let c = this._next_free_payment_code_address_index_receive[pc];
c < this._next_free_payment_code_address_index_receive[pc] + this.gap_limit;
c++
) {
const address = this._getBIP47Address(pc, c);
const address = this._getBIP47AddressReceive(pc, c);
if (txs[address] && Array.isArray(txs[address]) && txs[address].length > 0) {
// whoa, someone uses our wallet outside! better catch up
this._next_free_payment_code_address_index[pc] = c + 1;
this._next_free_payment_code_address_index_receive[pc] = c + 1;
}
}
}
@ -786,9 +816,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
addresses2fetch.push(this._getInternalAddressByIndex(c));
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._next_free_payment_code_address_index[pc] + this.gap_limit; c++) {
addresses2fetch.push(this._getBIP47Address(pc, c));
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._next_free_payment_code_address_index_receive[pc] + this.gap_limit; c++) {
addresses2fetch.push(this._getBIP47AddressReceive(pc, c));
}
}
@ -836,11 +866,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
for (const pc of this._sender_payment_codes) {
for (const pc of this._receive_payment_codes) {
let confirmed = 0;
let unconfirmed = 0;
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
const addr = this._getBIP47Address(pc, c);
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
const addr = this._getBIP47AddressReceive(pc, c);
if (balances.addresses[addr].confirmed || balances.addresses[addr].unconfirmed) {
confirmed = confirmed + balances.addresses[addr].confirmed;
unconfirmed = unconfirmed + balances.addresses[addr].unconfirmed;
@ -871,10 +901,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._next_free_payment_code_address_index[pc] + this.gap_limit; c++) {
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._next_free_payment_code_address_index_receive[pc] + this.gap_limit; c++) {
if (this._balances_by_payment_code_index?.[pc]?.c > 0) {
addressess.push(this._getBIP47Address(pc, c));
addressess.push(this._getBIP47AddressReceive(pc, c));
}
}
}
@ -891,10 +921,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._next_free_payment_code_address_index[pc] + this.gap_limit; c++) {
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._next_free_payment_code_address_index_receive[pc] + this.gap_limit; c++) {
if (this._balances_by_payment_code_index?.[pc]?.u > 0) {
addressess.push(this._getBIP47Address(pc, c));
addressess.push(this._getBIP47AddressReceive(pc, c));
}
}
}
@ -913,13 +943,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// this belongs in `.getUtxo()`
for (const u of this._utxo) {
u.txid = u.txId;
u.amount = u.value;
u.wif = this._getWifForAddress(u.address);
if (!u.confirmations && u.height) u.confirmations = BlueElectrum.estimateCurrentBlockheight() - u.height;
}
this._utxo = this._utxo.sort((a, b) => Number(a.amount) - Number(b.amount));
this._utxo = this._utxo.sort((a, b) => Number(a.value) - Number(b.value));
// more consistent, so txhex in unit tests wont change
}
@ -928,10 +956,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
* [ { height: 0,
* value: 666,
* address: 'string',
* txId: 'string',
* vout: 1,
* txid: 'string',
* amount: 666,
* wif: 'string',
* confirmations: 0 } ]
*
@ -964,9 +990,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + 1; c++) {
ownedAddressesHashmap[this._getBIP47Address(pc, c)] = true;
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + 1; c++) {
ownedAddressesHashmap[this._getBIP47AddressReceive(pc, c)] = true;
}
}
@ -980,11 +1006,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
const value = new BigNumber(output.value).multipliedBy(100000000).toNumber();
utxos.push({
txid: tx.txid,
txId: tx.txid,
vout: output.n,
address: String(address),
value,
amount: value,
confirmations: tx.confirmations,
wif: false,
height: BlueElectrum.estimateCurrentBlockheight() - (tx.confirmations ?? 0),
@ -1024,10 +1048,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c;
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
// not technically correct but well, to have at least somethign in PSBT...
if (this._getBIP47Address(pc, c) === address) return "m/47'/0'/0'/" + c;
if (this._getBIP47AddressReceive(pc, c) === address) return "m/47'/0'/0'/" + c;
}
}
@ -1046,9 +1070,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c);
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
if (this._getBIP47Address(pc, c) === address) return this._getBIP47PubkeyByIndex(pc, c);
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
if (this._getBIP47AddressReceive(pc, c) === address) return this._getBIP47PubkeyByIndex(pc, c);
}
}
@ -1068,9 +1092,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return this._getWIFByIndex(true, c);
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
if (this._getBIP47Address(pc, c) === address) return this._getBIP47WIF(pc, c);
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
if (this._getBIP47AddressReceive(pc, c) === address) return this._getBIP47WIF(pc, c);
}
}
return false;
@ -1090,9 +1114,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === cleanAddress) return true;
}
for (const pc of this._sender_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
if (this._getBIP47Address(pc, c) === address) return true;
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
if (this._getBIP47AddressReceive(pc, c) === address) return true;
}
}
return false;
@ -1100,7 +1124,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
/**
*
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos
* @param utxos {Array.<{vout: Number, value: Number, txid: String, address: String}>} List of spendable utxos
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
* @param feeRate {Number} satoshi per byte
* @param changeAddress {String} Excessive coins will go back to that address
@ -1111,12 +1135,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
*/
createTransaction(
utxos: CreateTransactionUtxo[],
targets: CoinSelectTarget[],
targets: CreateTransactionTarget[],
feeRate: number,
changeAddress: string,
sequence: number,
sequence: number = AbstractHDElectrumWallet.defaultRBFSequence,
skipSigning = false,
masterFingerprint: number,
masterFingerprint: number = 0,
): CreateTransactionResult {
if (targets.length === 0) throw new Error('No destination provided');
// compensating for coinselect inability to deal with segwit inputs, and overriding script length for proper vbytes calculation
@ -1131,13 +1155,43 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
for (const t of targets) {
if (t.address.startsWith('bc1')) {
if (t.address && t.address.startsWith('bc1')) {
// in case address is non-typical and takes more bytes than coinselect library anticipates by default
t.script = { length: bitcoin.address.toOutputScript(t.address).length + 3 };
}
if (t.script?.hex) {
// setting length for coinselect lib manually as it is not aware of our field `hex`
t.script.length = t.script.hex.length / 2 - 4;
}
}
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
let { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
const hasSilentPaymentOutput: boolean = !!outputs.find(o => o.address?.startsWith('sp1'));
if (hasSilentPaymentOutput) {
if (!this.allowSilentPaymentSend()) {
throw new Error('This wallet can not send to SilentPayment address');
}
// for a single wallet all utxos gona be the same type, so we define it only once:
let utxoType: SPUTXOType = 'non-eligible';
switch (this.segwitType) {
case 'p2sh(p2wpkh)':
utxoType = 'p2sh-p2wpkh';
break;
case 'p2wpkh':
utxoType = 'p2wpkh';
break;
default:
// @ts-ignore override
if (this.type === 'HDlegacyP2PKH') utxoType = 'p2pkh';
}
const spUtxos: SPUTXO[] = inputs.map(u => ({ ...u, utxoType, wif: u.wif! }));
const sp = new SilentPayment();
outputs = sp.createTransaction(spUtxos, outputs) as CoinSelectOutput[];
}
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
let psbt = new bitcoin.Psbt();
@ -1175,9 +1229,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
});
outputs.forEach(output => {
// if output has no address - this is change output
// if output has no address - this is change output or a custom script output
let change = false;
if (!output.address) {
// @ts-ignore
if (!output.address && !output.script?.hex) {
change = true;
output.address = changeAddress;
}
@ -1198,8 +1253,16 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
if (output.address?.startsWith('PM')) {
// ok its BIP47 payment code, so we need to unwrap a joint address for the receiver and use it instead:
output.address = this._getNextFreePaymentCodeAddressSend(output.address);
// ^^^ trusting that notification transaction is in place
}
psbt.addOutput({
address: output.address,
// @ts-ignore types from bitcoinjs are not exported so we cant define outputData separately and add fields conditionally (either address or script should be present)
script: output.script?.hex ? Buffer.from(output.script.hex, 'hex') : undefined,
value: output.value,
bip32Derivation:
change && path && pubkey
@ -1243,8 +1306,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
psbt.addInput({
// @ts-ignore
hash: input.txid || input.txId,
hash: input.txid,
index: input.vout,
sequence,
bip32Derivation: [
@ -1376,6 +1438,17 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
ret.push(this._getExternalAddressByIndex(c));
}
if (this.allowBIP47() && this.isBIP47Enabled()) {
// returning BIP47 joint addresses with everyone who can pay us because they are kinda our 'external' aka 'receive' addresses
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit / 4; c++) {
// ^^^ not full gap limit to reduce computation (theoretically, there should not be gaps at all)
ret.push(this._getBIP47AddressReceive(pc, c));
}
}
}
return ret;
}
@ -1443,7 +1516,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
/**
* @param seed {Buffer} Buffer object with seed
* @returns {string} Hex string of fingerprint derived from mnemonics. Always has lenght of 8 chars and correct leading zeroes. All caps
* @returns {string} Hex string of fingerprint derived from mnemonics. Always has length of 8 chars and correct leading zeroes. All caps
*/
static seedToFingerprint(seed: Buffer) {
const root = bip32.fromSeed(seed);
@ -1462,7 +1535,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
/**
* @returns {string} Hex string of fingerprint derived from wallet mnemonics. Always has lenght of 8 chars and correct leading zeroes
* @returns {string} Hex string of fingerprint derived from wallet mnemonics. Always has length of 8 chars and correct leading zeroes
*/
getMasterFingerprintHex() {
const seed = this._getSeed();
@ -1489,6 +1562,145 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return this._bip47_instance;
}
/**
* find and return _existing_ notification transaction for the given payment code
* (i.e. if it exists - we notified in the past and dont need to notify again)
*/
getBIP47NotificationTransaction(receiverPaymentCode: string): Transaction | undefined {
const publicBip47 = BIP47Factory(ecc).fromPaymentCode(receiverPaymentCode);
const remoteNotificationAddress = publicBip47.getNotificationAddress();
for (const tx of this.getTransactions()) {
for (const output of tx.outputs) {
if (output.scriptPubKey?.addresses?.includes(remoteNotificationAddress)) return tx;
// ^^^ if in the past we sent a tx to his notification address - most likely that was a proper notification
// transaction with OP_RETURN.
// but not gona verify it here, will just trust it
}
}
}
/**
* return BIP47 payment code of the counterparty of this transaction (someone who paid us, or someone we paid)
* or undefined if it was a non-BIP47 transaction
*/
getBip47CounterpartyByTxid(txid: string): string | undefined {
const foundTx = this.getTransactions().find(tx => tx.txid === txid);
if (foundTx) {
return this.getBip47CounterpartyByTx(foundTx);
}
return undefined;
}
/**
* return BIP47 payment code of the counterparty of this transaction (someone who paid us, or someone we paid)
* or undefined if it was a non-BIP47 transaction
*/
getBip47CounterpartyByTx(tx: Transaction): string | undefined {
for (const pc of Object.keys(this._txs_by_payment_code_index)) {
// iterating all payment codes
for (const txs of Object.values(this._txs_by_payment_code_index[pc])) {
for (const tx2 of txs) {
if (tx2.txid === tx.txid) {
return pc; // found it!
}
}
}
}
// checking txs we sent to counterparties
for (const pc of this._send_payment_codes) {
for (const out of tx.outputs) {
for (const address of out.scriptPubKey?.addresses ?? []) {
if (this._addresses_by_payment_code_send[pc] && Object.values(this._addresses_by_payment_code_send[pc]).includes(address)) {
// found it!
return pc;
}
}
}
}
return undefined; // found nothing
}
createBip47NotificationTransaction(utxos: CreateTransactionUtxo[], receiverPaymentCode: string, feeRate: number, changeAddress: string) {
const aliceBip47 = BIP47Factory(ecc).fromBip39Seed(this.getSecret(), undefined, this.getPassphrase());
const bobBip47 = BIP47Factory(ecc).fromPaymentCode(receiverPaymentCode);
assert(utxos[0], 'No UTXO');
assert(utxos[0].wif, 'No UTXO WIF');
// constructing targets: notification address, _dummy_ payload (+potential change might be added later)
const targetsTemp: CreateTransactionTarget[] = [];
targetsTemp.push({
address: bobBip47.getNotificationAddress(),
value: 546, // minimum permissible utxo size
});
targetsTemp.push({
value: 0,
script: {
hex: Buffer.alloc(83).toString('hex'), // no `address` here, its gonabe op_return. but we pass dummy data here with a correct size just to choose utxo
},
});
// creating temp transaction so that utxo can be selected:
const { inputs: inputsTemp } = this.createTransaction(
utxos,
targetsTemp,
feeRate,
changeAddress,
AbstractHDElectrumWallet.defaultRBFSequence,
false,
0,
);
assert(inputsTemp?.[0]?.wif, 'inputsTemp?.[0]?.wif assert failed');
// utxo selected. lets create op_return payload using the correct (first!) utxo and correct targets with that payload
const keyPair = ECPair.fromWIF(inputsTemp[0].wif);
const outputNumber = Buffer.from('00000000', 'hex');
outputNumber.writeUInt32LE(inputsTemp[0].vout);
const blindedPaymentCode = aliceBip47.getBlindedPaymentCode(
bobBip47,
keyPair.privateKey as Buffer,
// txid is reversed, as well as output number
Buffer.from(inputsTemp[0].txid, 'hex').reverse().toString('hex') + outputNumber.toString('hex'),
);
// targets:
const targets: CreateTransactionTarget[] = [];
targets.push({
address: bobBip47.getNotificationAddress(),
value: 546, // minimum permissible utxo size
});
targets.push({
value: 0,
script: {
hex: '6a4c50' + blindedPaymentCode, // no `address` here, only script (which is OP_RETURN + data payload)
},
});
// finally a transaction:
const { tx, outputs, inputs, fee, psbt } = this.createTransaction(
utxos,
targets,
feeRate,
changeAddress,
AbstractHDElectrumWallet.defaultRBFSequence,
false,
0,
);
assert(inputs && inputs[0] && inputs[0].wif, 'inputs && inputs[0] && inputs[0].wif assert failed');
assert(inputs[0].txid === inputsTemp[0].txid, 'inputs[0].txid === inputsTemp[0].txid assert failed'); // making sure that no funky business happened under the hood (its supposed to stay the same)
return { tx, inputs, outputs, fee, psbt };
}
getBIP47PaymentCode(): string {
if (!this._payment_code) {
this._payment_code = this.getBIP47FromSeed().getSerializedPaymentCode();
@ -1502,17 +1714,21 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return bip47Local.getNotificationAddress();
}
/**
* check our notification address, and decypher all payment codes people notified us
* about (so they can pay us)
*/
async fetchBIP47SenderPaymentCodes(): Promise<void> {
const bip47_instance = this.getBIP47FromSeed();
const address = bip47_instance.getNotificationAddress();
const histories = await BlueElectrum.multiGetHistoryByAddress([address]);
const txHashes = histories[address].map(({ tx_hash }) => tx_hash);
const txHexs = await BlueElectrum.multiGetTransactionByTxid(txHashes, 50, false);
const txHexs = await BlueElectrum.multiGetTransactionByTxid(txHashes, false);
for (const txHex of Object.values(txHexs)) {
try {
const paymentCode = bip47_instance.getPaymentCodeFromRawNotificationTransaction(txHex);
if (this._sender_payment_codes.includes(paymentCode)) continue; // already have it
if (this._receive_payment_codes.includes(paymentCode)) continue; // already have it
// final check if PC is even valid (could've been constructed by a buggy code, and our code would crash with that):
try {
@ -1521,8 +1737,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
continue;
}
this._sender_payment_codes.push(paymentCode);
this._next_free_payment_code_address_index[paymentCode] = 0; // initialize
this._receive_payment_codes.push(paymentCode);
this._next_free_payment_code_address_index_receive[paymentCode] = 0; // initialize
this._balances_by_payment_code_index[paymentCode] = { c: 0, u: 0 };
} catch (e) {
// do nothing
@ -1530,19 +1746,66 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
/**
* for counterparties we can pay, we sync shared addresses to find the one we havent used yet.
* this method could benefit from rewriting in batch requests, but not necessary - its only going to be called
* once in a while (when user decides to pay a given counterparty again)
*/
async syncBip47ReceiversAddresses(receiverPaymentCode: string) {
this._next_free_payment_code_address_index_send[receiverPaymentCode] =
this._next_free_payment_code_address_index_send[receiverPaymentCode] || 0; // init
for (let c = this._next_free_payment_code_address_index_send[receiverPaymentCode]; c < 999999; c++) {
const address = this._getBIP47AddressSend(receiverPaymentCode, c);
this._addresses_by_payment_code_send[receiverPaymentCode] = this._addresses_by_payment_code_send[receiverPaymentCode] || {}; // init
this._addresses_by_payment_code_send[receiverPaymentCode][c] = address;
const histories = await BlueElectrum.multiGetHistoryByAddress([address]);
if (histories?.[address]?.length > 0) {
// address is used;
continue;
}
// empty address, stop here, we found our latest index and filled array with shared addresses
this._next_free_payment_code_address_index_send[receiverPaymentCode] = c;
break;
}
}
/**
* payment codes of people who can pay us
*/
getBIP47SenderPaymentCodes(): string[] {
return this._sender_payment_codes;
return this._receive_payment_codes;
}
/**
* payment codes of people whom we can pay
*/
getBIP47ReceiverPaymentCodes(): string[] {
return this._send_payment_codes;
}
/**
* adding counterparty whom we can pay. trusting that notificaton transaction is in place already
*/
addBIP47Receiver(paymentCode: string) {
if (this._send_payment_codes.includes(paymentCode)) return; // duplicates
this._send_payment_codes.push(paymentCode);
}
_hdNodeToAddress(hdNode: BIP32Interface): string {
return this._nodeToBech32SegwitAddress(hdNode);
}
_getBIP47Address(paymentCode: string, index: number): string {
if (!this._addresses_by_payment_code[paymentCode]) this._addresses_by_payment_code[paymentCode] = [];
/**
* returns joint addresses to receive coins with a given counterparty
*/
_getBIP47AddressReceive(paymentCode: string, index: number): string {
if (!this._addresses_by_payment_code_receive[paymentCode]) this._addresses_by_payment_code_receive[paymentCode] = [];
if (this._addresses_by_payment_code[paymentCode][index]) {
return this._addresses_by_payment_code[paymentCode][index];
if (this._addresses_by_payment_code_receive[paymentCode][index]) {
return this._addresses_by_payment_code_receive[paymentCode][index];
}
const bip47_instance = this.getBIP47FromSeed();
@ -1551,12 +1814,38 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
const hdNode = bip47_instance.getPaymentWallet(remotePaymentNode, index);
const address = this._hdNodeToAddress(hdNode);
this._address_to_wif_cache[address] = hdNode.toWIF();
this._addresses_by_payment_code[paymentCode][index] = address;
this._addresses_by_payment_code_receive[paymentCode][index] = address;
return address;
}
_getNextFreePaymentCodeAddress(paymentCode: string) {
return this._next_free_payment_code_address_index[paymentCode] || 0;
/**
* returns joint addresses to send coins to
*/
_getBIP47AddressSend(paymentCode: string, index: number): string {
if (!this._addresses_by_payment_code_send[paymentCode]) this._addresses_by_payment_code_send[paymentCode] = [];
if (this._addresses_by_payment_code_send[paymentCode][index]) {
// cache hit
return this._addresses_by_payment_code_send[paymentCode][index];
}
const hdNode = this.getBIP47FromSeed().getReceiveWallet(BIP47Factory(ecc).fromPaymentCode(paymentCode).getPaymentCodeNode(), index);
const address = this._hdNodeToAddress(hdNode);
this._addresses_by_payment_code_send[paymentCode][index] = address;
return address;
}
_getNextFreePaymentCodeIndexReceive(paymentCode: string) {
return this._next_free_payment_code_address_index_receive[paymentCode] || 0;
}
/**
* when sending funds to a payee, this method will return next unused joint address for him.
* this method assumes that we synced our payee via `syncBip47ReceiversAddresses()`
*/
_getNextFreePaymentCodeAddressSend(paymentCode: string) {
this._next_free_payment_code_address_index_send[paymentCode] = this._next_free_payment_code_address_index_send[paymentCode] || 0;
return this._getBIP47AddressSend(paymentCode, this._next_free_payment_code_address_index_send[paymentCode]);
}
_getBalancesByPaymentCodeIndex(paymentCode: string): BalanceByIndex {

View File

@ -1,8 +1,9 @@
import { LegacyWallet } from './legacy-wallet';
import * as bip39 from 'bip39';
import { BIP32Interface } from 'bip32';
import * as bip39 from 'bip39';
import * as bip39custom from '../../blue_modules/bip39';
import BlueElectrum from '../../blue_modules/BlueElectrum';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import { LegacyWallet } from './legacy-wallet';
import { Transaction } from './types';
type AbstractHDWalletStatics = {
@ -13,8 +14,12 @@ type AbstractHDWalletStatics = {
* @deprecated
*/
export class AbstractHDWallet extends LegacyWallet {
static type = 'abstract';
static typeReadable = 'abstract';
static readonly type = 'abstract';
static readonly typeReadable = 'abstract';
// @ts-ignore: override
public readonly type = AbstractHDWallet.type;
// @ts-ignore: override
public readonly typeReadable = AbstractHDWallet.typeReadable;
next_free_address_index: number;
next_free_change_address_index: number;

View File

@ -1,14 +1,8 @@
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import b58 from 'bs58check';
import createHash from 'create-hash';
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
type WalletStatics = {
type: string;
typeReadable: string;
segwitType?: 'p2wpkh' | 'p2sh(p2wpkh)';
derivationPath?: string;
};
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
type WalletWithPassphrase = AbstractWallet & { getPassphrase: () => string };
type UtxoMetadata = {
@ -17,8 +11,12 @@ type UtxoMetadata = {
};
export class AbstractWallet {
static type = 'abstract';
static typeReadable = 'abstract';
static readonly type = 'abstract';
static readonly typeReadable = 'abstract';
// @ts-ignore: override
public readonly type = AbstractWallet.type;
// @ts-ignore: override
public readonly typeReadable = AbstractWallet.typeReadable;
static fromJson(obj: string): AbstractWallet {
const obj2 = JSON.parse(obj);
@ -31,8 +29,6 @@ export class AbstractWallet {
return temp;
}
type: string;
typeReadable: string;
segwitType?: 'p2wpkh' | 'p2sh(p2wpkh)';
_derivationPath?: string;
label: string;
@ -50,14 +46,9 @@ export class AbstractWallet {
_hideTransactionsInWalletsList: boolean;
_utxoMetadata: Record<string, UtxoMetadata>;
use_with_hardware_wallet: boolean;
masterFingerprint: number | false;
masterFingerprint: number;
constructor() {
const Constructor = this.constructor as unknown as WalletStatics;
this.type = Constructor.type;
this.typeReadable = Constructor.typeReadable;
this.segwitType = Constructor.segwitType;
this.label = '';
this.secret = ''; // private key or recovery phrase
this.balance = 0;
@ -73,7 +64,7 @@ export class AbstractWallet {
this._hideTransactionsInWalletsList = false;
this._utxoMetadata = {};
this.use_with_hardware_wallet = false;
this.masterFingerprint = false;
this.masterFingerprint = 0;
}
/**
@ -163,6 +154,10 @@ export class AbstractWallet {
return true;
}
allowSilentPaymentSend(): boolean {
return false;
}
allowRBF(): boolean {
return false;
}
@ -257,7 +252,7 @@ export class AbstractWallet {
parsedSecret = JSON.parse(newSecret);
}
if (parsedSecret && parsedSecret.keystore && parsedSecret.keystore.xpub) {
let masterFingerprint: number | false = false;
let masterFingerprint: number = 0;
if (parsedSecret.keystore.ckcc_xfp) {
// It is a ColdCard Hardware Wallet
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);
@ -374,7 +369,7 @@ export class AbstractWallet {
/**
*
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos
* @param utxos {Array.<{vout: Number, value: Number, txid: String, address: String}>} List of spendable utxos
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
* @param feeRate {Number} satoshi per byte
* @param changeAddress {String} Excessive coins will go back to that address

View File

@ -2,6 +2,7 @@ import { CipherSeed } from 'aezeed';
import BIP32Factory from 'bip32';
import * as bitcoin from 'bitcoinjs-lib';
import b58 from 'bs58check';
import ecc from '../../blue_modules/noble_ecc';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
@ -18,10 +19,14 @@ const bip32 = BIP32Factory(ecc);
* @see https://github.com/lightningnetwork/lnd/blob/master/keychain/derivation.go
*/
export class HDAezeedWallet extends AbstractHDElectrumWallet {
static type = 'HDAezeedWallet';
static typeReadable = 'HD Aezeed';
static segwitType = 'p2wpkh';
static derivationPath = "m/84'/0'/0'";
static readonly type = 'HDAezeedWallet';
static readonly typeReadable = 'HD Aezeed';
public readonly segwitType = 'p2wpkh';
static readonly derivationPath = "m/84'/0'/0'";
// @ts-ignore: override
public readonly type = HDAezeedWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDAezeedWallet.typeReadable;
private _entropyHex?: string;

View File

@ -1,10 +1,14 @@
import BIP32Factory, { BIP32Interface } from 'bip32';
import * as bitcoinjs from 'bitcoinjs-lib';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
import BIP32Factory from 'bip32';
import ecc from '../../blue_modules/noble_ecc';
import { Psbt } from 'bitcoinjs-lib';
import { CoinSelectReturnInput } from 'coinselect';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import { ElectrumHistory } from '../../blue_modules/BlueElectrum';
import ecc from '../../blue_modules/noble_ecc';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
const BlueElectrum = require('../../blue_modules/BlueElectrum');
const bip32 = BIP32Factory(ecc);
/**
@ -12,32 +16,46 @@ const bip32 = BIP32Factory(ecc);
* In particular, Breadwallet-compatible (Legacy addresses)
*/
export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
static type = 'HDLegacyBreadwallet';
static typeReadable = 'HD Legacy Breadwallet (P2PKH)';
static derivationPath = "m/0'";
static readonly type = 'HDLegacyBreadwallet';
static readonly typeReadable = 'HD Legacy Breadwallet (P2PKH)';
// @ts-ignore: override
public readonly type = HDLegacyBreadwalletWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDLegacyBreadwalletWallet.typeReadable;
static readonly derivationPath = "m/0'";
// track address index at which wallet switched to segwit
_external_segwit_index = null;
_internal_segwit_index = null;
_external_segwit_index: number | null = null;
_internal_segwit_index: number | null = null;
// we need a separate function without external_addresses_cache to use in binarySearch
_calcNodeAddressByIndex(node, index, p2wpkh = false) {
let _node;
_calcNodeAddressByIndex(node: number, index: number, p2wpkh: boolean = false) {
let _node: BIP32Interface | undefined;
if (node === 0) {
_node = this._node0 || (this._node0 = bip32.fromBase58(this.getXpub()).derive(node));
}
if (node === 1) {
_node = this._node1 || (this._node1 = bip32.fromBase58(this.getXpub()).derive(node));
}
if (!_node) {
throw new Error('Internal error: this._node0 or this._node1 is undefined');
}
const pubkey = _node.derive(index).publicKey;
const address = p2wpkh ? bitcoinjs.payments.p2wpkh({ pubkey }).address : bitcoinjs.payments.p2pkh({ pubkey }).address;
if (!address) {
throw new Error('Internal error: no address in _calcNodeAddressByIndex');
}
return address;
}
// this function is different from HDLegacyP2PKHWallet._getNodeAddressByIndex.
// It takes _external_segwit_index _internal_segwit_index for account
// and starts to generate segwit addresses if index more than them
_getNodeAddressByIndex(node, index) {
_getNodeAddressByIndex(node: number, index: number): string {
index = index * 1; // cast to int
if (node === 0) {
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
@ -64,6 +82,8 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
if (node === 1) {
return (this.internal_addresses_cache[index] = address);
}
throw new Error('Internal error: unknown node');
}
async fetchBalance() {
@ -96,8 +116,8 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
}
}
async _binarySearchIteration(startIndex, endIndex, node = 0, p2wpkh = false) {
const gerenateChunkAddresses = chunkNum => {
async _binarySearchIteration(startIndex: number, endIndex: number, node: number = 0, p2wpkh: boolean = false) {
const gerenateChunkAddresses = (chunkNum: number) => {
const ret = [];
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) {
ret.push(this._calcNodeAddressByIndex(node, c, p2wpkh));
@ -105,11 +125,11 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
return ret;
};
let lastChunkWithUsedAddressesNum = null;
let lastHistoriesWithUsedAddresses = null;
let lastChunkWithUsedAddressesNum: number;
let lastHistoriesWithUsedAddresses: Record<string, ElectrumHistory[]>;
for (let c = 0; c < Math.round(endIndex / this.gap_limit); c++) {
const histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
if (AbstractHDElectrumWallet._getTransactionsFromHistories(histories).length > 0) {
// in this particular chunk we have used addresses
lastChunkWithUsedAddressesNum = c;
lastHistoriesWithUsedAddresses = histories;
@ -121,11 +141,11 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
let lastUsedIndex = startIndex;
if (lastHistoriesWithUsedAddresses) {
if (lastHistoriesWithUsedAddresses!) {
// now searching for last used address in batch lastChunkWithUsedAddressesNum
for (
let c = lastChunkWithUsedAddressesNum * this.gap_limit;
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit;
let c = lastChunkWithUsedAddressesNum! * this.gap_limit;
c < lastChunkWithUsedAddressesNum! * this.gap_limit + this.gap_limit;
c++
) {
const address = this._calcNodeAddressByIndex(node, c, p2wpkh);
@ -138,11 +158,11 @@ export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet {
return lastUsedIndex;
}
_addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) {
_addPsbtInput(psbt: Psbt, input: CoinSelectReturnInput, sequence: number, masterFingerprintBuffer: Buffer) {
// hack to use
// AbstractHDElectrumWallet._addPsbtInput for bech32 address
// HDLegacyP2PKHWallet._addPsbtInput for legacy address
const ProxyClass = input.address.startsWith('bc1') ? AbstractHDElectrumWallet : HDLegacyP2PKHWallet;
const ProxyClass = input?.address?.startsWith('bc1') ? AbstractHDElectrumWallet : HDLegacyP2PKHWallet;
const proxy = new ProxyClass();
return proxy._addPsbtInput.apply(this, [psbt, input, sequence, masterFingerprintBuffer]);
}

View File

@ -1,6 +1,7 @@
import BIP32Factory from 'bip32';
import * as bitcoin from 'bitcoinjs-lib';
import * as mn from 'electrum-mnemonic';
import ecc from '../../blue_modules/noble_ecc';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
@ -19,9 +20,13 @@ type SeedOpts = {
* @see https://electrum.readthedocs.io/en/latest/seedphrase.html
*/
export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
static type = 'HDlegacyElectrumSeedP2PKH';
static typeReadable = 'HD Legacy Electrum (BIP32 P2PKH)';
static derivationPath = 'm';
static readonly type = 'HDlegacyElectrumSeedP2PKH';
static readonly typeReadable = 'HD Legacy Electrum (BIP32 P2PKH)';
// @ts-ignore: override
public readonly type = HDLegacyElectrumSeedP2PKHWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDLegacyElectrumSeedP2PKHWallet.typeReadable;
static readonly derivationPath = 'm';
validateMnemonic() {
return mn.validateMnemonic(this.secret, PREFIX);

View File

@ -1,10 +1,12 @@
import BIP32Factory, { BIP32Interface } from 'bip32';
import { Psbt } from 'bitcoinjs-lib';
import { CoinSelectReturnInput } from 'coinselect';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import ecc from '../../blue_modules/noble_ecc';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
const bip32 = BIP32Factory(ecc);
const BlueElectrum = require('../../blue_modules/BlueElectrum');
/**
* HD Wallet (BIP39).
@ -12,9 +14,13 @@ const BlueElectrum = require('../../blue_modules/BlueElectrum');
* @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
*/
export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
static type = 'HDlegacyP2PKH';
static typeReadable = 'HD Legacy (BIP44 P2PKH)';
static derivationPath = "m/44'/0'/0'";
static readonly type = 'HDlegacyP2PKH';
static readonly typeReadable = 'HD Legacy (BIP44 P2PKH)';
// @ts-ignore: override
public readonly type = HDLegacyP2PKHWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDLegacyP2PKHWallet.typeReadable;
static readonly derivationPath = "m/44'/0'/0'";
allowSend() {
return true;
@ -66,7 +72,6 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
// now we need to fetch txhash for each input as required by PSBT
const txhexes = await BlueElectrum.multiGetTransactionByTxid(
this.getUtxo().map(x => x.txid),
50,
false,
);
@ -104,4 +109,8 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
return psbt;
}
allowSilentPaymentSend(): boolean {
return true;
}
}

View File

@ -6,10 +6,14 @@ import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
*/
export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
static type = 'HDsegwitBech32';
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
static segwitType = 'p2wpkh';
static derivationPath = "m/84'/0'/0'";
static readonly type = 'HDsegwitBech32';
static readonly typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
// @ts-ignore: override
public readonly type = HDSegwitBech32Wallet.type;
// @ts-ignore: override
public readonly typeReadable = HDSegwitBech32Wallet.typeReadable;
public readonly segwitType = 'p2wpkh';
static readonly derivationPath = "m/84'/0'/0'";
allowSend() {
return true;
@ -46,4 +50,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
allowBIP47() {
return true;
}
allowSilentPaymentSend(): boolean {
return true;
}
}

View File

@ -2,6 +2,7 @@ import BIP32Factory from 'bip32';
import * as bitcoin from 'bitcoinjs-lib';
import b58 from 'bs58check';
import * as mn from 'electrum-mnemonic';
import ecc from '../../blue_modules/noble_ecc';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
@ -20,9 +21,13 @@ type SeedOpts = {
* @see https://electrum.readthedocs.io/en/latest/seedphrase.html
*/
export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
static type = 'HDSegwitElectrumSeedP2WPKHWallet';
static typeReadable = 'HD Electrum (BIP32 P2WPKH)';
static derivationPath = "m/0'";
static readonly type = 'HDSegwitElectrumSeedP2WPKHWallet';
static readonly typeReadable = 'HD Electrum (BIP32 P2WPKH)';
// @ts-ignore: override
public readonly type = HDSegwitElectrumSeedP2WPKHWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDSegwitElectrumSeedP2WPKHWallet.typeReadable;
static readonly derivationPath = "m/0'";
validateMnemonic() {
return mn.validateMnemonic(this.secret, PREFIX);

View File

@ -3,6 +3,7 @@ import * as bitcoin from 'bitcoinjs-lib';
import { Psbt } from 'bitcoinjs-lib';
import b58 from 'bs58check';
import { CoinSelectReturnInput } from 'coinselect';
import ecc from '../../blue_modules/noble_ecc';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
@ -14,10 +15,14 @@ const bip32 = BIP32Factory(ecc);
* @see https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
*/
export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
static type = 'HDsegwitP2SH';
static typeReadable = 'HD SegWit (BIP49 P2SH)';
static segwitType = 'p2sh(p2wpkh)';
static derivationPath = "m/49'/0'/0'";
static readonly type = 'HDsegwitP2SH';
static readonly typeReadable = 'HD SegWit (BIP49 P2SH)';
// @ts-ignore: override
public readonly type = HDSegwitP2SHWallet.type;
// @ts-ignore: override
public readonly typeReadable = HDSegwitP2SHWallet.typeReadable;
public readonly segwitType = 'p2sh(p2wpkh)';
static readonly derivationPath = "m/49'/0'/0'";
allowSend() {
return true;
@ -112,4 +117,8 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
isSegwit() {
return true;
}
allowSilentPaymentSend(): boolean {
return true;
}
}

View File

@ -1,16 +1,16 @@
import BigNumber from 'bignumber.js';
import bitcoinMessage from 'bitcoinjs-message';
import { randomBytes } from '../rng';
import { AbstractWallet } from './abstract-wallet';
import { HDSegwitBech32Wallet } from '..';
import * as bitcoin from 'bitcoinjs-lib';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import coinSelect, { CoinSelectOutput, CoinSelectReturnInput, CoinSelectTarget, CoinSelectUtxo } from 'coinselect';
import bitcoinMessage from 'bitcoinjs-message';
import coinSelect, { CoinSelectOutput, CoinSelectReturnInput, CoinSelectTarget } from 'coinselect';
import coinSelectSplit from 'coinselect/split';
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
import { ECPairAPI, ECPairFactory, Signer } from 'ecpair';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import ecc from '../../blue_modules/noble_ecc';
import { HDSegwitBech32Wallet } from '..';
import { randomBytes } from '../rng';
import { AbstractWallet } from './abstract-wallet';
import { CreateTransactionResult, CreateTransactionTarget, CreateTransactionUtxo, Transaction, Utxo } from './types';
const ECPair: ECPairAPI = ECPairFactory(ecc);
bitcoin.initEccLib(ecc);
@ -19,8 +19,12 @@ bitcoin.initEccLib(ecc);
* (legacy P2PKH compressed)
*/
export class LegacyWallet extends AbstractWallet {
static type = 'legacy';
static typeReadable = 'Legacy (P2PKH)';
static readonly type = 'legacy';
static readonly typeReadable = 'Legacy (P2PKH)';
// @ts-ignore: override
public readonly type = LegacyWallet.type;
// @ts-ignore: override
public readonly typeReadable = LegacyWallet.typeReadable;
_txs_by_external_index: Transaction[] = [];
_txs_by_internal_index: Transaction[] = [];
@ -59,18 +63,10 @@ export class LegacyWallet extends AbstractWallet {
}
async generateFromEntropy(user: Buffer): Promise<void> {
let i = 0;
do {
i += 1;
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0);
const buf = Buffer.concat([user, random], 32);
try {
this.secret = ECPair.fromPrivateKey(buf).toWIF();
return;
} catch (e) {
if (i === 5) throw e;
if (user.length !== 32) {
throw new Error('Entropy should be 32 bytes');
}
} while (true);
this.secret = ECPair.fromPrivateKey(user).toWIF();
}
getAddress(): string | false {
@ -135,14 +131,13 @@ export class LegacyWallet extends AbstractWallet {
// now we need to fetch txhash for each input as required by PSBT
if (LegacyWallet.type !== this.type) return; // but only for LEGACY single-address wallets
const txhexes = await BlueElectrum.multiGetTransactionByTxid(
this._utxo.map(u => u.txId),
50,
this._utxo.map(u => u.txid),
false,
);
const newUtxos = [];
for (const u of this._utxo) {
if (txhexes[u.txId]) u.txhex = txhexes[u.txId];
if (txhexes[u.txid]) u.txhex = txhexes[u.txid];
newUtxos.push(u);
}
@ -157,10 +152,8 @@ export class LegacyWallet extends AbstractWallet {
* [ { height: 0,
* value: 666,
* address: 'string',
* txId: 'string',
* vout: 1,
* txid: 'string',
* amount: 666,
* wif: 'string',
* confirmations: 0 } ]
*
@ -170,7 +163,6 @@ export class LegacyWallet extends AbstractWallet {
getUtxo(respectFrozen = false): Utxo[] {
let ret: Utxo[] = [];
for (const u of this._utxo) {
if (u.txId) u.txid = u.txId;
if (!u.confirmations && u.height) u.confirmations = BlueElectrum.estimateCurrentBlockheight() - u.height;
ret.push(u);
}
@ -207,11 +199,9 @@ export class LegacyWallet extends AbstractWallet {
const value = new BigNumber(output.value).multipliedBy(100000000).toNumber();
utxos.push({
txid: tx.txid,
txId: tx.txid,
vout: output.n,
address,
value,
amount: value,
confirmations: tx.confirmations,
wif: false,
height: BlueElectrum.estimateCurrentBlockheight() - (tx.confirmations ?? 0),
@ -276,7 +266,7 @@ export class LegacyWallet extends AbstractWallet {
// is safe because in that case our cache is filled
// next, batch fetching each txid we got
const txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs));
const txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs), true);
const transactions = Object.values(txdatas);
// now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too.
@ -288,7 +278,7 @@ export class LegacyWallet extends AbstractWallet {
// ^^^^ not all inputs have txid, some of them are Coinbase (newly-created coins)
}
}
const vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids);
const vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids, true);
// fetched all transactions from our inputs. now we need to combine it.
// iterating all _our_ transactions:
@ -371,24 +361,21 @@ export class LegacyWallet extends AbstractWallet {
}
coinselect(
utxos: CoinSelectUtxo[],
targets: CoinSelectTarget[],
utxos: CreateTransactionUtxo[],
targets: CreateTransactionTarget[],
feeRate: number,
changeAddress: string,
): {
inputs: CoinSelectReturnInput[];
outputs: CoinSelectOutput[];
fee: number;
} {
if (!changeAddress) throw new Error('No change address provided');
let algo = coinSelect;
// if targets has output without a value, we want send MAX to it
if (targets.some(i => !('value' in i))) {
algo = coinSelectSplit;
}
const { inputs, outputs, fee } = algo(utxos, targets, feeRate);
const { inputs, outputs, fee } = algo(utxos, targets as CoinSelectTarget[], feeRate);
// .inputs and .outputs will be undefined if no solution was found
if (!inputs || !outputs) {
@ -400,7 +387,7 @@ export class LegacyWallet extends AbstractWallet {
/**
*
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String, txhex: String, }>} List of spendable utxos
* @param utxos {Array.<{vout: Number, value: Number, txid: String, address: String, txhex: String, }>} List of spendable utxos
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
* @param feeRate {Number} satoshi per byte
* @param changeAddress {String} Excessive coins will go back to that address
@ -419,7 +406,7 @@ export class LegacyWallet extends AbstractWallet {
masterFingerprint: number,
): CreateTransactionResult {
if (targets.length === 0) throw new Error('No destination provided');
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
sequence = sequence || 0xffffffff; // disable RBF by default
const psbt = new bitcoin.Psbt();
let c = 0;

View File

@ -1,26 +1,36 @@
import { LegacyWallet } from './legacy-wallet';
import Frisbee from 'frisbee';
import bolt11 from 'bolt11';
import Frisbee from 'frisbee';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { LegacyWallet } from './legacy-wallet';
export class LightningCustodianWallet extends LegacyWallet {
static type = 'lightningCustodianWallet';
static typeReadable = 'Lightning';
static readonly type = 'lightningCustodianWallet';
static readonly typeReadable = 'Lightning';
// @ts-ignore: override
public readonly type = LightningCustodianWallet.type;
// @ts-ignore: override
public readonly typeReadable = LightningCustodianWallet.typeReadable;
constructor(props) {
super(props);
this.setBaseURI(); // no args to init with default value
baseURI?: string;
refresh_token: string = '';
access_token: string = '';
_refresh_token_created_ts: number = 0;
_access_token_created_ts: number = 0;
refill_addressess: string[] = [];
pending_transactions_raw: any[] = [];
transactions_raw: any[] = [];
user_invoices_raw: any[] = [];
info_raw = false;
preferredBalanceUnit = BitcoinUnit.SATS;
chain = Chain.OFFCHAIN;
private _api?: Frisbee;
last_paid_invoice_result?: any;
decoded_invoice_raw?: any;
constructor() {
super();
this.init();
this.refresh_token = '';
this.access_token = '';
this._refresh_token_created_ts = 0;
this._access_token_created_ts = 0;
this.refill_addressess = [];
this.pending_transactions_raw = [];
this.user_invoices_raw = [];
this.info_raw = false;
this.preferredBalanceUnit = BitcoinUnit.SATS;
this.chain = Chain.OFFCHAIN;
}
/**
@ -28,7 +38,7 @@ export class LightningCustodianWallet extends LegacyWallet {
*
* @param URI
*/
setBaseURI(URI) {
setBaseURI(URI: string | undefined) {
this.baseURI = URI;
}
@ -40,11 +50,11 @@ export class LightningCustodianWallet extends LegacyWallet {
return true;
}
getAddress() {
getAddress(): string | false {
if (this.refill_addressess.length > 0) {
return this.refill_addressess[0];
} else {
return undefined;
return false;
}
}
@ -60,8 +70,9 @@ export class LightningCustodianWallet extends LegacyWallet {
return (+new Date() - this._lastTxFetch) / 1000 > 300; // 5 min
}
static fromJson(param) {
static fromJson(param: any) {
const obj = super.fromJson(param);
// @ts-ignore: local init
obj.init();
return obj;
}
@ -84,21 +95,23 @@ export class LightningCustodianWallet extends LegacyWallet {
return (+new Date() - this._refresh_token_created_ts) / 1000 >= 3600 * 24 * 7; // 7d
}
generate() {
generate(): Promise<void> {
// nop
return Promise.resolve();
}
async createAccount(isTest) {
async createAccount(isTest: boolean = false) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.post('/create', {
body: { partnerid: 'bluewallet', accounttype: (isTest && 'test') || 'common' },
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + (json.message ? json.message : json.error) + ' (code ' + json.code + ')');
}
@ -109,7 +122,8 @@ export class LightningCustodianWallet extends LegacyWallet {
this.secret = 'lndhub://' + json.login + ':' + json.password;
}
async payInvoice(invoice, freeAmount = 0) {
async payInvoice(invoice: string, freeAmount: number = 0) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.post('/payinvoice', {
body: { invoice, amount: freeAmount },
headers: {
@ -130,11 +144,11 @@ export class LightningCustodianWallet extends LegacyWallet {
}
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.originalResponse));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -146,9 +160,10 @@ export class LightningCustodianWallet extends LegacyWallet {
*
* @return {Promise.<Array>}
*/
async getUserInvoices(limit = false) {
async getUserInvoices(limit: number | false = false) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
let limitString = '';
if (limit) limitString = '?limit=' + parseInt(limit, 10);
if (limit) limitString = '?limit=' + parseInt(limit as unknown as string, 10);
const response = await this._api.get('/getuserinvoices' + limitString, {
headers: {
'Access-Control-Allow-Origin': '*',
@ -157,11 +172,11 @@ export class LightningCustodianWallet extends LegacyWallet {
},
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.originalResponse));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -184,7 +199,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
}
this.user_invoices_raw = json.sort(function (a, b) {
this.user_invoices_raw = json.sort(function (a: { timestamp: number }, b: { timestamp: number }) {
return a.timestamp - b.timestamp;
});
@ -201,15 +216,16 @@ export class LightningCustodianWallet extends LegacyWallet {
await this.getUserInvoices();
}
isInvoiceGeneratedByWallet(paymentRequest) {
isInvoiceGeneratedByWallet(paymentRequest: string) {
return this.user_invoices_raw.some(invoice => invoice.payment_request === paymentRequest);
}
weOwnAddress(address) {
weOwnAddress(address: string) {
return this.refill_addressess.some(refillAddress => address === refillAddress);
}
async addInvoice(amt, memo) {
async addInvoice(amt: number, memo: string) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.post('/addinvoice', {
body: { amt: amt + '', memo },
headers: {
@ -219,11 +235,11 @@ export class LightningCustodianWallet extends LegacyWallet {
},
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.originalResponse));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -241,6 +257,7 @@ export class LightningCustodianWallet extends LegacyWallet {
* @return {Promise.<void>}
*/
async authorize() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
let login, password;
if (this.secret.indexOf('blitzhub://') !== -1) {
login = this.secret.replace('blitzhub://', '').split(':')[0];
@ -255,11 +272,11 @@ export class LightningCustodianWallet extends LegacyWallet {
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -296,17 +313,18 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async refreshAcessToken() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.post('/auth?type=refresh_token', {
body: { refresh_token: this.refresh_token },
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' },
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -321,6 +339,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async fetchBtcAddress() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.get('/getbtc', {
headers: {
'Access-Control-Allow-Origin': '*',
@ -330,11 +349,11 @@ export class LightningCustodianWallet extends LegacyWallet {
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -360,12 +379,9 @@ export class LightningCustodianWallet extends LegacyWallet {
}
getTransactions() {
let txs = [];
this.pending_transactions_raw = this.pending_transactions_raw || [];
this.user_invoices_raw = this.user_invoices_raw || [];
this.transactions_raw = this.transactions_raw || [];
let txs: any = [];
txs = txs.concat(this.pending_transactions_raw.slice(), this.transactions_raw.slice().reverse(), this.user_invoices_raw.slice()); // slice so array is cloned
// transforming to how wallets/list screen expects it
for (const tx of txs) {
tx.walletID = this.getID();
if (tx.amount) {
@ -378,7 +394,7 @@ export class LightningCustodianWallet extends LegacyWallet {
if (typeof tx.amt !== 'undefined' && typeof tx.fee !== 'undefined') {
// lnd tx outgoing
tx.value = parseInt((tx.amt * 1 + tx.fee * 1) * -1, 10);
tx.value = (tx.amt * 1 + tx.fee * 1) * -1;
}
if (tx.type === 'paid_invoice') {
@ -399,12 +415,13 @@ export class LightningCustodianWallet extends LegacyWallet {
tx.received = new Date(tx.timestamp * 1000).toString();
}
return txs.sort(function (a, b) {
return txs.sort(function (a: { timestamp: number }, b: { timestamp: number }) {
return b.timestamp - a.timestamp;
});
}
async fetchPendingTransactions() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.get('/getpending', {
headers: {
'Access-Control-Allow-Origin': '*',
@ -414,11 +431,11 @@ export class LightningCustodianWallet extends LegacyWallet {
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -426,6 +443,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async fetchTransactions() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
// TODO: iterate over all available pages
const limit = 10;
let queryRes = '';
@ -442,11 +460,11 @@ export class LightningCustodianWallet extends LegacyWallet {
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -462,7 +480,8 @@ export class LightningCustodianWallet extends LegacyWallet {
return this.balance;
}
async fetchBalance(noRetry) {
async fetchBalance(noRetry?: boolean): Promise<void> {
if (!this._api) throw new Error('Internal error: _api is not initialized');
await this.checkLogin();
const response = await this._api.get('/balance', {
@ -474,11 +493,11 @@ export class LightningCustodianWallet extends LegacyWallet {
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
if (json.error) {
if (json.code * 1 === 1 && !noRetry) {
await this.authorize();
return this.fetchBalance(true);
@ -490,7 +509,6 @@ export class LightningCustodianWallet extends LegacyWallet {
throw new Error('API unexpected response: ' + JSON.stringify(response.body));
}
this.balance_raw = json;
this.balance = json.BTC.AvailableBalance;
this._lastBalanceFetch = +new Date();
}
@ -511,14 +529,14 @@ export class LightningCustodianWallet extends LegacyWallet {
* @param invoice BOLT invoice string
* @return {payment_hash: string}
*/
decodeInvoice(invoice) {
decodeInvoice(invoice: string) {
const { payeeNodeKey, tags, satoshis, millisatoshis, timestamp } = bolt11.decode(invoice);
const decoded = {
const decoded: any = {
destination: payeeNodeKey,
num_satoshis: satoshis ? satoshis.toString() : '0',
num_millisatoshis: millisatoshis ? millisatoshis.toString() : '0',
timestamp: timestamp.toString(),
timestamp: timestamp?.toString() ?? '0',
fallback_addr: '',
route_hints: [],
};
@ -554,6 +572,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
async fetchInfo() {
if (!this._api) throw new Error('Internal error: _api is not initialized');
const response = await this._api.get('/getinfo', {
headers: {
'Access-Control-Allow-Origin': '*',
@ -563,21 +582,20 @@ export class LightningCustodianWallet extends LegacyWallet {
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
if (!json.identity_pubkey) {
throw new Error('API unexpected response: ' + JSON.stringify(response.body));
}
this.info_raw = json;
}
static async isValidNodeAddress(address) {
static async isValidNodeAddress(address: string) {
const apiCall = new Frisbee({
baseURI: address,
});
@ -588,11 +606,11 @@ export class LightningCustodianWallet extends LegacyWallet {
},
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.code && json.code !== 1) {
if (json.code && json.code !== 1) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
return true;
@ -622,7 +640,8 @@ export class LightningCustodianWallet extends LegacyWallet {
* @param invoice BOLT invoice string
* @return {Promise.<Object>}
*/
async decodeInvoiceRemote(invoice) {
async decodeInvoiceRemote(invoice: string) {
if (!this._api) throw new Error('Internal error: _api is not initialized');
await this.checkLogin();
const response = await this._api.get('/decodeinvoice?invoice=' + invoice, {
@ -634,11 +653,11 @@ export class LightningCustodianWallet extends LegacyWallet {
});
const json = response.body;
if (typeof json === 'undefined') {
if (!json) {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body));
}
if (json && json.error) {
if (json.error) {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
@ -649,7 +668,7 @@ export class LightningCustodianWallet extends LegacyWallet {
return (this.decoded_invoice_raw = json);
}
weOwnTransaction(txid) {
weOwnTransaction(txid: string) {
for (const tx of this.getTransactions()) {
if (tx && tx.payment_hash && tx.payment_hash === txid) return true;
}
@ -657,7 +676,7 @@ export class LightningCustodianWallet extends LegacyWallet {
return false;
}
authenticate(lnurl) {
authenticate(lnurl: any) {
return lnurl.authenticate(this.secret);
}
}

View File

@ -3,17 +3,18 @@ import * as bip39 from 'bip39';
import * as bitcoin from 'bitcoinjs-lib';
import { Psbt, Transaction } from 'bitcoinjs-lib';
import b58 from 'bs58check';
import { CoinSelectReturnInput, CoinSelectTarget } from 'coinselect';
import createHash from 'create-hash';
import { ECPairFactory } from 'ecpair';
import * as mn from 'electrum-mnemonic';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import ecc from '../../blue_modules/noble_ecc';
import { decodeUR } from '../../blue_modules/ur';
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
import { CoinSelectReturnInput, CoinSelectTarget } from 'coinselect';
import { CreateTransactionResult, CreateTransactionUtxo } from './types';
const ECPair = ECPairFactory(ecc);
const BlueElectrum = require('../../blue_modules/BlueElectrum');
const bip32 = BIP32Factory(ecc);
type SeedOpts = {
@ -55,8 +56,12 @@ const electrumStandart = (passphrase?: string): SeedOpts => ({
const ELECTRUM_SEED_PREFIX = 'electrumseed:';
export class MultisigHDWallet extends AbstractHDElectrumWallet {
static type = 'HDmultisig';
static typeReadable = 'Multisig Vault';
static readonly type = 'HDmultisig';
static readonly typeReadable = 'Multisig Vault';
// @ts-ignore: override
public readonly type = MultisigHDWallet.type;
// @ts-ignore: override
public readonly typeReadable = MultisigHDWallet.typeReadable;
static FORMAT_P2WSH = 'p2wsh';
static FORMAT_P2SH_P2WSH = 'p2sh-p2wsh';
@ -77,7 +82,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
private _isLegacy: boolean = false;
private _nodes: BIP32Interface[][] = [];
public _derivationPath: string = '';
public gap_limit: number = 10;
public gap_limit: number = 20;
isLegacy() {
return this._isLegacy;
@ -255,15 +260,15 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
/**
* Stored cosigner can be EITHER xpub (or Zpub or smth), OR mnemonic phrase. This method converts it to xpub
*
* @param cosigner {string} Zpub (or similar) or mnemonic seed
* @param index {number}
* @returns {string} xpub
* @private
*/
_getXpubFromCosigner(cosigner: string) {
protected _getXpubFromCosignerIndex(index: number) {
let cosigner: string = this._cosigners[index];
if (MultisigHDWallet.isXprvString(cosigner)) cosigner = MultisigHDWallet.convertXprvToXpub(cosigner);
let xpub = cosigner;
if (!MultisigHDWallet.isXpubString(cosigner)) {
const index = this._cosigners.indexOf(cosigner);
xpub = MultisigHDWallet.seedToXpub(
cosigner,
this._cosignersCustomPaths[index] || this._derivationPath,
@ -285,12 +290,12 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_getAddressFromNode(nodeIndex: number, index: number) {
const pubkeys = [];
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
for (const [cosignerIndex] of this._cosigners.entries()) {
this._nodes[nodeIndex] = this._nodes[nodeIndex] || [];
let _node;
if (!this._nodes[nodeIndex][cosignerIndex]) {
const xpub = this._getXpubFromCosigner(cosigner);
const xpub = this._getXpubFromCosignerIndex(cosignerIndex);
const hdNode = bip32.fromBase58(xpub);
_node = hdNode.derive(nodeIndex);
this._nodes[nodeIndex][cosignerIndex] = _node;
@ -721,7 +726,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_addPsbtInput(psbt: Psbt, input: CoinSelectReturnInput, sequence: number, masterFingerprintBuffer?: Buffer) {
const bip32Derivation = []; // array per each pubkey thats gona be used
const pubkeys = [];
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
for (const [cosignerIndex] of this._cosigners.entries()) {
if (!input.address) {
throw new Error('Could not find address in input');
}
@ -736,7 +741,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
throw new Error('Could not find derivation path for address ' + input.address);
}
const xpub = this._getXpubFromCosigner(cosigner);
const xpub = this._getXpubFromCosignerIndex(cosignerIndex);
const hdNode0 = bip32.fromBase58(xpub);
const splt = path.split('/');
const internal = +splt[splt.length - 2];
@ -766,8 +771,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
const witnessScript = p2wsh.redeem.output;
psbt.addInput({
// @ts-ignore: fix me txid || txId issue
hash: input.txid || input.txId,
hash: input.txid,
index: input.vout,
sequence,
bip32Derivation,
@ -834,7 +838,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
_getOutputDataForChange(address: string): TOutputData {
const bip32Derivation: TBip32Derivation = []; // array per each pubkey thats gona be used
const pubkeys = [];
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
for (const [cosignerIndex] of this._cosigners.entries()) {
const path = this._getDerivationPathByAddressWithCustomPath(
address,
this._cosignersCustomPaths[cosignerIndex] || this._derivationPath,
@ -846,7 +850,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
throw new Error('Could not find derivation path for address ' + address);
}
const xpub = this._getXpubFromCosigner(cosigner);
const xpub = this._getXpubFromCosignerIndex(cosignerIndex);
const hdNode0 = bip32.fromBase58(xpub);
const splt = path.split('/');
const internal = +splt[splt.length - 2];
@ -954,7 +958,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
}
}
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
let psbt = new bitcoin.Psbt();
@ -1069,7 +1073,6 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
// now we need to fetch txhash for each input as required by PSBT
const txhexes = await BlueElectrum.multiGetTransactionByTxid(
this.getUtxo(true).map(x => x.txid),
50,
false,
);

View File

@ -1,16 +1,21 @@
import * as bitcoin from 'bitcoinjs-lib';
import { CoinSelectTarget } from 'coinselect';
import { ECPairFactory } from 'ecpair';
import ecc from '../../blue_modules/noble_ecc';
import { LegacyWallet } from './legacy-wallet';
import { CreateTransactionResult, CreateTransactionUtxo } from './types';
import { CoinSelectTarget } from 'coinselect';
const ECPair = ECPairFactory(ecc);
export class SegwitBech32Wallet extends LegacyWallet {
static type = 'segwitBech32';
static typeReadable = 'P2 WPKH';
static segwitType = 'p2wpkh';
static readonly type = 'segwitBech32';
static readonly typeReadable = 'P2 WPKH';
// @ts-ignore: override
public readonly type = SegwitBech32Wallet.type;
// @ts-ignore: override
public readonly typeReadable = SegwitBech32Wallet.typeReadable;
public readonly segwitType = 'p2wpkh';
getAddress(): string | false {
if (this._address) return this._address;
@ -80,7 +85,7 @@ export class SegwitBech32Wallet extends LegacyWallet {
for (const u of utxos) {
u.script = { length: 27 };
}
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
sequence = sequence || 0xffffffff; // disable RBF by default
const psbt = new bitcoin.Psbt();
let c = 0;

View File

@ -1,6 +1,7 @@
import * as bitcoin from 'bitcoinjs-lib';
import { CoinSelectTarget } from 'coinselect';
import { ECPairFactory } from 'ecpair';
import ecc from '../../blue_modules/noble_ecc';
import { LegacyWallet } from './legacy-wallet';
import { CreateTransactionResult, CreateTransactionUtxo } from './types';
@ -21,9 +22,13 @@ function pubkeyToP2shSegwitAddress(pubkey: Buffer): string | false {
}
export class SegwitP2SHWallet extends LegacyWallet {
static type = 'segwitP2SH';
static typeReadable = 'SegWit (P2SH)';
static segwitType = 'p2sh(p2wpkh)';
static readonly type = 'segwitP2SH';
static readonly typeReadable = 'SegWit (P2SH)';
// @ts-ignore: override
public readonly type = SegwitP2SHWallet.type;
// @ts-ignore: override
public readonly typeReadable = SegwitP2SHWallet.typeReadable;
public readonly segwitType = 'p2sh(p2wpkh)';
static witnessToAddress(witness: string): string | false {
try {
@ -75,7 +80,7 @@ export class SegwitP2SHWallet extends LegacyWallet {
/**
*
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String, txhex: String, }>} List of spendable utxos
* @param utxos {Array.<{vout: Number, value: Number, txid: String, address: String, txhex: String, }>} List of spendable utxos
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
* @param feeRate {Number} satoshi per byte
* @param changeAddress {String} Excessive coins will go back to that address
@ -98,7 +103,7 @@ export class SegwitP2SHWallet extends LegacyWallet {
for (const u of utxos) {
u.script = { length: 50 };
}
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
sequence = sequence || 0xffffffff; // disable RBF by default
const psbt = new bitcoin.Psbt();
let c = 0;

View File

@ -1,30 +1,37 @@
import slip39 from 'slip39';
import { WORD_LIST } from 'slip39/dist/slip39_helper';
import createHash from 'create-hash';
import slip39 from 'slip39';
import { WORD_LIST } from 'slip39/src/slip39_helper';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
type TWalletThis = Omit<HDLegacyP2PKHWallet | HDSegwitP2SHWallet | HDSegwitBech32Wallet, 'secret'> & {
secret: string[];
};
// collection of SLIP39 functions
const SLIP39Mixin = {
_getSeed() {
const master = slip39.recoverSecret(this.secret, this.passphrase);
const self = this as unknown as TWalletThis;
const master = slip39.recoverSecret(self.secret, self.passphrase);
return Buffer.from(master);
},
validateMnemonic() {
if (!this.secret.every(m => slip39.validateMnemonic(m))) return false;
const self = this as unknown as TWalletThis;
if (!self.secret.every(m => slip39.validateMnemonic(m))) return false;
try {
slip39.recoverSecret(this.secret);
slip39.recoverSecret(self.secret);
} catch (e) {
return false;
}
return true;
},
setSecret(newSecret) {
setSecret(newSecret: string) {
const self = this as unknown as TWalletThis;
// Try to match words to the default slip39 wordlist and complete partial words
const lookupMap = WORD_LIST.reduce((map, word) => {
const prefix3 = word.substr(0, 3);
@ -36,7 +43,7 @@ const SLIP39Mixin = {
return map;
}, new Map());
this.secret = newSecret
self.secret = newSecret
.trim()
.split('\n')
.filter(s => s)
@ -54,18 +61,23 @@ const SLIP39Mixin = {
return secret;
});
return this;
return self;
},
getID() {
const string2hash = this.secret.sort().join(',') + (this.getPassphrase() || '');
const self = this as unknown as TWalletThis;
const string2hash = self.secret.sort().join(',') + (self.getPassphrase() || '');
return createHash('sha256').update(string2hash).digest().toString('hex');
},
};
export class SLIP39LegacyP2PKHWallet extends HDLegacyP2PKHWallet {
static type = 'SLIP39legacyP2PKH';
static typeReadable = 'SLIP39 Legacy (P2PKH)';
static readonly type = 'SLIP39legacyP2PKH';
static readonly typeReadable = 'SLIP39 Legacy (P2PKH)';
// @ts-ignore: override
public readonly type = SLIP39LegacyP2PKHWallet.type;
// @ts-ignore: override
public readonly typeReadable = SLIP39LegacyP2PKHWallet.typeReadable;
allowBIP47() {
return false;
@ -73,23 +85,33 @@ export class SLIP39LegacyP2PKHWallet extends HDLegacyP2PKHWallet {
_getSeed = SLIP39Mixin._getSeed;
validateMnemonic = SLIP39Mixin.validateMnemonic;
// @ts-ignore: this type mismatch
setSecret = SLIP39Mixin.setSecret;
getID = SLIP39Mixin.getID;
}
export class SLIP39SegwitP2SHWallet extends HDSegwitP2SHWallet {
static type = 'SLIP39segwitP2SH';
static typeReadable = 'SLIP39 SegWit (P2SH)';
static readonly type = 'SLIP39segwitP2SH';
static readonly typeReadable = 'SLIP39 SegWit (P2SH)';
// @ts-ignore: override
public readonly type = SLIP39SegwitP2SHWallet.type;
// @ts-ignore: override
public readonly typeReadable = SLIP39SegwitP2SHWallet.typeReadable;
_getSeed = SLIP39Mixin._getSeed;
validateMnemonic = SLIP39Mixin.validateMnemonic;
// @ts-ignore: this type mismatch
setSecret = SLIP39Mixin.setSecret;
getID = SLIP39Mixin.getID;
}
export class SLIP39SegwitBech32Wallet extends HDSegwitBech32Wallet {
static type = 'SLIP39segwitBech32';
static typeReadable = 'SLIP39 SegWit (Bech32)';
static readonly type = 'SLIP39segwitBech32';
static readonly typeReadable = 'SLIP39 SegWit (Bech32)';
// @ts-ignore: override
public readonly type = SLIP39SegwitBech32Wallet.type;
// @ts-ignore: override
public readonly typeReadable = SLIP39SegwitBech32Wallet.typeReadable;
allowBIP47() {
return false;
@ -97,6 +119,7 @@ export class SLIP39SegwitBech32Wallet extends HDSegwitBech32Wallet {
_getSeed = SLIP39Mixin._getSeed;
validateMnemonic = SLIP39Mixin.validateMnemonic;
// @ts-ignore: this type mismatch
setSecret = SLIP39Mixin.setSecret;
getID = SLIP39Mixin.getID;
}

View File

@ -1,10 +1,15 @@
import * as bitcoin from 'bitcoinjs-lib';
import { SegwitBech32Wallet } from './segwit-bech32-wallet';
const bitcoin = require('bitcoinjs-lib');
export class TaprootWallet extends SegwitBech32Wallet {
static type = 'taproot';
static typeReadable = 'P2 TR';
static segwitType = 'p2wpkh';
static readonly type = 'taproot';
static readonly typeReadable = 'P2 TR';
// @ts-ignore: override
public readonly type = TaprootWallet.type;
// @ts-ignore: override
public readonly typeReadable = TaprootWallet.typeReadable;
public readonly segwitType = 'p2wpkh';
/**
* Converts script pub key to a Taproot address if it can. Returns FALSE if it cant.
@ -12,7 +17,7 @@ export class TaprootWallet extends SegwitBech32Wallet {
* @param scriptPubKey
* @returns {boolean|string} Either bech32 address or false
*/
static scriptPubKeyToAddress(scriptPubKey) {
static scriptPubKeyToAddress(scriptPubKey: string): string | false {
try {
const publicKey = Buffer.from(scriptPubKey, 'hex');
return bitcoin.address.fromOutputScript(publicKey, bitcoin.networks.bitcoin);

View File

@ -1,5 +1,7 @@
import bitcoin from 'bitcoinjs-lib';
import { CoinSelectOutput, CoinSelectReturnInput } from 'coinselect';
import { CoinSelectOutput, CoinSelectReturnInput, CoinSelectUtxo } from 'coinselect';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { HDAezeedWallet } from './hd-aezeed-wallet';
import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet';
import { HDLegacyElectrumSeedP2PKHWallet } from './hd-legacy-electrum-seed-p2pkh-wallet';
@ -19,30 +21,30 @@ export type Utxo = {
// Returned by BlueElectrum
height: number;
address: string;
txId: string;
txid: string;
vout: number;
value: number;
// Others
txhex?: string;
txid?: string; // TODO: same as txId, do we really need it?
confirmations?: number;
amount?: number; // TODO: same as value, do we really need it?
wif?: string | false;
};
/**
* basically the same as coinselect.d.ts/CoinselectUtxo
* and should be unified as soon as bullshit with txid/txId is sorted
* same as coinselect.d.ts/CoinSelectUtxo
*/
export type CreateTransactionUtxo = {
txId: string;
txid: string; // TODO: same as txId, do we really need it?
txhex: string;
vout: number;
value: number;
export interface CreateTransactionUtxo extends CoinSelectUtxo {}
/**
* if address is missing and `script.hex` is set - this is a custom script (like OP_RETURN)
*/
export type CreateTransactionTarget = {
address?: string;
value?: number;
script?: {
length: number;
length?: number; // either length or hex should be present
hex?: string;
};
};
@ -77,6 +79,17 @@ export type TransactionOutput = {
};
};
export type LightningTransaction = {
memo?: string;
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';
payment_hash?: string | { data: string };
category?: 'receive';
timestamp?: number;
expire_time?: number;
ispaid?: boolean;
walletID?: string;
};
export type Transaction = {
txid: string;
hash: string;
@ -88,11 +101,25 @@ export type Transaction = {
inputs: TransactionInput[];
outputs: TransactionOutput[];
blockhash: string;
confirmations?: number;
confirmations: number;
time: number;
blocktime: number;
received?: number;
value?: number;
/**
* if known, who is on the other end of the transaction (BIP47 payment code)
*/
counterparty?: string;
};
/**
* in some cases we add additional data to each tx object so the code that works with that transaction can find the
* wallet that owns it etc
*/
export type ExtendedTransaction = Transaction & {
walletID: string;
walletPreferredBalanceUnit: BitcoinUnit;
};
export type TWallet =
@ -112,3 +139,5 @@ export type TWallet =
| SegwitBech32Wallet
| SegwitP2SHWallet
| WatchOnlyWallet;
export type THDWalletForWatchOnly = HDSegwitBech32Wallet | HDSegwitP2SHWallet | HDLegacyP2PKHWallet;

View File

@ -1,22 +1,28 @@
import { LegacyWallet } from './legacy-wallet';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import BIP32Factory from 'bip32';
import * as bitcoin from 'bitcoinjs-lib';
import ecc from '../../blue_modules/noble_ecc';
import { AbstractWallet } from './abstract-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
import BIP32Factory from 'bip32';
import ecc from '../../blue_modules/noble_ecc';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import { LegacyWallet } from './legacy-wallet';
import { THDWalletForWatchOnly } from './types';
const bitcoin = require('bitcoinjs-lib');
const bip32 = BIP32Factory(ecc);
export class WatchOnlyWallet extends LegacyWallet {
static type = 'watchOnly';
static typeReadable = 'Watch-only';
static readonly type = 'watchOnly';
static readonly typeReadable = 'Watch-only';
// @ts-ignore: override
public readonly type = WatchOnlyWallet.type;
// @ts-ignore: override
public readonly typeReadable = WatchOnlyWallet.typeReadable;
public isWatchOnlyWarningVisible = true;
constructor() {
super();
this.use_with_hardware_wallet = false;
this.masterFingerprint = false;
}
public _hdWalletInstance?: THDWalletForWatchOnly;
use_with_hardware_wallet = false;
masterFingerprint: number = 0;
/**
* @inheritDoc
@ -37,7 +43,7 @@ export class WatchOnlyWallet extends LegacyWallet {
}
allowSend() {
return this.useWithHardwareWalletEnabled() && this.isHd() && this._hdWalletInstance.allowSend();
return this.useWithHardwareWalletEnabled() && this.isHd() && this._hdWalletInstance!.allowSend();
}
allowSignVerifyMessage() {
@ -65,11 +71,9 @@ export class WatchOnlyWallet extends LegacyWallet {
* this method creates appropriate HD wallet class, depending on whether we have xpub, ypub or zpub
* as a property of `this`, and in case such property exists - it recreates it and copies data from old one.
* this is needed after serialization/save/load/deserialization procedure.
*
* @return {WatchOnlyWallet} this
*/
init() {
let hdWalletInstance;
let hdWalletInstance: THDWalletForWatchOnly;
if (this.secret.startsWith('xpub')) hdWalletInstance = new HDLegacyP2PKHWallet();
else if (this.secret.startsWith('ypub')) hdWalletInstance = new HDSegwitP2SHWallet();
else if (this.secret.startsWith('zpub')) hdWalletInstance = new HDSegwitBech32Wallet();
@ -84,6 +88,7 @@ export class WatchOnlyWallet extends LegacyWallet {
if (this._hdWalletInstance) {
// now, porting all properties from old object to new one
for (const k of Object.keys(this._hdWalletInstance)) {
// @ts-ignore: JS magic here
hdWalletInstance[k] = this._hdWalletInstance[k];
}
@ -117,6 +122,7 @@ export class WatchOnlyWallet extends LegacyWallet {
async fetchBalance() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) {
if (!this._hdWalletInstance) this.init();
if (!this._hdWalletInstance) throw new Error('Internal error: _hdWalletInstance is not initialized');
return this._hdWalletInstance.fetchBalance();
} else {
// return LegacyWallet.prototype.fetchBalance.call(this);
@ -127,6 +133,7 @@ export class WatchOnlyWallet extends LegacyWallet {
async fetchTransactions() {
if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) {
if (!this._hdWalletInstance) this.init();
if (!this._hdWalletInstance) throw new Error('Internal error: _hdWalletInstance is not initialized');
return this._hdWalletInstance.fetchTransactions();
} else {
// return LegacyWallet.prototype.fetchBalance.call(this);
@ -134,18 +141,18 @@ export class WatchOnlyWallet extends LegacyWallet {
}
}
async getAddressAsync() {
async getAddressAsync(): Promise<string> {
if (this.isAddressValid(this.secret)) return new Promise(resolve => resolve(this.secret));
if (this._hdWalletInstance) return this._hdWalletInstance.getAddressAsync();
throw new Error('Not initialized');
}
_getExternalAddressByIndex(index) {
_getExternalAddressByIndex(index: number) {
if (this._hdWalletInstance) return this._hdWalletInstance._getExternalAddressByIndex(index);
throw new Error('Not initialized');
}
_getInternalAddressByIndex(index) {
_getInternalAddressByIndex(index: number) {
if (this._hdWalletInstance) return this._hdWalletInstance._getInternalAddressByIndex(index);
throw new Error('Not initialized');
}
@ -170,29 +177,30 @@ export class WatchOnlyWallet extends LegacyWallet {
throw new Error('Not initialized');
}
getUtxo(...args) {
getUtxo(...args: Parameters<THDWalletForWatchOnly['getUtxo']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getUtxo(...args);
throw new Error('Not initialized');
}
combinePsbt(base64one, base64two) {
if (this._hdWalletInstance) return this._hdWalletInstance.combinePsbt(base64one, base64two);
combinePsbt(...args: Parameters<THDWalletForWatchOnly['combinePsbt']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.combinePsbt(...args);
throw new Error('Not initialized');
}
broadcastTx(hex) {
if (this._hdWalletInstance) return this._hdWalletInstance.broadcastTx(hex);
broadcastTx(...args: Parameters<THDWalletForWatchOnly['broadcastTx']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.broadcastTx(...args);
throw new Error('Not initialized');
}
/**
* signature of this method is the same ad BIP84 createTransaction, BUT this method should be used to create
* unsinged PSBT to be used with HW wallet (or other external signer)
* @see HDSegwitBech32Wallet.createTransaction
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
createTransaction(...args: Parameters<THDWalletForWatchOnly['createTransaction']>) {
const [utxos, targets, feeRate, changeAddress, sequence] = args;
if (this._hdWalletInstance && this.isHd()) {
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, this.getMasterFingerprint());
const masterFingerprint = this.getMasterFingerprint();
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, masterFingerprint);
} else {
throw new Error('Not a HD watch-only wallet, cant create PSBT (or just not initialized)');
}
@ -224,7 +232,7 @@ export class WatchOnlyWallet extends LegacyWallet {
return this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub');
}
weOwnAddress(address) {
weOwnAddress(address: string) {
if (this.isHd()) {
if (this._hdWalletInstance) return this._hdWalletInstance.weOwnAddress(address);
throw new Error('Not initialized');
@ -243,7 +251,7 @@ export class WatchOnlyWallet extends LegacyWallet {
return !!this.use_with_hardware_wallet;
}
setUseWithHardwareWalletEnabled(enabled) {
setUseWithHardwareWalletEnabled(enabled: boolean) {
this.use_with_hardware_wallet = !!enabled;
}
@ -262,7 +270,7 @@ export class WatchOnlyWallet extends LegacyWallet {
if (this.secret.startsWith('zpub')) {
xpub = this._zpubToXpub(this.secret);
} else if (this.secret.startsWith('ypub')) {
xpub = this.constructor._ypubToXpub(this.secret);
xpub = AbstractWallet._ypubToXpub(this.secret);
} else {
xpub = this.secret;
}
@ -275,32 +283,32 @@ export class WatchOnlyWallet extends LegacyWallet {
return false;
}
addressIsChange(...args) {
addressIsChange(...args: Parameters<THDWalletForWatchOnly['addressIsChange']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.addressIsChange(...args);
return super.addressIsChange(...args);
}
getUTXOMetadata(...args) {
getUTXOMetadata(...args: Parameters<THDWalletForWatchOnly['getUTXOMetadata']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getUTXOMetadata(...args);
return super.getUTXOMetadata(...args);
}
setUTXOMetadata(...args) {
setUTXOMetadata(...args: Parameters<THDWalletForWatchOnly['setUTXOMetadata']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.setUTXOMetadata(...args);
return super.setUTXOMetadata(...args);
}
getDerivationPath(...args) {
getDerivationPath(...args: Parameters<THDWalletForWatchOnly['getDerivationPath']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getDerivationPath(...args);
throw new Error("Not a HD watch-only wallet, can't use derivation path");
}
setDerivationPath(...args) {
setDerivationPath(...args: Parameters<THDWalletForWatchOnly['setDerivationPath']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.setDerivationPath(...args);
throw new Error("Not a HD watch-only wallet, can't use derivation path");
}
isSegwit() {
isSegwit(): boolean {
if (this._hdWalletInstance) return this._hdWalletInstance.isSegwit();
return super.isSegwit();
}

View File

@ -1,9 +1,13 @@
import React from 'react';
import { Image, Keyboard, Text, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import loc from '../loc';
import React, { useCallback, useMemo } from 'react';
import { Image, Keyboard, Platform, StyleSheet, Text, TextInput, View } from 'react-native';
import { scanQrHelper } from '../helpers/scan-qr';
import loc from '../loc';
import { useTheme } from './themes';
import { useNavigation } from '@react-navigation/native';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
import Clipboard from '@react-native-clipboard/clipboard';
import presentAlert from './Alert';
import ToolTipMenu from './TooltipMenu';
interface AddressInputProps {
isLoading?: boolean;
@ -46,7 +50,6 @@ const AddressInput = ({
keyboardType = 'default',
}: AddressInputProps) => {
const { colors } = useTheme();
const { navigate } = useNavigation();
const stylesHook = StyleSheet.create({
root: {
borderColor: colors.formBorder,
@ -66,6 +69,64 @@ const AddressInput = ({
Keyboard.dismiss();
};
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
Keyboard.dismiss();
if (launchedBy) scanQrHelper(launchedBy).then(value => onBarScanned({ data: value }));
}, [launchedBy, onBarScanned, scanButtonTapped]);
const onMenuItemPressed = useCallback(
(action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) {
case actionKeys.ScanQR:
scanButtonTapped();
if (launchedBy) {
scanQrHelper(launchedBy)
.then(value => onBarScanned({ data: value }))
.catch(error => {
presentAlert({ message: error.message });
});
}
break;
case actionKeys.CopyFromClipboard:
Clipboard.getString()
.then(onChangeText)
.catch(error => {
presentAlert({ message: error.message });
});
break;
case actionKeys.ChoosePhoto:
showImagePickerAndReadImage()
.then(value => {
if (value) {
onChangeText(value);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
case actionKeys.ImportFile:
showFilePickerAndReadFile()
.then(value => {
if (value.data) {
onChangeText(value.data);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
}
Keyboard.dismiss();
},
[launchedBy, onBarScanned, onChangeText, scanButtonTapped],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
return (
<View style={[styles.root, stylesHook.root]}>
<TextInput
@ -85,17 +146,14 @@ const AddressInput = ({
keyboardType={keyboardType}
/>
{editable ? (
<TouchableOpacity
<ToolTipMenu
actions={actions}
isButton
onPressMenuItem={onMenuItemPressed}
testID="BlueAddressInputScanQrButton"
disabled={isLoading}
onPress={async () => {
await scanButtonTapped();
Keyboard.dismiss();
// @ts-ignore: Fix later
scanQrHelper(navigate, launchedBy).then(onBarScanned);
}}
accessibilityRole="button"
style={[styles.scan, stylesHook.scan]}
onPress={toolTipOnPress}
buttonStyle={buttonStyle}
accessibilityLabel={loc.send.details_scan}
accessibilityHint={loc.send.details_scan_hint}
>
@ -103,7 +161,7 @@ const AddressInput = ({
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
{loc.send.details_scan}
</Text>
</TouchableOpacity>
</ToolTipMenu>
) : null}
</View>
);
@ -142,4 +200,49 @@ const styles = StyleSheet.create({
},
});
const actionKeys = {
ScanQR: 'scan_qr',
CopyFromClipboard: 'copy_from_clipboard',
ChoosePhoto: 'choose_photo',
ImportFile: 'import_file',
};
const actionIcons = {
ScanQR: {
iconValue: Platform.OS === 'ios' ? 'qrcode' : 'ic_menu_camera',
},
ImportFile: {
iconValue: 'doc',
},
ChoosePhoto: {
iconValue: Platform.OS === 'ios' ? 'photo' : 'ic_menu_gallery',
},
Clipboard: {
iconValue: Platform.OS === 'ios' ? 'doc' : 'ic_menu_file',
},
};
const actions = [
{
id: actionKeys.ScanQR,
text: loc.wallets.list_long_scan,
icon: actionIcons.ScanQR,
},
{
id: actionKeys.CopyFromClipboard,
text: loc.wallets.list_long_clipboard,
icon: actionIcons.Clipboard,
},
{
id: actionKeys.ChoosePhoto,
text: loc.wallets.list_long_choose,
icon: actionIcons.ChoosePhoto,
},
{
id: actionKeys.ImportFile,
text: loc.wallets.import_file,
icon: actionIcons.ImportFile,
},
];
export default AddressInput;

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