mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 01:40:12 +01:00
Merge branch 'master' into ldk
This commit is contained in:
commit
9bdd0787a7
@ -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:
|
||||
|
@ -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": [
|
||||
{
|
||||
|
183
.github/workflows/build-ios-release-browserstack.yml
vendored
183
.github/workflows/build-ios-release-browserstack.yml
vendored
@ -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
|
||||
});
|
||||
|
@ -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 }}
|
||||
|
180
.github/workflows/build-ios-release-testflight.yml
vendored
180
.github/workflows/build-ios-release-testflight.yml
vendored
@ -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
|
16
.github/workflows/build-release-apk.yml
vendored
16
.github/workflows/build-release-apk.yml
vendored
@ -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
|
||||
|
137
.github/workflows/ci.yml
vendored
137
.github/workflows/ci.yml
vendored
@ -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
12
.gitignore
vendored
@ -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 +1 @@
|
||||
2.7.6
|
||||
3.1.6
|
874
.yarn/releases/yarn-3.6.4.cjs
vendored
Executable file
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
5
.yarnrc.yml
Normal file
@ -0,0 +1,5 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.6.4.cjs
|
||||
|
||||
# checksumBehavior: 'update'
|
307
App.js
307
App.js
@ -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
32
App.tsx
Normal 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;
|
@ -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
10
Gemfile
@ -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"
|
101
Gemfile.lock
101
Gemfile.lock
@ -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
23
MasterView.tsx
Normal 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;
|
614
Navigation.tsx
614
Navigation.tsx
@ -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;
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
15
README.md
15
README.md
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
@ -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)
|
@ -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" />
|
||||
|
408
android/app/src/main/assets/fiatUnits.json
Normal file
408
android/app/src/main/assets/fiatUnits.json
Normal 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)"
|
||||
}
|
||||
}
|
@ -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}")
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
5
android/app/src/main/res/drawable/widget_background.xml
Normal file
5
android/app/src/main/res/drawable/widget_background.xml
Normal 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>
|
BIN
android/app/src/main/res/drawable/widget_preview.png
Normal file
BIN
android/app/src/main/res/drawable/widget_preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
@ -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>
|
||||
|
99
android/app/src/main/res/layout/widget_layout.xml
Normal file
99
android/app/src/main/res/layout/widget_layout.xml
Normal 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>
|
@ -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>
|
||||
|
||||
|
9
android/app/src/main/res/values-night/styles.xml
Normal file
9
android/app/src/main/res/values-night/styles.xml
Normal 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>
|
@ -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>
|
1
android/app/src/main/res/values/dimens.xml
Normal file
1
android/app/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1 @@
|
||||
<dimen name="widget_corner_radius">16dp</dimen>
|
@ -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>
|
||||
|
@ -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>
|
10
android/app/src/main/res/xml/bitcoin_price_widget_info.xml
Normal file
10
android/app/src/main/res/xml/bitcoin_price_widget_info.xml
Normal 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"
|
||||
/>
|
@ -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 { } }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
98
blue_modules/BlueElectrum.d.ts
vendored
98
blue_modules/BlueElectrum.d.ts
vendored
@ -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
@ -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;
|
@ -1,8 +0,0 @@
|
||||
function WidgetCommunication(props) {
|
||||
WidgetCommunication.isBalanceDisplayAllowed = () => {};
|
||||
WidgetCommunication.setBalanceDisplayAllowed = () => {};
|
||||
WidgetCommunication.reloadAllTimelines = () => {};
|
||||
return null;
|
||||
}
|
||||
|
||||
export default WidgetCommunication;
|
@ -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;
|
@ -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
15
blue_modules/base43.ts
Normal 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;
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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 };
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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;
|
70
blue_modules/start-and-decrypt.ts
Normal file
70
blue_modules/start-and-decrypt.ts
Normal 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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -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) {
|
||||
|
@ -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 };
|
@ -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
@ -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
42
class/contact-list.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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';
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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';
|
||||
|
@ -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'];
|
||||
|
@ -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' };
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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]);
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user