mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-23 15:20:55 +01:00
Merge branch 'master' into up-android-11
This commit is contained in:
commit
490889ea46
351 changed files with 14219 additions and 21930 deletions
|
@ -2,7 +2,7 @@ version: 2
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:10.16.3
|
- image: circleci/node:10.24.1
|
||||||
|
|
||||||
working_directory: ~/repo
|
working_directory: ~/repo
|
||||||
|
|
||||||
|
|
50
.eslintrc
50
.eslintrc
|
@ -1,19 +1,22 @@
|
||||||
{
|
{
|
||||||
"parser": "babel-eslint",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react-native", // for no-inline-styles rule
|
"@typescript-eslint",
|
||||||
|
"react-native" // for no-inline-styles rule
|
||||||
],
|
],
|
||||||
"extends": [
|
"extends": [
|
||||||
"standard",
|
"standard",
|
||||||
"standard-react",
|
"standard-react",
|
||||||
|
"standard-jsx",
|
||||||
"plugin:react-hooks/recommended",
|
"plugin:react-hooks/recommended",
|
||||||
// "@react-native-community",
|
"plugin:react/recommended",
|
||||||
"plugin:prettier/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"prettier/react",
|
// "@react-native-community", // TODO: try to enable
|
||||||
"prettier/standard",
|
"plugin:prettier/recommended" // removes all eslint rules that can mess up with prettier
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/jsx-handler-names": "off", // activated by standard-react config
|
"react/jsx-handler-names": "off", // activated by standard-react config
|
||||||
|
"react/display-name": "off",
|
||||||
"react-native/no-inline-styles": "error",
|
"react-native/no-inline-styles": "error",
|
||||||
"prettier/prettier": [
|
"prettier/prettier": [
|
||||||
"warn",
|
"warn",
|
||||||
|
@ -23,9 +26,40 @@
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"@typescript-eslint/no-empty-function": "off", // used often in the codebase, useful e.g. in testing
|
||||||
|
"@typescript-eslint/ban-ts-comment": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ts-expect-error": "allow-with-description",
|
||||||
|
"ts-ignore": "allow-with-description", // temporary allow to ease the migration
|
||||||
|
"ts-nocheck": true,
|
||||||
|
"ts-check": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||||
|
|
||||||
|
// disable rules that are superseded by @typescript-eslint rules
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-use-before-define": "off",
|
||||||
|
|
||||||
|
// disable rules that we want to enforce only for typescript files
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": "off"
|
||||||
},
|
},
|
||||||
"env":{
|
"overrides": [
|
||||||
|
{
|
||||||
|
// enable the rule specifically for TypeScript files
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": ["error"],
|
||||||
|
"@typescript-eslint/no-var-requires": ["error"],
|
||||||
|
"@typescript-eslint/no-use-before-define": ["error", { "variables": false }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
"es6": true
|
"es6": true
|
||||||
},
|
},
|
||||||
"globals": { "fetch": false }
|
"globals": { "fetch": false }
|
||||||
|
|
75
.flowconfig
75
.flowconfig
|
@ -1,75 +0,0 @@
|
||||||
[ignore]
|
|
||||||
; We fork some components by platform
|
|
||||||
.*/*[.]android.js
|
|
||||||
|
|
||||||
; Ignore "BUCK" generated dirs
|
|
||||||
<PROJECT_ROOT>/\.buckd/
|
|
||||||
|
|
||||||
; Ignore polyfills
|
|
||||||
node_modules/react-native/Libraries/polyfills/.*
|
|
||||||
|
|
||||||
; These should not be required directly
|
|
||||||
; require from fbjs/lib instead: require('fbjs/lib/warning')
|
|
||||||
node_modules/warning/.*
|
|
||||||
|
|
||||||
; Flow doesn't support platforms
|
|
||||||
.*/Libraries/Utilities/LoadingView.js
|
|
||||||
|
|
||||||
[untyped]
|
|
||||||
.*/node_modules/@react-native-community/cli/.*/.*
|
|
||||||
|
|
||||||
[include]
|
|
||||||
|
|
||||||
[libs]
|
|
||||||
node_modules/react-native/interface.js
|
|
||||||
node_modules/react-native/flow/
|
|
||||||
|
|
||||||
[options]
|
|
||||||
emoji=true
|
|
||||||
|
|
||||||
esproposal.optional_chaining=enable
|
|
||||||
esproposal.nullish_coalescing=enable
|
|
||||||
|
|
||||||
module.file_ext=.js
|
|
||||||
module.file_ext=.json
|
|
||||||
module.file_ext=.ios.js
|
|
||||||
|
|
||||||
munge_underscores=true
|
|
||||||
|
|
||||||
module.name_mapper='^react-native/\(.*\)$' -> '<PROJECT_ROOT>/node_modules/react-native/\1'
|
|
||||||
module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/Image/RelativeImageStub'
|
|
||||||
|
|
||||||
suppress_type=$FlowIssue
|
|
||||||
suppress_type=$FlowFixMe
|
|
||||||
suppress_type=$FlowFixMeProps
|
|
||||||
suppress_type=$FlowFixMeState
|
|
||||||
|
|
||||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
|
|
||||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
|
|
||||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
sketchy-null-number=warn
|
|
||||||
sketchy-null-mixed=warn
|
|
||||||
sketchy-number=warn
|
|
||||||
untyped-type-import=warn
|
|
||||||
nonstrict-import=warn
|
|
||||||
deprecated-type=warn
|
|
||||||
unsafe-getters-setters=warn
|
|
||||||
inexact-spread=warn
|
|
||||||
unnecessary-invariant=warn
|
|
||||||
signature-verification-failure=warn
|
|
||||||
deprecated-utility=error
|
|
||||||
|
|
||||||
[strict]
|
|
||||||
deprecated-type
|
|
||||||
nonstrict-import
|
|
||||||
sketchy-null
|
|
||||||
unclear-type
|
|
||||||
unsafe-getters-setters
|
|
||||||
untyped-import
|
|
||||||
untyped-type-import
|
|
||||||
|
|
||||||
[version]
|
|
||||||
^0.122.0
|
|
||||||
|
|
3
.gitattributes
vendored
3
.gitattributes
vendored
|
@ -1,4 +1,5 @@
|
||||||
*.pbxproj -text
|
*.pbxproj -text
|
||||||
*.patch -text
|
*.patch -text
|
||||||
# specific for windows script files
|
# Windows files should use crlf line endings
|
||||||
|
# https://help.github.com/articles/dealing-with-line-endings/
|
||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
23
.github/ISSUE_TEMPLATE/issue-template.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/issue-template.md
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
name: Issue template
|
||||||
|
about: general issue template
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Do you need support? Just email bluewallet@bluewallet.io
|
||||||
|
|
||||||
|
## Are you reporting a bug?
|
||||||
|
|
||||||
|
Please provide:
|
||||||
|
|
||||||
|
* your phone model and OS version
|
||||||
|
* BlueWallet app version (settings->about->scroll down)
|
||||||
|
* self-test passes? Open settings->about->scroll down, tap "Run self-test"
|
||||||
|
* unique ID for our crash reporting service (settings->about->scroll down, tap "copy")
|
||||||
|
|
||||||
|
## Proposing a feature?
|
||||||
|
|
||||||
|
Go right ahead
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -7,7 +7,7 @@ on: [pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: macos-latest
|
runs-on: macos-10.15 # tmp fix for https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout project
|
- name: Checkout project
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
@ -44,6 +44,9 @@ jobs:
|
||||||
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
|
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
|
||||||
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
|
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
|
||||||
HD_MNEMONIC_BREAD: ${{ secrets.HD_MNEMONIC_BREAD }}
|
HD_MNEMONIC_BREAD: ${{ secrets.HD_MNEMONIC_BREAD }}
|
||||||
|
FAULTY_ZPUB: ${{ secrets.FAULTY_ZPUB }}
|
||||||
|
MNEMONICS_COBO: ${{ secrets.MNEMONICS_COBO }}
|
||||||
|
MNEMONICS_COLDCARD: ${{ secrets.MNEMONICS_COLDCARD }}
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
@ -89,6 +92,7 @@ jobs:
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
with:
|
with:
|
||||||
api-level: 29
|
api-level: 29
|
||||||
|
emulator-build: 6110076 # tmp fix for https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
||||||
target: google_apis
|
target: google_apis
|
||||||
avd-name: Pixel_API_29_AOSP
|
avd-name: Pixel_API_29_AOSP
|
||||||
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none
|
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
name: 'Pull request reviewer reminder'
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Check reviews every weekday, 10:00 and 17:00
|
|
||||||
- cron: '0 10,17 * * 1-5'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pull-request-reviewer-reminder:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: tommykw/pull-request-reviewer-reminder-action@v1
|
|
||||||
with:
|
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
reminder_message: 'One business day has passed since the review started. Give priority to reviews as much as possible.' # Required. Messages to send to reviewers on Github.
|
|
||||||
review_turnaround_hours: 24 # Required. This is the deadline for reviews. If this time is exceeded, a reminder wil be send.
|
|
|
@ -90,5 +90,3 @@ script:
|
||||||
- npm i -g detox-cli
|
- npm i -g detox-cli
|
||||||
- npm run e2e:release-build
|
- npm run e2e:release-build
|
||||||
- npm run e2e:release-test || npm run e2e:release-test || npm run e2e:release-test
|
- npm run e2e:release-test || npm run e2e:release-test || npm run e2e:release-test
|
||||||
|
|
||||||
after_failure: ./tests/e2e/upload-artifacts.sh
|
|
||||||
|
|
13
App.js
13
App.js
|
@ -21,8 +21,6 @@ import * as NavigationService from './NavigationService';
|
||||||
import { BlueTextCentered, BlueButton, SecondButton } from './BlueComponents';
|
import { BlueTextCentered, BlueButton, SecondButton } from './BlueComponents';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
import { Chain } from './models/bitcoinUnits';
|
import { Chain } from './models/bitcoinUnits';
|
||||||
import QuickActions from 'react-native-quick-actions';
|
|
||||||
import * as Sentry from '@sentry/react-native';
|
|
||||||
import OnAppLaunch from './class/on-app-launch';
|
import OnAppLaunch from './class/on-app-launch';
|
||||||
import DeeplinkSchemaMatch from './class/deeplink-schema-match';
|
import DeeplinkSchemaMatch from './class/deeplink-schema-match';
|
||||||
import loc from './loc';
|
import loc from './loc';
|
||||||
|
@ -30,6 +28,7 @@ import { BlueDefaultTheme, BlueDarkTheme, BlueCurrentTheme } from './components/
|
||||||
import BottomModal from './components/BottomModal';
|
import BottomModal from './components/BottomModal';
|
||||||
import InitRoot from './Navigation';
|
import InitRoot from './Navigation';
|
||||||
import BlueClipboard from './blue_modules/clipboard';
|
import BlueClipboard from './blue_modules/clipboard';
|
||||||
|
import { isDesktop } from './blue_modules/environment';
|
||||||
import { BlueStorageContext } from './blue_modules/storage-context';
|
import { BlueStorageContext } from './blue_modules/storage-context';
|
||||||
import WatchConnectivity from './WatchConnectivity';
|
import WatchConnectivity from './WatchConnectivity';
|
||||||
import DeviceQuickActions from './class/quick-actions';
|
import DeviceQuickActions from './class/quick-actions';
|
||||||
|
@ -42,12 +41,6 @@ const A = require('./blue_modules/analytics');
|
||||||
|
|
||||||
const eventEmitter = new NativeEventEmitter(NativeModules.EventEmitter);
|
const eventEmitter = new NativeEventEmitter(NativeModules.EventEmitter);
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
|
||||||
Sentry.init({
|
|
||||||
dsn: 'https://23377936131848ca8003448a893cb622@sentry.io/1295736',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClipboardContentType = Object.freeze({
|
const ClipboardContentType = Object.freeze({
|
||||||
BITCOIN: 'BITCOIN',
|
BITCOIN: 'BITCOIN',
|
||||||
LIGHTNING: 'LIGHTNING',
|
LIGHTNING: 'LIGHTNING',
|
||||||
|
@ -125,7 +118,7 @@ const App = () => {
|
||||||
Linking.addEventListener('url', handleOpenURL);
|
Linking.addEventListener('url', handleOpenURL);
|
||||||
AppState.addEventListener('change', handleAppStateChange);
|
AppState.addEventListener('change', handleAppStateChange);
|
||||||
DeviceEventEmitter.addListener('quickActionShortcut', walletQuickActions);
|
DeviceEventEmitter.addListener('quickActionShortcut', walletQuickActions);
|
||||||
QuickActions.popInitialAction().then(popInitialAction);
|
DeviceQuickActions.popInitialAction().then(popInitialAction);
|
||||||
handleAppStateChange(undefined);
|
handleAppStateChange(undefined);
|
||||||
/*
|
/*
|
||||||
When a notification on iOS is shown while the app is on foreground;
|
When a notification on iOS is shown while the app is on foreground;
|
||||||
|
@ -343,8 +336,8 @@ const App = () => {
|
||||||
<Notifications onProcessNotifications={processPushNotifications} />
|
<Notifications onProcessNotifications={processPushNotifications} />
|
||||||
{renderClipboardContentModal()}
|
{renderClipboardContentModal()}
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
{walletsInitialized && !isDesktop && <WatchConnectivity />}
|
||||||
</View>
|
</View>
|
||||||
<WatchConnectivity />
|
|
||||||
<DeviceQuickActions />
|
<DeviceQuickActions />
|
||||||
<WalletImport />
|
<WalletImport />
|
||||||
<Biometric />
|
<Biometric />
|
||||||
|
|
11
BlueApp.js
11
BlueApp.js
|
@ -4,8 +4,8 @@ import { Platform } from 'react-native';
|
||||||
import loc from './loc';
|
import loc from './loc';
|
||||||
const prompt = require('./blue_modules/prompt');
|
const prompt = require('./blue_modules/prompt');
|
||||||
const currency = require('./blue_modules/currency');
|
const currency = require('./blue_modules/currency');
|
||||||
const BlueElectrum = require('./blue_modules/BlueElectrum'); // eslint-disable-line no-unused-vars
|
const BlueElectrum = require('./blue_modules/BlueElectrum'); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
const BlueApp: AppStorage = new AppStorage();
|
const BlueApp = new AppStorage();
|
||||||
// If attempt reaches 10, a wipe keychain option will be provided to the user.
|
// If attempt reaches 10, a wipe keychain option will be provided to the user.
|
||||||
let unlockAttempt = 0;
|
let unlockAttempt = 0;
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ const startAndDecrypt = async retry => {
|
||||||
console.log('App already has some wallets, so we are in already started state, exiting startAndDecrypt');
|
console.log('App already has some wallets, so we are in already started state, exiting startAndDecrypt');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
await BlueApp.migrateKeys();
|
||||||
let password = false;
|
let password = false;
|
||||||
if (await BlueApp.storageIsEncrypted()) {
|
if (await BlueApp.storageIsEncrypted()) {
|
||||||
do {
|
do {
|
||||||
|
@ -28,7 +29,7 @@ const startAndDecrypt = async retry => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// in case of exception reading from keystore, lets retry instead of assuming there is no storage and
|
// in case of exception reading from keystore, lets retry instead of assuming there is no storage and
|
||||||
// proceeding with no wallets
|
// proceeding with no wallets
|
||||||
console.warn(error);
|
console.warn('exception loading from disk:', error);
|
||||||
wasException = true;
|
wasException = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +38,9 @@ const startAndDecrypt = async retry => {
|
||||||
try {
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000)); // sleep
|
await new Promise(resolve => setTimeout(resolve, 3000)); // sleep
|
||||||
success = await BlueApp.loadFromDisk(password);
|
success = await BlueApp.loadFromDisk(password);
|
||||||
} catch (_) {}
|
} catch (error) {
|
||||||
|
console.warn('second exception loading from disk:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|
|
@ -34,12 +34,12 @@ import WalletGradient from './class/wallet-gradient';
|
||||||
import { BlurView } from '@react-native-community/blur';
|
import { BlurView } from '@react-native-community/blur';
|
||||||
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees';
|
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees';
|
||||||
import Biometric from './class/biometrics';
|
import Biometric from './class/biometrics';
|
||||||
import { encodeUR } from 'bc-ur/dist';
|
import { encodeUR } from './blue_modules/ur';
|
||||||
import QRCode from 'react-native-qrcode-svg';
|
import QRCode from 'react-native-qrcode-svg';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useNavigation, useTheme } from '@react-navigation/native';
|
import { useNavigation, useTheme } from '@react-navigation/native';
|
||||||
import { BlueCurrentTheme } from './components/themes';
|
import { BlueCurrentTheme } from './components/themes';
|
||||||
import loc, { formatBalance, formatBalanceWithoutSuffix, transactionTimeToReadable } from './loc';
|
import loc, { formatBalance, formatStringAddTwoWhiteSpaces, formatBalanceWithoutSuffix, transactionTimeToReadable } from './loc';
|
||||||
import Lnurl from './class/lnurl';
|
import Lnurl from './class/lnurl';
|
||||||
import { BlueStorageContext } from './blue_modules/storage-context';
|
import { BlueStorageContext } from './blue_modules/storage-context';
|
||||||
import ToolTipMenu from './components/TooltipMenu';
|
import ToolTipMenu from './components/TooltipMenu';
|
||||||
|
@ -80,6 +80,7 @@ export const BlueButton = props => {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
}}
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
@ -101,6 +102,7 @@ export const SecondButton = forwardRef((props, ref) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderWidth: 0.7,
|
borderWidth: 0.7,
|
||||||
|
@ -127,7 +129,7 @@ export const SecondButton = forwardRef((props, ref) => {
|
||||||
export const BitcoinButton = props => {
|
export const BitcoinButton = props => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity testID={props.testID} onPress={props.onPress}>
|
<TouchableOpacity accessibilityRole="button" testID={props.testID} onPress={props.onPress}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
borderColor: (props.active && colors.newBlue) || colors.buttonDisabledBackgroundColor,
|
borderColor: (props.active && colors.newBlue) || colors.buttonDisabledBackgroundColor,
|
||||||
|
@ -146,8 +148,19 @@ export const BitcoinButton = props => {
|
||||||
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/bitcoin.png')} />
|
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/bitcoin.png')} />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ color: colors.newBlue, fontWeight: 'bold', fontSize: 18 }}>{loc.wallets.add_bitcoin}</Text>
|
<Text style={{ color: colors.newBlue, fontWeight: 'bold', fontSize: 18, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr' }}>
|
||||||
<Text style={{ color: colors.alternativeTextColor, fontSize: 13, fontWeight: '500' }}>{loc.wallets.add_bitcoin_explain}</Text>
|
{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>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -158,7 +171,7 @@ export const BitcoinButton = props => {
|
||||||
export const VaultButton = props => {
|
export const VaultButton = props => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity testID={props.testID} onPress={props.onPress}>
|
<TouchableOpacity accessibilityRole="button" testID={props.testID} onPress={props.onPress}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
borderColor: (props.active && colors.foregroundColor) || colors.buttonDisabledBackgroundColor,
|
borderColor: (props.active && colors.foregroundColor) || colors.buttonDisabledBackgroundColor,
|
||||||
|
@ -176,8 +189,24 @@ export const VaultButton = props => {
|
||||||
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/vault.png')} />
|
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/vault.png')} />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ color: colors.foregroundColor, fontWeight: 'bold', fontSize: 18 }}>{loc.multisig.multisig_vault}</Text>
|
<Text
|
||||||
<Text style={{ color: colors.alternativeTextColor, fontSize: 13, fontWeight: '500' }}>
|
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}
|
{loc.multisig.multisig_vault_explain}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
@ -190,7 +219,7 @@ export const VaultButton = props => {
|
||||||
export const LightningButton = props => {
|
export const LightningButton = props => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={props.onPress}>
|
<TouchableOpacity accessibilityRole="button" onPress={props.onPress}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
borderColor: (props.active && colors.lnborderColor) || colors.buttonDisabledBackgroundColor,
|
borderColor: (props.active && colors.lnborderColor) || colors.buttonDisabledBackgroundColor,
|
||||||
|
@ -209,8 +238,21 @@ export const LightningButton = props => {
|
||||||
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/lightning.png')} />
|
<Image style={{ width: 34, height: 34, marginRight: 8 }} source={require('./img/addWallet/lightning.png')} />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ color: colors.lnborderColor, fontWeight: 'bold', fontSize: 18 }}>{loc.wallets.add_lightning}</Text>
|
<Text
|
||||||
<Text style={{ color: colors.alternativeTextColor, fontSize: 13, fontWeight: '500' }}>{loc.wallets.add_lightning_explain}</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>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -383,6 +425,7 @@ export class BlueWalletNavigationHeader extends Component {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={styles.balance}
|
style={styles.balance}
|
||||||
onPress={this.changeWalletBalanceUnit}
|
onPress={this.changeWalletBalanceUnit}
|
||||||
ref={this.walletBalanceText}
|
ref={this.walletBalanceText}
|
||||||
|
@ -409,7 +452,7 @@ export class BlueWalletNavigationHeader extends Component {
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{this.state.wallet.type === LightningCustodianWallet.type && this.state.allowOnchainAddress && (
|
{this.state.wallet.type === LightningCustodianWallet.type && this.state.allowOnchainAddress && (
|
||||||
<TouchableOpacity onPress={this.manageFundsPressed}>
|
<TouchableOpacity accessibilityRole="button" onPress={this.manageFundsPressed}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
marginTop: 14,
|
marginTop: 14,
|
||||||
|
@ -437,7 +480,7 @@ export class BlueWalletNavigationHeader extends Component {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{this.state.wallet.type === MultisigHDWallet.type && (
|
{this.state.wallet.type === MultisigHDWallet.type && (
|
||||||
<TouchableOpacity onPress={this.manageFundsPressed}>
|
<TouchableOpacity accessibilityRole="button" onPress={this.manageFundsPressed}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
marginTop: 14,
|
marginTop: 14,
|
||||||
|
@ -469,10 +512,16 @@ export class BlueWalletNavigationHeader extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: remove this comment once this file gets properly converted to typescript.
|
||||||
|
*
|
||||||
|
* @type {React.FC<any>}
|
||||||
|
*/
|
||||||
export const BlueButtonLink = forwardRef((props, ref) => {
|
export const BlueButtonLink = forwardRef((props, ref) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={{
|
style={{
|
||||||
minHeight: 60,
|
minHeight: 60,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
|
@ -517,7 +566,7 @@ export const BluePrivateBalance = () => {
|
||||||
|
|
||||||
export const BlueCopyToClipboardButton = ({ stringToCopy, displayText = false }) => {
|
export const BlueCopyToClipboardButton = ({ stringToCopy, displayText = false }) => {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={() => Clipboard.setString(stringToCopy)}>
|
<TouchableOpacity accessibilityRole="button" onPress={() => Clipboard.setString(stringToCopy)}>
|
||||||
<Text style={{ fontSize: 13, fontWeight: '400', color: '#68bbe1' }}>{displayText || loc.transactions.details_copy}</Text>
|
<Text style={{ fontSize: 13, fontWeight: '400', color: '#68bbe1' }}>{displayText || loc.transactions.details_copy}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
@ -559,8 +608,13 @@ export class BlueCopyTextToClipboard extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<View style={{ justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16 }}>
|
<View style={{ justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16 }}>
|
||||||
<TouchableOpacity onPress={this.copyToClipboard} disabled={this.state.hasTappedText} testID="BlueCopyTextToClipboard">
|
<TouchableOpacity
|
||||||
<Animated.Text style={styleCopyTextToClipboard.address} numberOfLines={0}>
|
accessibilityRole="button"
|
||||||
|
onPress={this.copyToClipboard}
|
||||||
|
disabled={this.state.hasTappedText}
|
||||||
|
testID="BlueCopyTextToClipboard"
|
||||||
|
>
|
||||||
|
<Animated.Text style={styleCopyTextToClipboard.address} numberOfLines={0} testID="AddressValue">
|
||||||
{this.state.address}
|
{this.state.address}
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -596,7 +650,9 @@ export const BlueText = props => {
|
||||||
{...props}
|
{...props}
|
||||||
style={{
|
style={{
|
||||||
color: colors.foregroundColor,
|
color: colors.foregroundColor,
|
||||||
|
|
||||||
...props.style,
|
...props.style,
|
||||||
|
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -670,7 +726,17 @@ export const BlueListItem = React.memo(props => {
|
||||||
export const BlueFormLabel = props => {
|
export const BlueFormLabel = props => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
return <Text {...props} style={{ color: colors.foregroundColor, fontWeight: '400', marginHorizontal: 20 }} />;
|
return (
|
||||||
|
<Text
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
color: colors.foregroundColor,
|
||||||
|
fontWeight: '400',
|
||||||
|
marginHorizontal: 20,
|
||||||
|
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BlueFormInput = props => {
|
export const BlueFormInput = props => {
|
||||||
|
@ -770,6 +836,7 @@ export const BlueHeaderDefaultSub = props => {
|
||||||
export const BlueHeaderDefaultMain = props => {
|
export const BlueHeaderDefaultMain = props => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { isDrawerList } = props;
|
const { isDrawerList } = props;
|
||||||
|
const { isImportingWallet } = useContext(BlueStorageContext);
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
leftComponent={{
|
leftComponent={{
|
||||||
|
@ -793,7 +860,7 @@ export const BlueHeaderDefaultMain = props => {
|
||||||
bottomDivider={false}
|
bottomDivider={false}
|
||||||
topDivider={false}
|
topDivider={false}
|
||||||
backgroundColor={isDrawerList ? colors.elevated : colors.background}
|
backgroundColor={isDrawerList ? colors.elevated : colors.background}
|
||||||
rightComponent={<BluePlusIcon onPress={props.onNewWalletPress} Component={TouchableOpacity} />}
|
rightComponent={isImportingWallet ? undefined : <BluePlusIcon onPress={props.onNewWalletPress} Component={TouchableOpacity} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1141,7 +1208,7 @@ export const BlueReceiveButtonIcon = props => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity {...props} style={{ flex: 1 }}>
|
<TouchableOpacity accessibilityRole="button" {...props} style={{ flex: 1 }}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
@ -1175,7 +1242,7 @@ export const BlueReceiveButtonIcon = props => {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loc.receive.header}
|
{formatStringAddTwoWhiteSpaces(loc.receive.header)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -1349,13 +1416,7 @@ export const BlueTransactionListItem = React.memo(({ item, itemPriceUnit = Bitco
|
||||||
if (item.hash) {
|
if (item.hash) {
|
||||||
navigate('TransactionStatus', { hash: item.hash });
|
navigate('TransactionStatus', { hash: item.hash });
|
||||||
} else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') {
|
} else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') {
|
||||||
const lightningWallet = wallets.filter(wallet => {
|
const lightningWallet = wallets.filter(wallet => wallet?.getID() === item.walletID);
|
||||||
if (typeof wallet === 'object') {
|
|
||||||
if ('secret' in wallet) {
|
|
||||||
return wallet.getSecret() === item.fromWallet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (lightningWallet.length === 1) {
|
if (lightningWallet.length === 1) {
|
||||||
try {
|
try {
|
||||||
// is it a successful lnurl-pay?
|
// is it a successful lnurl-pay?
|
||||||
|
@ -1567,7 +1628,7 @@ export class BlueReplaceFeeSuggestions extends Component {
|
||||||
active: selectedFeeType === NetworkTransactionFeeType.FAST,
|
active: selectedFeeType === NetworkTransactionFeeType.FAST,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: loc.send.fee_medium,
|
label: formatStringAddTwoWhiteSpaces(loc.send.fee_medium),
|
||||||
time: loc.send.fee_3h,
|
time: loc.send.fee_3h,
|
||||||
type: NetworkTransactionFeeType.MEDIUM,
|
type: NetworkTransactionFeeType.MEDIUM,
|
||||||
rate: networkFees.mediumFee,
|
rate: networkFees.mediumFee,
|
||||||
|
@ -1582,6 +1643,7 @@ export class BlueReplaceFeeSuggestions extends Component {
|
||||||
},
|
},
|
||||||
].map(({ label, type, time, rate, active }, index) => (
|
].map(({ label, type, time, rate, active }, index) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
key={label}
|
key={label}
|
||||||
onPress={() => this.onFeeSelected(type)}
|
onPress={() => this.onFeeSelected(type)}
|
||||||
style={[
|
style={[
|
||||||
|
@ -1608,6 +1670,7 @@ export class BlueReplaceFeeSuggestions extends Component {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
onPress={() => this.customTextInput.focus()}
|
onPress={() => this.customTextInput.focus()}
|
||||||
style={[
|
style={[
|
||||||
{ paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 },
|
{ paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 },
|
||||||
|
@ -1618,7 +1681,9 @@ export class BlueReplaceFeeSuggestions extends Component {
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<Text style={{ fontSize: 22, color: BlueCurrentTheme.colors.successColor, fontWeight: '600' }}>{loc.send.fee_custom}</Text>
|
<Text style={{ fontSize: 22, color: BlueCurrentTheme.colors.successColor, fontWeight: '600' }}>
|
||||||
|
{formatStringAddTwoWhiteSpaces(loc.send.fee_custom)}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center', marginTop: 5 }}>
|
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center', marginTop: 5 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -1706,6 +1771,7 @@ export const BlueTabs = ({ active, onSwitch, tabs }) => (
|
||||||
{tabs.map((Tab, i) => (
|
{tabs.map((Tab, i) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={i}
|
key={i}
|
||||||
|
accessibilityRole="button"
|
||||||
onPress={() => onSwitch(i)}
|
onPress={() => onSwitch(i)}
|
||||||
style={[
|
style={[
|
||||||
tabsStyles.tabRoot,
|
tabsStyles.tabRoot,
|
||||||
|
@ -1813,18 +1879,21 @@ export class DynamicQRCode extends Component {
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
<View style={animatedQRCodeStyle.controller}>
|
<View style={animatedQRCodeStyle.controller}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-start' }]}
|
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-start' }]}
|
||||||
onPress={this.moveToPreviousFragment}
|
onPress={this.moveToPreviousFragment}
|
||||||
>
|
>
|
||||||
<Text style={animatedQRCodeStyle.text}>{loc.send.dynamic_prev}</Text>
|
<Text style={animatedQRCodeStyle.text}>{loc.send.dynamic_prev}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={[animatedQRCodeStyle.button, { width: '50%' }]}
|
style={[animatedQRCodeStyle.button, { width: '50%' }]}
|
||||||
onPress={this.state.intervalHandler ? this.stopAutoMove : this.startAutoMove}
|
onPress={this.state.intervalHandler ? this.stopAutoMove : this.startAutoMove}
|
||||||
>
|
>
|
||||||
<Text style={animatedQRCodeStyle.text}>{this.state.intervalHandler ? loc.send.dynamic_stop : loc.send.dynamic_start}</Text>
|
<Text style={animatedQRCodeStyle.text}>{this.state.intervalHandler ? loc.send.dynamic_stop : loc.send.dynamic_start}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-end' }]}
|
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-end' }]}
|
||||||
onPress={this.moveToNextFragment}
|
onPress={this.moveToNextFragment}
|
||||||
>
|
>
|
||||||
|
|
|
@ -80,7 +80,7 @@ import LnurlPay from './screen/lnd/lnurlPay';
|
||||||
import LnurlPaySuccess from './screen/lnd/lnurlPaySuccess';
|
import LnurlPaySuccess from './screen/lnd/lnurlPaySuccess';
|
||||||
import UnlockWith from './UnlockWith';
|
import UnlockWith from './UnlockWith';
|
||||||
import DrawerList from './screen/wallets/drawerList';
|
import DrawerList from './screen/wallets/drawerList';
|
||||||
import { isCatalyst, isTablet } from './blue_modules/environment';
|
import { isDesktop, isTablet } from './blue_modules/environment';
|
||||||
import SettingsPrivacy from './screen/settings/SettingsPrivacy';
|
import SettingsPrivacy from './screen/settings/SettingsPrivacy';
|
||||||
import LNDViewAdditionalInvoicePreImage from './screen/lnd/lndViewAdditionalInvoicePreImage';
|
import LNDViewAdditionalInvoicePreImage from './screen/lnd/lndViewAdditionalInvoicePreImage';
|
||||||
|
|
||||||
|
@ -89,7 +89,6 @@ const defaultScreenOptions =
|
||||||
? ({ route, navigation }) => ({
|
? ({ route, navigation }) => ({
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
cardOverlayEnabled: true,
|
cardOverlayEnabled: true,
|
||||||
cardStyle: { backgroundColor: '#FFFFFF' },
|
|
||||||
headerStatusBarHeight: navigation.dangerouslyGetState().routes.indexOf(route) > 0 ? 10 : undefined,
|
headerStatusBarHeight: navigation.dangerouslyGetState().routes.indexOf(route) > 0 ? 10 : undefined,
|
||||||
...TransitionPresets.ModalPresentationIOS,
|
...TransitionPresets.ModalPresentationIOS,
|
||||||
gestureResponseDistance: { vertical: Dimensions.get('window').height, horizontal: 50 },
|
gestureResponseDistance: { vertical: Dimensions.get('window').height, horizontal: 50 },
|
||||||
|
@ -104,7 +103,6 @@ const defaultStackScreenOptions =
|
||||||
? {
|
? {
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
cardOverlayEnabled: true,
|
cardOverlayEnabled: true,
|
||||||
cardStyle: { backgroundColor: '#FFFFFF' },
|
|
||||||
headerStatusBarHeight: 10,
|
headerStatusBarHeight: 10,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
@ -349,7 +347,7 @@ const Drawer = createDrawerNavigator();
|
||||||
function DrawerRoot() {
|
function DrawerRoot() {
|
||||||
const dimensions = useWindowDimensions();
|
const dimensions = useWindowDimensions();
|
||||||
const isLargeScreen =
|
const isLargeScreen =
|
||||||
Platform.OS === 'android' ? isTablet() : dimensions.width >= Dimensions.get('screen').width / 2 && (isTablet() || isCatalyst);
|
Platform.OS === 'android' ? isTablet() : dimensions.width >= Dimensions.get('screen').width / 2 && (isTablet() || isDesktop);
|
||||||
const drawerStyle = { width: '0%' };
|
const drawerStyle = { width: '0%' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -12,7 +12,7 @@ Built with React Native and Electrum.
|
||||||
[](https://itunes.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040?l=ru&ls=1&mt=8)
|
[](https://itunes.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040?l=ru&ls=1&mt=8)
|
||||||
[](https://play.google.com/store/apps/details?id=io.bluewallet.bluewallet)
|
[](https://play.google.com/store/apps/details?id=io.bluewallet.bluewallet)
|
||||||
|
|
||||||
Website: [bluewallet.io](http://bluewallet.io)
|
Website: [bluewallet.io](https://bluewallet.io)
|
||||||
|
|
||||||
Community: [telegram group](https://t.me/bluewallet)
|
Community: [telegram group](https://t.me/bluewallet)
|
||||||
|
|
||||||
|
|
|
@ -97,13 +97,13 @@ const UnlockWith = () => {
|
||||||
const color = colorScheme === 'dark' ? '#FFFFFF' : '#000000';
|
const color = colorScheme === 'dark' ? '#FFFFFF' : '#000000';
|
||||||
if ((biometricType === Biometric.TouchID || biometricType === Biometric.Biometrics) && !isStorageEncryptedEnabled) {
|
if ((biometricType === Biometric.TouchID || biometricType === Biometric.Biometrics) && !isStorageEncryptedEnabled) {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity disabled={isAuthenticating} onPress={unlockWithBiometrics}>
|
<TouchableOpacity accessibilityRole="button" disabled={isAuthenticating} onPress={unlockWithBiometrics}>
|
||||||
<Icon name="fingerprint" size={64} type="font-awesome5" color={color} />
|
<Icon name="fingerprint" size={64} type="font-awesome5" color={color} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
} else if (biometricType === Biometric.FaceID && !isStorageEncryptedEnabled) {
|
} else if (biometricType === Biometric.FaceID && !isStorageEncryptedEnabled) {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity disabled={isAuthenticating} onPress={unlockWithBiometrics}>
|
<TouchableOpacity accessibilityRole="button" disabled={isAuthenticating} onPress={unlockWithBiometrics}>
|
||||||
<Image
|
<Image
|
||||||
source={colorScheme === 'dark' ? require('./img/faceid-default.png') : require('./img/faceid-dark.png')}
|
source={colorScheme === 'dark' ? require('./img/faceid-default.png') : require('./img/faceid-dark.png')}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
|
@ -112,7 +112,7 @@ const UnlockWith = () => {
|
||||||
);
|
);
|
||||||
} else if (isStorageEncryptedEnabled) {
|
} else if (isStorageEncryptedEnabled) {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity disabled={isAuthenticating} onPress={unlockWithKey}>
|
<TouchableOpacity accessibilityRole="button" disabled={isAuthenticating} onPress={unlockWithKey}>
|
||||||
<Icon name="lock" size={64} type="font-awesome5" color={color} />
|
<Icon name="lock" size={64} type="font-awesome5" color={color} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,6 +45,10 @@ function WatchConnectivity() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [walletsInitialized, wallets, isReachable, isInstalled]);
|
}, [walletsInitialized, wallets, isReachable, isInstalled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateApplicationContext({ isWalletsInitialized: walletsInitialized, randomID: Math.floor(Math.random() * 11) });
|
||||||
|
}, [walletsInitialized]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInstalled && isReachable && walletsInitialized && preferredFiatCurrency) {
|
if (isInstalled && isReachable && walletsInitialized && preferredFiatCurrency) {
|
||||||
const preferredFiatCurrencyParsed = JSON.parse(preferredFiatCurrency);
|
const preferredFiatCurrencyParsed = JSON.parse(preferredFiatCurrency);
|
||||||
|
@ -69,11 +73,17 @@ function WatchConnectivity() {
|
||||||
if (message.request === 'createInvoice') {
|
if (message.request === 'createInvoice') {
|
||||||
handleLightningInvoiceCreateRequest(message.walletIndex, message.amount, message.description)
|
handleLightningInvoiceCreateRequest(message.walletIndex, message.amount, message.description)
|
||||||
.then(createInvoiceRequest => reply({ invoicePaymentRequest: createInvoiceRequest }))
|
.then(createInvoiceRequest => reply({ invoicePaymentRequest: createInvoiceRequest }))
|
||||||
.catch(e => console.log(e));
|
.catch(e => {
|
||||||
|
console.log(e);
|
||||||
|
reply({});
|
||||||
|
});
|
||||||
} else if (message.message === 'sendApplicationContext') {
|
} else if (message.message === 'sendApplicationContext') {
|
||||||
sendWalletsToWatch();
|
sendWalletsToWatch();
|
||||||
|
reply({});
|
||||||
} else if (message.message === 'fetchTransactions') {
|
} else if (message.message === 'fetchTransactions') {
|
||||||
fetchWalletTransactions().then(() => saveToDisk());
|
fetchWalletTransactions()
|
||||||
|
.then(() => saveToDisk())
|
||||||
|
.finally(() => reply({}));
|
||||||
} else if (message.message === 'hideBalance') {
|
} else if (message.message === 'hideBalance') {
|
||||||
const walletIndex = message.walletIndex;
|
const walletIndex = message.walletIndex;
|
||||||
const wallet = wallets[walletIndex];
|
const wallet = wallets[walletIndex];
|
||||||
|
@ -110,34 +120,31 @@ function WatchConnectivity() {
|
||||||
if (!Array.isArray(wallets)) {
|
if (!Array.isArray(wallets)) {
|
||||||
console.log('No Wallets set to sync with Watch app. Exiting...');
|
console.log('No Wallets set to sync with Watch app. Exiting...');
|
||||||
return;
|
return;
|
||||||
} else if (wallets.length === 0) {
|
}
|
||||||
console.log('Wallets array is set. No Wallets set to sync with Watch app. Exiting...');
|
if (!walletsInitialized) {
|
||||||
updateApplicationContext({ wallets: [], randomID: Math.floor(Math.random() * 11) });
|
console.log('Wallets not initialized. Exiting...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const walletsToProcess = [];
|
const walletsToProcess = [];
|
||||||
|
|
||||||
for (const wallet of wallets) {
|
for (const wallet of wallets) {
|
||||||
let receiveAddress;
|
let receiveAddress;
|
||||||
if (wallet.getAddressAsync) {
|
if (wallet.chain === Chain.ONCHAIN) {
|
||||||
if (wallet.chain === Chain.ONCHAIN) {
|
try {
|
||||||
try {
|
receiveAddress = await wallet.getAddressAsync();
|
||||||
receiveAddress = await wallet.getAddressAsync();
|
} catch (_) {}
|
||||||
} catch (_) {}
|
if (!receiveAddress) {
|
||||||
if (!receiveAddress) {
|
// either sleep expired or getAddressAsync threw an exception
|
||||||
// either sleep expired or getAddressAsync threw an exception
|
receiveAddress = wallet._getExternalAddressByIndex(wallet.next_free_address_index);
|
||||||
receiveAddress = wallet._getExternalAddressByIndex(wallet.next_free_address_index);
|
}
|
||||||
}
|
} else if (wallet.chain === Chain.OFFCHAIN) {
|
||||||
} else if (wallet.chain === Chain.OFFCHAIN) {
|
try {
|
||||||
try {
|
await wallet.getAddressAsync();
|
||||||
await wallet.getAddressAsync();
|
receiveAddress = wallet.getAddress();
|
||||||
receiveAddress = wallet.getAddress();
|
} catch (_) {}
|
||||||
} catch (_) {}
|
if (!receiveAddress) {
|
||||||
if (!receiveAddress) {
|
// either sleep expired or getAddressAsync threw an exception
|
||||||
// either sleep expired or getAddressAsync threw an exception
|
receiveAddress = wallet.getAddress();
|
||||||
receiveAddress = wallet.getAddress();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const transactions = wallet.getTransactions(10);
|
const transactions = wallet.getTransactions(10);
|
||||||
|
@ -207,7 +214,7 @@ function WatchConnectivity() {
|
||||||
hideBalance: wallet.hideBalance,
|
hideBalance: wallet.hideBalance,
|
||||||
};
|
};
|
||||||
if (wallet.chain === Chain.ONCHAIN && wallet.type !== MultisigHDWallet.type) {
|
if (wallet.chain === Chain.ONCHAIN && wallet.type !== MultisigHDWallet.type) {
|
||||||
walletInformation.push({ xpub: wallet.getXpub() ? wallet.getXpub() : wallet.getSecret() });
|
walletInformation.xpub = wallet.getXpub() ? wallet.getXpub() : wallet.getSecret();
|
||||||
}
|
}
|
||||||
walletsToProcess.push(walletInformation);
|
walletsToProcess.push(walletInformation);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export default from '@react-native-async-storage/async-storage/jest/async-storage-mock'
|
import AsyncStorageMock from '@react-native-async-storage/async-storage/jest/async-storage-mock';
|
||||||
|
|
||||||
|
export default AsyncStorageMock;
|
||||||
|
|
4
_editorconfig
Normal file
4
_editorconfig
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Windows files
|
||||||
|
[*.bat]
|
||||||
|
end_of_line = crlf
|
||||||
|
|
|
@ -20,7 +20,7 @@ import com.android.build.OutputFile
|
||||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||||
* entryFile: "index.android.js",
|
* entryFile: "index.android.js",
|
||||||
*
|
*
|
||||||
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
|
* // https://reactnative.dev/docs/performance#enable-the-ram-format
|
||||||
* bundleCommand: "ram-bundle",
|
* bundleCommand: "ram-bundle",
|
||||||
*
|
*
|
||||||
* // whether to bundle JS and assets in debug mode
|
* // whether to bundle JS and assets in debug mode
|
||||||
|
@ -121,7 +121,10 @@ def jscFlavor = 'org.webkit:android-jsc-intl:+'
|
||||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
ndkVersion rootProject.ext.ndkVersion
|
||||||
|
|
||||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
@ -136,7 +139,7 @@ android {
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "6.1.2"
|
versionName "6.2.3"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
|
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
|
||||||
|
@ -150,10 +153,11 @@ android {
|
||||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// Caution! In production, you need to generate your own keystore file.
|
// Caution! In production, you need to generate your own keystore file.
|
||||||
// see https://facebook.github.io/react-native/docs/signed-apk-android.
|
// see https://reactnative.dev/docs/signed-apk-android.
|
||||||
minifyEnabled enableProguardInReleaseBuilds
|
minifyEnabled enableProguardInReleaseBuilds
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
}
|
}
|
||||||
|
@ -164,11 +168,11 @@ android {
|
||||||
variant.outputs.each { output ->
|
variant.outputs.each { output ->
|
||||||
// For each separate APK per architecture, set a unique version code as described here:
|
// For each separate APK per architecture, set a unique version code as described here:
|
||||||
// https://developer.android.com/studio/build/configure-apk-splits.html
|
// https://developer.android.com/studio/build/configure-apk-splits.html
|
||||||
|
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
|
||||||
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
|
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
|
||||||
def abi = output.getFilter(OutputFile.ABI)
|
def abi = output.getFilter(OutputFile.ABI)
|
||||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||||
output.versionCodeOverride =
|
output.versionCodeOverride = versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
|
||||||
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -177,13 +181,8 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation files("../../node_modules/react-native-tor/android/libs/sifir_android.aar")
|
|
||||||
//noinspection GradleDynamicVersion
|
//noinspection GradleDynamicVersion
|
||||||
|
implementation files("../../node_modules/react-native-tor/android/libs/sifir_android.aar")
|
||||||
androidTestImplementation('com.wix:detox:+') {
|
|
||||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||||
|
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||||
|
@ -194,12 +193,17 @@ dependencies {
|
||||||
|
|
||||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||||
exclude group:'com.facebook.flipper'
|
exclude group:'com.facebook.flipper'
|
||||||
|
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||||
}
|
}
|
||||||
|
|
||||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||||
exclude group:'com.facebook.flipper'
|
exclude group:'com.facebook.flipper'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidTestImplementation('com.wix:detox:+') {
|
||||||
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
|
||||||
|
}
|
||||||
|
|
||||||
if (enableHermes) {
|
if (enableHermes) {
|
||||||
def hermesPath = "../../node_modules/hermes-engine/android/";
|
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||||
|
@ -217,4 +221,4 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.gms.google-services' // Google Services plugin
|
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"); applyNativeModulesAppBuildGradle(project)
|
|
@ -4,5 +4,7 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
|
||||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -93,7 +93,6 @@
|
||||||
/>
|
/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -81,4 +81,3 @@ public class MainApplication extends Application implements ReactApplication {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="android:textColor">#000000</item>
|
<item name="android:textColor">#000000</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,18 +8,20 @@ buildscript {
|
||||||
compileSdkVersion = 30
|
compileSdkVersion = 30
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 30
|
||||||
googlePlayServicesVersion = "16.+"
|
googlePlayServicesVersion = "16.+"
|
||||||
|
googlePlayServicesIidVersion = "16.0.1"
|
||||||
firebaseVersion = "17.3.4"
|
firebaseVersion = "17.3.4"
|
||||||
firebaseMessagingVersion = "20.2.1"
|
firebaseMessagingVersion = "20.2.1"
|
||||||
|
ndkVersion = "20.1.5948944"
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
ext.kotlinVersion = '1.3.+'
|
ext.kotlinVersion = '1.4.32'
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath('com.android.tools.build:gradle:4.0.1')
|
classpath('com.android.tools.build:gradle:4.2.1')
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||||
classpath 'com.google.gms:google-services:4.3.3' // Google Services plugin
|
classpath 'com.google.gms:google-services:4.3.5' // Google Services plugin
|
||||||
|
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
@ -29,6 +31,25 @@ buildscript {
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
|
jcenter() {
|
||||||
|
content {
|
||||||
|
includeModule("com.facebook.yoga", "proguard-annotations")
|
||||||
|
includeModule("com.facebook.fbjni", "fbjni-java-only")
|
||||||
|
includeModule("com.facebook.fresco", "fresco")
|
||||||
|
includeModule("com.facebook.fresco", "stetho")
|
||||||
|
includeModule("com.facebook.fresco", "fbcore")
|
||||||
|
includeModule("com.facebook.fresco", "drawee")
|
||||||
|
includeModule("com.facebook.fresco", "imagepipeline")
|
||||||
|
includeModule("com.facebook.fresco", "imagepipeline-native")
|
||||||
|
includeModule("com.facebook.fresco", "memory-type-native")
|
||||||
|
includeModule("com.facebook.fresco", "memory-type-java")
|
||||||
|
includeModule("com.facebook.fresco", "nativeimagefilters")
|
||||||
|
includeModule("com.facebook.stetho", "stetho")
|
||||||
|
includeModule("com.wei.android.lib", "fingerprintidentify")
|
||||||
|
includeModule("com.eightbitlab", "blurview")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
maven {
|
maven {
|
||||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||||
|
@ -43,7 +64,6 @@ allprojects {
|
||||||
url "$rootDir/../node_modules/detox/Detox-android"
|
url "$rootDir/../node_modules/detox/Detox-android"
|
||||||
}
|
}
|
||||||
google()
|
google()
|
||||||
jcenter()
|
|
||||||
maven { url 'https://www.jitpack.io' }
|
maven { url 'https://www.jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +74,10 @@ subprojects {
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion '30.0.3'
|
buildToolsVersion '30.0.3'
|
||||||
|
compileSdkVersion 29
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 28
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,4 +31,4 @@ org.gradle.configureondemand=true
|
||||||
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
|
|
||||||
# Version of flipper SDK to use with React Native
|
# Version of flipper SDK to use with React Native
|
||||||
FLIPPER_VERSION=0.54.0
|
FLIPPER_VERSION=0.75.1
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||||
|
|
7
android/gradlew.bat
vendored
7
android/gradlew.bat
vendored
|
@ -37,7 +37,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
@ -51,7 +51,7 @@ goto fail
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
@ -82,8 +82,7 @@ set CMD_LINE_ARGS=%*
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
|
@ -18,13 +18,7 @@ if [ -f $FILENAME ]; then
|
||||||
PR=`node scripts/appcenter-post-build-get-pr-number.js`
|
PR=`node scripts/appcenter-post-build-get-pr-number.js`
|
||||||
echo PR: $PR
|
echo PR: $PR
|
||||||
|
|
||||||
# uploading file
|
DLOAD_APK="https://lambda-download-android-build.herokuapp.com/download/$BUILD_BUILDID"
|
||||||
HASH=`date +%s`
|
|
||||||
FILENAME_UNIQ="$APPCENTER_OUTPUT_DIRECTORY/$BRANCH-$HASH.apk"
|
|
||||||
cp "$FILENAME" "$FILENAME_UNIQ"
|
|
||||||
curl "http://filestorage.bluewallet.io:1488/upload.php" -F "fileToUpload=@$FILENAME_UNIQ"
|
|
||||||
rm "$FILENAME_UNIQ"
|
|
||||||
DLOAD_APK="http://filestorage.bluewallet.io:1488/$BRANCH-$HASH.apk"
|
|
||||||
|
|
||||||
curl -X POST --data "{\"body\":\"♫ This was a triumph. I'm making a note here: HUGE SUCCESS ♫\n\n [android in browser] $APPURL\n\n[download apk]($DLOAD_APK) \"}" -u "$GITHUB" "https://api.github.com/repos/BlueWallet/BlueWallet/issues/$PR/comments"
|
curl -X POST --data "{\"body\":\"♫ This was a triumph. I'm making a note here: HUGE SUCCESS ♫\n\n [android in browser] $APPURL\n\n[download apk]($DLOAD_APK) \"}" -u "$GITHUB" "https://api.github.com/repos/BlueWallet/BlueWallet/issues/$PR/comments"
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['module:metro-react-native-babel-preset'],
|
presets: ['module:metro-react-native-babel-preset'],
|
||||||
|
plugins: ['react-native-reanimated/plugin'], // required by react-native-reanimated v2 https://docs.swmansion.com/react-native-reanimated/docs/installation/
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
/* global alert */
|
/* global alert */
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { Alert } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
import { AppStorage, LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet } from '../class';
|
import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet } from '../class';
|
||||||
import DefaultPreference from 'react-native-default-preference';
|
import DefaultPreference from 'react-native-default-preference';
|
||||||
import RNWidgetCenter from 'react-native-widget-center';
|
|
||||||
import loc from '../loc';
|
import loc from '../loc';
|
||||||
|
import { isTorCapable } from './environment';
|
||||||
|
import WidgetCommunication from './WidgetCommunication';
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
const ElectrumClient = require('electrum-client');
|
const ElectrumClient = require('electrum-client');
|
||||||
const reverse = require('buffer-reverse');
|
const reverse = require('buffer-reverse');
|
||||||
const BigNumber = require('bignumber.js');
|
const BigNumber = require('bignumber.js');
|
||||||
const torrific = require('../blue_modules/torrific');
|
const torrific = require('./torrific');
|
||||||
const Realm = require('realm');
|
const Realm = require('realm');
|
||||||
|
|
||||||
|
const ELECTRUM_HOST = 'electrum_host';
|
||||||
|
const ELECTRUM_TCP_PORT = 'electrum_tcp_port';
|
||||||
|
const ELECTRUM_SSL_PORT = 'electrum_ssl_port';
|
||||||
|
const ELECTRUM_SERVER_HISTORY = 'electrum_server_history';
|
||||||
|
|
||||||
let _realm;
|
let _realm;
|
||||||
async function _getRealm() {
|
async function _getRealm() {
|
||||||
if (_realm) return _realm;
|
if (_realm) return _realm;
|
||||||
|
@ -79,16 +85,16 @@ async function connectMain() {
|
||||||
try {
|
try {
|
||||||
if (usingPeer.host.endsWith('onion')) {
|
if (usingPeer.host.endsWith('onion')) {
|
||||||
const randomPeer = await getRandomHardcodedPeer();
|
const randomPeer = await getRandomHardcodedPeer();
|
||||||
await DefaultPreference.set(AppStorage.ELECTRUM_HOST, randomPeer.host);
|
await DefaultPreference.set(ELECTRUM_HOST, randomPeer.host);
|
||||||
await DefaultPreference.set(AppStorage.ELECTRUM_TCP_PORT, randomPeer.tcp);
|
await DefaultPreference.set(ELECTRUM_TCP_PORT, randomPeer.tcp);
|
||||||
await DefaultPreference.set(AppStorage.ELECTRUM_SSL_PORT, randomPeer.ssl);
|
await DefaultPreference.set(ELECTRUM_SSL_PORT, randomPeer.ssl);
|
||||||
} else {
|
} else {
|
||||||
await DefaultPreference.set(AppStorage.ELECTRUM_HOST, usingPeer.host);
|
await DefaultPreference.set(ELECTRUM_HOST, usingPeer.host);
|
||||||
await DefaultPreference.set(AppStorage.ELECTRUM_TCP_PORT, usingPeer.tcp);
|
await DefaultPreference.set(ELECTRUM_TCP_PORT, usingPeer.tcp);
|
||||||
await DefaultPreference.set(AppStorage.ELECTRUM_SSL_PORT, usingPeer.ssl);
|
await DefaultPreference.set(ELECTRUM_SSL_PORT, usingPeer.ssl);
|
||||||
}
|
}
|
||||||
|
|
||||||
RNWidgetCenter.reloadAllTimelines();
|
WidgetCommunication.reloadAllTimelines();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Must be running on Android
|
// Must be running on Android
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
@ -97,12 +103,13 @@ async function connectMain() {
|
||||||
try {
|
try {
|
||||||
console.log('begin connection:', JSON.stringify(usingPeer));
|
console.log('begin connection:', JSON.stringify(usingPeer));
|
||||||
mainClient = new ElectrumClient(
|
mainClient = new ElectrumClient(
|
||||||
usingPeer.host.endsWith('.onion') ? torrific : global.net,
|
usingPeer.host.endsWith('.onion') && isTorCapable ? torrific : global.net,
|
||||||
global.tls,
|
global.tls,
|
||||||
usingPeer.ssl || usingPeer.tcp,
|
usingPeer.ssl || usingPeer.tcp,
|
||||||
usingPeer.host,
|
usingPeer.host,
|
||||||
usingPeer.ssl ? 'tls' : 'tcp',
|
usingPeer.ssl ? 'tls' : 'tcp',
|
||||||
);
|
);
|
||||||
|
|
||||||
mainClient.onError = function (e) {
|
mainClient.onError = function (e) {
|
||||||
console.log('electrum mainClient.onError():', e.message);
|
console.log('electrum mainClient.onError():', e.message);
|
||||||
if (mainConnected) {
|
if (mainConnected) {
|
||||||
|
@ -187,15 +194,15 @@ async function presentNetworkErrorAlert(usingPeer) {
|
||||||
text: loc._.ok,
|
text: loc._.ok,
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
await AsyncStorage.setItem(AppStorage.ELECTRUM_HOST, '');
|
await AsyncStorage.setItem(ELECTRUM_HOST, '');
|
||||||
await AsyncStorage.setItem(AppStorage.ELECTRUM_TCP_PORT, '');
|
await AsyncStorage.setItem(ELECTRUM_TCP_PORT, '');
|
||||||
await AsyncStorage.setItem(AppStorage.ELECTRUM_SSL_PORT, '');
|
await AsyncStorage.setItem(ELECTRUM_SSL_PORT, '');
|
||||||
try {
|
try {
|
||||||
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
||||||
await DefaultPreference.clear(AppStorage.ELECTRUM_HOST);
|
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||||
await DefaultPreference.clear(AppStorage.ELECTRUM_SSL_PORT);
|
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||||
await DefaultPreference.clear(AppStorage.ELECTRUM_TCP_PORT);
|
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||||
RNWidgetCenter.reloadAllTimelines();
|
WidgetCommunication.reloadAllTimelines();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Must be running on Android
|
// Must be running on Android
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
@ -236,9 +243,9 @@ async function getRandomHardcodedPeer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSavedPeer() {
|
async function getSavedPeer() {
|
||||||
const host = await AsyncStorage.getItem(AppStorage.ELECTRUM_HOST);
|
const host = await AsyncStorage.getItem(ELECTRUM_HOST);
|
||||||
const port = await AsyncStorage.getItem(AppStorage.ELECTRUM_TCP_PORT);
|
const port = await AsyncStorage.getItem(ELECTRUM_TCP_PORT);
|
||||||
const sslPort = await AsyncStorage.getItem(AppStorage.ELECTRUM_SSL_PORT);
|
const sslPort = await AsyncStorage.getItem(ELECTRUM_SSL_PORT);
|
||||||
return { host, tcp: port, ssl: sslPort };
|
return { host, tcp: port, ssl: sslPort };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,7 +597,9 @@ module.exports.multiGetTransactionByTxid = async function (txids, batchsize, ver
|
||||||
if (txdata.error && txdata.error.code === -32600) {
|
if (txdata.error && txdata.error.code === -32600) {
|
||||||
// response too large
|
// response too large
|
||||||
// lets do single call, that should go through okay:
|
// lets do single call, that should go through okay:
|
||||||
txdata.result = await mainClient.blockchainTransaction_get(txdata.param, verbose);
|
txdata.result = await mainClient.blockchainTransaction_get(txdata.param, false);
|
||||||
|
// since we used VERBOSE=false, server sent us plain txhex which we must decode on our end:
|
||||||
|
txdata.result = txhexToElectrumTransaction(txdata.result);
|
||||||
}
|
}
|
||||||
ret[txdata.param] = txdata.result;
|
ret[txdata.param] = txdata.result;
|
||||||
if (ret[txdata.param]) delete ret[txdata.param].hex; // compact
|
if (ret[txdata.param]) delete ret[txdata.param].hex; // compact
|
||||||
|
@ -801,7 +810,7 @@ module.exports.calculateBlockTime = function (height) {
|
||||||
*/
|
*/
|
||||||
module.exports.testConnection = async function (host, tcpPort, sslPort) {
|
module.exports.testConnection = async function (host, tcpPort, sslPort) {
|
||||||
const client = new ElectrumClient(
|
const client = new ElectrumClient(
|
||||||
host.endsWith('.onion') ? torrific : global.net,
|
host.endsWith('.onion') && isTorCapable ? torrific : global.net,
|
||||||
global.tls,
|
global.tls,
|
||||||
sslPort || tcpPort,
|
sslPort || tcpPort,
|
||||||
host,
|
host,
|
||||||
|
@ -813,7 +822,7 @@ module.exports.testConnection = async function (host, tcpPort, sslPort) {
|
||||||
try {
|
try {
|
||||||
const rez = await Promise.race([
|
const rez = await Promise.race([
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
timeoutId = setTimeout(() => resolve('timeout'), host.endsWith('.onion') ? 21000 : 5000);
|
timeoutId = setTimeout(() => resolve('timeout'), host.endsWith('.onion') && isTorCapable ? 21000 : 5000);
|
||||||
}),
|
}),
|
||||||
client.connect(),
|
client.connect(),
|
||||||
]);
|
]);
|
||||||
|
@ -845,6 +854,10 @@ module.exports.setBatchingEnabled = () => {
|
||||||
|
|
||||||
module.exports.hardcodedPeers = hardcodedPeers;
|
module.exports.hardcodedPeers = hardcodedPeers;
|
||||||
module.exports.getRandomHardcodedPeer = getRandomHardcodedPeer;
|
module.exports.getRandomHardcodedPeer = getRandomHardcodedPeer;
|
||||||
|
module.exports.ELECTRUM_HOST = ELECTRUM_HOST;
|
||||||
|
module.exports.ELECTRUM_TCP_PORT = ELECTRUM_TCP_PORT;
|
||||||
|
module.exports.ELECTRUM_SSL_PORT = ELECTRUM_SSL_PORT;
|
||||||
|
module.exports.ELECTRUM_SERVER_HISTORY = ELECTRUM_SERVER_HISTORY;
|
||||||
|
|
||||||
const splitIntoChunks = function (arr, chunkSize) {
|
const splitIntoChunks = function (arr, chunkSize) {
|
||||||
const groups = [];
|
const groups = [];
|
||||||
|
|
|
@ -29,6 +29,10 @@ function WidgetCommunication() {
|
||||||
setValues();
|
setValues();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WidgetCommunication.reloadAllTimelines = () => {
|
||||||
|
RNWidgetCenter.reloadAllTimelines();
|
||||||
|
};
|
||||||
|
|
||||||
const allWalletsBalanceAndTransactionTime = async () => {
|
const allWalletsBalanceAndTransactionTime = async () => {
|
||||||
if ((await isStorageEncrypted()) || !(await WidgetCommunication.isBalanceDisplayAllowed())) {
|
if ((await isStorageEncrypted()) || !(await WidgetCommunication.isBalanceDisplayAllowed())) {
|
||||||
return { allWalletsBalance: 0, latestTransactionTime: 0 };
|
return { allWalletsBalance: 0, latestTransactionTime: 0 };
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
function WidgetCommunication(props) {
|
function WidgetCommunication(props) {
|
||||||
WidgetCommunication.isBalanceDisplayAllowed = false;
|
WidgetCommunication.isBalanceDisplayAllowed = () => {};
|
||||||
WidgetCommunication.setBalanceDisplayAllowed = () => {};
|
WidgetCommunication.setBalanceDisplayAllowed = () => {};
|
||||||
|
WidgetCommunication.reloadAllTimelines = () => {};
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
|
import * as Sentry from '@sentry/react-native';
|
||||||
import amplitude from 'amplitude-js';
|
import amplitude from 'amplitude-js';
|
||||||
import { getVersion, getSystemName } from 'react-native-device-info';
|
import { getVersion, getSystemName, getUniqueId } from 'react-native-device-info';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
const BlueApp = require('../BlueApp');
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: 'https://23377936131848ca8003448a893cb622@sentry.io/1295736',
|
||||||
|
});
|
||||||
|
Sentry.setUser({ id: getUniqueId() });
|
||||||
|
}
|
||||||
|
|
||||||
amplitude.getInstance().init('8b7cf19e8eea3cdcf16340f5fbf16330', null, {
|
amplitude.getInstance().init('8b7cf19e8eea3cdcf16340f5fbf16330', null, {
|
||||||
useNativeDeviceInfo: true,
|
useNativeDeviceInfo: true,
|
||||||
|
@ -8,6 +17,10 @@ amplitude.getInstance().init('8b7cf19e8eea3cdcf16340f5fbf16330', null, {
|
||||||
});
|
});
|
||||||
amplitude.getInstance().setVersionName(getVersion());
|
amplitude.getInstance().setVersionName(getVersion());
|
||||||
amplitude.getInstance().options.apiEndpoint = 'api2.amplitude.com';
|
amplitude.getInstance().options.apiEndpoint = 'api2.amplitude.com';
|
||||||
|
BlueApp.isDoNotTrackEnabled().then(value => {
|
||||||
|
if (value) Sentry.close();
|
||||||
|
amplitude.getInstance().setOptOut(value);
|
||||||
|
});
|
||||||
|
|
||||||
const A = async event => {
|
const A = async event => {
|
||||||
console.log('posting analytics...', event);
|
console.log('posting analytics...', event);
|
||||||
|
@ -28,4 +41,9 @@ A.ENUM = {
|
||||||
NAVIGATED_TO_WALLETS_HODLHODL: 'NAVIGATED_TO_WALLETS_HODLHODL',
|
NAVIGATED_TO_WALLETS_HODLHODL: 'NAVIGATED_TO_WALLETS_HODLHODL',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
A.setOptOut = value => {
|
||||||
|
if (value) Sentry.close();
|
||||||
|
return amplitude.getInstance().setOptOut(value);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = A;
|
module.exports = A;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# bip38
|
# bip38
|
||||||
|
|
||||||
[](http://travis-ci.org/bitcoinjs/bip38)
|
[](https://travis-ci.org/bitcoinjs/bip38)
|
||||||
[](https://coveralls.io/r/cryptocoinjs/bip38)
|
[](https://coveralls.io/r/cryptocoinjs/bip38)
|
||||||
[](https://www.npmjs.org/package/bip38)
|
[](https://www.npmjs.org/package/bip38)
|
||||||
|
|
||||||
[](https://github.com/feross/standard)
|
[](https://github.com/feross/standard)
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import DefaultPreference from 'react-native-default-preference';
|
import DefaultPreference from 'react-native-default-preference';
|
||||||
import RNWidgetCenter from 'react-native-widget-center';
|
|
||||||
import * as RNLocalize from 'react-native-localize';
|
import * as RNLocalize from 'react-native-localize';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
import { AppStorage } from '../class';
|
|
||||||
import { FiatUnit, getFiatRate } from '../models/fiatUnit';
|
import { FiatUnit, getFiatRate } from '../models/fiatUnit';
|
||||||
|
import WidgetCommunication from './WidgetCommunication';
|
||||||
|
|
||||||
|
const PREFERRED_CURRENCY = 'preferredCurrency';
|
||||||
|
const EXCHANGE_RATES = 'currency';
|
||||||
|
|
||||||
let preferredFiatCurrency = FiatUnit.USD;
|
let preferredFiatCurrency = FiatUnit.USD;
|
||||||
const exchangeRates = {};
|
const exchangeRates = {};
|
||||||
|
@ -22,15 +23,15 @@ const STRUCT = {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function setPrefferedCurrency(item) {
|
async function setPrefferedCurrency(item) {
|
||||||
await AsyncStorage.setItem(AppStorage.PREFERRED_CURRENCY, JSON.stringify(item));
|
await AsyncStorage.setItem(PREFERRED_CURRENCY, JSON.stringify(item));
|
||||||
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
||||||
await DefaultPreference.set('preferredCurrency', item.endPointKey);
|
await DefaultPreference.set('preferredCurrency', item.endPointKey);
|
||||||
await DefaultPreference.set('preferredCurrencyLocale', item.locale.replace('-', '_'));
|
await DefaultPreference.set('preferredCurrencyLocale', item.locale.replace('-', '_'));
|
||||||
RNWidgetCenter.reloadAllTimelines();
|
WidgetCommunication.reloadAllTimelines();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPreferredCurrency() {
|
async function getPreferredCurrency() {
|
||||||
const preferredCurrency = await JSON.parse(await AsyncStorage.getItem(AppStorage.PREFERRED_CURRENCY));
|
const preferredCurrency = await JSON.parse(await AsyncStorage.getItem(PREFERRED_CURRENCY));
|
||||||
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
||||||
await DefaultPreference.set('preferredCurrency', preferredCurrency.endPointKey);
|
await DefaultPreference.set('preferredCurrency', preferredCurrency.endPointKey);
|
||||||
await DefaultPreference.set('preferredCurrencyLocale', preferredCurrency.locale.replace('-', '_'));
|
await DefaultPreference.set('preferredCurrencyLocale', preferredCurrency.locale.replace('-', '_'));
|
||||||
|
@ -44,7 +45,7 @@ async function updateExchangeRate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
preferredFiatCurrency = JSON.parse(await AsyncStorage.getItem(AppStorage.PREFERRED_CURRENCY));
|
preferredFiatCurrency = JSON.parse(await AsyncStorage.getItem(PREFERRED_CURRENCY));
|
||||||
if (preferredFiatCurrency === null) {
|
if (preferredFiatCurrency === null) {
|
||||||
throw Error('No Preferred Fiat selected');
|
throw Error('No Preferred Fiat selected');
|
||||||
}
|
}
|
||||||
|
@ -61,15 +62,15 @@ async function updateExchangeRate() {
|
||||||
try {
|
try {
|
||||||
rate = await getFiatRate(preferredFiatCurrency.endPointKey);
|
rate = await getFiatRate(preferredFiatCurrency.endPointKey);
|
||||||
} catch (Err) {
|
} catch (Err) {
|
||||||
const lastSavedExchangeRate = JSON.parse(await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES));
|
const lastSavedExchangeRate = JSON.parse(await AsyncStorage.getItem(EXCHANGE_RATES));
|
||||||
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = lastSavedExchangeRate['BTC_' + preferredFiatCurrency.endPointKey] * 1;
|
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = lastSavedExchangeRate['BTC_' + preferredFiatCurrency.endPointKey] * 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
exchangeRates[STRUCT.LAST_UPDATED] = +new Date();
|
exchangeRates[STRUCT.LAST_UPDATED] = +new Date();
|
||||||
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = rate;
|
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = rate;
|
||||||
await AsyncStorage.setItem(AppStorage.EXCHANGE_RATES, JSON.stringify(exchangeRates));
|
await AsyncStorage.setItem(EXCHANGE_RATES, JSON.stringify(exchangeRates));
|
||||||
await AsyncStorage.setItem(AppStorage.PREFERRED_CURRENCY, JSON.stringify(preferredFiatCurrency));
|
await AsyncStorage.setItem(PREFERRED_CURRENCY, JSON.stringify(preferredFiatCurrency));
|
||||||
await setPrefferedCurrency(preferredFiatCurrency);
|
await setPrefferedCurrency(preferredFiatCurrency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,3 +180,5 @@ module.exports.btcToSatoshi = btcToSatoshi;
|
||||||
module.exports.getCurrencySymbol = getCurrencySymbol;
|
module.exports.getCurrencySymbol = getCurrencySymbol;
|
||||||
module.exports._setPreferredFiatCurrency = _setPreferredFiatCurrency; // export it to mock data in tests
|
module.exports._setPreferredFiatCurrency = _setPreferredFiatCurrency; // export it to mock data in tests
|
||||||
module.exports._setExchangeRate = _setExchangeRate; // export it to mock data in tests
|
module.exports._setExchangeRate = _setExchangeRate; // export it to mock data in tests
|
||||||
|
module.exports.PREFERRED_CURRENCY = PREFERRED_CURRENCY;
|
||||||
|
module.exports.EXCHANGE_RATES = EXCHANGE_RATES;
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
import { getSystemName, isTablet } from 'react-native-device-info';
|
import { Platform } from 'react-native';
|
||||||
import isCatalyst from 'react-native-is-catalyst';
|
import { getSystemName, isTablet, getDeviceType } from 'react-native-device-info';
|
||||||
|
|
||||||
const isMacCatalina = getSystemName() === 'Mac OS X';
|
const isMacCatalina = getSystemName() === 'Mac OS X';
|
||||||
|
const isDesktop = getDeviceType() === 'Desktop';
|
||||||
|
const getIsTorCapable = () => {
|
||||||
|
let capable = true;
|
||||||
|
if (Platform.OS === 'android' && Platform.Version < 26) {
|
||||||
|
capable = false;
|
||||||
|
} else if (isDesktop) {
|
||||||
|
capable = false;
|
||||||
|
}
|
||||||
|
return capable;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.isMacCatalina = isMacCatalina;
|
export const isHandset = getDeviceType() === 'Handset';
|
||||||
module.exports.isCatalyst = isCatalyst;
|
export const isTorCapable = getIsTorCapable();
|
||||||
module.exports.isTablet = isTablet;
|
export { isMacCatalina, isDesktop, isTablet };
|
||||||
|
|
|
@ -4,20 +4,52 @@ import RNFS from 'react-native-fs';
|
||||||
import Share from 'react-native-share';
|
import Share from 'react-native-share';
|
||||||
import loc from '../loc';
|
import loc from '../loc';
|
||||||
import DocumentPicker from 'react-native-document-picker';
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
import isCatalyst from 'react-native-is-catalyst';
|
|
||||||
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
|
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
|
||||||
import { presentCameraNotAuthorizedAlert } from '../class/camera';
|
import { presentCameraNotAuthorizedAlert } from '../class/camera';
|
||||||
|
import { isDesktop } from '../blue_modules/environment';
|
||||||
import ActionSheet from '../screen/ActionSheet';
|
import ActionSheet from '../screen/ActionSheet';
|
||||||
import BlueClipboard from './clipboard';
|
import BlueClipboard from './clipboard';
|
||||||
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
|
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
|
||||||
|
|
||||||
|
const writeFileAndExportToAndroidDestionation = async ({ filename, contents, destinationLocalizedString, destination }) => {
|
||||||
|
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
||||||
|
title: loc.send.permission_storage_title,
|
||||||
|
message: loc.send.permission_storage_message,
|
||||||
|
buttonNeutral: loc.send.permission_storage_later,
|
||||||
|
buttonNegative: loc._.cancel,
|
||||||
|
buttonPositive: loc._.ok,
|
||||||
|
});
|
||||||
|
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
||||||
|
const filePath = destination + `/${filename}`;
|
||||||
|
try {
|
||||||
|
await RNFS.writeFile(filePath, contents);
|
||||||
|
alert(loc.formatString(loc._.file_saved, { filePath: filename, destination: destinationLocalizedString }));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Storage Permission: Denied');
|
||||||
|
Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [
|
||||||
|
{
|
||||||
|
text: loc.send.open_settings,
|
||||||
|
onPress: () => {
|
||||||
|
Linking.openSettings();
|
||||||
|
},
|
||||||
|
style: 'default',
|
||||||
|
},
|
||||||
|
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const writeFileAndExport = async function (filename, contents) {
|
const writeFileAndExport = async function (filename, contents) {
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`;
|
const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`;
|
||||||
await RNFS.writeFile(filePath, contents);
|
await RNFS.writeFile(filePath, contents);
|
||||||
Share.open({
|
Share.open({
|
||||||
url: 'file://' + filePath,
|
url: 'file://' + filePath,
|
||||||
saveToFiles: isCatalyst,
|
saveToFiles: isDesktop,
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
@ -26,37 +58,37 @@ const writeFileAndExport = async function (filename, contents) {
|
||||||
RNFS.unlink(filePath);
|
RNFS.unlink(filePath);
|
||||||
});
|
});
|
||||||
} else if (Platform.OS === 'android') {
|
} else if (Platform.OS === 'android') {
|
||||||
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
Alert.alert(
|
||||||
title: loc.send.permission_storage_title,
|
loc._.file_save_title,
|
||||||
message: loc.send.permission_storage_message,
|
|
||||||
buttonNeutral: loc.send.permission_storage_later,
|
|
||||||
buttonNegative: loc._.cancel,
|
|
||||||
buttonPositive: loc._.ok,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
loc.formatString(loc._.file_save_location, { filePath: filename }),
|
||||||
console.log('Storage Permission: Granted');
|
[
|
||||||
const filePath = RNFS.DownloadDirectoryPath + `/${filename}`;
|
|
||||||
try {
|
|
||||||
await RNFS.writeFile(filePath, contents);
|
|
||||||
alert(loc.formatString(loc._.file_saved, { filePath: filename }));
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
alert(e.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Storage Permission: Denied');
|
|
||||||
Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [
|
|
||||||
{
|
|
||||||
text: loc.send.open_settings,
|
|
||||||
onPress: () => {
|
|
||||||
Linking.openSettings();
|
|
||||||
},
|
|
||||||
style: 'default',
|
|
||||||
},
|
|
||||||
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
|
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
|
||||||
]);
|
{
|
||||||
}
|
text: loc._.downloads_folder,
|
||||||
|
onPress: () => {
|
||||||
|
writeFileAndExportToAndroidDestionation({
|
||||||
|
filename,
|
||||||
|
contents,
|
||||||
|
destinationLocalizedString: loc._.downloads_folder,
|
||||||
|
destination: RNFS.DownloadDirectoryPath,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: loc._.external_storage,
|
||||||
|
onPress: async () => {
|
||||||
|
writeFileAndExportToAndroidDestionation({
|
||||||
|
filename,
|
||||||
|
contents,
|
||||||
|
destination: RNFS.ExternalStorageDirectoryPath,
|
||||||
|
destinationLocalizedString: loc._.external_storage,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ cancelable: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -103,6 +135,8 @@ const showImagePickerAndReadImage = () => {
|
||||||
title: null,
|
title: null,
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
takePhotoButtonTitle: null,
|
takePhotoButtonTitle: null,
|
||||||
|
maxHeight: 800,
|
||||||
|
maxWidth: 600,
|
||||||
},
|
},
|
||||||
response => {
|
response => {
|
||||||
if (response.uri) {
|
if (response.uri) {
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
3.0.0 / 2019-03-12
|
|
||||||
------------------
|
|
||||||
- **breaking** Import gives an object with two functions `scrypt` and `scryptSync`
|
|
||||||
- `scryptSync` is the old synchronus function.
|
|
||||||
- `scrypt` will return a promise with the buffer.
|
|
||||||
|
|
||||||
2.0.0 / 2016-05-26
|
|
||||||
------------------
|
|
||||||
- **breaking** Node v0.10 not supported anymore.
|
|
||||||
|
|
||||||
1.2.1 / 2015-03-01
|
|
||||||
------------------
|
|
||||||
- now using standard for code formatting
|
|
||||||
- now using `pbkdf2` module over `pbkdf2-sha256`, huge performance increase in Node
|
|
||||||
|
|
||||||
1.2.0 / 2014-12-11
|
|
||||||
------------------
|
|
||||||
- upgraded `pbkdf2-sha256` from `1.0.1` to `1.1.0`
|
|
||||||
- removed `browser` field for `crypto`; not-necessary anymore
|
|
||||||
|
|
||||||
1.1.0 / 2014-07-28
|
|
||||||
------------------
|
|
||||||
- added `progressCallback` (Nadav Ivgi / #4)[https://github.com/cryptocoinjs/scryptsy/pull/4]
|
|
||||||
|
|
||||||
1.0.0 / 2014-06-10
|
|
||||||
------------------
|
|
||||||
- moved tests to fixtures
|
|
||||||
- removed semilcolons per http://cryptocoinjs.com/about/contributing/#semicolons
|
|
||||||
- changed `module.exports.scrypt = funct..` to `module.exports = funct...`
|
|
||||||
- removed `terst` from dev deps
|
|
||||||
- upgraded `"pbkdf2-sha256": "~0.1.1"` to `"pbkdf2-sha256": "^1.0.1"`
|
|
||||||
- added `crypto-browserify` dev dep for `pbkdf2-sha256` tests
|
|
||||||
- added TravisCI
|
|
||||||
- added Coveralls
|
|
||||||
- added testling
|
|
||||||
|
|
||||||
0.2.0 / 2014-03-05
|
|
||||||
------------------
|
|
||||||
- made a lot of scrypt functions internal along with variables to make thread safe
|
|
||||||
|
|
||||||
0.1.0 / 2014-02-18
|
|
||||||
------------------
|
|
||||||
- changed spacing from 4 to 2
|
|
||||||
- removed unneeded JavaScript implementations. Using `pbkdf2-sha256` dep now.
|
|
||||||
- add browser test support
|
|
||||||
- convert from `Array` to typed arrays and `Buffer`
|
|
||||||
|
|
||||||
0.0.1 / 2014-02-18
|
|
||||||
------------------
|
|
||||||
- initial release. Forked from https://github.com/cheongwy/node-scrypt-js and added tests.
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 cryptocoinjs
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,157 +0,0 @@
|
||||||
scryptsy
|
|
||||||
========
|
|
||||||
|
|
||||||
[](http://travis-ci.org/cryptocoinjs/scryptsy)
|
|
||||||
[](https://coveralls.io/r/cryptocoinjs/scryptsy)
|
|
||||||
[](https://www.npmjs.org/package/scryptsy)
|
|
||||||
|
|
||||||
`scryptsy` is a pure Javascript implementation of the [scrypt][wiki] key derivation function that is fully compatible with Node.js and the browser (via Browserify).
|
|
||||||
|
|
||||||
|
|
||||||
Why?
|
|
||||||
----
|
|
||||||
|
|
||||||
`Scrypt` is an integral part of many crypto currencies. It's a part of the [BIP38](https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki) standard for encrypting private Bitcoin keys. It also serves as the [proof-of-work system](http://en.wikipedia.org/wiki/Proof-of-work_system) for many crypto currencies, most notably: Litecoin and Dogecoin.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
|
|
||||||
npm install --save scryptsy
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Browserify Note
|
|
||||||
------------
|
|
||||||
|
|
||||||
When using a browserified bundle, be sure to add `setImmediate` as a shim.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
|
|
||||||
```js
|
|
||||||
const scrypt = require('scryptsy')
|
|
||||||
|
|
||||||
async function main () {
|
|
||||||
var key = "pleaseletmein"
|
|
||||||
var salt = "SodiumChloride"
|
|
||||||
var data1 = scrypt(key, salt, 16384, 8, 1, 64)
|
|
||||||
console.log(data1.toString('hex'))
|
|
||||||
// => 7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887
|
|
||||||
|
|
||||||
// async is actually slower, but it will free up the event loop occasionally
|
|
||||||
// which will allow for front end GUI elements to update and cause it to not
|
|
||||||
// freeze up.
|
|
||||||
// See benchmarks below
|
|
||||||
// Passing 300 below means every 300 iterations internally will call setImmediate once
|
|
||||||
var data2 = await scrypt.async(key, salt, 16384, 8, 1, 64, undefined, 300)
|
|
||||||
console.log(data2.toString('hex'))
|
|
||||||
// => 7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887
|
|
||||||
}
|
|
||||||
main().catch(console.error)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Benchmarks
|
|
||||||
-------
|
|
||||||
|
|
||||||
Internal iterations are N * p, so changing r doesn't affect the number of calls to setImmediate.
|
|
||||||
Decreasing pI decreases performance in exchange for more frequently freeing the event loop.
|
|
||||||
(pI Default is 5000 loops per setImmediate call)
|
|
||||||
|
|
||||||
Note: these benchmarks were done on node v10 on a CPU with good single thread performance.
|
|
||||||
browsers show a much larger difference. Please tinker with the pI setting to balance between
|
|
||||||
performance and GUI responsiveness.
|
|
||||||
|
|
||||||
If `pI >= N`, setImmediate will only be called `p * 2` times total (on the i = 0 of each for loop).
|
|
||||||
|
|
||||||
```
|
|
||||||
---------------------------
|
|
||||||
time : type : (N,r,p,pI) (pI = promiseInterval)
|
|
||||||
---------------------------
|
|
||||||
2266 ms : sync (2^16,16,1)
|
|
||||||
2548 ms : async (2^16,16,1,5000)
|
|
||||||
12.44% increase
|
|
||||||
---------------------------
|
|
||||||
2616 ms : sync (2^16,1,16)
|
|
||||||
2995 ms : async (2^16,1,16,5000)
|
|
||||||
14.49% increase
|
|
||||||
---------------------------
|
|
||||||
2685 ms : sync (2^20,1,1)
|
|
||||||
3090 ms : async (2^20,1,1,5000)
|
|
||||||
15.08% increase
|
|
||||||
---------------------------
|
|
||||||
2235 ms : sync (2^16,16,1)
|
|
||||||
2627 ms : async (2^16,16,1,10)
|
|
||||||
17.54% increase
|
|
||||||
---------------------------
|
|
||||||
2592 ms : sync (2^16,1,16)
|
|
||||||
3305 ms : async (2^16,1,16,10)
|
|
||||||
27.51% increase
|
|
||||||
---------------------------
|
|
||||||
2705 ms : sync (2^20,1,1)
|
|
||||||
3363 ms : async (2^20,1,1,10)
|
|
||||||
24.33% increase
|
|
||||||
---------------------------
|
|
||||||
2278 ms : sync (2^16,16,1)
|
|
||||||
2773 ms : async (2^16,16,1,1)
|
|
||||||
21.73% increase
|
|
||||||
---------------------------
|
|
||||||
2617 ms : sync (2^16,1,16)
|
|
||||||
5632 ms : async (2^16,1,16,1)
|
|
||||||
115.21% increase
|
|
||||||
---------------------------
|
|
||||||
2727 ms : sync (2^20,1,1)
|
|
||||||
5723 ms : async (2^20,1,1,1)
|
|
||||||
109.86% increase
|
|
||||||
---------------------------
|
|
||||||
```
|
|
||||||
|
|
||||||
API
|
|
||||||
---
|
|
||||||
|
|
||||||
### scrypt(key, salt, N, r, p, keyLenBytes, [progressCallback])
|
|
||||||
|
|
||||||
- **key**: The key. Either `Buffer` or `string`.
|
|
||||||
- **salt**: The salt. Either `Buffer` or `string`.
|
|
||||||
- **N**: The number of iterations. `number` (integer)
|
|
||||||
- **r**: Memory factor. `number` (integer)
|
|
||||||
- **p**: Parallelization factor. `number` (integer)
|
|
||||||
- **keyLenBytes**: The number of bytes to return. `number` (integer)
|
|
||||||
- **progressCallback**: Call callback on every `1000` ops. Passes in `{current, total, percent}` as first parameter to `progressCallback()`.
|
|
||||||
|
|
||||||
Returns `Buffer`.
|
|
||||||
|
|
||||||
### scrypt.async(key, salt, N, r, p, keyLenBytes, [progressCallback, promiseInterval])
|
|
||||||
|
|
||||||
- **key**: The key. Either `Buffer` or `string`.
|
|
||||||
- **salt**: The salt. Either `Buffer` or `string`.
|
|
||||||
- **N**: The number of iterations. `number` (integer)
|
|
||||||
- **r**: Memory factor. `number` (integer)
|
|
||||||
- **p**: Parallelization factor. `number` (integer)
|
|
||||||
- **keyLenBytes**: The number of bytes to return. `number` (integer)
|
|
||||||
- **progressCallback**: Call callback on every `1000` ops. Passes in `{current, total, percent}` as first parameter to `progressCallback()`.
|
|
||||||
- **promiseInterval**: The number of internal iterations before calling setImmediate once to free the event loop.
|
|
||||||
|
|
||||||
Returns `Promise<Buffer>`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Resources
|
|
||||||
---------
|
|
||||||
- [Tarsnap Blurb on Scrypt][tarsnap]
|
|
||||||
- [Scrypt Whitepaper](http://www.tarsnap.com/scrypt/scrypt.pdf)
|
|
||||||
- [IETF Scrypt](https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-00) (Test vector params are [incorrect](https://twitter.com/dchest/status/247734446881640448).)
|
|
||||||
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
||||||
|
|
||||||
[wiki]: http://en.wikipedia.org/wiki/Scrypt
|
|
||||||
[tarsnap]: http://www.tarsnap.com/scrypt.html
|
|
|
@ -1,3 +0,0 @@
|
||||||
const scrypt = require('./scryptSync')
|
|
||||||
scrypt.async = require('./scrypt')
|
|
||||||
module.exports = scrypt
|
|
|
@ -1,26 +0,0 @@
|
||||||
let pbkdf2 = require('pbkdf2')
|
|
||||||
const {
|
|
||||||
checkAndInit,
|
|
||||||
smix
|
|
||||||
} = require('./utils')
|
|
||||||
|
|
||||||
// N = Cpu cost, r = Memory cost, p = parallelization cost
|
|
||||||
async function scrypt (key, salt, N, r, p, dkLen, progressCallback, promiseInterval) {
|
|
||||||
const {
|
|
||||||
XY,
|
|
||||||
V,
|
|
||||||
B32,
|
|
||||||
x,
|
|
||||||
_X,
|
|
||||||
B,
|
|
||||||
tickCallback
|
|
||||||
} = checkAndInit(key, salt, N, r, p, dkLen, progressCallback)
|
|
||||||
|
|
||||||
for (var i = 0; i < p; i++) {
|
|
||||||
await smix(B, i * 128 * r, r, N, V, XY, _X, B32, x, tickCallback, promiseInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pbkdf2.pbkdf2Sync(key, B, 1, dkLen, 'sha256')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = scrypt
|
|
|
@ -1,26 +0,0 @@
|
||||||
let pbkdf2 = require('pbkdf2')
|
|
||||||
const {
|
|
||||||
checkAndInit,
|
|
||||||
smixSync
|
|
||||||
} = require('./utils')
|
|
||||||
|
|
||||||
// N = Cpu cost, r = Memory cost, p = parallelization cost
|
|
||||||
function scrypt (key, salt, N, r, p, dkLen, progressCallback) {
|
|
||||||
const {
|
|
||||||
XY,
|
|
||||||
V,
|
|
||||||
B32,
|
|
||||||
x,
|
|
||||||
_X,
|
|
||||||
B,
|
|
||||||
tickCallback
|
|
||||||
} = checkAndInit(key, salt, N, r, p, dkLen, progressCallback)
|
|
||||||
|
|
||||||
for (var i = 0; i < p; i++) {
|
|
||||||
smixSync(B, i * 128 * r, r, N, V, XY, _X, B32, x, tickCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pbkdf2.pbkdf2Sync(key, B, 1, dkLen, 'sha256')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = scrypt
|
|
|
@ -1,216 +0,0 @@
|
||||||
let pbkdf2 = require('pbkdf2')
|
|
||||||
const MAX_VALUE = 0x7fffffff
|
|
||||||
const DEFAULT_PROMISE_INTERVAL = 5000
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
|
|
||||||
function checkAndInit (key, salt, N, r, p, dkLen, progressCallback) {
|
|
||||||
if (N === 0 || (N & (N - 1)) !== 0) throw Error('N must be > 0 and a power of 2')
|
|
||||||
|
|
||||||
if (N > MAX_VALUE / 128 / r) throw Error('Parameter N is too large')
|
|
||||||
if (r > MAX_VALUE / 128 / p) throw Error('Parameter r is too large')
|
|
||||||
|
|
||||||
let XY = Buffer.alloc(256 * r)
|
|
||||||
let V = Buffer.alloc(128 * r * N)
|
|
||||||
|
|
||||||
// pseudo global
|
|
||||||
let B32 = new Int32Array(16) // salsa20_8
|
|
||||||
let x = new Int32Array(16) // salsa20_8
|
|
||||||
let _X = Buffer.alloc(64) // blockmix_salsa8
|
|
||||||
|
|
||||||
// pseudo global
|
|
||||||
let B = pbkdf2.pbkdf2Sync(key, salt, 1, p * 128 * r, 'sha256')
|
|
||||||
|
|
||||||
let tickCallback
|
|
||||||
if (progressCallback) {
|
|
||||||
let totalOps = p * N * 2
|
|
||||||
let currentOp = 0
|
|
||||||
|
|
||||||
tickCallback = function () {
|
|
||||||
++currentOp
|
|
||||||
|
|
||||||
// send progress notifications once every 1,000 ops
|
|
||||||
if (currentOp % 1000 === 0) {
|
|
||||||
progressCallback({
|
|
||||||
current: currentOp,
|
|
||||||
total: totalOps,
|
|
||||||
percent: (currentOp / totalOps) * 100.0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
XY,
|
|
||||||
V,
|
|
||||||
B32,
|
|
||||||
x,
|
|
||||||
_X,
|
|
||||||
B,
|
|
||||||
tickCallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function smix (B, Bi, r, N, V, XY, _X, B32, x, tickCallback, promiseInterval) {
|
|
||||||
promiseInterval = promiseInterval || DEFAULT_PROMISE_INTERVAL
|
|
||||||
let Xi = 0
|
|
||||||
let Yi = 128 * r
|
|
||||||
let i
|
|
||||||
|
|
||||||
B.copy(XY, Xi, Bi, Bi + Yi)
|
|
||||||
|
|
||||||
for (i = 0; i < N; i++) {
|
|
||||||
XY.copy(V, i * Yi, Xi, Xi + Yi)
|
|
||||||
if (i % promiseInterval === 0) {
|
|
||||||
await new Promise(resolve => setImmediate(resolve))
|
|
||||||
}
|
|
||||||
blockmix_salsa8(XY, Xi, Yi, r, _X, B32, x)
|
|
||||||
|
|
||||||
if (tickCallback) tickCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < N; i++) {
|
|
||||||
let offset = Xi + (2 * r - 1) * 64
|
|
||||||
let j = XY.readUInt32LE(offset) & (N - 1)
|
|
||||||
blockxor(V, j * Yi, XY, Xi, Yi)
|
|
||||||
if (i % promiseInterval === 0) {
|
|
||||||
await new Promise(resolve => setImmediate(resolve))
|
|
||||||
}
|
|
||||||
blockmix_salsa8(XY, Xi, Yi, r, _X, B32, x)
|
|
||||||
|
|
||||||
if (tickCallback) tickCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
XY.copy(B, Bi, Xi, Xi + Yi)
|
|
||||||
}
|
|
||||||
|
|
||||||
function smixSync (B, Bi, r, N, V, XY, _X, B32, x, tickCallback) {
|
|
||||||
let Xi = 0
|
|
||||||
let Yi = 128 * r
|
|
||||||
let i
|
|
||||||
|
|
||||||
B.copy(XY, Xi, Bi, Bi + Yi)
|
|
||||||
|
|
||||||
for (i = 0; i < N; i++) {
|
|
||||||
XY.copy(V, i * Yi, Xi, Xi + Yi)
|
|
||||||
blockmix_salsa8(XY, Xi, Yi, r, _X, B32, x)
|
|
||||||
|
|
||||||
if (tickCallback) tickCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < N; i++) {
|
|
||||||
let offset = Xi + (2 * r - 1) * 64
|
|
||||||
let j = XY.readUInt32LE(offset) & (N - 1)
|
|
||||||
blockxor(V, j * Yi, XY, Xi, Yi)
|
|
||||||
blockmix_salsa8(XY, Xi, Yi, r, _X, B32, x)
|
|
||||||
|
|
||||||
if (tickCallback) tickCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
XY.copy(B, Bi, Xi, Xi + Yi)
|
|
||||||
}
|
|
||||||
|
|
||||||
function blockmix_salsa8 (BY, Bi, Yi, r, _X, B32, x) {
|
|
||||||
let i
|
|
||||||
|
|
||||||
arraycopy(BY, Bi + (2 * r - 1) * 64, _X, 0, 64)
|
|
||||||
|
|
||||||
for (i = 0; i < 2 * r; i++) {
|
|
||||||
blockxor(BY, i * 64, _X, 0, 64)
|
|
||||||
salsa20_8(_X, B32, x)
|
|
||||||
arraycopy(_X, 0, BY, Yi + (i * 64), 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < r; i++) {
|
|
||||||
arraycopy(BY, Yi + (i * 2) * 64, BY, Bi + (i * 64), 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < r; i++) {
|
|
||||||
arraycopy(BY, Yi + (i * 2 + 1) * 64, BY, Bi + (i + r) * 64, 64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function R (a, b) {
|
|
||||||
return (a << b) | (a >>> (32 - b))
|
|
||||||
}
|
|
||||||
|
|
||||||
function salsa20_8 (B, B32, x) {
|
|
||||||
let i
|
|
||||||
|
|
||||||
for (i = 0; i < 16; i++) {
|
|
||||||
B32[i] = (B[i * 4 + 0] & 0xff) << 0
|
|
||||||
B32[i] |= (B[i * 4 + 1] & 0xff) << 8
|
|
||||||
B32[i] |= (B[i * 4 + 2] & 0xff) << 16
|
|
||||||
B32[i] |= (B[i * 4 + 3] & 0xff) << 24
|
|
||||||
// B32[i] = B.readUInt32LE(i*4) <--- this is signficantly slower even in Node.js
|
|
||||||
}
|
|
||||||
|
|
||||||
arraycopy(B32, 0, x, 0, 16)
|
|
||||||
|
|
||||||
for (i = 8; i > 0; i -= 2) {
|
|
||||||
x[4] ^= R(x[0] + x[12], 7)
|
|
||||||
x[8] ^= R(x[4] + x[0], 9)
|
|
||||||
x[12] ^= R(x[8] + x[4], 13)
|
|
||||||
x[0] ^= R(x[12] + x[8], 18)
|
|
||||||
x[9] ^= R(x[5] + x[1], 7)
|
|
||||||
x[13] ^= R(x[9] + x[5], 9)
|
|
||||||
x[1] ^= R(x[13] + x[9], 13)
|
|
||||||
x[5] ^= R(x[1] + x[13], 18)
|
|
||||||
x[14] ^= R(x[10] + x[6], 7)
|
|
||||||
x[2] ^= R(x[14] + x[10], 9)
|
|
||||||
x[6] ^= R(x[2] + x[14], 13)
|
|
||||||
x[10] ^= R(x[6] + x[2], 18)
|
|
||||||
x[3] ^= R(x[15] + x[11], 7)
|
|
||||||
x[7] ^= R(x[3] + x[15], 9)
|
|
||||||
x[11] ^= R(x[7] + x[3], 13)
|
|
||||||
x[15] ^= R(x[11] + x[7], 18)
|
|
||||||
x[1] ^= R(x[0] + x[3], 7)
|
|
||||||
x[2] ^= R(x[1] + x[0], 9)
|
|
||||||
x[3] ^= R(x[2] + x[1], 13)
|
|
||||||
x[0] ^= R(x[3] + x[2], 18)
|
|
||||||
x[6] ^= R(x[5] + x[4], 7)
|
|
||||||
x[7] ^= R(x[6] + x[5], 9)
|
|
||||||
x[4] ^= R(x[7] + x[6], 13)
|
|
||||||
x[5] ^= R(x[4] + x[7], 18)
|
|
||||||
x[11] ^= R(x[10] + x[9], 7)
|
|
||||||
x[8] ^= R(x[11] + x[10], 9)
|
|
||||||
x[9] ^= R(x[8] + x[11], 13)
|
|
||||||
x[10] ^= R(x[9] + x[8], 18)
|
|
||||||
x[12] ^= R(x[15] + x[14], 7)
|
|
||||||
x[13] ^= R(x[12] + x[15], 9)
|
|
||||||
x[14] ^= R(x[13] + x[12], 13)
|
|
||||||
x[15] ^= R(x[14] + x[13], 18)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < 16; ++i) B32[i] = x[i] + B32[i]
|
|
||||||
|
|
||||||
for (i = 0; i < 16; i++) {
|
|
||||||
let bi = i * 4
|
|
||||||
B[bi + 0] = (B32[i] >> 0 & 0xff)
|
|
||||||
B[bi + 1] = (B32[i] >> 8 & 0xff)
|
|
||||||
B[bi + 2] = (B32[i] >> 16 & 0xff)
|
|
||||||
B[bi + 3] = (B32[i] >> 24 & 0xff)
|
|
||||||
// B.writeInt32LE(B32[i], i*4) //<--- this is signficantly slower even in Node.js
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// naive approach... going back to loop unrolling may yield additional performance
|
|
||||||
function blockxor (S, Si, D, Di, len) {
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
D[Di + i] ^= S[Si + i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function arraycopy (src, srcPos, dest, destPos, length) {
|
|
||||||
if (Buffer.isBuffer(src) && Buffer.isBuffer(dest)) {
|
|
||||||
src.copy(dest, destPos, srcPos, srcPos + length)
|
|
||||||
} else {
|
|
||||||
while (length--) {
|
|
||||||
dest[destPos++] = src[srcPos++]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
checkAndInit,
|
|
||||||
smix,
|
|
||||||
smixSync
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
{
|
|
||||||
"_from": "scryptsy@2.1.0",
|
|
||||||
"_id": "scryptsy@2.1.0",
|
|
||||||
"_inBundle": false,
|
|
||||||
"_integrity": "sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w==",
|
|
||||||
"_location": "/scryptsy",
|
|
||||||
"_phantomChildren": {},
|
|
||||||
"_requested": {
|
|
||||||
"type": "version",
|
|
||||||
"registry": true,
|
|
||||||
"raw": "scryptsy@2.1.0",
|
|
||||||
"name": "scryptsy",
|
|
||||||
"escapedName": "scryptsy",
|
|
||||||
"rawSpec": "2.1.0",
|
|
||||||
"saveSpec": null,
|
|
||||||
"fetchSpec": "2.1.0"
|
|
||||||
},
|
|
||||||
"_requiredBy": [
|
|
||||||
"#USER",
|
|
||||||
"/"
|
|
||||||
],
|
|
||||||
"_resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz",
|
|
||||||
"_shasum": "8d1e8d0c025b58fdd25b6fa9a0dc905ee8faa790",
|
|
||||||
"_spec": "scryptsy@2.1.0",
|
|
||||||
"_where": "/home/overtorment/Documents/BlueWallet",
|
|
||||||
"author": "",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/cryptocoinjs/scryptsy/issues"
|
|
||||||
},
|
|
||||||
"bundleDependencies": false,
|
|
||||||
"dependencies": {},
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "Pure JavaScript implementation of the scrypt key deriviation function that is fully compatible with Node.js and the browser.",
|
|
||||||
"devDependencies": {},
|
|
||||||
"files": [
|
|
||||||
"lib"
|
|
||||||
],
|
|
||||||
"homepage": "https://github.com/cryptocoinjs/scryptsy#readme",
|
|
||||||
"keywords": [
|
|
||||||
"crytpo",
|
|
||||||
"cryptography",
|
|
||||||
"scrypt",
|
|
||||||
"kdf",
|
|
||||||
"litecoin",
|
|
||||||
"dogecoin",
|
|
||||||
"bitcoin",
|
|
||||||
"bip38"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"main": "lib/index.js",
|
|
||||||
"name": "scryptsy",
|
|
||||||
"repository": {
|
|
||||||
"url": "git+ssh://git@github.com/cryptocoinjs/scryptsy.git",
|
|
||||||
"type": "git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"browser-test": "mochify --wd -R spec",
|
|
||||||
"coverage": "nyc --check-coverage --statements 80 --branches 60 --functions 90 --lines 90 mocha",
|
|
||||||
"coveralls": "npm run-script coverage && coveralls < coverage/lcov.info",
|
|
||||||
"lint": "standard",
|
|
||||||
"test": "mocha --ui bdd",
|
|
||||||
"unit": "mocha"
|
|
||||||
},
|
|
||||||
"version": "2.1.0"
|
|
||||||
}
|
|
|
@ -1,197 +0,0 @@
|
||||||
//module.exports = { "extends": "standard" };
|
|
||||||
module.exports = {
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"sourceType": "module",
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
|
|
||||||
//
|
|
||||||
//Possible Errors
|
|
||||||
//
|
|
||||||
// The following rules point out areas where you might have made mistakes.
|
|
||||||
//
|
|
||||||
"comma-dangle": 2, // disallow or enforce trailing commas
|
|
||||||
"no-cond-assign": 2, // disallow assignment in conditional expressions
|
|
||||||
// NOTE: "no-console": 1, // disallow use of console (off by default in the node environment)
|
|
||||||
"no-constant-condition": 2, // disallow use of constant expressions in conditions
|
|
||||||
"no-control-regex": 2, // disallow control characters in regular expressions
|
|
||||||
"no-debugger": 2, // disallow use of debugger
|
|
||||||
"no-dupe-args": 2, // disallow duplicate arguments in functions
|
|
||||||
"no-dupe-keys": 2, // disallow duplicate keys when creating object literals
|
|
||||||
"no-duplicate-case": 2, // disallow a duplicate case label.
|
|
||||||
"no-empty": 2, // disallow empty statements
|
|
||||||
"no-empty-character-class": 2, // disallow the use of empty character classes in regular expressions
|
|
||||||
"no-ex-assign": 2, // disallow assigning to the exception in a catch block
|
|
||||||
"no-extra-boolean-cast": 2, // disallow double-negation boolean casts in a boolean context
|
|
||||||
"no-extra-parens": 0, // disallow unnecessary parentheses (off by default)
|
|
||||||
"no-extra-semi": 2, // disallow unnecessary semicolons
|
|
||||||
"no-func-assign": 2, // disallow overwriting functions written as function declarations
|
|
||||||
"no-inner-declarations": 2, // disallow function or variable declarations in nested blocks
|
|
||||||
"no-invalid-regexp": 2, // disallow invalid regular expression strings in the RegExp constructor
|
|
||||||
"no-irregular-whitespace": 2, // disallow irregular whitespace outside of strings and comments
|
|
||||||
"no-negated-in-lhs": 2, // disallow negation of the left operand of an in expression
|
|
||||||
"no-obj-calls": 2, // disallow the use of object properties of the global object (Math and JSON) as functions
|
|
||||||
"no-regex-spaces": 2, // disallow multiple spaces in a regular expression literal
|
|
||||||
"quote-props": 2, // disallow reserved words being used as object literal keys (off by default)
|
|
||||||
"no-sparse-arrays": 2, // disallow sparse arrays
|
|
||||||
"no-unreachable": 2, // disallow unreachable statements after a return, throw, continue, or break statement
|
|
||||||
"use-isnan": 2, // disallow comparisons with the value NaN
|
|
||||||
"valid-jsdoc": 2, // Ensure JSDoc comments are valid (off by default)
|
|
||||||
"valid-typeof": 2, // Ensure that the results of typeof are compared against a valid string
|
|
||||||
|
|
||||||
//
|
|
||||||
// Best Practices
|
|
||||||
//
|
|
||||||
// These are rules designed to prevent you from making mistakes.
|
|
||||||
// They either prescribe a better way of doing something or help you avoid footguns.
|
|
||||||
//
|
|
||||||
"block-scoped-var": 0, // treat var statements as if they were block scoped (off by default). 0: deep destructuring is not compatible https://github.com/eslint/eslint/issues/1863
|
|
||||||
"complexity": 0, // specify the maximum cyclomatic complexity allowed in a program (off by default)
|
|
||||||
//"consistent-return": 2, // require return statements to either always or never specify values
|
|
||||||
"curly": 2, // specify curly brace conventions for all control statements
|
|
||||||
"default-case": 2, // require default case in switch statements (off by default)
|
|
||||||
"dot-notation": 2, // encourages use of dot notation whenever possible
|
|
||||||
"eqeqeq": 2, // require the use of === and !==
|
|
||||||
//"guard-for-in": 2, // make sure for-in loops have an if statement (off by default)
|
|
||||||
"no-alert": 2, // disallow the use of alert, confirm, and prompt
|
|
||||||
"no-caller": 2, // disallow use of arguments.caller or arguments.callee
|
|
||||||
"no-div-regex": 2, // disallow division operators explicitly at beginning of regular expression (off by default)
|
|
||||||
"no-else-return": 2, // disallow else after a return in an if (off by default)
|
|
||||||
"no-labels": 2, // disallow use of labels for anything other then loops and switches
|
|
||||||
"no-eq-null": 2, // disallow comparisons to null without a type-checking operator (off by default)
|
|
||||||
"no-eval": 2, // disallow use of eval()
|
|
||||||
//"no-extend-native": [0, { "exceptions": ["Object", "String"] }], // disallow adding to native types
|
|
||||||
"no-extra-bind": 2, // disallow unnecessary function binding
|
|
||||||
"no-fallthrough": 2, // disallow fallthrough of case statements
|
|
||||||
"no-floating-decimal": 2, // disallow the use of leading or trailing decimal points in numeric literals (off by default)
|
|
||||||
"no-implied-eval": 2, // disallow use of eval()-like methods
|
|
||||||
"no-iterator": 2, // disallow usage of __iterator__ property
|
|
||||||
"no-labels": 2, // disallow use of labeled statements
|
|
||||||
"no-lone-blocks": 2, // disallow unnecessary nested blocks
|
|
||||||
"no-loop-func": 2, // disallow creation of functions within loops
|
|
||||||
"no-multi-spaces": 2, // disallow use of multiple spaces
|
|
||||||
"no-multi-str": 2, // disallow use of multiline strings
|
|
||||||
"no-native-reassign": 2, // disallow reassignments of native objects
|
|
||||||
"no-new": 2, // disallow use of new operator when not part of the assignment or comparison
|
|
||||||
"no-new-func": 2, // disallow use of new operator for Function object
|
|
||||||
"no-new-wrappers": 2, // disallows creating new instances of String,Number, and Boolean
|
|
||||||
"no-octal": 2, // disallow use of octal literals
|
|
||||||
"no-octal-escape": 2, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251";
|
|
||||||
"no-param-reassign": 2, // disallow reassignment of function parameters (off by default)
|
|
||||||
"no-process-env": 2, // disallow use of process.env (off by default)
|
|
||||||
"no-proto": 2, // disallow usage of __proto__ property
|
|
||||||
"no-redeclare": 2, // disallow declaring the same variable more then once
|
|
||||||
"no-return-assign": 2, // disallow use of assignment in return statement
|
|
||||||
"no-script-url": 2, // disallow use of javascript: urls.
|
|
||||||
"no-self-compare": 2, // disallow comparisons where both sides are exactly the same (off by default)
|
|
||||||
"no-sequences": 2, // disallow use of comma operator
|
|
||||||
"no-throw-literal": 2, // restrict what can be thrown as an exception (off by default)
|
|
||||||
//"no-unused-expressions": 2, // disallow usage of expressions in statement position
|
|
||||||
"no-void": 2, // disallow use of void operator (off by default)
|
|
||||||
"no-warning-comments": [0, {"terms": ["todo", "fixme"], "location": "start"}], // disallow usage of configurable warning terms in comments": 2, // e.g. TODO or FIXME (off by default)
|
|
||||||
"no-with": 2, // disallow use of the with statement
|
|
||||||
"radix": 1, // require use of the second argument for parseInt() (off by default)
|
|
||||||
"vars-on-top": 2, // requires to declare all vars on top of their containing scope (off by default)
|
|
||||||
"wrap-iife": 2, // require immediate function invocation to be wrapped in parentheses (off by default)
|
|
||||||
"yoda": 2, // require or disallow Yoda conditions
|
|
||||||
|
|
||||||
//
|
|
||||||
// Strict Mode
|
|
||||||
//
|
|
||||||
// These rules relate to using strict mode.
|
|
||||||
//
|
|
||||||
"strict": 0, // controls location of Use Strict Directives. 0: required by `babel-eslint`
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
//
|
|
||||||
// These rules have to do with variable declarations.
|
|
||||||
//
|
|
||||||
"no-catch-shadow": 2, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment)
|
|
||||||
"no-delete-var": 2, // disallow deletion of variables
|
|
||||||
"no-label-var": 2, // disallow labels that share a name with a variable
|
|
||||||
"no-shadow": 2, // disallow declaration of variables already declared in the outer scope
|
|
||||||
"no-shadow-restricted-names": 2, // disallow shadowing of names such as arguments
|
|
||||||
// "no-undef": 2, // disallow use of undeclared variables unless mentioned in a /*global */ block
|
|
||||||
"no-undef-init": 2, // disallow use of undefined when initializing variables
|
|
||||||
"no-undefined": 2, // disallow use of undefined variable (off by default)
|
|
||||||
"no-unused-vars": 2, // disallow declaration of variables that are not used in the code
|
|
||||||
// "no-use-before-define": 2, // disallow use of variables before they are defined
|
|
||||||
|
|
||||||
//
|
|
||||||
//Stylistic Issues
|
|
||||||
//
|
|
||||||
// These rules are purely matters of style and are quite subjective.
|
|
||||||
//
|
|
||||||
"indent": [1, 2], // this option sets a specific tab width for your code (off by default)
|
|
||||||
"brace-style": 1, // enforce one true brace style (off by default)
|
|
||||||
"camelcase": 1, // require camel case names
|
|
||||||
"comma-spacing": [1, {"before": false, "after": true}], // enforce spacing before and after comma
|
|
||||||
"comma-style": [1, "last"], // enforce one true comma style (off by default)
|
|
||||||
"consistent-this": [1, "_this"], // enforces consistent naming when capturing the current execution context (off by default)
|
|
||||||
"eol-last": 1, // enforce newline at the end of file, with no multiple empty lines
|
|
||||||
"func-names": 0, // require function expressions to have a name (off by default)
|
|
||||||
"func-style": 0, // enforces use of function declarations or expressions (off by default)
|
|
||||||
"key-spacing": [1, {"beforeColon": false, "afterColon": true}], // enforces spacing between keys and values in object literal properties
|
|
||||||
//"max-nested-callbacks": [1, 3], // specify the maximum depth callbacks can be nested (off by default)
|
|
||||||
"new-cap": [1, {newIsCap: true, capIsNew: false}], // require a capital letter for constructors
|
|
||||||
"new-parens": 1, // disallow the omission of parentheses when invoking a constructor with no arguments
|
|
||||||
"newline-after-var": 0, // allow/disallow an empty newline after var statement (off by default)
|
|
||||||
//"no-array-constructor": 1, // disallow use of the Array constructor
|
|
||||||
"no-inline-comments": 1, // disallow comments inline after code (off by default)
|
|
||||||
"no-lonely-if": 1, // disallow if as the only statement in an else block (off by default)
|
|
||||||
"no-mixed-spaces-and-tabs": 1, // disallow mixed spaces and tabs for indentation
|
|
||||||
"no-multiple-empty-lines": [1, {"max": 2}], // disallow multiple empty lines (off by default)
|
|
||||||
"no-nested-ternary": 1, // disallow nested ternary expressions (off by default)
|
|
||||||
"no-new-object": 1, // disallow use of the Object constructor
|
|
||||||
"no-spaced-func": 1, // disallow space between function identifier and application
|
|
||||||
"no-ternary": 0, // disallow the use of ternary operators (off by default)
|
|
||||||
"no-trailing-spaces": 1, // disallow trailing whitespace at the end of lines
|
|
||||||
"no-underscore-dangle": 1, // disallow dangling underscores in identifiers
|
|
||||||
"no-extra-parens": 1, // disallow wrapping of non-IIFE statements in parens
|
|
||||||
"one-var": [1, "never"], // allow just one var statement per function (off by default)
|
|
||||||
"operator-assignment": [1, "never"], // require assignment operator shorthand where possible or prohibit it entirely (off by default)
|
|
||||||
"padded-blocks": [1, "never"], // enforce padding within blocks (off by default)
|
|
||||||
"quote-props": [1, "as-needed"], // require quotes around object literal property names (off by default)
|
|
||||||
"quotes": [1, "single"], // specify whether double or single quotes should be used
|
|
||||||
"semi": [1, "always"], // require or disallow use of semicolons instead of ASI
|
|
||||||
"semi-spacing": [1, {"before": false, "after": true}], // enforce spacing before and after semicolons
|
|
||||||
"sort-vars": 0, // sort variables within the same declaration block (off by default)
|
|
||||||
"space-before-blocks": [1, "always"], // require or disallow space before blocks (off by default)
|
|
||||||
"space-before-function-paren": [1, {"anonymous": "always", "named": "never"}], // require or disallow space before function opening parenthesis (off by default)
|
|
||||||
"object-curly-spacing": [1, "never"], // require or disallow spaces inside brackets (off by default)
|
|
||||||
"space-in-parens": [1, "never"], // require or disallow spaces inside parentheses (off by default)
|
|
||||||
//"space-infix-ops": [1, "always"], // require spaces around operators
|
|
||||||
"keyword-spacing": 2, // require a space after return, throw, and case
|
|
||||||
"space-unary-ops": [1, {"words": true, "nonwords": false}], // Require or disallow spaces before/after unary operators (words on by default, nonwords off by default)
|
|
||||||
"spaced-comment": [1, "always"], // require or disallow a space immediately following the // in a line comment (off by default)
|
|
||||||
"wrap-regex": 0, // require regex literals to be wrapped in parentheses (off by default)
|
|
||||||
|
|
||||||
//
|
|
||||||
// ECMAScript 6
|
|
||||||
//
|
|
||||||
// These rules are only relevant to ES6 environments and are off by default.
|
|
||||||
//
|
|
||||||
"no-var": 2, // require let or const instead of var (off by default)
|
|
||||||
"generator-star-spacing": [2, "before"], // enforce the spacing around the * in generator functions (off by default)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Legacy
|
|
||||||
//
|
|
||||||
// The following rules are included for compatibility with JSHint and JSLint.
|
|
||||||
// While the names of the rules may not match up with the JSHint/JSLint counterpart,
|
|
||||||
// the functionality is the same.
|
|
||||||
//
|
|
||||||
"max-depth": [2, 3], // specify the maximum depth that blocks can be nested (off by default)
|
|
||||||
"max-len": [2, 100, 2, { "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreUrls": true }], // specify the maximum length of a line in your program (off by default)
|
|
||||||
"max-params": [2, 8], // limits the number of parameters that can be used in the function declaration. (off by default)
|
|
||||||
"max-statements": 0, // specify the maximum number of statement allowed in a function (off by default)
|
|
||||||
"no-bitwise": 0, // disallow use of bitwise operators (off by default)
|
|
||||||
//"no-plusplus": 2, // disallow use of unary operators, ++ and -- (off by default)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
v0.1.0
|
|
||||||
* Initial release
|
|
||||||
|
|
||||||
v0.1.1
|
|
||||||
* Code clean up and addes some unit tests
|
|
||||||
|
|
||||||
v0.1.2
|
|
||||||
* Added length to encodeBigInt()
|
|
||||||
|
|
||||||
v0.1.5-dev.1
|
|
||||||
* Bumped version, changed versioning format
|
|
||||||
|
|
||||||
v0.1.5
|
|
||||||
* Bumped version
|
|
||||||
* Added nodejs.yml
|
|
||||||
* Merge pull requests from different contributors
|
|
||||||
|
|
||||||
v0.1.6
|
|
||||||
* Fixed ilap/slip39-js#12
|
|
||||||
* Some cosmetic fixes
|
|
||||||
|
|
||||||
v0.1.7
|
|
||||||
* Merge pull requests from different contributors
|
|
||||||
* Fixed ilap/slip39-js#14
|
|
||||||
* Fixed ilap/slip39-js#18
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2019 Pal Dorogi "ilap"<pal.dorogi@gmail.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,261 +0,0 @@
|
||||||
# Bluewallet
|
|
||||||
|
|
||||||
It's original [slip39](https://github.com/ilap/slip39-js/) but compiled with `babel-plugin-transform-bigint` to replace js `BigInt` with `JSBI`.
|
|
||||||
|
|
||||||
To update:
|
|
||||||
- sync src folder
|
|
||||||
- run `npm build`
|
|
||||||
|
|
||||||
|
|
||||||
# SLIP39
|
|
||||||
|
|
||||||
[](https://www.npmjs.org/package/slip39)
|
|
||||||
|
|
||||||
|
|
||||||
The javascript implementation of the [SLIP39](https://github.com/satoshilabs/slips/blob/master/slip-0039.md) for Shamir's Secret-Sharing for Mnemonic Codes.
|
|
||||||
|
|
||||||
The code based on my [Dart implementation of SLIP-0039](https://github.com/ilap/slip39-dart/).
|
|
||||||
|
|
||||||
# DISCLAIMER
|
|
||||||
|
|
||||||
This project is still in early development phase. Use it at your own risk.
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
This SLIP39 implementation uses a 3 level height (l=3) of a 16 degree (d=16) tree (T), which is represented as an array of the level two nodes (groups, G).
|
|
||||||
|
|
||||||
The degree (d) and the level (l) of the tree are 16 and 3 respectively,
|
|
||||||
which means that max d^(l-1), i.e. 16^2, leaf nodes (M) can be in a complete tree (or forest).
|
|
||||||
|
|
||||||
The first level (l=1) node of the tree is the the root (R), the level 2 ones are the `SSS` groups (Gs or group nodes) e.g. `[G0, ..., Gd]`.
|
|
||||||
|
|
||||||
The last, the third, level nodes are the only leafs (M, group members) which contains the generated mnemonics.
|
|
||||||
|
|
||||||
Every node has two values:
|
|
||||||
- the N and
|
|
||||||
- M i.e. n(N,M).
|
|
||||||
|
|
||||||
Whihc means, that N (`threshold`) number of M children are required to reconstruct the node's secret.
|
|
||||||
|
|
||||||
## Format
|
|
||||||
|
|
||||||
The tree's human friendly array representation only uses the group (l=2) nodes as arrays.
|
|
||||||
For example. : ``` [[1,1], [1,1], [3,5], [2,6]]```
|
|
||||||
The group's first parameter is the `N` (group threshold) while the second is the `M`, the number of members in the group. See, and example in [Using](#Using).
|
|
||||||
|
|
||||||
## Installing
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install slip39
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using
|
|
||||||
See `example/main.js`
|
|
||||||
|
|
||||||
``` javascript
|
|
||||||
const slip39 = require('../src/slip39.js');
|
|
||||||
const assert = require('assert');
|
|
||||||
// threshold (N) number of group shares required to reconstruct the master secret.
|
|
||||||
const threshold = 2;
|
|
||||||
const masterSecret = 'ABCDEFGHIJKLMNOP'.slip39EncodeHex();
|
|
||||||
const passphrase = 'TREZOR';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 4 groups shares.
|
|
||||||
* = two for Alice
|
|
||||||
* = one for friends and
|
|
||||||
* = one for family members
|
|
||||||
* Two of these group shares are required to reconstruct the master secret.
|
|
||||||
*/
|
|
||||||
const groups = [
|
|
||||||
// Alice group shares. 1 is enough to reconstruct a group share,
|
|
||||||
// therefore she needs at least two group shares to be reconstructed,
|
|
||||||
[1, 1],
|
|
||||||
[1, 1],
|
|
||||||
// 3 of 5 Friends' shares are required to reconstruct this group share
|
|
||||||
[3, 5],
|
|
||||||
// 2 of 6 Family's shares are required to reconstruct this group share
|
|
||||||
[2, 6]
|
|
||||||
];
|
|
||||||
|
|
||||||
const slip = slip39.fromArray({
|
|
||||||
masterSecret: masterSecret,
|
|
||||||
passphrase: passphrase,
|
|
||||||
threshold: threshold,
|
|
||||||
groups: groups
|
|
||||||
});
|
|
||||||
|
|
||||||
// One of Alice's share
|
|
||||||
const aliceShare = slip.fromPath('r/0').mnemonics;
|
|
||||||
|
|
||||||
// and any two of family's shares.
|
|
||||||
const familyShares = slip.fromPath('r/3/1').mnemonics
|
|
||||||
.concat(slip.fromPath('r/3/3').mnemonics);
|
|
||||||
|
|
||||||
const allShares = aliceShare.concat(familyShares);
|
|
||||||
|
|
||||||
console.log('Shares used for restoring the master secret:');
|
|
||||||
allShares.forEach((s) => console.log(s));
|
|
||||||
|
|
||||||
const recoveredSecret = slip39.recoverSecret(allShares, passphrase);
|
|
||||||
console.log('Master secret: ' + masterSecret.slip39DecodeHex());
|
|
||||||
console.log('Recovered one: ' + recoveredSecret.slip39DecodeHex());
|
|
||||||
assert(masterSecret.slip39DecodeHex() === recoveredSecret.slip39DecodeHex());
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
$ npm install
|
|
||||||
$ npm test
|
|
||||||
|
|
||||||
Basic Tests
|
|
||||||
Test threshold 1 with 5 of 7 shares of a group combinations
|
|
||||||
✓ Test combination 0 1 2 3 4.
|
|
||||||
✓ Test combination 0 1 2 3 5.
|
|
||||||
✓ Test combination 0 1 2 3 6.
|
|
||||||
✓ Test combination 0 1 2 4 5.
|
|
||||||
✓ Test combination 0 1 2 4 6.
|
|
||||||
✓ Test combination 0 1 2 5 6.
|
|
||||||
✓ Test combination 0 1 3 4 5.
|
|
||||||
✓ Test combination 0 1 3 4 6.
|
|
||||||
✓ Test combination 0 1 3 5 6.
|
|
||||||
✓ Test combination 0 1 4 5 6.
|
|
||||||
✓ Test combination 0 2 3 4 5.
|
|
||||||
✓ Test combination 0 2 3 4 6.
|
|
||||||
✓ Test combination 0 2 3 5 6.
|
|
||||||
✓ Test combination 0 2 4 5 6.
|
|
||||||
✓ Test combination 0 3 4 5 6.
|
|
||||||
✓ Test combination 1 2 3 4 5.
|
|
||||||
✓ Test combination 1 2 3 4 6.
|
|
||||||
✓ Test combination 1 2 3 5 6.
|
|
||||||
✓ Test combination 1 2 4 5 6.
|
|
||||||
✓ Test combination 1 3 4 5 6.
|
|
||||||
✓ Test combination 2 3 4 5 6.
|
|
||||||
Test passhrase
|
|
||||||
✓ should return valid mastersecret when user submits valid passphrse
|
|
||||||
✓ should NOT return valid mastersecret when user submits invalid passphrse
|
|
||||||
✓ should return valid mastersecret when user does not submit passphrse
|
|
||||||
Test iteration exponent
|
|
||||||
✓ should return valid mastersecret when user apply valid iteration exponent (44ms)
|
|
||||||
✓ should throw an Error when user submits invalid iteration exponent
|
|
||||||
|
|
||||||
Group Shares Tests
|
|
||||||
Test all valid combinations of mnemonics
|
|
||||||
✓ should return the valid mastersecret when valid mnemonics used for recovery
|
|
||||||
Original test vectors Tests
|
|
||||||
✓ 1. Valid mnemonic without sharing (128 bits)
|
|
||||||
✓ 2. Mnemonic with invalid checksum (128 bits)
|
|
||||||
✓ 3. Mnemonic with invalid padding (128 bits)
|
|
||||||
✓ 4. Basic sharing 2-of-3 (128 bits)
|
|
||||||
✓ 5. Basic sharing 2-of-3 (128 bits)
|
|
||||||
✓ 6. Mnemonics with different identifiers (128 bits)
|
|
||||||
✓ 7. Mnemonics with different iteration exponents (128 bits)
|
|
||||||
✓ 8. Mnemonics with mismatching group thresholds (128 bits)
|
|
||||||
✓ 9. Mnemonics with mismatching group counts (128 bits)
|
|
||||||
✓ 10. Mnemonics with greater group threshold than group counts (128 bits)
|
|
||||||
✓ 11. Mnemonics with duplicate member indices (128 bits)
|
|
||||||
✓ 12. Mnemonics with mismatching member thresholds (128 bits)
|
|
||||||
✓ 13. Mnemonics giving an invalid digest (128 bits)
|
|
||||||
✓ 14. Insufficient number of groups (128 bits, case 1)
|
|
||||||
✓ 15. Insufficient number of groups (128 bits, case 2)
|
|
||||||
✓ 16. Threshold number of groups, but insufficient number of members in one group (128 bits)
|
|
||||||
✓ 17. Threshold number of groups and members in each group (128 bits, case 1)
|
|
||||||
✓ 18. Threshold number of groups and members in each group (128 bits, case 2)
|
|
||||||
✓ 19. Threshold number of groups and members in each group (128 bits, case 3)
|
|
||||||
✓ 20. Valid mnemonic without sharing (256 bits)
|
|
||||||
✓ 21. Mnemonic with invalid checksum (256 bits)
|
|
||||||
✓ 22. Mnemonic with invalid padding (256 bits)
|
|
||||||
✓ 23. Basic sharing 2-of-3 (256 bits)
|
|
||||||
✓ 24. Basic sharing 2-of-3 (256 bits)
|
|
||||||
✓ 25. Mnemonics with different identifiers (256 bits)
|
|
||||||
✓ 26. Mnemonics with different iteration exponents (256 bits)
|
|
||||||
✓ 27. Mnemonics with mismatching group thresholds (256 bits)
|
|
||||||
✓ 28. Mnemonics with mismatching group counts (256 bits)
|
|
||||||
✓ 29. Mnemonics with greater group threshold than group counts (256 bits)
|
|
||||||
✓ 30. Mnemonics with duplicate member indices (256 bits)
|
|
||||||
✓ 31. Mnemonics with mismatching member thresholds (256 bits)
|
|
||||||
✓ 32. Mnemonics giving an invalid digest (256 bits)
|
|
||||||
✓ 33. Insufficient number of groups (256 bits, case 1)
|
|
||||||
✓ 34. Insufficient number of groups (256 bits, case 2)
|
|
||||||
✓ 35. Threshold number of groups, but insufficient number of members in one group (256 bits)
|
|
||||||
✓ 36. Threshold number of groups and members in each group (256 bits, case 1)
|
|
||||||
✓ 37. Threshold number of groups and members in each group (256 bits, case 2)
|
|
||||||
✓ 38. Threshold number of groups and members in each group (256 bits, case 3)
|
|
||||||
✓ 39. Mnemonic with insufficient length
|
|
||||||
✓ 40. Mnemonic with invalid master secret length
|
|
||||||
Invalid Shares
|
|
||||||
✓ Short master secret
|
|
||||||
✓ Odd length master secret
|
|
||||||
✓ Group threshold exceeds number of groups
|
|
||||||
✓ Invalid group threshold.
|
|
||||||
✓ Member threshold exceeds number of members
|
|
||||||
✓ Invalid member threshold
|
|
||||||
✓ Group with multiple members and threshold 1
|
|
||||||
|
|
||||||
|
|
||||||
74 passing (477ms)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## TODOS
|
|
||||||
|
|
||||||
- [x] Add unit tests.
|
|
||||||
- [x] Test with the reference code's test vectors.
|
|
||||||
- [ ] Refactor the helpers to different helper classes e.g. `CryptoHelper()`, `ShamirHelper()` etc.
|
|
||||||
- [ ] Add `JSON` representation, see [JSON representation](#json-representation) below.
|
|
||||||
- [ ] Refactor to much simpler code.
|
|
||||||
|
|
||||||
### JSON Representation
|
|
||||||
|
|
||||||
``` json
|
|
||||||
{
|
|
||||||
"name": "Slip39",
|
|
||||||
"threshold": 2,
|
|
||||||
"shares": [
|
|
||||||
{
|
|
||||||
"name": "My Primary",
|
|
||||||
"threshold": 1,
|
|
||||||
"shares": [
|
|
||||||
"Primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "My Secondary",
|
|
||||||
"threshold": 1,
|
|
||||||
"shares": [
|
|
||||||
"Secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Friends",
|
|
||||||
"threshold": 3,
|
|
||||||
"shares": [
|
|
||||||
"Alice",
|
|
||||||
"Bob",
|
|
||||||
"Charlie",
|
|
||||||
"David",
|
|
||||||
"Erin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Family",
|
|
||||||
"threshold": 2,
|
|
||||||
"shares": [
|
|
||||||
"Adam",
|
|
||||||
"Brenda",
|
|
||||||
"Carol",
|
|
||||||
"Dan",
|
|
||||||
"Edward",
|
|
||||||
"Frank"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
# LICENSE
|
|
||||||
|
|
||||||
CopyRight (c) 2019 Pal Dorogi `"iLap"` <pal.dorogi@gmail.com>
|
|
||||||
|
|
||||||
[MIT License](LICENSE)
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
"plugins": [
|
|
||||||
"babel-plugin-transform-bigint",
|
|
||||||
]
|
|
||||||
}
|
|
236
blue_modules/slip39/dist/slip39.js
vendored
236
blue_modules/slip39/dist/slip39.js
vendored
|
@ -1,236 +0,0 @@
|
||||||
var maybeJSBI = {
|
|
||||||
BigInt: function BigInt(a) {
|
|
||||||
return JSBI.BigInt(a);
|
|
||||||
},
|
|
||||||
toNumber: function toNumber(a) {
|
|
||||||
return typeof a === "object" ? JSBI.toNumber(a) : Number(a);
|
|
||||||
},
|
|
||||||
add: function add(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.add(a, b) : a + b;
|
|
||||||
},
|
|
||||||
subtract: function subtract(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.subtract(a, b) : a - b;
|
|
||||||
},
|
|
||||||
multiply: function multiply(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.multiply(a, b) : a * b;
|
|
||||||
},
|
|
||||||
divide: function divide(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.divide(a, b) : a / b;
|
|
||||||
},
|
|
||||||
remainder: function remainder(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.remainder(a, b) : a % b;
|
|
||||||
},
|
|
||||||
exponentiate: function exponentiate(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.exponentiate(a, b) : typeof a === "bigint" && typeof b === "bigint" ? new Function("a**b", "a", "b")(a, b) : Math.pow(a, b);
|
|
||||||
},
|
|
||||||
leftShift: function leftShift(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.leftShift(a, b) : a << b;
|
|
||||||
},
|
|
||||||
signedRightShift: function signedRightShift(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.signedRightShift(a, b) : a >> b;
|
|
||||||
},
|
|
||||||
bitwiseAnd: function bitwiseAnd(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.bitwiseAnd(a, b) : a & b;
|
|
||||||
},
|
|
||||||
bitwiseOr: function bitwiseOr(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.bitwiseOr(a, b) : a | b;
|
|
||||||
},
|
|
||||||
bitwiseXor: function bitwiseXor(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.bitwiseXor(a, b) : a ^ b;
|
|
||||||
},
|
|
||||||
lessThan: function lessThan(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.lessThan(a, b) : a < b;
|
|
||||||
},
|
|
||||||
greaterThan: function greaterThan(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.greaterThan(a, b) : a > b;
|
|
||||||
},
|
|
||||||
lessThanOrEqual: function lessOrEqualThan(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.lessThanOrEqual(a, b) : a <= b;
|
|
||||||
},
|
|
||||||
greaterThanOrEqual: function greaterOrEqualThan(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.greaterThanOrEqual(a, b) : a >= b;
|
|
||||||
},
|
|
||||||
equal: function equal(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.equal(a, b) : a === b;
|
|
||||||
},
|
|
||||||
notEqual: function notEqual(a, b) {
|
|
||||||
return typeof a === "object" && typeof b === "object" ? JSBI.notEqual(a, b) : a !== b;
|
|
||||||
},
|
|
||||||
unaryMinus: function unaryMinus(a) {
|
|
||||||
return typeof a === "object" ? JSBI.unaryMinus(a) : -a;
|
|
||||||
},
|
|
||||||
bitwiseNot: function bitwiseNot(a) {
|
|
||||||
return typeof a === "object" ? JSBI.bitwiseNot(a) : ~a;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const JSBI = require("jsbi/dist/jsbi-cjs.js");
|
|
||||||
|
|
||||||
/* eslint-disable radix */
|
|
||||||
const slipHelper = require('./slip39_helper.js');
|
|
||||||
|
|
||||||
const MAX_DEPTH = 2;
|
|
||||||
/**
|
|
||||||
* Slip39Node
|
|
||||||
* For root node, description refers to the whole set's title e.g. "Hardware wallet X SSSS shares"
|
|
||||||
* For children nodes, description refers to the group e.g. "Family group: mom, dad, sister, wife"
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Slip39Node {
|
|
||||||
constructor(index = 0, description = '', mnemonic = '', children = []) {
|
|
||||||
this.index = index;
|
|
||||||
this.description = description;
|
|
||||||
this.mnemonic = mnemonic;
|
|
||||||
this.children = children;
|
|
||||||
}
|
|
||||||
|
|
||||||
get mnemonics() {
|
|
||||||
if (this.children.length === 0) {
|
|
||||||
return [this.mnemonic];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = this.children.reduce((prev, item) => {
|
|
||||||
return prev.concat(item.mnemonics);
|
|
||||||
}, []);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} //
|
|
||||||
// The javascript implementation of the SLIP-0039: Shamir's Secret-Sharing for Mnemonic Codes
|
|
||||||
// see: https://github.com/satoshilabs/slips/blob/master/slip-0039.md)
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
class Slip39 {
|
|
||||||
constructor({
|
|
||||||
iterationExponent = 0,
|
|
||||||
identifier,
|
|
||||||
groupCount,
|
|
||||||
groupThreshold
|
|
||||||
} = {}) {
|
|
||||||
this.iterationExponent = iterationExponent;
|
|
||||||
this.identifier = identifier;
|
|
||||||
this.groupCount = groupCount;
|
|
||||||
this.groupThreshold = groupThreshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromArray(masterSecret, {
|
|
||||||
passphrase = '',
|
|
||||||
threshold = 1,
|
|
||||||
groups = [[1, 1, 'Default 1-of-1 group share']],
|
|
||||||
iterationExponent = 0,
|
|
||||||
title = 'My default slip39 shares'
|
|
||||||
} = {}) {
|
|
||||||
if (masterSecret.length * 8 < slipHelper.MIN_ENTROPY_BITS) {
|
|
||||||
throw Error(`The length of the master secret (${masterSecret.length} bytes) must be at least ${slipHelper.bitsToBytes(slipHelper.MIN_ENTROPY_BITS)} bytes.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (masterSecret.length % 2 !== 0) {
|
|
||||||
throw Error('The length of the master secret in bytes must be an even number.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^[\x20-\x7E]*$/.test(passphrase)) {
|
|
||||||
throw Error('The passphrase must contain only printable ASCII characters (code points 32-126).');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maybeJSBI.greaterThan(threshold, groups.length)) {
|
|
||||||
throw Error(`The requested group threshold (${threshold}) must not exceed the number of groups (${groups.length}).`);
|
|
||||||
}
|
|
||||||
|
|
||||||
groups.forEach(item => {
|
|
||||||
if (item[0] === 1 && item[1] > 1) {
|
|
||||||
throw Error(`Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead. ${groups.join()}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const identifier = slipHelper.generateIdentifier();
|
|
||||||
const slip = new Slip39({
|
|
||||||
iterationExponent: iterationExponent,
|
|
||||||
identifier: identifier,
|
|
||||||
groupCount: groups.length,
|
|
||||||
groupThreshold: threshold
|
|
||||||
});
|
|
||||||
const encryptedMasterSecret = slipHelper.crypt(masterSecret, passphrase, iterationExponent, slip.identifier);
|
|
||||||
const root = slip.buildRecursive(new Slip39Node(0, title), groups, encryptedMasterSecret, threshold);
|
|
||||||
slip.root = root;
|
|
||||||
return slip;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRecursive(currentNode, nodes, secret, threshold, index) {
|
|
||||||
// It means it's a leaf.
|
|
||||||
if (nodes.length === 0) {
|
|
||||||
const mnemonic = slipHelper.encodeMnemonic(this.identifier, this.iterationExponent, index, this.groupThreshold, this.groupCount, currentNode.index, threshold, secret);
|
|
||||||
currentNode.mnemonic = mnemonic;
|
|
||||||
return currentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretShares = slipHelper.splitSecret(threshold, nodes.length, secret);
|
|
||||||
let children = [];
|
|
||||||
let idx = 0;
|
|
||||||
nodes.forEach(item => {
|
|
||||||
// n=threshold
|
|
||||||
const n = item[0]; // m=members
|
|
||||||
|
|
||||||
const m = item[1]; // d=description
|
|
||||||
|
|
||||||
const d = item[2] || ''; // Generate leaf members, means their `m` is `0`
|
|
||||||
|
|
||||||
const members = Array().slip39Generate(m, () => [n, 0, d]);
|
|
||||||
const node = new Slip39Node(idx, d);
|
|
||||||
const branch = this.buildRecursive(node, members, secretShares[idx], n, currentNode.index);
|
|
||||||
children = children.concat(branch);
|
|
||||||
idx = idx + 1;
|
|
||||||
});
|
|
||||||
currentNode.children = children;
|
|
||||||
return currentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
static recoverSecret(mnemonics, passphrase) {
|
|
||||||
return slipHelper.combineMnemonics(mnemonics, passphrase);
|
|
||||||
}
|
|
||||||
|
|
||||||
static validateMnemonic(mnemonic) {
|
|
||||||
return slipHelper.validateMnemonic(mnemonic);
|
|
||||||
}
|
|
||||||
|
|
||||||
fromPath(path) {
|
|
||||||
this.validatePath(path);
|
|
||||||
const children = this.parseChildren(path);
|
|
||||||
|
|
||||||
if (typeof children === 'undefined' || children.length === 0) {
|
|
||||||
return this.root;
|
|
||||||
}
|
|
||||||
|
|
||||||
return children.reduce((prev, childNumber) => {
|
|
||||||
let childrenLen = prev.children.length;
|
|
||||||
|
|
||||||
if (childNumber >= childrenLen) {
|
|
||||||
throw new Error(`The path index (${childNumber}) exceeds the children index (${childrenLen - 1}).`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev.children[childNumber];
|
|
||||||
}, this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
validatePath(path) {
|
|
||||||
if (!path.match(/(^r)(\/\d{1,2}){0,2}$/)) {
|
|
||||||
throw new Error('Expected valid path e.g. "r/0/0".');
|
|
||||||
}
|
|
||||||
|
|
||||||
const depth = path.split('/');
|
|
||||||
const pathLength = depth.length - 1;
|
|
||||||
|
|
||||||
if (pathLength > MAX_DEPTH) {
|
|
||||||
throw new Error(`Path\'s (${path}) max depth (${MAX_DEPTH}) is exceeded (${pathLength}).`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseChildren(path) {
|
|
||||||
const splitted = path.split('/').slice(1);
|
|
||||||
const result = splitted.map(pathFragment => {
|
|
||||||
return parseInt(pathFragment);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
exports = module.exports = Slip39;
|
|
686
blue_modules/slip39/dist/slip39_helper.js
vendored
686
blue_modules/slip39/dist/slip39_helper.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,122 +0,0 @@
|
||||||
const slip39 = require('../src/slip39.js');
|
|
||||||
const assert = require('assert');
|
|
||||||
// threshold (N) number of group-shares required to reconstruct the master secret.
|
|
||||||
const groupThreshold = 2;
|
|
||||||
const masterSecret = 'ABCDEFGHIJKLMNOP'.slip39EncodeHex();
|
|
||||||
const passphrase = 'TREZOR';
|
|
||||||
|
|
||||||
function recover(groupShares, pass) {
|
|
||||||
const recoveredSecret = slip39.recoverSecret(groupShares, pass);
|
|
||||||
console.log('\tMaster secret: ' + masterSecret.slip39DecodeHex());
|
|
||||||
console.log('\tRecovered one: ' + recoveredSecret.slip39DecodeHex());
|
|
||||||
assert(masterSecret.slip39DecodeHex() === recoveredSecret.slip39DecodeHex());
|
|
||||||
}
|
|
||||||
|
|
||||||
function printShares(shares) {
|
|
||||||
shares.forEach((s, i) => console.log(`\t${i + 1}) ${s}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 4 groups shares:
|
|
||||||
* = two for Alice
|
|
||||||
* = one for friends and
|
|
||||||
* = one for family members
|
|
||||||
* Any two (see threshold) of these four group-shares are required to reconstruct the master secret
|
|
||||||
* i.e. to recover the master secret the goal is to reconstruct any 2-of-4 group-shares.
|
|
||||||
* To reconstruct each group share, we need at least N of its M member-shares.
|
|
||||||
* Thus all possible master secret recovery combinations:
|
|
||||||
* Case 1) [requires 1 person: Alice] Alice alone with her 1-of-1 member-shares reconstructs both the 1st and 2nd group-shares
|
|
||||||
* Case 2) [requires 4 persons: Alice + any 3 of her 5 friends] Alice with her 1-of-1 member-shares reconstructs 1st (or 2nd) group-share + any 3-of-5 friend member-shares reconstruct the 3rd group-share
|
|
||||||
* Case 3) [requires 3 persons: Alice + any 2 of her 6 family relatives] Alice with her 1-of-1 member-shares reconstructs 1st (or 2nd) group-share + any 2-of-6 family member-shares reconstruct the 4th group-share
|
|
||||||
* Case 4) [requires 5 persons: any 3 of her 5 friends + any 2 of her 6 family relatives] any 3-of-5 friend member-shares reconstruct the 3rd group-share + any 2-of-6 family member-shares reconstruct the 4th group-share
|
|
||||||
*/
|
|
||||||
const groups = [
|
|
||||||
// Alice group-shares. 1 is enough to reconstruct a group-share,
|
|
||||||
// therefore she needs at least two group-shares to reconstruct the master secret.
|
|
||||||
[1, 1, 'Alice personal group share 1'],
|
|
||||||
[1, 1, 'Alice personal group share 2'],
|
|
||||||
// 3 of 5 Friends' shares are required to reconstruct this group-share
|
|
||||||
[3, 5, 'Friends group share for Bob, Charlie, Dave, Frank and Grace'],
|
|
||||||
// 2 of 6 Family's shares are required to reconstruct this group-share
|
|
||||||
[2, 6, 'Family group share for mom, dad, brother, sister and wife']
|
|
||||||
];
|
|
||||||
|
|
||||||
const slip = slip39.fromArray(masterSecret, {
|
|
||||||
passphrase: passphrase,
|
|
||||||
threshold: groupThreshold,
|
|
||||||
groups: groups,
|
|
||||||
title: 'Slip39 example for 2-level SSSS'
|
|
||||||
});
|
|
||||||
|
|
||||||
let requiredGroupShares;
|
|
||||||
let aliceBothGroupShares;
|
|
||||||
let aliceFirstGroupShare;
|
|
||||||
let aliceSecondGroupShare;
|
|
||||||
let friendGroupShares;
|
|
||||||
let familyGroupShares;
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Example of Case 1
|
|
||||||
*/
|
|
||||||
// The 1st, and only, member-share (member 0) of the 1st group-share (group 0) + the 1st, and only, member-share (member 0) of the 2nd group-share (group 1)
|
|
||||||
aliceBothGroupShares = slip.fromPath('r/0/0').mnemonics
|
|
||||||
.concat(slip.fromPath('r/1/0').mnemonics);
|
|
||||||
|
|
||||||
requiredGroupShares = aliceBothGroupShares;
|
|
||||||
|
|
||||||
console.log(`\n* Shares used by Alice alone for restoring the master secret (total of ${requiredGroupShares.length} member-shares):`);
|
|
||||||
printShares(requiredGroupShares);
|
|
||||||
recover(requiredGroupShares, passphrase);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Example of Case 2
|
|
||||||
*/
|
|
||||||
// The 1st, and only, member-share (member 0) of the 2nd group-share (group 1)
|
|
||||||
aliceSecondGroupShare = slip.fromPath('r/1/0').mnemonics;
|
|
||||||
|
|
||||||
// ...plus the 3rd member-share (member 2) + the 4th member-share (member 3) + the 5th member-share (member 4) of the 3rd group-share (group 2)
|
|
||||||
friendGroupShares = slip.fromPath('r/2/2').mnemonics
|
|
||||||
.concat(slip.fromPath('r/2/3').mnemonics)
|
|
||||||
.concat(slip.fromPath('r/2/4').mnemonics);
|
|
||||||
|
|
||||||
requiredGroupShares = aliceSecondGroupShare.concat(friendGroupShares);
|
|
||||||
|
|
||||||
console.log(`\n* Shares used by Alice + 3 friends for restoring the master secret (total of ${requiredGroupShares.length} member-shares):`);
|
|
||||||
printShares(requiredGroupShares);
|
|
||||||
recover(requiredGroupShares, passphrase);
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Example of Case 3
|
|
||||||
*/
|
|
||||||
// The 1st, and only, member-share (member 0) of the 1st group-share (group 0)
|
|
||||||
aliceFirstGroupShare = slip.fromPath('r/0/0').mnemonics;
|
|
||||||
|
|
||||||
// ...plus the 2nd member-share (member 1) + the 3rd member-share (member 2) of the 4th group-share (group 3)
|
|
||||||
familyGroupShares = slip.fromPath('r/3/1').mnemonics
|
|
||||||
.concat(slip.fromPath('r/3/2').mnemonics);
|
|
||||||
|
|
||||||
requiredGroupShares = aliceFirstGroupShare.concat(familyGroupShares);
|
|
||||||
|
|
||||||
console.log(`\n* Shares used by Alice + 2 family members for restoring the master secret (total of ${requiredGroupShares.length} member-shares):`);
|
|
||||||
printShares(requiredGroupShares);
|
|
||||||
recover(requiredGroupShares, passphrase);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Example of Case 4
|
|
||||||
*/
|
|
||||||
// The 3rd member-share (member 2) + the 4th member-share (member 3) + the 5th member-share (member 4) of the 3rd group-share (group 2)
|
|
||||||
friendGroupShares = slip.fromPath('r/2/2').mnemonics
|
|
||||||
.concat(slip.fromPath('r/2/3').mnemonics)
|
|
||||||
.concat(slip.fromPath('r/2/4').mnemonics);
|
|
||||||
|
|
||||||
// ...plus the 2nd member-share (member 1) + the 3rd member-share (member 2) of the 4th group-share (group 3)
|
|
||||||
familyGroupShares = slip.fromPath('r/3/1').mnemonics
|
|
||||||
.concat(slip.fromPath('r/3/2').mnemonics);
|
|
||||||
|
|
||||||
requiredGroupShares = friendGroupShares.concat(familyGroupShares);
|
|
||||||
|
|
||||||
console.log(`\n* Shares used by 3 friends + 2 family members for restoring the master secret (total of ${requiredGroupShares.length} member-shares):`);
|
|
||||||
printShares(requiredGroupShares);
|
|
||||||
recover(requiredGroupShares, passphrase);
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"indent_with_tabs": false,
|
|
||||||
"indent_size": 2,
|
|
||||||
"max_preserve_newlines": 2,
|
|
||||||
"preserve_newlines": true,
|
|
||||||
"keep_array_indentation": true,
|
|
||||||
"break_chained_methods": true,
|
|
||||||
"wrap_line_length": 120,
|
|
||||||
"end_with_newline": true,
|
|
||||||
"brace_style": "collapse,preserve-inline",
|
|
||||||
"unformatted": ["a", "abbr", "area", "audio", "b", "bdi", "bdo", "br", "button", "canvas", "cite", "code", "data",
|
|
||||||
"datalist", "del", "dfn", "em", "embed", "i", "iframe", "img", "input", "ins", "kbd", "keygen", "label", "map",
|
|
||||||
"mark", "math", "meter", "noscript", "object", "output", "progress", "q", "ruby", "s", "samp", "select", "small",
|
|
||||||
"span", "strong", "sub", "sup", "template", "textarea", "time", "u", "var", "video", "wbr", "text", "acronym",
|
|
||||||
"address", "big", "dt", "ins", "small", "strike", "tt", "pre", "h1", "h2", "h3", "h4", "h5", "h6"]
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"**/node_modules/*"
|
|
||||||
],
|
|
||||||
"include": [
|
|
||||||
"src/**/*"
|
|
||||||
]
|
|
||||||
}
|
|
2999
blue_modules/slip39/package-lock.json
generated
2999
blue_modules/slip39/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"name": "slip39",
|
|
||||||
"version": "0.1.7",
|
|
||||||
"description": "The javascript implementation of the SLIP39 for Shamir's Secret-Sharing for Mnemonic Codes.",
|
|
||||||
"main": "dist/slip39.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "npx babel src --out-dir dist && ./sed.sh",
|
|
||||||
"test": "mocha"
|
|
||||||
},
|
|
||||||
"author": "Pal Dorogi \"iLap\" <pal.dorogi@gmail.com>",
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/ilap/slip39-js.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"SLIP39",
|
|
||||||
"crypto",
|
|
||||||
"Shamir",
|
|
||||||
"Shamir's Secret Sharing",
|
|
||||||
"Shamir's secret-sharing scheme",
|
|
||||||
"SSS"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/cli": "^7.13.14",
|
|
||||||
"@babel/core": "^7.13.15",
|
|
||||||
"babel-plugin-transform-bigint": "^1.0.9",
|
|
||||||
"mocha": "^6.2.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"create-hmac": "^1.1.3",
|
|
||||||
"jsbi": "^3.1.4",
|
|
||||||
"pbkdf2": "^3.0.9",
|
|
||||||
"randombytes": "^2.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
PROJ_DIR=`dirname $0`/..
|
|
||||||
|
|
||||||
cd "${PROJ_DIR}"
|
|
||||||
|
|
||||||
sudo npm install . -g
|
|
||||||
sudo npm link
|
|
||||||
|
|
||||||
mkdir ../pubtest
|
|
||||||
cd ../pubtest
|
|
||||||
cat > package.json << EOF
|
|
||||||
{
|
|
||||||
"name": "test-slip39",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
npm install ../slip39-js
|
|
||||||
|
|
||||||
cd -
|
|
||||||
sudo npm uninstall . -g
|
|
||||||
sudo npm unlink
|
|
||||||
rm -rf ../pubtest
|
|
|
@ -1,2 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
sed -i '' 's/import JSBI from \"jsbi\"/const JSBI = require(\"jsbi\/dist\/jsbi-cjs.js\")/' dist/*.js
|
|
|
@ -1,190 +0,0 @@
|
||||||
/* eslint-disable radix */
|
|
||||||
const slipHelper = require('./slip39_helper.js');
|
|
||||||
|
|
||||||
const MAX_DEPTH = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slip39Node
|
|
||||||
* For root node, description refers to the whole set's title e.g. "Hardware wallet X SSSS shares"
|
|
||||||
* For children nodes, description refers to the group e.g. "Family group: mom, dad, sister, wife"
|
|
||||||
*/
|
|
||||||
class Slip39Node {
|
|
||||||
constructor(index = 0, description = '', mnemonic = '', children = []) {
|
|
||||||
this.index = index;
|
|
||||||
this.description = description;
|
|
||||||
this.mnemonic = mnemonic;
|
|
||||||
this.children = children;
|
|
||||||
}
|
|
||||||
|
|
||||||
get mnemonics() {
|
|
||||||
if (this.children.length === 0) {
|
|
||||||
return [this.mnemonic];
|
|
||||||
}
|
|
||||||
const result = this.children.reduce((prev, item) => {
|
|
||||||
return prev.concat(item.mnemonics);
|
|
||||||
}, []);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// The javascript implementation of the SLIP-0039: Shamir's Secret-Sharing for Mnemonic Codes
|
|
||||||
// see: https://github.com/satoshilabs/slips/blob/master/slip-0039.md)
|
|
||||||
//
|
|
||||||
class Slip39 {
|
|
||||||
constructor({
|
|
||||||
iterationExponent = 0,
|
|
||||||
identifier,
|
|
||||||
groupCount,
|
|
||||||
groupThreshold
|
|
||||||
} = {}) {
|
|
||||||
this.iterationExponent = iterationExponent;
|
|
||||||
this.identifier = identifier;
|
|
||||||
this.groupCount = groupCount;
|
|
||||||
this.groupThreshold = groupThreshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromArray(masterSecret, {
|
|
||||||
passphrase = '',
|
|
||||||
threshold = 1,
|
|
||||||
groups = [
|
|
||||||
[1, 1, 'Default 1-of-1 group share']
|
|
||||||
],
|
|
||||||
iterationExponent = 0,
|
|
||||||
title = 'My default slip39 shares'
|
|
||||||
} = {}) {
|
|
||||||
if (masterSecret.length * 8 < slipHelper.MIN_ENTROPY_BITS) {
|
|
||||||
throw Error(`The length of the master secret (${masterSecret.length} bytes) must be at least ${slipHelper.bitsToBytes(slipHelper.MIN_ENTROPY_BITS)} bytes.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (masterSecret.length % 2 !== 0) {
|
|
||||||
throw Error('The length of the master secret in bytes must be an even number.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^[\x20-\x7E]*$/.test(passphrase)) {
|
|
||||||
throw Error('The passphrase must contain only printable ASCII characters (code points 32-126).');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threshold > groups.length) {
|
|
||||||
throw Error(`The requested group threshold (${threshold}) must not exceed the number of groups (${groups.length}).`);
|
|
||||||
}
|
|
||||||
|
|
||||||
groups.forEach((item) => {
|
|
||||||
if (item[0] === 1 && item[1] > 1) {
|
|
||||||
throw Error(`Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead. ${groups.join()}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const identifier = slipHelper.generateIdentifier();
|
|
||||||
|
|
||||||
const slip = new Slip39({
|
|
||||||
iterationExponent: iterationExponent,
|
|
||||||
identifier: identifier,
|
|
||||||
groupCount: groups.length,
|
|
||||||
groupThreshold: threshold
|
|
||||||
});
|
|
||||||
|
|
||||||
const encryptedMasterSecret = slipHelper.crypt(
|
|
||||||
masterSecret, passphrase, iterationExponent, slip.identifier);
|
|
||||||
|
|
||||||
const root = slip.buildRecursive(
|
|
||||||
new Slip39Node(0, title),
|
|
||||||
groups,
|
|
||||||
encryptedMasterSecret,
|
|
||||||
threshold
|
|
||||||
);
|
|
||||||
|
|
||||||
slip.root = root;
|
|
||||||
return slip;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRecursive(currentNode, nodes, secret, threshold, index) {
|
|
||||||
// It means it's a leaf.
|
|
||||||
if (nodes.length === 0) {
|
|
||||||
const mnemonic = slipHelper.encodeMnemonic(this.identifier, this.iterationExponent, index,
|
|
||||||
this.groupThreshold, this.groupCount, currentNode.index, threshold, secret);
|
|
||||||
|
|
||||||
currentNode.mnemonic = mnemonic;
|
|
||||||
return currentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretShares = slipHelper.splitSecret(threshold, nodes.length, secret);
|
|
||||||
let children = [];
|
|
||||||
let idx = 0;
|
|
||||||
|
|
||||||
nodes.forEach((item) => {
|
|
||||||
// n=threshold
|
|
||||||
const n = item[0];
|
|
||||||
// m=members
|
|
||||||
const m = item[1];
|
|
||||||
// d=description
|
|
||||||
const d = item[2] || '';
|
|
||||||
|
|
||||||
// Generate leaf members, means their `m` is `0`
|
|
||||||
const members = Array().slip39Generate(m, () => [n, 0, d]);
|
|
||||||
|
|
||||||
const node = new Slip39Node(idx, d);
|
|
||||||
const branch = this.buildRecursive(
|
|
||||||
node,
|
|
||||||
members,
|
|
||||||
secretShares[idx],
|
|
||||||
n,
|
|
||||||
currentNode.index);
|
|
||||||
|
|
||||||
children = children.concat(branch);
|
|
||||||
idx = idx + 1;
|
|
||||||
});
|
|
||||||
currentNode.children = children;
|
|
||||||
return currentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
static recoverSecret(mnemonics, passphrase) {
|
|
||||||
return slipHelper.combineMnemonics(mnemonics, passphrase);
|
|
||||||
}
|
|
||||||
|
|
||||||
static validateMnemonic(mnemonic) {
|
|
||||||
return slipHelper.validateMnemonic(mnemonic);
|
|
||||||
}
|
|
||||||
|
|
||||||
fromPath(path) {
|
|
||||||
this.validatePath(path);
|
|
||||||
|
|
||||||
const children = this.parseChildren(path);
|
|
||||||
|
|
||||||
if (typeof children === 'undefined' || children.length === 0) {
|
|
||||||
return this.root;
|
|
||||||
}
|
|
||||||
|
|
||||||
return children.reduce((prev, childNumber) => {
|
|
||||||
let childrenLen = prev.children.length;
|
|
||||||
if (childNumber >= childrenLen) {
|
|
||||||
throw new Error(`The path index (${childNumber}) exceeds the children index (${childrenLen - 1}).`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev.children[childNumber];
|
|
||||||
}, this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
validatePath(path) {
|
|
||||||
if (!path.match(/(^r)(\/\d{1,2}){0,2}$/)) {
|
|
||||||
throw new Error('Expected valid path e.g. "r/0/0".');
|
|
||||||
}
|
|
||||||
|
|
||||||
const depth = path.split('/');
|
|
||||||
const pathLength = depth.length - 1;
|
|
||||||
if (pathLength > MAX_DEPTH) {
|
|
||||||
throw new Error(`Path\'s (${path}) max depth (${MAX_DEPTH}) is exceeded (${pathLength}).`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseChildren(path) {
|
|
||||||
const splitted = path.split('/').slice(1);
|
|
||||||
|
|
||||||
const result = splitted.map((pathFragment) => {
|
|
||||||
return parseInt(pathFragment);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports = module.exports = Slip39;
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,353 +0,0 @@
|
||||||
const assert = require('assert');
|
|
||||||
const slip39 = require('../dist/slip39');
|
|
||||||
|
|
||||||
const MASTERSECRET = 'ABCDEFGHIJKLMNOP';
|
|
||||||
const MS = MASTERSECRET.slip39EncodeHex();
|
|
||||||
const PASSPHRASE = 'TREZOR';
|
|
||||||
const ONE_GROUP = [
|
|
||||||
[5, 7]
|
|
||||||
];
|
|
||||||
|
|
||||||
const slip15 = slip39.fromArray(MS, {
|
|
||||||
passphrase: PASSPHRASE,
|
|
||||||
threshold: 1,
|
|
||||||
groups: ONE_GROUP
|
|
||||||
});
|
|
||||||
|
|
||||||
const slip15NoPW = slip39.fromArray(MS, {
|
|
||||||
threshold: 1,
|
|
||||||
groups: ONE_GROUP
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Shuffle
|
|
||||||
//
|
|
||||||
function shuffle(array) {
|
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Combination C(n, k) of the grooups
|
|
||||||
//
|
|
||||||
function getCombinations(array, k) {
|
|
||||||
let result = [];
|
|
||||||
let combinations = [];
|
|
||||||
|
|
||||||
function helper(level, start) {
|
|
||||||
for (let i = start; i < array.length - k + level + 1; i++) {
|
|
||||||
combinations[level] = array[i];
|
|
||||||
|
|
||||||
if (level < k - 1) {
|
|
||||||
helper(level + 1, i + 1);
|
|
||||||
} else {
|
|
||||||
result.push(combinations.slice(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
helper(0, 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Basic Tests', () => {
|
|
||||||
describe('Test threshold 1 with 5 of 7 shares of a group combinations', () => {
|
|
||||||
let mnemonics = slip15.fromPath('r/0').mnemonics;
|
|
||||||
|
|
||||||
let combinations = getCombinations([0, 1, 2, 3, 4, 5, 6], 5);
|
|
||||||
combinations.forEach((item) => {
|
|
||||||
shuffle(item);
|
|
||||||
let description = `Test shuffled combination ${item.join(' ')}.`;
|
|
||||||
it(description, () => {
|
|
||||||
let shares = item.map((idx) => mnemonics[idx]);
|
|
||||||
assert(MS.slip39DecodeHex() === slip39.recoverSecret(shares, PASSPHRASE)
|
|
||||||
.slip39DecodeHex());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Test passhrase', () => {
|
|
||||||
let mnemonics = slip15.fromPath('r/0').mnemonics;
|
|
||||||
let nopwMnemonics = slip15NoPW.fromPath('r/0').mnemonics;
|
|
||||||
|
|
||||||
it('should return valid mastersecret when user submits valid passphrase', () => {
|
|
||||||
assert(MS.slip39DecodeHex() === slip39.recoverSecret(mnemonics.slice(0, 5), PASSPHRASE)
|
|
||||||
.slip39DecodeHex());
|
|
||||||
});
|
|
||||||
it('should NOT return valid mastersecret when user submits invalid passphrse', () => {
|
|
||||||
assert(MS.slip39DecodeHex() !== slip39.recoverSecret(mnemonics.slice(0, 5))
|
|
||||||
.slip39DecodeHex());
|
|
||||||
});
|
|
||||||
it('should return valid mastersecret when user does not submit passphrase', () => {
|
|
||||||
assert(MS.slip39DecodeHex() === slip39.recoverSecret(nopwMnemonics.slice(0, 5))
|
|
||||||
.slip39DecodeHex());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Test iteration exponent', () => {
|
|
||||||
const slip1 = slip39.fromArray(MS, {
|
|
||||||
iterationExponent: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
const slip2 = slip39.fromArray(MS, {
|
|
||||||
iterationExponent: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return valid mastersecret when user apply valid iteration exponent', () => {
|
|
||||||
assert(MS.slip39DecodeHex() === slip39.recoverSecret(slip1.fromPath('r/0').mnemonics)
|
|
||||||
.slip39DecodeHex());
|
|
||||||
|
|
||||||
assert(MS.slip39DecodeHex() === slip39.recoverSecret(slip2.fromPath('r/0').mnemonics)
|
|
||||||
.slip39DecodeHex());
|
|
||||||
});
|
|
||||||
/**
|
|
||||||
* assert.throws(() => x.y.z);
|
|
||||||
* assert.throws(() => x.y.z, ReferenceError);
|
|
||||||
* assert.throws(() => x.y.z, ReferenceError, /is not defined/);
|
|
||||||
* assert.throws(() => x.y.z, /is not defined/);
|
|
||||||
* assert.doesNotThrow(() => 42);
|
|
||||||
* assert.throws(() => x.y.z, Error);
|
|
||||||
* assert.throws(() => model.get.z, /Property does not exist in model schema./)
|
|
||||||
* Ref: https://stackoverflow.com/questions/21587122/mocha-chai-expect-to-throw-not-catching-thrown-errors
|
|
||||||
*/
|
|
||||||
it('should throw an Error when user submits invalid iteration exponent', () => {
|
|
||||||
assert.throws(() => slip39.fromArray(MS, {
|
|
||||||
iterationExponent: -1
|
|
||||||
}), Error);
|
|
||||||
assert.throws(() => slip39.fromArray(MS, {
|
|
||||||
iterationExponent: 33
|
|
||||||
}), Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: finish it.
|
|
||||||
describe('Group Sharing Tests', () => {
|
|
||||||
describe('Test all valid combinations of mnemonics', () => {
|
|
||||||
const groups = [
|
|
||||||
[3, 5, 'Group 0'],
|
|
||||||
[3, 3, 'Group 1'],
|
|
||||||
[2, 5, 'Group 2'],
|
|
||||||
[1, 1, 'Group 3']
|
|
||||||
];
|
|
||||||
const slip = slip39.fromArray(MS, {
|
|
||||||
threshold: 2,
|
|
||||||
groups: groups,
|
|
||||||
title: 'Trezor one SSSS'
|
|
||||||
});
|
|
||||||
|
|
||||||
const group2Mnemonics = slip.fromPath('r/2').mnemonics;
|
|
||||||
const group3Mnemonic = slip.fromPath('r/3').mnemonics[0];
|
|
||||||
|
|
||||||
it('Should include overall split title', () => {
|
|
||||||
assert.equal(slip.fromPath('r').description, 'Trezor one SSSS');
|
|
||||||
});
|
|
||||||
it('Should include group descriptions', () => {
|
|
||||||
assert.equal(slip.fromPath('r/0').description, 'Group 0');
|
|
||||||
assert.equal(slip.fromPath('r/1').description, 'Group 1');
|
|
||||||
assert.equal(slip.fromPath('r/2').description, 'Group 2');
|
|
||||||
assert.equal(slip.fromPath('r/3').description, 'Group 3');
|
|
||||||
});
|
|
||||||
it('Should return the valid master secret when it tested with minimal sets of mnemonics.', () => {
|
|
||||||
const mnemonics = group2Mnemonics.filter((_, index) => {
|
|
||||||
return index === 0 || index === 2;
|
|
||||||
}).concat(group3Mnemonic);
|
|
||||||
|
|
||||||
assert(MS.slip39DecodeHex() === slip39.recoverSecret(mnemonics).slip39DecodeHex());
|
|
||||||
});
|
|
||||||
it('TODO: Should NOT return the valid master secret when one complete group and one incomplete group out of two groups required', () => {
|
|
||||||
assert(true);
|
|
||||||
});
|
|
||||||
it('TODO: Should return the valid master secret when one group of two required but only one applied.', () => {
|
|
||||||
assert(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Original test vectors Tests', () => {
|
|
||||||
let fs = require('fs');
|
|
||||||
let path = require('path');
|
|
||||||
let filePath = path.join(__dirname, 'vectors.json');
|
|
||||||
|
|
||||||
let content = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
|
|
||||||
const tests = JSON.parse(content);
|
|
||||||
tests.forEach((item) => {
|
|
||||||
let description = item[0];
|
|
||||||
let mnemonics = item[1];
|
|
||||||
let masterSecret = Buffer.from(item[2], 'hex');
|
|
||||||
|
|
||||||
it(description, () => {
|
|
||||||
if (masterSecret.length !== 0) {
|
|
||||||
let ms = slip39.recoverSecret(mnemonics, PASSPHRASE);
|
|
||||||
assert(masterSecret.every((v, i) => v === ms[i]));
|
|
||||||
} else {
|
|
||||||
assert.throws(() => slip39.recoverSecret(mnemonics, PASSPHRASE), Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Invalid Shares', () => {
|
|
||||||
const tests = [
|
|
||||||
['Short master secret', 1, [
|
|
||||||
[2, 3]
|
|
||||||
], MS.slice(0, 14)],
|
|
||||||
['Odd length master secret', 1, [
|
|
||||||
[2, 3]
|
|
||||||
], MS.concat([55])],
|
|
||||||
['Group threshold exceeds number of groups', 3, [
|
|
||||||
[3, 5],
|
|
||||||
[2, 5]
|
|
||||||
], MS],
|
|
||||||
['Invalid group threshold.', 0, [
|
|
||||||
[3, 5],
|
|
||||||
[2, 5]
|
|
||||||
], MS],
|
|
||||||
['Member threshold exceeds number of members', 2, [
|
|
||||||
[3, 2],
|
|
||||||
[2, 5]
|
|
||||||
], MS],
|
|
||||||
['Invalid member threshold', 2, [
|
|
||||||
[0, 2],
|
|
||||||
[2, 5]
|
|
||||||
], MS],
|
|
||||||
['Group with multiple members and threshold 1', 2, [
|
|
||||||
[3, 5],
|
|
||||||
[1, 3],
|
|
||||||
[2, 5]
|
|
||||||
], MS]
|
|
||||||
];
|
|
||||||
|
|
||||||
tests.forEach((item) => {
|
|
||||||
let description = item[0];
|
|
||||||
let threshold = item[1];
|
|
||||||
|
|
||||||
let groups = item[2];
|
|
||||||
let secret = item[3];
|
|
||||||
|
|
||||||
it(description, () => {
|
|
||||||
assert.throws(() =>
|
|
||||||
slip39.fromArray(secret, {
|
|
||||||
threshold: threshold,
|
|
||||||
groups: groups
|
|
||||||
}), Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mnemonic Validation', () => {
|
|
||||||
describe('Valid Mnemonics', () => {
|
|
||||||
let mnemonics = slip15.fromPath('r/0').mnemonics;
|
|
||||||
|
|
||||||
mnemonics.forEach((mnemonic, index) => {
|
|
||||||
it(`Mnemonic at index ${index} should be valid`, () => {
|
|
||||||
const isValid = slip39.validateMnemonic(mnemonic);
|
|
||||||
|
|
||||||
assert(isValid);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const vectors = [
|
|
||||||
[
|
|
||||||
'2. Mnemonic with invalid checksum (128 bits)',
|
|
||||||
[
|
|
||||||
'duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision kidney'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'21. Mnemonic with invalid checksum (256 bits)',
|
|
||||||
[
|
|
||||||
'theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect lunar'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'3. Mnemonic with invalid padding (128 bits)',
|
|
||||||
[
|
|
||||||
'duckling enlarge academic academic email result length solution fridge kidney coal piece deal husband erode duke ajar music cargo fitness'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'22. Mnemonic with invalid padding (256 bits)',
|
|
||||||
[
|
|
||||||
'theory painting academic academic campus sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips facility obtain sister'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'10. Mnemonics with greater group threshold than group counts (128 bits)',
|
|
||||||
[
|
|
||||||
'music husband acrobat acid artist finance center either graduate swimming object bike medical clothes station aspect spider maiden bulb welcome',
|
|
||||||
'music husband acrobat agency advance hunting bike corner density careful material civil evil tactics remind hawk discuss hobo voice rainbow',
|
|
||||||
'music husband beard academic black tricycle clock mayor estimate level photo episode exclude ecology papa source amazing salt verify divorce'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'29. Mnemonics with greater group threshold than group counts (256 bits)',
|
|
||||||
[
|
|
||||||
'smirk pink acrobat acid auction wireless impulse spine sprinkle fortune clogs elbow guest hush loyalty crush dictate tracks airport talent',
|
|
||||||
'smirk pink acrobat agency dwarf emperor ajar organize legs slice harvest plastic dynamic style mobile float bulb health coding credit',
|
|
||||||
'smirk pink beard academic alto strategy carve shame language rapids ruin smart location spray training acquire eraser endorse submit peaceful'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'39. Mnemonic with insufficient length',
|
|
||||||
[
|
|
||||||
'junk necklace academic academic acne isolate join hesitate lunar roster dough calcium chemical ladybug amount mobile glasses verify cylinder'
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'40. Mnemonic with invalid master secret length',
|
|
||||||
[
|
|
||||||
'fraction necklace academic academic award teammate mouse regular testify coding building member verdict purchase blind camera duration email prepare spirit quarter'
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
vectors.forEach((item) => {
|
|
||||||
const description = item[0];
|
|
||||||
const mnemonics = item[1];
|
|
||||||
|
|
||||||
describe(description, () => {
|
|
||||||
mnemonics.forEach((mnemonic, index) => {
|
|
||||||
it(`Mnemonic at index ${index} should be invalid`, () => {
|
|
||||||
const isValid = slip39.validateMnemonic(mnemonic);
|
|
||||||
|
|
||||||
assert(isValid === false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function itTestArray(t, g, gs) {
|
|
||||||
it(
|
|
||||||
`recover master secret for ${t} shares (threshold=${t}) of ${g} '[1, 1,]' groups",`,
|
|
||||||
() => {
|
|
||||||
let slip = slip39.fromArray(MS, {
|
|
||||||
groups: gs.slice(0, g),
|
|
||||||
passphrase: PASSPHRASE,
|
|
||||||
threshold: t
|
|
||||||
});
|
|
||||||
|
|
||||||
let mnemonics = slip.fromPath('r').mnemonics.slice(0, t);
|
|
||||||
|
|
||||||
let recoveredSecret =
|
|
||||||
slip39.recoverSecret(mnemonics, PASSPHRASE);
|
|
||||||
|
|
||||||
assert(MASTERSECRET === String.fromCharCode(...recoveredSecret));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Groups test (T=1, N=1 e.g. [1,1]) - ', () => {
|
|
||||||
let totalGroups = 16;
|
|
||||||
let groups = Array.from(Array(totalGroups), () => [1, 1]);
|
|
||||||
|
|
||||||
for (group = 1; group <= totalGroups; group++) {
|
|
||||||
for (threshold = 1; threshold <= group; threshold++) {
|
|
||||||
itTestArray(threshold, group, groups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,322 +0,0 @@
|
||||||
[
|
|
||||||
[
|
|
||||||
"1. Valid mnemonic without sharing (128 bits)",
|
|
||||||
[
|
|
||||||
"duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision keyboard"
|
|
||||||
],
|
|
||||||
"bb54aac4b89dc868ba37d9cc21b2cece"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"2. Mnemonic with invalid checksum (128 bits)",
|
|
||||||
[
|
|
||||||
"duckling enlarge academic academic agency result length solution fridge kidney coal piece deal husband erode duke ajar critical decision kidney"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"3. Mnemonic with invalid padding (128 bits)",
|
|
||||||
[
|
|
||||||
"duckling enlarge academic academic email result length solution fridge kidney coal piece deal husband erode duke ajar music cargo fitness"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"4. Basic sharing 2-of-3 (128 bits)",
|
|
||||||
[
|
|
||||||
"shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed",
|
|
||||||
"shadow pistol academic acid actress prayer class unknown daughter sweater depict flip twice unkind craft early superior advocate guest smoking"
|
|
||||||
],
|
|
||||||
"b43ceb7e57a0ea8766221624d01b0864"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"5. Basic sharing 2-of-3 (128 bits)",
|
|
||||||
[
|
|
||||||
"shadow pistol academic always adequate wildlife fancy gross oasis cylinder mustang wrist rescue view short owner flip making coding armed"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"6. Mnemonics with different identifiers (128 bits)",
|
|
||||||
[
|
|
||||||
"adequate smoking academic acid debut wine petition glen cluster slow rhyme slow simple epidemic rumor junk tracks treat olympic tolerate",
|
|
||||||
"adequate stay academic agency agency formal party ting frequent learn upstairs remember smear leaf damage anatomy ladle market hush corner"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"7. Mnemonics with different iteration exponents (128 bits)",
|
|
||||||
[
|
|
||||||
"peasant leaves academic acid desert exact olympic math alive axle trial tackle drug deny decent smear dominant desert bucket remind",
|
|
||||||
"peasant leader academic agency cultural blessing percent network envelope medal junk primary human pumps jacket fragment payroll ticket evoke voice"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"8. Mnemonics with mismatching group thresholds (128 bits)",
|
|
||||||
[
|
|
||||||
"liberty category beard echo animal fawn temple briefing math username various wolf aviation fancy visual holy thunder yelp helpful payment",
|
|
||||||
"liberty category beard email beyond should fancy romp founder easel pink holy hairy romp loyalty material victim owner toxic custody",
|
|
||||||
"liberty category academic easy being hazard crush diminish oral lizard reaction cluster force dilemma deploy force club veteran expect photo"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"9. Mnemonics with mismatching group counts (128 bits)",
|
|
||||||
[
|
|
||||||
"average senior academic leaf broken teacher expect surface hour capture obesity desire negative dynamic dominant pistol mineral mailman iris aide",
|
|
||||||
"average senior academic agency curious pants blimp spew clothes slice script dress wrap firm shaft regular slavery negative theater roster"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"10. Mnemonics with greater group threshold than group counts (128 bits)",
|
|
||||||
[
|
|
||||||
"music husband acrobat acid artist finance center either graduate swimming object bike medical clothes station aspect spider maiden bulb welcome",
|
|
||||||
"music husband acrobat agency advance hunting bike corner density careful material civil evil tactics remind hawk discuss hobo voice rainbow",
|
|
||||||
"music husband beard academic black tricycle clock mayor estimate level photo episode exclude ecology papa source amazing salt verify divorce"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"11. Mnemonics with duplicate member indices (128 bits)",
|
|
||||||
[
|
|
||||||
"device stay academic always dive coal antenna adult black exceed stadium herald advance soldier busy dryer daughter evaluate minister laser",
|
|
||||||
"device stay academic always dwarf afraid robin gravity crunch adjust soul branch walnut coastal dream costume scholar mortgage mountain pumps"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"12. Mnemonics with mismatching member thresholds (128 bits)",
|
|
||||||
[
|
|
||||||
"hour painting academic academic device formal evoke guitar random modern justice filter withdraw trouble identify mailman insect general cover oven",
|
|
||||||
"hour painting academic agency artist again daisy capital beaver fiber much enjoy suitable symbolic identify photo editor romp float echo"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"13. Mnemonics giving an invalid digest (128 bits)",
|
|
||||||
[
|
|
||||||
"guilt walnut academic acid deliver remove equip listen vampire tactics nylon rhythm failure husband fatigue alive blind enemy teaspoon rebound",
|
|
||||||
"guilt walnut academic agency brave hamster hobo declare herd taste alpha slim criminal mild arcade formal romp branch pink ambition"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"14. Insufficient number of groups (128 bits, case 1)",
|
|
||||||
[
|
|
||||||
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"15. Insufficient number of groups (128 bits, case 2)",
|
|
||||||
[
|
|
||||||
"eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join",
|
|
||||||
"eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"16. Threshold number of groups, but insufficient number of members in one group (128 bits)",
|
|
||||||
[
|
|
||||||
"eraser senior decision shadow artist work morning estate greatest pipeline plan ting petition forget hormone flexible general goat admit surface",
|
|
||||||
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"17. Threshold number of groups and members in each group (128 bits, case 1)",
|
|
||||||
[
|
|
||||||
"eraser senior decision roster beard treat identify grumpy salt index fake aviation theater cubic bike cause research dragon emphasis counter",
|
|
||||||
"eraser senior ceramic snake clay various huge numb argue hesitate auction category timber browser greatest hanger petition script leaf pickup",
|
|
||||||
"eraser senior ceramic shaft dynamic become junior wrist silver peasant force math alto coal amazing segment yelp velvet image paces",
|
|
||||||
"eraser senior ceramic round column hawk trust auction smug shame alive greatest sheriff living perfect corner chest sled fumes adequate",
|
|
||||||
"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing"
|
|
||||||
],
|
|
||||||
"7c3397a292a5941682d7a4ae2d898d11"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"18. Threshold number of groups and members in each group (128 bits, case 2)",
|
|
||||||
[
|
|
||||||
"eraser senior decision smug corner ruin rescue cubic angel tackle skin skunk program roster trash rumor slush angel flea amazing",
|
|
||||||
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice",
|
|
||||||
"eraser senior decision scared cargo theory device idea deliver modify curly include pancake both news skin realize vitamins away join"
|
|
||||||
],
|
|
||||||
"7c3397a292a5941682d7a4ae2d898d11"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"19. Threshold number of groups and members in each group (128 bits, case 3)",
|
|
||||||
[
|
|
||||||
"eraser senior beard romp adorn nuclear spill corner cradle style ancient family general leader ambition exchange unusual garlic promise voice",
|
|
||||||
"eraser senior acrobat romp bishop medical gesture pumps secret alive ultimate quarter priest subject class dictate spew material endless market"
|
|
||||||
],
|
|
||||||
"7c3397a292a5941682d7a4ae2d898d11"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"20. Valid mnemonic without sharing (256 bits)",
|
|
||||||
[
|
|
||||||
"theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect luck"
|
|
||||||
],
|
|
||||||
"989baf9dcaad5b10ca33dfd8cc75e42477025dce88ae83e75a230086a0e00e92"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"21. Mnemonic with invalid checksum (256 bits)",
|
|
||||||
[
|
|
||||||
"theory painting academic academic armed sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips brave detect lunar"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"22. Mnemonic with invalid padding (256 bits)",
|
|
||||||
[
|
|
||||||
"theory painting academic academic campus sweater year military elder discuss acne wildlife boring employer fused large satoshi bundle carbon diagnose anatomy hamster leaves tracks paces beyond phantom capital marvel lips facility obtain sister"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"23. Basic sharing 2-of-3 (256 bits)",
|
|
||||||
[
|
|
||||||
"humidity disease academic always aluminum jewelry energy woman receiver strategy amuse duckling lying evidence network walnut tactics forget hairy rebound impulse brother survive clothes stadium mailman rival ocean reward venture always armed unwrap",
|
|
||||||
"humidity disease academic agency actress jacket gross physics cylinder solution fake mortgage benefit public busy prepare sharp friar change work slow purchase ruler again tricycle involve viral wireless mixture anatomy desert cargo upgrade"
|
|
||||||
],
|
|
||||||
"c938b319067687e990e05e0da0ecce1278f75ff58d9853f19dcaeed5de104aae"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"24. Basic sharing 2-of-3 (256 bits)",
|
|
||||||
[
|
|
||||||
"humidity disease academic always aluminum jewelry energy woman receiver strategy amuse duckling lying evidence network walnut tactics forget hairy rebound impulse brother survive clothes stadium mailman rival ocean reward venture always armed unwrap"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"25. Mnemonics with different identifiers (256 bits)",
|
|
||||||
[
|
|
||||||
"smear husband academic acid deadline scene venture distance dive overall parking bracelet elevator justice echo burning oven chest duke nylon",
|
|
||||||
"smear isolate academic agency alpha mandate decorate burden recover guard exercise fatal force syndrome fumes thank guest drift dramatic mule"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"26. Mnemonics with different iteration exponents (256 bits)",
|
|
||||||
[
|
|
||||||
"finger trash academic acid average priority dish revenue academic hospital spirit western ocean fact calcium syndrome greatest plan losing dictate",
|
|
||||||
"finger traffic academic agency building lilac deny paces subject threaten diploma eclipse window unknown health slim piece dragon focus smirk"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"27. Mnemonics with mismatching group thresholds (256 bits)",
|
|
||||||
[
|
|
||||||
"flavor pink beard echo depart forbid retreat become frost helpful juice unwrap reunion credit math burning spine black capital lair",
|
|
||||||
"flavor pink beard email diet teaspoon freshman identify document rebound cricket prune headset loyalty smell emission skin often square rebound",
|
|
||||||
"flavor pink academic easy credit cage raisin crazy closet lobe mobile become drink human tactics valuable hand capture sympathy finger"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"28. Mnemonics with mismatching group counts (256 bits)",
|
|
||||||
[
|
|
||||||
"column flea academic leaf debut extra surface slow timber husky lawsuit game behavior husky swimming already paper episode tricycle scroll",
|
|
||||||
"column flea academic agency blessing garbage party software stadium verify silent umbrella therapy decorate chemical erode dramatic eclipse replace apart"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"29. Mnemonics with greater group threshold than group counts (256 bits)",
|
|
||||||
[
|
|
||||||
"smirk pink acrobat acid auction wireless impulse spine sprinkle fortune clogs elbow guest hush loyalty crush dictate tracks airport talent",
|
|
||||||
"smirk pink acrobat agency dwarf emperor ajar organize legs slice harvest plastic dynamic style mobile float bulb health coding credit",
|
|
||||||
"smirk pink beard academic alto strategy carve shame language rapids ruin smart location spray training acquire eraser endorse submit peaceful"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"30. Mnemonics with duplicate member indices (256 bits)",
|
|
||||||
[
|
|
||||||
"fishing recover academic always device craft trend snapshot gums skin downtown watch device sniff hour clock public maximum garlic born",
|
|
||||||
"fishing recover academic always aircraft view software cradle fangs amazing package plastic evaluate intend penalty epidemic anatomy quarter cage apart"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"31. Mnemonics with mismatching member thresholds (256 bits)",
|
|
||||||
[
|
|
||||||
"evoke garden academic academic answer wolf scandal modern warmth station devote emerald market physics surface formal amazing aquatic gesture medical",
|
|
||||||
"evoke garden academic agency deal revenue knit reunion decrease magazine flexible company goat repair alarm military facility clogs aide mandate"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"32. Mnemonics giving an invalid digest (256 bits)",
|
|
||||||
[
|
|
||||||
"river deal academic acid average forbid pistol peanut custody bike class aunt hairy merit valid flexible learn ajar very easel",
|
|
||||||
"river deal academic agency camera amuse lungs numb isolate display smear piece traffic worthy year patrol crush fact fancy emission"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"33. Insufficient number of groups (256 bits, case 1)",
|
|
||||||
[
|
|
||||||
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"34. Insufficient number of groups (256 bits, case 2)",
|
|
||||||
[
|
|
||||||
"wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen",
|
|
||||||
"wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"35. Threshold number of groups, but insufficient number of members in one group (256 bits)",
|
|
||||||
[
|
|
||||||
"wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club",
|
|
||||||
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"36. Threshold number of groups and members in each group (256 bits, case 1)",
|
|
||||||
[
|
|
||||||
"wildlife deal ceramic round aluminum pitch goat racism employer miracle percent math decision episode dramatic editor lily prospect program scene rebuild display sympathy have single mustang junction relate often chemical society wits estate",
|
|
||||||
"wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen",
|
|
||||||
"wildlife deal ceramic scatter argue equip vampire together ruin reject literary rival distance aquatic agency teammate rebound false argue miracle stay again blessing peaceful unknown cover beard acid island language debris industry idle",
|
|
||||||
"wildlife deal ceramic snake agree voter main lecture axis kitchen physics arcade velvet spine idea scroll promise platform firm sharp patrol divorce ancestor fantasy forbid goat ajar believe swimming cowboy symbolic plastic spelling",
|
|
||||||
"wildlife deal decision shadow analysis adjust bulb skunk muscle mandate obesity total guitar coal gravity carve slim jacket ruin rebuild ancestor numerous hour mortgage require herd maiden public ceiling pecan pickup shadow club"
|
|
||||||
],
|
|
||||||
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"37. Threshold number of groups and members in each group (256 bits, case 2)",
|
|
||||||
[
|
|
||||||
"wildlife deal decision scared acne fatal snake paces obtain election dryer dominant romp tactics railroad marvel trust helpful flip peanut theory theater photo luck install entrance taxi step oven network dictate intimate listen",
|
|
||||||
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium",
|
|
||||||
"wildlife deal decision smug ancestor genuine move huge cubic strategy smell game costume extend swimming false desire fake traffic vegan senior twice timber submit leader payroll fraction apart exact forward pulse tidy install"
|
|
||||||
],
|
|
||||||
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"38. Threshold number of groups and members in each group (256 bits, case 3)",
|
|
||||||
[
|
|
||||||
"wildlife deal beard romp alcohol space mild usual clothes union nuclear testify course research heat listen task location thank hospital slice smell failure fawn helpful priest ambition average recover lecture process dough stadium",
|
|
||||||
"wildlife deal acrobat romp anxiety axis starting require metric flexible geology game drove editor edge screw helpful have huge holy making pitch unknown carve holiday numb glasses survive already tenant adapt goat fangs"
|
|
||||||
],
|
|
||||||
"5385577c8cfc6c1a8aa0f7f10ecde0a3318493262591e78b8c14c6686167123b"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"39. Mnemonic with insufficient length",
|
|
||||||
[
|
|
||||||
"junk necklace academic academic acne isolate join hesitate lunar roster dough calcium chemical ladybug amount mobile glasses verify cylinder"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"40. Mnemonic with invalid master secret length",
|
|
||||||
[
|
|
||||||
"fraction necklace academic academic award teammate mouse regular testify coding building member verdict purchase blind camera duration email prepare spirit quarter"
|
|
||||||
],
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
|
@ -2,10 +2,11 @@
|
||||||
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
|
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
|
||||||
import React, { createContext, useEffect, useState } from 'react';
|
import React, { createContext, useEffect, useState } from 'react';
|
||||||
import { LayoutAnimation } from 'react-native';
|
import { LayoutAnimation } from 'react-native';
|
||||||
import { AppStorage } from '../class';
|
|
||||||
import { FiatUnit } from '../models/fiatUnit';
|
import { FiatUnit } from '../models/fiatUnit';
|
||||||
|
import loc from '../loc';
|
||||||
const BlueApp = require('../BlueApp');
|
const BlueApp = require('../BlueApp');
|
||||||
const BlueElectrum = require('./BlueElectrum');
|
const BlueElectrum = require('./BlueElectrum');
|
||||||
|
const currency = require('../blue_modules/currency');
|
||||||
|
|
||||||
const _lastTimeTriedToRefetchWallet = {}; // hashmap of timestamps we _started_ refetching some wallet
|
const _lastTimeTriedToRefetchWallet = {}; // hashmap of timestamps we _started_ refetching some wallet
|
||||||
|
|
||||||
|
@ -13,20 +14,20 @@ export const WalletTransactionsStatus = { NONE: false, ALL: true };
|
||||||
export const BlueStorageContext = createContext();
|
export const BlueStorageContext = createContext();
|
||||||
export const BlueStorageProvider = ({ children }) => {
|
export const BlueStorageProvider = ({ children }) => {
|
||||||
const [wallets, setWallets] = useState([]);
|
const [wallets, setWallets] = useState([]);
|
||||||
const [pendingWallets, setPendingWallets] = useState([]);
|
const [isImportingWallet, setIsImportingWallet] = useState(false);
|
||||||
const [selectedWallet, setSelectedWallet] = useState('');
|
const [selectedWallet, setSelectedWallet] = useState('');
|
||||||
const [walletTransactionUpdateStatus, setWalletTransactionUpdateStatus] = useState(WalletTransactionsStatus.NONE);
|
const [walletTransactionUpdateStatus, setWalletTransactionUpdateStatus] = useState(WalletTransactionsStatus.NONE);
|
||||||
const [walletsInitialized, setWalletsInitialized] = useState(false);
|
const [walletsInitialized, setWalletsInitialized] = useState(false);
|
||||||
const [preferredFiatCurrency, _setPreferredFiatCurrency] = useState(FiatUnit.USD);
|
const [preferredFiatCurrency, _setPreferredFiatCurrency] = useState(FiatUnit.USD);
|
||||||
const [language, _setLanguage] = useState();
|
const [language, _setLanguage] = useState();
|
||||||
const getPreferredCurrencyAsyncStorage = useAsyncStorage(AppStorage.PREFERRED_CURRENCY).getItem;
|
const getPreferredCurrencyAsyncStorage = useAsyncStorage(currency.PREFERRED_CURRENCY).getItem;
|
||||||
const getLanguageAsyncStorage = useAsyncStorage(AppStorage.LANG).getItem;
|
const getLanguageAsyncStorage = useAsyncStorage(loc.LANG).getItem;
|
||||||
const [isHandOffUseEnabled, setIsHandOffUseEnabled] = useState(false);
|
const [isHandOffUseEnabled, setIsHandOffUseEnabled] = useState(false);
|
||||||
const [isDrawerListBlurred, _setIsDrawerListBlurred] = useState(false);
|
const [isDrawerListBlurred, _setIsDrawerListBlurred] = useState(false);
|
||||||
|
|
||||||
const setIsHandOffUseEnabledAsyncStorage = value => {
|
const setIsHandOffUseEnabledAsyncStorage = value => {
|
||||||
setIsHandOffUseEnabled(value);
|
setIsHandOffUseEnabled(value);
|
||||||
return BlueApp.setItem(AppStorage.HANDOFF_STORAGE_KEY, value === true ? '1' : '');
|
return BlueApp.setIsHandoffEnabled(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveToDisk = async () => {
|
const saveToDisk = async () => {
|
||||||
|
@ -43,7 +44,7 @@ export const BlueStorageProvider = ({ children }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const enabledHandoff = await BlueApp.getItem(AppStorage.HANDOFF_STORAGE_KEY);
|
const enabledHandoff = await BlueApp.isHandoffEnabled();
|
||||||
setIsHandOffUseEnabled(!!enabledHandoff);
|
setIsHandOffUseEnabled(!!enabledHandoff);
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
setIsHandOffUseEnabledAsyncStorage(false);
|
setIsHandOffUseEnabledAsyncStorage(false);
|
||||||
|
@ -175,6 +176,8 @@ export const BlueStorageProvider = ({ children }) => {
|
||||||
const getHodlHodlSignatureKey = BlueApp.getHodlHodlSignatureKey;
|
const getHodlHodlSignatureKey = BlueApp.getHodlHodlSignatureKey;
|
||||||
const addHodlHodlContract = BlueApp.addHodlHodlContract;
|
const addHodlHodlContract = BlueApp.addHodlHodlContract;
|
||||||
const getHodlHodlContracts = BlueApp.getHodlHodlContracts;
|
const getHodlHodlContracts = BlueApp.getHodlHodlContracts;
|
||||||
|
const setDoNotTrack = BlueApp.setDoNotTrack;
|
||||||
|
const isDoNotTrackEnabled = BlueApp.isDoNotTrackEnabled;
|
||||||
const getItem = BlueApp.getItem;
|
const getItem = BlueApp.getItem;
|
||||||
const setItem = BlueApp.setItem;
|
const setItem = BlueApp.setItem;
|
||||||
|
|
||||||
|
@ -183,8 +186,8 @@ export const BlueStorageProvider = ({ children }) => {
|
||||||
value={{
|
value={{
|
||||||
wallets,
|
wallets,
|
||||||
setWalletsWithNewOrder,
|
setWalletsWithNewOrder,
|
||||||
pendingWallets,
|
isImportingWallet,
|
||||||
setPendingWallets,
|
setIsImportingWallet,
|
||||||
txMetadata,
|
txMetadata,
|
||||||
saveToDisk,
|
saveToDisk,
|
||||||
getTransactions,
|
getTransactions,
|
||||||
|
@ -227,6 +230,8 @@ export const BlueStorageProvider = ({ children }) => {
|
||||||
setWalletTransactionUpdateStatus,
|
setWalletTransactionUpdateStatus,
|
||||||
isDrawerListBlurred,
|
isDrawerListBlurred,
|
||||||
setIsDrawerListBlurred,
|
setIsDrawerListBlurred,
|
||||||
|
setDoNotTrack,
|
||||||
|
isDoNotTrackEnabled,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
228
blue_modules/ur/index.js
Normal file
228
blue_modules/ur/index.js
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import { URDecoder } from '@ngraveio/bc-ur';
|
||||||
|
import b58 from 'bs58check';
|
||||||
|
import {
|
||||||
|
CryptoHDKey,
|
||||||
|
CryptoKeypath,
|
||||||
|
CryptoOutput,
|
||||||
|
PathComponent,
|
||||||
|
ScriptExpressions,
|
||||||
|
CryptoPSBT,
|
||||||
|
CryptoAccount,
|
||||||
|
Bytes,
|
||||||
|
} from '@keystonehq/bc-ur-registry/dist';
|
||||||
|
import { decodeUR as origDecodeUr, encodeUR as origEncodeUR, extractSingleWorkload as origExtractSingleWorkload } from '../bc-ur/dist';
|
||||||
|
import { MultisigCosigner, MultisigHDWallet } from '../../class';
|
||||||
|
import { Psbt } from 'bitcoinjs-lib';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
const USE_UR_V1 = 'USE_UR_V1';
|
||||||
|
|
||||||
|
let useURv1 = false;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
useURv1 = !!(await AsyncStorage.getItem(USE_UR_V1));
|
||||||
|
} catch (_) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
async function isURv1Enabled() {
|
||||||
|
try {
|
||||||
|
return !!(await AsyncStorage.getItem(USE_UR_V1));
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setUseURv1() {
|
||||||
|
useURv1 = true;
|
||||||
|
return AsyncStorage.setItem(USE_UR_V1, '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearUseURv1() {
|
||||||
|
useURv1 = false;
|
||||||
|
return AsyncStorage.removeItem(USE_UR_V1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeUR(arg1, arg2) {
|
||||||
|
return useURv1 ? encodeURv1(arg1, arg2) : encodeURv2(arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeURv1(arg1, arg2) {
|
||||||
|
// first, lets check that its not a cosigner's json, which we do NOT encode at all:
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(arg1);
|
||||||
|
if (json && json.xpub && json.path && json.xfp) return [arg1];
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return origEncodeUR(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
|
||||||
|
* @return {string[]} txt fragments ready to be displayed in dynamic QR
|
||||||
|
*/
|
||||||
|
function encodeURv2(str, len) {
|
||||||
|
// now, lets do some intelligent guessing what we've got here, psbt hex, or json with a multisig cosigner..?
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cosigner = new MultisigCosigner(str);
|
||||||
|
|
||||||
|
if (cosigner.isValid()) {
|
||||||
|
let scriptExpressions = false;
|
||||||
|
|
||||||
|
if (cosigner.isNativeSegwit()) {
|
||||||
|
scriptExpressions = [ScriptExpressions.WITNESS_SCRIPT_HASH];
|
||||||
|
} else if (cosigner.isWrappedSegwit()) {
|
||||||
|
scriptExpressions = [ScriptExpressions.SCRIPT_HASH, ScriptExpressions.WITNESS_SCRIPT_HASH];
|
||||||
|
} else if (cosigner.isLegacy()) {
|
||||||
|
scriptExpressions = [ScriptExpressions.SCRIPT_HASH];
|
||||||
|
} else {
|
||||||
|
return ['unsupported multisig type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cryptoKeyPathComponents = [];
|
||||||
|
for (const component of cosigner.getPath().split('/')) {
|
||||||
|
if (component === 'm') continue;
|
||||||
|
const index = parseInt(component);
|
||||||
|
const hardened = component.endsWith('h') || component.endsWith("'");
|
||||||
|
cryptoKeyPathComponents.push(new PathComponent({ index, hardened }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cryptoAccount = new CryptoAccount(Buffer.from(cosigner.getFp(), 'hex'), [
|
||||||
|
new CryptoOutput(
|
||||||
|
scriptExpressions,
|
||||||
|
new CryptoHDKey({
|
||||||
|
isMaster: false,
|
||||||
|
key: Buffer.from(cosigner.getKeyHex(), 'hex'),
|
||||||
|
chainCode: Buffer.from(cosigner.getChainCodeHex(), 'hex'),
|
||||||
|
origin: new CryptoKeypath(
|
||||||
|
cryptoKeyPathComponents,
|
||||||
|
Buffer.from(cosigner.getFp(), 'hex'),
|
||||||
|
cosigner.getDepthNumber(),
|
||||||
|
),
|
||||||
|
parentFingerprint: Buffer.from(cosigner.getParentFingerprintHex(), 'hex'),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
const ur = cryptoAccount.toUREncoder(2000).nextPart();
|
||||||
|
return [ur];
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// not account. lets try psbt
|
||||||
|
|
||||||
|
try {
|
||||||
|
Psbt.fromHex(str); // will throw if not PSBT hex
|
||||||
|
const data = Buffer.from(str, 'hex');
|
||||||
|
const cryptoPSBT = new CryptoPSBT(data);
|
||||||
|
const encoder = cryptoPSBT.toUREncoder(len);
|
||||||
|
|
||||||
|
const ret = [];
|
||||||
|
for (let c = 1; c <= encoder.fragmentsLength; c++) {
|
||||||
|
const ur = encoder.nextPart();
|
||||||
|
ret.push(ur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// fail. fallback to bytes
|
||||||
|
|
||||||
|
const bytes = new Bytes(Buffer.from(str, 'hex'));
|
||||||
|
const encoder = bytes.toUREncoder(len);
|
||||||
|
|
||||||
|
const ret = [];
|
||||||
|
for (let c = 1; c <= encoder.fragmentsLength; c++) {
|
||||||
|
const ur = encoder.nextPart();
|
||||||
|
ret.push(ur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSingleWorkload(arg) {
|
||||||
|
return origExtractSingleWorkload(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeUR(arg) {
|
||||||
|
try {
|
||||||
|
return origDecodeUr(arg);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
const decoder = new URDecoder();
|
||||||
|
|
||||||
|
for (const part of arg) {
|
||||||
|
decoder.receivePart(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decoder.isSuccess()) {
|
||||||
|
throw new Error(decoder.resultError());
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = decoder.resultUR();
|
||||||
|
|
||||||
|
if (decoded.type === 'crypto-psbt') {
|
||||||
|
const cryptoPsbt = CryptoPSBT.fromCBOR(decoded.cbor);
|
||||||
|
return cryptoPsbt.getPSBT().toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded.type === 'bytes') {
|
||||||
|
const b = Bytes.fromCBOR(decoded.cbor);
|
||||||
|
return b.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cryptoAccount = CryptoAccount.fromCBOR(decoded.cbor);
|
||||||
|
|
||||||
|
// now, crafting zpub out of data we have
|
||||||
|
const hdKey = cryptoAccount.outputDescriptors[0].getCryptoKey();
|
||||||
|
const derivationPath = 'm/' + hdKey.getOrigin().getPath();
|
||||||
|
const script = cryptoAccount.outputDescriptors[0].getScriptExpressions()[0].getExpression();
|
||||||
|
const isMultisig =
|
||||||
|
script === ScriptExpressions.WITNESS_SCRIPT_HASH.getExpression() ||
|
||||||
|
// fallback to paths (unreliable).
|
||||||
|
// dont know how to add ms p2sh (legacy) or p2sh-p2wsh (wrapped segwit) atm
|
||||||
|
derivationPath === MultisigHDWallet.PATH_LEGACY ||
|
||||||
|
derivationPath === MultisigHDWallet.PATH_WRAPPED_SEGWIT ||
|
||||||
|
derivationPath === MultisigHDWallet.PATH_NATIVE_SEGWIT;
|
||||||
|
const version = Buffer.from(isMultisig ? '02aa7ed3' : '04b24746', 'hex');
|
||||||
|
const parentFingerprint = hdKey.getParentFingerprint();
|
||||||
|
const depth = hdKey.getOrigin().getDepth();
|
||||||
|
const depthBuf = Buffer.alloc(1);
|
||||||
|
depthBuf.writeUInt8(depth);
|
||||||
|
const components = hdKey.getOrigin().getComponents();
|
||||||
|
const lastComponents = components[components.length - 1];
|
||||||
|
const index = lastComponents.isHardened() ? lastComponents.getIndex() + 0x80000000 : lastComponents.getIndex();
|
||||||
|
const indexBuf = Buffer.alloc(4);
|
||||||
|
indexBuf.writeUInt32BE(index);
|
||||||
|
const chainCode = hdKey.getChainCode();
|
||||||
|
const key = hdKey.getKey();
|
||||||
|
const data = Buffer.concat([version, depthBuf, parentFingerprint, indexBuf, chainCode, key]);
|
||||||
|
|
||||||
|
const zpub = b58.encode(data);
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
result.ExtPubKey = zpub;
|
||||||
|
result.MasterFingerprint = cryptoAccount.getMasterFingerprint().toString('hex').toUpperCase();
|
||||||
|
result.AccountKeyPath = derivationPath;
|
||||||
|
|
||||||
|
const str = JSON.stringify(result);
|
||||||
|
return Buffer.from(str, 'ascii').toString('hex'); // we are expected to return hex-encoded string
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlueURDecoder extends URDecoder {
|
||||||
|
toString() {
|
||||||
|
const decoded = this.resultUR();
|
||||||
|
|
||||||
|
if (decoded.type === 'crypto-psbt') {
|
||||||
|
const cryptoPsbt = CryptoPSBT.fromCBOR(decoded.cbor);
|
||||||
|
return cryptoPsbt.getPSBT().toString('base64');
|
||||||
|
} else if (decoded.type === 'bytes') {
|
||||||
|
const bytes = Bytes.fromCBOR(decoded.cbor);
|
||||||
|
return Buffer.from(bytes.getData(), 'hex').toString('ascii');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { decodeUR, encodeUR, extractSingleWorkload, BlueURDecoder, isURv1Enabled, setUseURv1, clearUseURv1 };
|
|
@ -1,6 +1,7 @@
|
||||||
/* global alert */
|
/* global alert */
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
|
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
|
||||||
|
import * as Keychain from 'react-native-keychain';
|
||||||
import {
|
import {
|
||||||
HDLegacyBreadwalletWallet,
|
HDLegacyBreadwalletWallet,
|
||||||
HDSegwitP2SHWallet,
|
HDSegwitP2SHWallet,
|
||||||
|
@ -20,27 +21,25 @@ import {
|
||||||
SLIP39LegacyP2PKHWallet,
|
SLIP39LegacyP2PKHWallet,
|
||||||
SLIP39SegwitBech32Wallet,
|
SLIP39SegwitBech32Wallet,
|
||||||
} from './';
|
} from './';
|
||||||
|
import { randomBytes } from './rng';
|
||||||
const encryption = require('../blue_modules/encryption');
|
const encryption = require('../blue_modules/encryption');
|
||||||
const Realm = require('realm');
|
const Realm = require('realm');
|
||||||
const createHash = require('create-hash');
|
const createHash = require('create-hash');
|
||||||
let usedBucketNum = false;
|
let usedBucketNum = false;
|
||||||
|
let savingInProgress = 0; // its both a flag and a counter of attempts to write to disk
|
||||||
|
|
||||||
export class AppStorage {
|
export class AppStorage {
|
||||||
static FLAG_ENCRYPTED = 'data_encrypted';
|
static FLAG_ENCRYPTED = 'data_encrypted';
|
||||||
static LANG = 'lang';
|
|
||||||
static EXCHANGE_RATES = 'currency';
|
|
||||||
static LNDHUB = 'lndhub';
|
static LNDHUB = 'lndhub';
|
||||||
static ELECTRUM_HOST = 'electrum_host';
|
|
||||||
static ELECTRUM_TCP_PORT = 'electrum_tcp_port';
|
|
||||||
static ELECTRUM_SSL_PORT = 'electrum_ssl_port';
|
|
||||||
static ELECTRUM_SERVER_HISTORY = 'electrum_server_history';
|
|
||||||
static PREFERRED_CURRENCY = 'preferredCurrency';
|
|
||||||
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
|
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
|
||||||
|
static DO_NOT_TRACK = 'donottrack';
|
||||||
static HODL_HODL_API_KEY = 'HODL_HODL_API_KEY';
|
static HODL_HODL_API_KEY = 'HODL_HODL_API_KEY';
|
||||||
static HODL_HODL_SIGNATURE_KEY = 'HODL_HODL_SIGNATURE_KEY';
|
static HODL_HODL_SIGNATURE_KEY = 'HODL_HODL_SIGNATURE_KEY';
|
||||||
static HODL_HODL_CONTRACTS = 'HODL_HODL_CONTRACTS';
|
static HODL_HODL_CONTRACTS = 'HODL_HODL_CONTRACTS';
|
||||||
static HANDOFF_STORAGE_KEY = 'HandOff';
|
static HANDOFF_STORAGE_KEY = 'HandOff';
|
||||||
|
|
||||||
|
static keys2migrate = [AppStorage.HANDOFF_STORAGE_KEY, AppStorage.DO_NOT_TRACK, AppStorage.ADVANCED_MODE_ENABLED];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
/** {Array.<AbstractWallet>} */
|
/** {Array.<AbstractWallet>} */
|
||||||
this.wallets = [];
|
this.wallets = [];
|
||||||
|
@ -48,6 +47,19 @@ export class AppStorage {
|
||||||
this.cachedPassword = false;
|
this.cachedPassword = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async migrateKeys() {
|
||||||
|
if (!(typeof navigator !== 'undefined' && navigator.product === 'ReactNative')) return;
|
||||||
|
for (const key of this.constructor.keys2migrate) {
|
||||||
|
try {
|
||||||
|
const value = await RNSecureKeyStore.get(key);
|
||||||
|
if (value) {
|
||||||
|
await AsyncStorage.setItem(key, value);
|
||||||
|
await RNSecureKeyStore.remove(key);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
|
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
|
||||||
* used for cli/tests
|
* used for cli/tests
|
||||||
|
@ -79,11 +91,36 @@ export class AppStorage {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Error
|
||||||
|
* @param key {string}
|
||||||
|
* @returns {Promise<*>|null}
|
||||||
|
*/
|
||||||
|
getItemWithFallbackToRealm = async key => {
|
||||||
|
let value;
|
||||||
|
try {
|
||||||
|
return await this.getItem(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('error reading', key, error.message);
|
||||||
|
console.warn('fallback to realm');
|
||||||
|
const realmKeyValue = await this.openRealmKeyValue();
|
||||||
|
const obj = realmKeyValue.objectForPrimaryKey('KeyValue', key); // search for a realm object with a primary key
|
||||||
|
value = obj?.value;
|
||||||
|
realmKeyValue.close();
|
||||||
|
if (value) {
|
||||||
|
console.warn('successfully recovered', value.length, 'bytes from realm for key', key);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
storageIsEncrypted = async () => {
|
storageIsEncrypted = async () => {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await this.getItem(AppStorage.FLAG_ENCRYPTED);
|
data = await this.getItemWithFallbackToRealm(AppStorage.FLAG_ENCRYPTED);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.warn('error reading `' + AppStorage.FLAG_ENCRYPTED + '` key:', error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,6 +248,58 @@ export class AppStorage {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns instace of the Realm database, which is encrypted by device unique id
|
||||||
|
* Database file is static.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Realm>}
|
||||||
|
*/
|
||||||
|
async openRealmKeyValue() {
|
||||||
|
const service = 'realm_encryption_key';
|
||||||
|
let password;
|
||||||
|
const credentials = await Keychain.getGenericPassword({ service });
|
||||||
|
if (credentials) {
|
||||||
|
password = credentials.password;
|
||||||
|
} else {
|
||||||
|
const buf = await randomBytes(64);
|
||||||
|
password = buf.toString('hex');
|
||||||
|
await Keychain.setGenericPassword(service, password, { service });
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = Buffer.from(password, 'hex');
|
||||||
|
const encryptionKey = Int8Array.from(buf);
|
||||||
|
const path = 'keyvalue.realm';
|
||||||
|
|
||||||
|
const schema = [
|
||||||
|
{
|
||||||
|
name: 'KeyValue',
|
||||||
|
primaryKey: 'key',
|
||||||
|
properties: {
|
||||||
|
key: { type: 'string', indexed: true },
|
||||||
|
value: 'string', // stringified json, or whatever
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return Realm.open({
|
||||||
|
schema,
|
||||||
|
path,
|
||||||
|
encryptionKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToRealmKeyValue(realmkeyValue, key, value) {
|
||||||
|
realmkeyValue.write(() => {
|
||||||
|
realmkeyValue.create(
|
||||||
|
'KeyValue',
|
||||||
|
{
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
},
|
||||||
|
Realm.UpdateMode.Modified,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads from storage all wallets and
|
* Loads from storage all wallets and
|
||||||
* maps them to `this.wallets`
|
* maps them to `this.wallets`
|
||||||
|
@ -219,7 +308,7 @@ export class AppStorage {
|
||||||
* @returns {Promise.<boolean>}
|
* @returns {Promise.<boolean>}
|
||||||
*/
|
*/
|
||||||
async loadFromDisk(password) {
|
async loadFromDisk(password) {
|
||||||
let data = await this.getItem('data');
|
let data = await this.getItemWithFallbackToRealm('data');
|
||||||
if (password) {
|
if (password) {
|
||||||
data = this.decryptData(data, password);
|
data = this.decryptData(data, password);
|
||||||
if (data) {
|
if (data) {
|
||||||
|
@ -275,6 +364,14 @@ export class AppStorage {
|
||||||
break;
|
break;
|
||||||
case HDAezeedWallet.type:
|
case HDAezeedWallet.type:
|
||||||
unserializedWallet = HDAezeedWallet.fromJson(key);
|
unserializedWallet = HDAezeedWallet.fromJson(key);
|
||||||
|
// migrate password to this.passphrase field
|
||||||
|
// remove this code somewhere in year 2022
|
||||||
|
if (unserializedWallet.secret.includes(':')) {
|
||||||
|
const [mnemonic, passphrase] = unserializedWallet.secret.split(':');
|
||||||
|
unserializedWallet.secret = mnemonic;
|
||||||
|
unserializedWallet.passphrase = passphrase;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case SLIP39SegwitP2SHWallet.type:
|
case SLIP39SegwitP2SHWallet.type:
|
||||||
unserializedWallet = SLIP39SegwitP2SHWallet.fromJson(key);
|
unserializedWallet = SLIP39SegwitP2SHWallet.fromJson(key);
|
||||||
|
@ -302,8 +399,7 @@ export class AppStorage {
|
||||||
console.log('using wallet-wide settings ', lndhub, 'for ln wallet');
|
console.log('using wallet-wide settings ', lndhub, 'for ln wallet');
|
||||||
unserializedWallet.setBaseURI(lndhub);
|
unserializedWallet.setBaseURI(lndhub);
|
||||||
} else {
|
} else {
|
||||||
console.log('using default', LightningCustodianWallet.defaultBaseUri, 'for ln wallet');
|
console.log('wallet does not have a baseURI. Continuing init...');
|
||||||
unserializedWallet.setBaseURI(LightningCustodianWallet.defaultBaseUri);
|
|
||||||
}
|
}
|
||||||
unserializedWallet.init();
|
unserializedWallet.init();
|
||||||
break;
|
break;
|
||||||
|
@ -405,71 +501,87 @@ export class AppStorage {
|
||||||
* @returns {Promise} Result of storage save
|
* @returns {Promise} Result of storage save
|
||||||
*/
|
*/
|
||||||
async saveToDisk() {
|
async saveToDisk() {
|
||||||
const walletsToSave = [];
|
if (savingInProgress) {
|
||||||
const realm = await this.getRealm();
|
console.warn('saveToDisk is in progress');
|
||||||
for (const key of this.wallets) {
|
if (++savingInProgress > 10) alert('Critical error. Last actions were not saved'); // should never happen
|
||||||
if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue;
|
await new Promise(resolve => setTimeout(resolve, 1000 * savingInProgress)); // sleep
|
||||||
key.prepareForSerialization();
|
return this.saveToDisk();
|
||||||
delete key.current;
|
|
||||||
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
|
|
||||||
if (key._hdWalletInstance) keyCloned._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
|
|
||||||
this.offloadWalletToRealm(realm, key);
|
|
||||||
// stripping down:
|
|
||||||
if (key._txs_by_external_index) {
|
|
||||||
keyCloned._txs_by_external_index = {};
|
|
||||||
keyCloned._txs_by_internal_index = {};
|
|
||||||
}
|
|
||||||
if (key._hdWalletInstance) {
|
|
||||||
keyCloned._hdWalletInstance._txs_by_external_index = {};
|
|
||||||
keyCloned._hdWalletInstance._txs_by_internal_index = {};
|
|
||||||
}
|
|
||||||
walletsToSave.push(JSON.stringify({ ...keyCloned, type: keyCloned.type }));
|
|
||||||
}
|
}
|
||||||
realm.close();
|
savingInProgress = 1;
|
||||||
let data = {
|
|
||||||
wallets: walletsToSave,
|
|
||||||
tx_metadata: this.tx_metadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.cachedPassword) {
|
|
||||||
// should find the correct bucket, encrypt and then save
|
|
||||||
let buckets = await this.getItem('data');
|
|
||||||
buckets = JSON.parse(buckets);
|
|
||||||
const newData = [];
|
|
||||||
let num = 0;
|
|
||||||
for (const bucket of buckets) {
|
|
||||||
let decrypted;
|
|
||||||
// if we had `usedBucketNum` during loadFromDisk(), no point to try to decode each bucket to find the one we
|
|
||||||
// need, we just to find bucket with the same index
|
|
||||||
if (usedBucketNum !== false) {
|
|
||||||
if (num === usedBucketNum) {
|
|
||||||
decrypted = true;
|
|
||||||
}
|
|
||||||
num++;
|
|
||||||
} else {
|
|
||||||
// we dont have `usedBucketNum` for whatever reason, so lets try to decrypt each bucket after bucket
|
|
||||||
// till we find the right one
|
|
||||||
decrypted = encryption.decrypt(bucket, this.cachedPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decrypted) {
|
|
||||||
// no luck decrypting, its not our bucket
|
|
||||||
newData.push(bucket);
|
|
||||||
} else {
|
|
||||||
// decrypted ok, this is our bucket
|
|
||||||
// we serialize our object's data, encrypt it, and add it to buckets
|
|
||||||
newData.push(encryption.encrypt(JSON.stringify(data), this.cachedPassword));
|
|
||||||
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = newData;
|
|
||||||
} else {
|
|
||||||
await this.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return await this.setItem('data', JSON.stringify(data));
|
const walletsToSave = [];
|
||||||
|
const realm = await this.getRealm();
|
||||||
|
for (const key of this.wallets) {
|
||||||
|
if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue;
|
||||||
|
key.prepareForSerialization();
|
||||||
|
delete key.current;
|
||||||
|
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
|
||||||
|
if (key._hdWalletInstance) keyCloned._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
|
||||||
|
this.offloadWalletToRealm(realm, key);
|
||||||
|
// stripping down:
|
||||||
|
if (key._txs_by_external_index) {
|
||||||
|
keyCloned._txs_by_external_index = {};
|
||||||
|
keyCloned._txs_by_internal_index = {};
|
||||||
|
}
|
||||||
|
if (key._hdWalletInstance) {
|
||||||
|
keyCloned._hdWalletInstance._txs_by_external_index = {};
|
||||||
|
keyCloned._hdWalletInstance._txs_by_internal_index = {};
|
||||||
|
}
|
||||||
|
walletsToSave.push(JSON.stringify({ ...keyCloned, type: keyCloned.type }));
|
||||||
|
}
|
||||||
|
realm.close();
|
||||||
|
let data = {
|
||||||
|
wallets: walletsToSave,
|
||||||
|
tx_metadata: this.tx_metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.cachedPassword) {
|
||||||
|
// should find the correct bucket, encrypt and then save
|
||||||
|
let buckets = await this.getItemWithFallbackToRealm('data');
|
||||||
|
buckets = JSON.parse(buckets);
|
||||||
|
const newData = [];
|
||||||
|
let num = 0;
|
||||||
|
for (const bucket of buckets) {
|
||||||
|
let decrypted;
|
||||||
|
// if we had `usedBucketNum` during loadFromDisk(), no point to try to decode each bucket to find the one we
|
||||||
|
// need, we just to find bucket with the same index
|
||||||
|
if (usedBucketNum !== false) {
|
||||||
|
if (num === usedBucketNum) {
|
||||||
|
decrypted = true;
|
||||||
|
}
|
||||||
|
num++;
|
||||||
|
} else {
|
||||||
|
// we dont have `usedBucketNum` for whatever reason, so lets try to decrypt each bucket after bucket
|
||||||
|
// till we find the right one
|
||||||
|
decrypted = encryption.decrypt(bucket, this.cachedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decrypted) {
|
||||||
|
// no luck decrypting, its not our bucket
|
||||||
|
newData.push(bucket);
|
||||||
|
} else {
|
||||||
|
// decrypted ok, this is our bucket
|
||||||
|
// we serialize our object's data, encrypt it, and add it to buckets
|
||||||
|
newData.push(encryption.encrypt(JSON.stringify(data), this.cachedPassword));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setItem('data', JSON.stringify(data));
|
||||||
|
await this.setItem(AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
|
||||||
|
|
||||||
|
// now, backing up same data in realm:
|
||||||
|
const realmkeyValue = await this.openRealmKeyValue();
|
||||||
|
this.saveToRealmKeyValue(realmkeyValue, 'data', JSON.stringify(data));
|
||||||
|
this.saveToRealmKeyValue(realmkeyValue, AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
|
||||||
|
realmkeyValue.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
console.error('save to disk exception:', error.message);
|
||||||
|
alert('save to disk exception: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
savingInProgress = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -644,13 +756,35 @@ export class AppStorage {
|
||||||
|
|
||||||
isAdancedModeEnabled = async () => {
|
isAdancedModeEnabled = async () => {
|
||||||
try {
|
try {
|
||||||
return !!(await this.getItem(AppStorage.ADVANCED_MODE_ENABLED));
|
return !!(await AsyncStorage.getItem(AppStorage.ADVANCED_MODE_ENABLED));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
setIsAdancedModeEnabled = async value => {
|
setIsAdancedModeEnabled = async value => {
|
||||||
await this.setItem(AppStorage.ADVANCED_MODE_ENABLED, value ? '1' : '');
|
await AsyncStorage.setItem(AppStorage.ADVANCED_MODE_ENABLED, value ? '1' : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
isHandoffEnabled = async () => {
|
||||||
|
try {
|
||||||
|
return !!(await AsyncStorage.getItem(AppStorage.HANDOFF_STORAGE_KEY));
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsHandoffEnabled = async value => {
|
||||||
|
await AsyncStorage.setItem(AppStorage.HANDOFF_STORAGE_KEY, value ? '1' : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
isDoNotTrackEnabled = async () => {
|
||||||
|
try {
|
||||||
|
return !!(await AsyncStorage.getItem(AppStorage.DO_NOT_TRACK));
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
setDoNotTrack = async value => {
|
||||||
|
await AsyncStorage.setItem(AppStorage.DO_NOT_TRACK, value ? '1' : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,9 +8,10 @@ import RNSecureKeyStore from 'react-native-secure-key-store';
|
||||||
import loc from '../loc';
|
import loc from '../loc';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { BlueStorageContext } from '../blue_modules/storage-context';
|
import { BlueStorageContext } from '../blue_modules/storage-context';
|
||||||
|
import * as Sentry from '@sentry/react-native';
|
||||||
|
|
||||||
function Biometric() {
|
function Biometric() {
|
||||||
const { getItem, setItem, setResetOnAppUninstallTo } = useContext(BlueStorageContext);
|
const { getItem, setItem } = useContext(BlueStorageContext);
|
||||||
Biometric.STORAGEKEY = 'Biometrics';
|
Biometric.STORAGEKEY = 'Biometrics';
|
||||||
Biometric.FaceID = 'Face ID';
|
Biometric.FaceID = 'Face ID';
|
||||||
Biometric.TouchID = 'Touch ID';
|
Biometric.TouchID = 'Touch ID';
|
||||||
|
@ -42,10 +43,9 @@ function Biometric() {
|
||||||
try {
|
try {
|
||||||
const enabledBiometrics = await getItem(Biometric.STORAGEKEY);
|
const enabledBiometrics = await getItem(Biometric.STORAGEKEY);
|
||||||
return !!enabledBiometrics;
|
return !!enabledBiometrics;
|
||||||
} catch (_e) {
|
} catch (_) {}
|
||||||
await setItem(Biometric.STORAGEKEY, '');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Biometric.isBiometricUseCapableAndEnabled = async () => {
|
Biometric.isBiometricUseCapableAndEnabled = async () => {
|
||||||
|
@ -72,10 +72,10 @@ function Biometric() {
|
||||||
};
|
};
|
||||||
|
|
||||||
Biometric.clearKeychain = async () => {
|
Biometric.clearKeychain = async () => {
|
||||||
|
Sentry.captureMessage('Biometric.clearKeychain()');
|
||||||
await RNSecureKeyStore.remove('data');
|
await RNSecureKeyStore.remove('data');
|
||||||
await RNSecureKeyStore.remove('data_encrypted');
|
await RNSecureKeyStore.remove('data_encrypted');
|
||||||
await RNSecureKeyStore.remove(Biometric.STORAGEKEY);
|
await RNSecureKeyStore.remove(Biometric.STORAGEKEY);
|
||||||
await setResetOnAppUninstallTo(true);
|
|
||||||
NavigationService.dispatch(StackActions.replace('WalletsRoot'));
|
NavigationService.dispatch(StackActions.replace('WalletsRoot'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -158,6 +158,7 @@ class DeeplinkSchemaMatch {
|
||||||
|
|
||||||
const safelloStateToken = urlObject.query['safello-state-token'];
|
const safelloStateToken = urlObject.query['safello-state-token'];
|
||||||
let wallet;
|
let wallet;
|
||||||
|
// eslint-disable-next-line no-unreachable-loop
|
||||||
for (const w of context.wallets) {
|
for (const w of context.wallets) {
|
||||||
wallet = w;
|
wallet = w;
|
||||||
break;
|
break;
|
||||||
|
@ -254,8 +255,7 @@ class DeeplinkSchemaMatch {
|
||||||
{
|
{
|
||||||
screen: 'LappBrowser',
|
screen: 'LappBrowser',
|
||||||
params: {
|
params: {
|
||||||
fromSecret: lnWallet.getSecret(),
|
walletID: lnWallet.getID(),
|
||||||
fromWallet: lnWallet,
|
|
||||||
url: urlObject.query.url,
|
url: urlObject.query.url,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -348,6 +348,7 @@ export class HDSegwitBech32Transaction {
|
||||||
|
|
||||||
let add = 0;
|
let add = 0;
|
||||||
while (add <= 128) {
|
while (add <= 128) {
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
var { tx, inputs, outputs, fee } = this._wallet.createTransaction(
|
var { tx, inputs, outputs, fee } = this._wallet.createTransaction(
|
||||||
unconfirmedUtxos,
|
unconfirmedUtxos,
|
||||||
[{ address: myAddress }],
|
[{ address: myAddress }],
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import bech32 from 'bech32';
|
import { bech32 } from 'bech32';
|
||||||
import bolt11 from 'bolt11';
|
import bolt11 from 'bolt11';
|
||||||
const CryptoJS = require('crypto-js');
|
const CryptoJS = require('crypto-js');
|
||||||
|
|
||||||
const createHash = require('create-hash');
|
const createHash = require('create-hash');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,7 +19,7 @@ export default class Lnurl {
|
||||||
}
|
}
|
||||||
|
|
||||||
static findlnurl(bodyOfText) {
|
static findlnurl(bodyOfText) {
|
||||||
var res = /^(?:http.*[&?]lightning=|lightning:)?(lnurl1[02-9ac-hj-np-z]+)/.exec(bodyOfText.toLowerCase());
|
const res = /^(?:http.*[&?]lightning=|lightning:)?(lnurl1[02-9ac-hj-np-z]+)/.exec(bodyOfText.toLowerCase());
|
||||||
if (res) {
|
if (res) {
|
||||||
return res[1];
|
return res[1];
|
||||||
}
|
}
|
||||||
|
@ -97,7 +96,14 @@ export default class Lnurl {
|
||||||
if (!this._lnurlPayServicePayload) throw new Error('this._lnurlPayServicePayload is not set');
|
if (!this._lnurlPayServicePayload) throw new Error('this._lnurlPayServicePayload is not set');
|
||||||
if (!this._lnurlPayServicePayload.callback) throw new Error('this._lnurlPayServicePayload.callback is not set');
|
if (!this._lnurlPayServicePayload.callback) throw new Error('this._lnurlPayServicePayload.callback is not set');
|
||||||
if (amountSat < this._lnurlPayServicePayload.min || amountSat > this._lnurlPayServicePayload.max)
|
if (amountSat < this._lnurlPayServicePayload.min || amountSat > this._lnurlPayServicePayload.max)
|
||||||
throw new Error('amount is not right, ' + amountSat + ' should be between ' + this._lnurlPayServicePayload.min + ' and ' + this._lnurlPayServicePayload.max);
|
throw new Error(
|
||||||
|
'amount is not right, ' +
|
||||||
|
amountSat +
|
||||||
|
' should be between ' +
|
||||||
|
this._lnurlPayServicePayload.min +
|
||||||
|
' and ' +
|
||||||
|
this._lnurlPayServicePayload.max,
|
||||||
|
);
|
||||||
const nonce = Math.floor(Math.random() * 2e16).toString(16);
|
const nonce = Math.floor(Math.random() * 2e16).toString(16);
|
||||||
const separator = this._lnurlPayServicePayload.callback.indexOf('?') === -1 ? '?' : '&';
|
const separator = this._lnurlPayServicePayload.callback.indexOf('?') === -1 ? '?' : '&';
|
||||||
const urlToFetch = this._lnurlPayServicePayload.callback + separator + 'amount=' + Math.floor(amountSat * 1000) + '&nonce=' + nonce;
|
const urlToFetch = this._lnurlPayServicePayload.callback + separator + 'amount=' + Math.floor(amountSat * 1000) + '&nonce=' + nonce;
|
||||||
|
@ -131,8 +137,8 @@ export default class Lnurl {
|
||||||
const data = reply;
|
const data = reply;
|
||||||
|
|
||||||
// parse metadata and extract things from it
|
// parse metadata and extract things from it
|
||||||
var image;
|
let image;
|
||||||
var description;
|
let description;
|
||||||
const kvs = JSON.parse(data.metadata);
|
const kvs = JSON.parse(data.metadata);
|
||||||
for (let i = 0; i < kvs.length; i++) {
|
for (let i = 0; i < kvs.length; i++) {
|
||||||
const [k, v] = kvs[i];
|
const [k, v] = kvs[i];
|
||||||
|
@ -156,7 +162,7 @@ export default class Lnurl {
|
||||||
fixed: min === max,
|
fixed: min === max,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
domain: data.callback.match(new RegExp('https://([^/]+)/'))[1],
|
domain: data.callback.match(/https:\/\/([^/]+)\//)[1],
|
||||||
metadata: data.metadata,
|
metadata: data.metadata,
|
||||||
description,
|
description,
|
||||||
image,
|
image,
|
||||||
|
|
|
@ -69,6 +69,21 @@ export class MultisigCosigner {
|
||||||
this._valid = false;
|
this._valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// is it cobo crypto-account URv2 ?
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
if (json && json.ExtPubKey && json.MasterFingerprint && json.AccountKeyPath) {
|
||||||
|
this._fp = json.MasterFingerprint;
|
||||||
|
this._xpub = json.ExtPubKey;
|
||||||
|
this._path = json.AccountKeyPath;
|
||||||
|
this._cosigners = [true];
|
||||||
|
this._valid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
this._valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
// is it coldcard json?
|
// is it coldcard json?
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(data);
|
const json = JSON.parse(data);
|
||||||
|
@ -149,4 +164,48 @@ export class MultisigCosigner {
|
||||||
getAllCosigners() {
|
getAllCosigners() {
|
||||||
return this._cosigners;
|
return this._cosigners;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isNativeSegwit() {
|
||||||
|
return this.getXpub().startsWith('Zpub');
|
||||||
|
}
|
||||||
|
|
||||||
|
isWrappedSegwit() {
|
||||||
|
return this.getXpub().startsWith('Ypub');
|
||||||
|
}
|
||||||
|
|
||||||
|
isLegacy() {
|
||||||
|
return this.getXpub().startsWith('xpub');
|
||||||
|
}
|
||||||
|
|
||||||
|
getChainCodeHex() {
|
||||||
|
let data = b58.decode(this.getXpub());
|
||||||
|
data = data.slice(4);
|
||||||
|
data = data.slice(1);
|
||||||
|
data = data.slice(4);
|
||||||
|
data = data.slice(4, 36);
|
||||||
|
return data.toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeyHex() {
|
||||||
|
let data = b58.decode(this.getXpub());
|
||||||
|
data = data.slice(4);
|
||||||
|
data = data.slice(1);
|
||||||
|
data = data.slice(4);
|
||||||
|
data = data.slice(36);
|
||||||
|
return data.toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentFingerprintHex() {
|
||||||
|
let data = b58.decode(this.getXpub());
|
||||||
|
data = data.slice(4);
|
||||||
|
data = data.slice(1);
|
||||||
|
data = data.slice(0, 4);
|
||||||
|
return data.toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
getDepthNumber() {
|
||||||
|
let data = b58.decode(this.getXpub());
|
||||||
|
data = data.slice(4, 5);
|
||||||
|
return data.readInt8();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,11 @@ function DeviceQuickActions() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DeviceQuickActions.popInitialAction = async () => {
|
||||||
|
const data = await QuickActions.popInitialAction();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
DeviceQuickActions.getEnabled = async () => {
|
DeviceQuickActions.getEnabled = async () => {
|
||||||
try {
|
try {
|
||||||
const isEnabled = await AsyncStorage.getItem(DeviceQuickActions.STORAGE_KEY);
|
const isEnabled = await AsyncStorage.getItem(DeviceQuickActions.STORAGE_KEY);
|
||||||
|
|
|
@ -7,6 +7,8 @@ function DeviceQuickActions() {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DeviceQuickActions.popInitialAction = () => {};
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,13 @@ import {
|
||||||
HDLegacyP2PKHWallet,
|
HDLegacyP2PKHWallet,
|
||||||
HDSegwitBech32Wallet,
|
HDSegwitBech32Wallet,
|
||||||
LightningCustodianWallet,
|
LightningCustodianWallet,
|
||||||
PlaceholderWallet,
|
|
||||||
SegwitBech32Wallet,
|
SegwitBech32Wallet,
|
||||||
HDLegacyElectrumSeedP2PKHWallet,
|
HDLegacyElectrumSeedP2PKHWallet,
|
||||||
HDSegwitElectrumSeedP2WPKHWallet,
|
HDSegwitElectrumSeedP2WPKHWallet,
|
||||||
HDAezeedWallet,
|
HDAezeedWallet,
|
||||||
MultisigHDWallet,
|
MultisigHDWallet,
|
||||||
SLIP39LegacyP2PKHWallet,
|
SLIP39LegacyP2PKHWallet,
|
||||||
|
PlaceholderWallet,
|
||||||
SLIP39SegwitP2SHWallet,
|
SLIP39SegwitP2SHWallet,
|
||||||
SLIP39SegwitBech32Wallet,
|
SLIP39SegwitBech32Wallet,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
@ -30,16 +30,15 @@ const wif = require('wif');
|
||||||
const prompt = require('../blue_modules/prompt');
|
const prompt = require('../blue_modules/prompt');
|
||||||
|
|
||||||
function WalletImport() {
|
function WalletImport() {
|
||||||
const { wallets, pendingWallets, setPendingWallets, saveToDisk, addWallet } = useContext(BlueStorageContext);
|
const { wallets, saveToDisk, addWallet, setIsImportingWallet } = useContext(BlueStorageContext);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param w {AbstractWallet}
|
* @param w {AbstractWallet}
|
||||||
* @param additionalProperties key-values passed from outside. Used only to set up `masterFingerprint` property for watch-only wallet
|
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
WalletImport._saveWallet = async (w, additionalProperties) => {
|
WalletImport._saveWallet = async w => {
|
||||||
IdleTimerManager.setIdleTimerDisabled(false);
|
IdleTimerManager.setIdleTimerDisabled(false);
|
||||||
if (WalletImport.isWalletImported(w)) {
|
if (WalletImport.isWalletImported(w)) {
|
||||||
WalletImport.presentWalletAlreadyExistsAlert();
|
WalletImport.presentWalletAlreadyExistsAlert();
|
||||||
|
@ -49,11 +48,6 @@ function WalletImport() {
|
||||||
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
|
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
|
||||||
if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable);
|
if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable);
|
||||||
w.setUserHasSavedExport(true);
|
w.setUserHasSavedExport(true);
|
||||||
if (additionalProperties) {
|
|
||||||
for (const [key, value] of Object.entries(additionalProperties)) {
|
|
||||||
w[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addWallet(w);
|
addWallet(w);
|
||||||
await saveToDisk();
|
await saveToDisk();
|
||||||
A(A.ENUM.CREATED_WALLET);
|
A(A.ENUM.CREATED_WALLET);
|
||||||
|
@ -61,15 +55,20 @@ function WalletImport() {
|
||||||
Notifications.majorTomToGroundControl(w.getAllExternalAddresses(), [], []);
|
Notifications.majorTomToGroundControl(w.getAllExternalAddresses(), [], []);
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletImport.removePlaceholderWallet = () => {
|
WalletImport.isWalletImported = w => {
|
||||||
setPendingWallets([]);
|
const wallet = wallets.some(wallet => wallet.getSecret() === w.secret || wallet.getID() === w.getID());
|
||||||
|
return !!wallet;
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletImport.isWalletImported = w => {
|
WalletImport.removePlaceholderWallet = () => {
|
||||||
const wallet = wallets.some(
|
setIsImportingWallet(false);
|
||||||
wallet => (wallet.getSecret() === w.secret || wallet.getID() === w.getID()) && wallet.type !== PlaceholderWallet.type,
|
};
|
||||||
);
|
|
||||||
return !!wallet;
|
WalletImport.addPlaceholderWallet = (importText, isFailure = false) => {
|
||||||
|
const placeholderWallet = new PlaceholderWallet();
|
||||||
|
placeholderWallet.setSecret(importText);
|
||||||
|
placeholderWallet.setIsFailure(isFailure);
|
||||||
|
setIsImportingWallet(placeholderWallet);
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletImport.presentWalletAlreadyExistsAlert = () => {
|
WalletImport.presentWalletAlreadyExistsAlert = () => {
|
||||||
|
@ -77,25 +76,80 @@ function WalletImport() {
|
||||||
alert('This wallet has been previously imported.');
|
alert('This wallet has been previously imported.');
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletImport.addPlaceholderWallet = (importText, isFailure = false) => {
|
/**
|
||||||
const wallet = new PlaceholderWallet();
|
*
|
||||||
wallet.setSecret(importText);
|
* @param importText
|
||||||
wallet.setIsFailure(isFailure);
|
* @returns {Promise<void>}
|
||||||
setPendingWallets([...pendingWallets, wallet]);
|
* @returns {Promise<{text: string, password: string|void}>}
|
||||||
return wallet;
|
*/
|
||||||
};
|
WalletImport.askPasswordIfNeeded = async importText => {
|
||||||
|
const text = importText.trim();
|
||||||
|
let password;
|
||||||
|
|
||||||
WalletImport.isCurrentlyImportingWallet = () => {
|
// BIP38 password required
|
||||||
return wallets.some(wallet => wallet.type === PlaceholderWallet.type);
|
if (text.startsWith('6P')) {
|
||||||
|
do {
|
||||||
|
password = await prompt(loc.wallets.looks_like_bip38, loc.wallets.enter_bip38_password);
|
||||||
|
} while (!password);
|
||||||
|
return { text, password };
|
||||||
|
}
|
||||||
|
|
||||||
|
// HD BIP39 wallet password is optinal
|
||||||
|
const hd = new HDSegwitBech32Wallet();
|
||||||
|
hd.setSecret(text);
|
||||||
|
if (hd.validateMnemonic()) {
|
||||||
|
password = await prompt(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
|
||||||
|
return { text, password };
|
||||||
|
}
|
||||||
|
|
||||||
|
// AEZEED password needs to be correct
|
||||||
|
const aezeed = new HDAezeedWallet();
|
||||||
|
aezeed.setSecret(text);
|
||||||
|
if (await aezeed.mnemonicInvalidPassword()) {
|
||||||
|
do {
|
||||||
|
password = await prompt('', loc.wallets.enter_bip38_password);
|
||||||
|
aezeed.setPassphrase(password);
|
||||||
|
} while (await aezeed.mnemonicInvalidPassword());
|
||||||
|
return { text, password };
|
||||||
|
}
|
||||||
|
|
||||||
|
// SLIP39 wallet password is optinal
|
||||||
|
if (text.includes('\n')) {
|
||||||
|
const s1 = new SLIP39SegwitP2SHWallet();
|
||||||
|
s1.setSecret(text);
|
||||||
|
|
||||||
|
if (s1.validateMnemonic()) {
|
||||||
|
password = await prompt(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
|
||||||
|
return { text, password };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ELECTRUM segwit wallet password is optinal
|
||||||
|
const electrum1 = new HDSegwitElectrumSeedP2WPKHWallet();
|
||||||
|
electrum1.setSecret(importText);
|
||||||
|
if (electrum1.validateMnemonic()) {
|
||||||
|
password = await prompt(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
|
||||||
|
return { text, password };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ELECTRUM legacy wallet password is optinal
|
||||||
|
const electrum2 = new HDLegacyElectrumSeedP2PKHWallet();
|
||||||
|
electrum2.setSecret(importText);
|
||||||
|
if (electrum2.validateMnemonic()) {
|
||||||
|
password = await prompt(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
|
||||||
|
return { text, password };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text, password };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param importText
|
* @param importText
|
||||||
* @param additionalProperties key-values passed from outside. Used only to set up `masterFingerprint` property for watch-only wallet
|
* @param password
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
WalletImport.processImportText = async (importText, additionalProperties) => {
|
WalletImport.processImportText = async (importText, password) => {
|
||||||
IdleTimerManager.setIdleTimerDisabled(true);
|
IdleTimerManager.setIdleTimerDisabled(true);
|
||||||
// Plan:
|
// Plan:
|
||||||
// -2. check if BIP38 encrypted
|
// -2. check if BIP38 encrypted
|
||||||
|
@ -107,6 +161,7 @@ function WalletImport() {
|
||||||
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
|
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
|
||||||
// 3.1 check HD Electrum legacy
|
// 3.1 check HD Electrum legacy
|
||||||
// 3.2 check if its AEZEED
|
// 3.2 check if its AEZEED
|
||||||
|
// 3.3 check if its SLIP39
|
||||||
// 4. check if its Segwit WIF (P2SH)
|
// 4. check if its Segwit WIF (P2SH)
|
||||||
// 5. check if its Legacy WIF
|
// 5. check if its Legacy WIF
|
||||||
// 6. check if its address (watch-only wallet)
|
// 6. check if its address (watch-only wallet)
|
||||||
|
@ -116,11 +171,6 @@ function WalletImport() {
|
||||||
importText = importText.trim();
|
importText = importText.trim();
|
||||||
|
|
||||||
if (importText.startsWith('6P')) {
|
if (importText.startsWith('6P')) {
|
||||||
let password = false;
|
|
||||||
do {
|
|
||||||
password = await prompt(loc.wallets.looks_like_bip38, loc.wallets.enter_bip38_password, false);
|
|
||||||
} while (!password);
|
|
||||||
|
|
||||||
const decryptedKey = await bip38.decrypt(importText, password);
|
const decryptedKey = await bip38.decrypt(importText, password);
|
||||||
|
|
||||||
if (decryptedKey) {
|
if (decryptedKey) {
|
||||||
|
@ -147,9 +197,6 @@ function WalletImport() {
|
||||||
const split = importText.split('@');
|
const split = importText.split('@');
|
||||||
lnd.setBaseURI(split[1]);
|
lnd.setBaseURI(split[1]);
|
||||||
lnd.setSecret(split[0]);
|
lnd.setSecret(split[0]);
|
||||||
} else {
|
|
||||||
lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri);
|
|
||||||
lnd.setSecret(importText);
|
|
||||||
}
|
}
|
||||||
lnd.init();
|
lnd.init();
|
||||||
await lnd.authorize();
|
await lnd.authorize();
|
||||||
|
@ -164,8 +211,7 @@ function WalletImport() {
|
||||||
const hd4 = new HDSegwitBech32Wallet();
|
const hd4 = new HDSegwitBech32Wallet();
|
||||||
hd4.setSecret(importText);
|
hd4.setSecret(importText);
|
||||||
if (hd4.validateMnemonic()) {
|
if (hd4.validateMnemonic()) {
|
||||||
// OK its a valid BIP39 seed
|
hd4.setPassphrase(password);
|
||||||
|
|
||||||
if (await hd4.wasEverUsed()) {
|
if (await hd4.wasEverUsed()) {
|
||||||
await hd4.fetchBalance(); // fetching balance for BIP84 only on purpose
|
await hd4.fetchBalance(); // fetching balance for BIP84 only on purpose
|
||||||
return WalletImport._saveWallet(hd4);
|
return WalletImport._saveWallet(hd4);
|
||||||
|
@ -173,18 +219,21 @@ function WalletImport() {
|
||||||
|
|
||||||
const hd2 = new HDSegwitP2SHWallet();
|
const hd2 = new HDSegwitP2SHWallet();
|
||||||
hd2.setSecret(importText);
|
hd2.setSecret(importText);
|
||||||
|
hd2.setPassphrase(password);
|
||||||
if (await hd2.wasEverUsed()) {
|
if (await hd2.wasEverUsed()) {
|
||||||
return WalletImport._saveWallet(hd2);
|
return WalletImport._saveWallet(hd2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hd3 = new HDLegacyP2PKHWallet();
|
const hd3 = new HDLegacyP2PKHWallet();
|
||||||
hd3.setSecret(importText);
|
hd3.setSecret(importText);
|
||||||
|
hd3.setPassphrase(password);
|
||||||
if (await hd3.wasEverUsed()) {
|
if (await hd3.wasEverUsed()) {
|
||||||
return WalletImport._saveWallet(hd3);
|
return WalletImport._saveWallet(hd3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hd1 = new HDLegacyBreadwalletWallet();
|
const hd1 = new HDLegacyBreadwalletWallet();
|
||||||
hd1.setSecret(importText);
|
hd1.setSecret(importText);
|
||||||
|
hd1.setPassphrase(password);
|
||||||
if (await hd1.wasEverUsed()) {
|
if (await hd1.wasEverUsed()) {
|
||||||
return WalletImport._saveWallet(hd1);
|
return WalletImport._saveWallet(hd1);
|
||||||
}
|
}
|
||||||
|
@ -230,21 +279,32 @@ function WalletImport() {
|
||||||
|
|
||||||
// if we're here - nope, its not a valid WIF
|
// if we're here - nope, its not a valid WIF
|
||||||
|
|
||||||
|
// maybe its a watch-only address?
|
||||||
|
const watchOnly = new WatchOnlyWallet();
|
||||||
|
watchOnly.setSecret(importText);
|
||||||
|
if (watchOnly.valid()) {
|
||||||
|
await watchOnly.fetchBalance();
|
||||||
|
return WalletImport._saveWallet(watchOnly);
|
||||||
|
}
|
||||||
|
// nope, not watch-only
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hdElectrumSeedLegacy = new HDSegwitElectrumSeedP2WPKHWallet();
|
const hdElectrum = new HDSegwitElectrumSeedP2WPKHWallet();
|
||||||
hdElectrumSeedLegacy.setSecret(importText);
|
hdElectrum.setSecret(importText);
|
||||||
if (hdElectrumSeedLegacy.validateMnemonic()) {
|
hdElectrum.setPassphrase(password);
|
||||||
|
if (hdElectrum.validateMnemonic()) {
|
||||||
// not fetching txs or balances, fuck it, yolo, life is too short
|
// not fetching txs or balances, fuck it, yolo, life is too short
|
||||||
return WalletImport._saveWallet(hdElectrumSeedLegacy);
|
return WalletImport._saveWallet(hdElectrum);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hdElectrumSeedLegacy = new HDLegacyElectrumSeedP2PKHWallet();
|
const hdElectrum = new HDLegacyElectrumSeedP2PKHWallet();
|
||||||
hdElectrumSeedLegacy.setSecret(importText);
|
hdElectrum.setSecret(importText);
|
||||||
if (hdElectrumSeedLegacy.validateMnemonic()) {
|
hdElectrum.setPassphrase(password);
|
||||||
|
if (hdElectrum.validateMnemonic()) {
|
||||||
// not fetching txs or balances, fuck it, yolo, life is too short
|
// not fetching txs or balances, fuck it, yolo, life is too short
|
||||||
return WalletImport._saveWallet(hdElectrumSeedLegacy);
|
return WalletImport._saveWallet(hdElectrum);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
@ -252,33 +312,13 @@ function WalletImport() {
|
||||||
try {
|
try {
|
||||||
const aezeed = new HDAezeedWallet();
|
const aezeed = new HDAezeedWallet();
|
||||||
aezeed.setSecret(importText);
|
aezeed.setSecret(importText);
|
||||||
|
aezeed.setPassphrase(password);
|
||||||
if (await aezeed.validateMnemonicAsync()) {
|
if (await aezeed.validateMnemonicAsync()) {
|
||||||
// not fetching txs or balances, fuck it, yolo, life is too short
|
// not fetching txs or balances, fuck it, yolo, life is too short
|
||||||
return WalletImport._saveWallet(aezeed);
|
return WalletImport._saveWallet(aezeed);
|
||||||
} else {
|
|
||||||
// there is a chance that a password is required
|
|
||||||
if (await aezeed.mnemonicInvalidPassword()) {
|
|
||||||
const password = await prompt(loc.wallets.enter_bip38_password, '', false);
|
|
||||||
if (!password) {
|
|
||||||
// no passord is basically cancel whole aezeed import process
|
|
||||||
throw new Error(loc._.bad_password);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mnemonics = importText.split(':')[0];
|
|
||||||
return WalletImport.processImportText(mnemonics + ':' + password);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// not valid? maybe its a watch-only address?
|
|
||||||
|
|
||||||
const watchOnly = new WatchOnlyWallet();
|
|
||||||
watchOnly.setSecret(importText);
|
|
||||||
if (watchOnly.valid()) {
|
|
||||||
await watchOnly.fetchBalance();
|
|
||||||
return WalletImport._saveWallet(watchOnly, additionalProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it is multi-line string, then it is probably SLIP39 wallet
|
// if it is multi-line string, then it is probably SLIP39 wallet
|
||||||
// each line - one share
|
// each line - one share
|
||||||
if (importText.includes('\n')) {
|
if (importText.includes('\n')) {
|
||||||
|
@ -286,11 +326,13 @@ function WalletImport() {
|
||||||
s1.setSecret(importText);
|
s1.setSecret(importText);
|
||||||
|
|
||||||
if (s1.validateMnemonic()) {
|
if (s1.validateMnemonic()) {
|
||||||
|
s1.setPassphrase(password);
|
||||||
if (await s1.wasEverUsed()) {
|
if (await s1.wasEverUsed()) {
|
||||||
return WalletImport._saveWallet(s1);
|
return WalletImport._saveWallet(s1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const s2 = new SLIP39LegacyP2PKHWallet();
|
const s2 = new SLIP39LegacyP2PKHWallet();
|
||||||
|
s2.setPassphrase(password);
|
||||||
s2.setSecret(importText);
|
s2.setSecret(importText);
|
||||||
if (await s2.wasEverUsed()) {
|
if (await s2.wasEverUsed()) {
|
||||||
return WalletImport._saveWallet(s2);
|
return WalletImport._saveWallet(s2);
|
||||||
|
@ -298,9 +340,8 @@ function WalletImport() {
|
||||||
|
|
||||||
const s3 = new SLIP39SegwitBech32Wallet();
|
const s3 = new SLIP39SegwitBech32Wallet();
|
||||||
s3.setSecret(importText);
|
s3.setSecret(importText);
|
||||||
if (await s3.wasEverUsed()) {
|
s3.setPassphrase(password);
|
||||||
return WalletImport._saveWallet(s3);
|
return WalletImport._saveWallet(s3);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import bip39 from 'bip39';
|
import * as bip39 from 'bip39';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import b58 from 'bs58check';
|
import b58 from 'bs58check';
|
||||||
|
|
||||||
|
@ -824,11 +824,18 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
weOwnAddress(address) {
|
weOwnAddress(address) {
|
||||||
|
if (!address) return false;
|
||||||
|
let cleanAddress = address;
|
||||||
|
|
||||||
|
if (this.segwitType === 'p2wpkh') {
|
||||||
|
cleanAddress = address.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||||
if (this._getExternalAddressByIndex(c) === address) return true;
|
if (this._getExternalAddressByIndex(c) === cleanAddress) return true;
|
||||||
}
|
}
|
||||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||||
if (this._getInternalAddressByIndex(c) === address) return true;
|
if (this._getInternalAddressByIndex(c) === cleanAddress) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1116,7 +1123,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||||
* @returns {string} Hex fingerprint
|
* @returns {string} Hex fingerprint
|
||||||
*/
|
*/
|
||||||
static mnemonicToFingerprint(mnemonic) {
|
static mnemonicToFingerprint(mnemonic) {
|
||||||
const seed = bip39.mnemonicToSeed(mnemonic);
|
const seed = bip39.mnemonicToSeedSync(mnemonic);
|
||||||
return AbstractHDElectrumWallet.seedToFingerprint(seed);
|
return AbstractHDElectrumWallet.seedToFingerprint(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,8 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||||
*/
|
*/
|
||||||
_getSeed() {
|
_getSeed() {
|
||||||
const mnemonic = this.secret;
|
const mnemonic = this.secret;
|
||||||
return bip39.mnemonicToSeed(mnemonic);
|
const passphrase = this.passphrase;
|
||||||
|
return bip39.mnemonicToSeedSync(mnemonic, passphrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSecret(newSecret) {
|
setSecret(newSecret) {
|
||||||
|
@ -61,6 +62,14 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPassphrase(passphrase) {
|
||||||
|
this.passphrase = passphrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPassphrase() {
|
||||||
|
return this.passphrase;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {Boolean} is mnemonic in `this.secret` valid
|
* @return {Boolean} is mnemonic in `this.secret` valid
|
||||||
*/
|
*/
|
||||||
|
@ -68,10 +77,6 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||||
return bip39.validateMnemonic(this.secret);
|
return bip39.validateMnemonic(this.secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMnemonicToSeedHex() {
|
|
||||||
return bip39.mnemonicToSeedHex(this.secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives from hierarchy, returns next free address
|
* Derives from hierarchy, returns next free address
|
||||||
* (the one that has no transactions). Looks for several,
|
* (the one that has no transactions). Looks for several,
|
||||||
|
@ -242,15 +247,6 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
weOwnAddress(addr) {
|
|
||||||
const hashmap = {};
|
|
||||||
for (const a of this.usedAddresses) {
|
|
||||||
hashmap[a] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hashmap[addr] === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getDerivationPathByAddress(address) {
|
_getDerivationPathByAddress(address) {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,64 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||||
import b58 from 'bs58check';
|
import b58 from 'bs58check';
|
||||||
const createHash = require('create-hash');
|
import createHash from 'create-hash';
|
||||||
|
|
||||||
|
type WalletStatics = {
|
||||||
|
type: string;
|
||||||
|
typeReadable: string;
|
||||||
|
segwitType?: string;
|
||||||
|
derivationPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WalletWithPassphrase = AbstractWallet & { getPassphrase: () => string };
|
||||||
|
type UtxoMetadata = {
|
||||||
|
frozen?: boolean;
|
||||||
|
memo?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class AbstractWallet {
|
export class AbstractWallet {
|
||||||
static type = 'abstract';
|
static type = 'abstract';
|
||||||
static typeReadable = 'abstract';
|
static typeReadable = 'abstract';
|
||||||
|
|
||||||
static fromJson(obj) {
|
static fromJson(obj: string): AbstractWallet {
|
||||||
const obj2 = JSON.parse(obj);
|
const obj2 = JSON.parse(obj);
|
||||||
const temp = new this();
|
const temp = new this();
|
||||||
for (const key2 of Object.keys(obj2)) {
|
for (const key2 of Object.keys(obj2)) {
|
||||||
|
// @ts-ignore This kind of magic is not allowed in typescript, we should try and be more specific
|
||||||
temp[key2] = obj2[key2];
|
temp[key2] = obj2[key2];
|
||||||
}
|
}
|
||||||
|
|
||||||
return temp;
|
return temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type: string;
|
||||||
|
typeReadable: string;
|
||||||
|
segwitType?: string;
|
||||||
|
_derivationPath?: string;
|
||||||
|
label: string;
|
||||||
|
secret: string;
|
||||||
|
balance: number;
|
||||||
|
unconfirmed_balance: number; // eslint-disable-line camelcase
|
||||||
|
_address: string | false;
|
||||||
|
utxo: string[];
|
||||||
|
_lastTxFetch: number;
|
||||||
|
_lastBalanceFetch: number;
|
||||||
|
preferredBalanceUnit: BitcoinUnit;
|
||||||
|
chain: Chain;
|
||||||
|
hideBalance: boolean;
|
||||||
|
userHasSavedExport: boolean;
|
||||||
|
_hideTransactionsInWalletsList: boolean;
|
||||||
|
_utxoMetadata: Record<string, UtxoMetadata>;
|
||||||
|
use_with_hardware_wallet: boolean; // eslint-disable-line camelcase
|
||||||
|
masterFingerprint: number | false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = this.constructor.type;
|
const Constructor = (this.constructor as unknown) as WalletStatics;
|
||||||
this.typeReadable = this.constructor.typeReadable;
|
|
||||||
this.segwitType = this.constructor.segwitType;
|
this.type = Constructor.type;
|
||||||
this._derivationPath = this.constructor.derivationPath;
|
this.typeReadable = Constructor.typeReadable;
|
||||||
|
this.segwitType = Constructor.segwitType;
|
||||||
|
this._derivationPath = Constructor.derivationPath;
|
||||||
this.label = '';
|
this.label = '';
|
||||||
this.secret = ''; // private key or recovery phrase
|
this.secret = ''; // private key or recovery phrase
|
||||||
this.balance = 0;
|
this.balance = 0;
|
||||||
|
@ -35,36 +73,42 @@ export class AbstractWallet {
|
||||||
this.userHasSavedExport = false;
|
this.userHasSavedExport = false;
|
||||||
this._hideTransactionsInWalletsList = false;
|
this._hideTransactionsInWalletsList = false;
|
||||||
this._utxoMetadata = {};
|
this._utxoMetadata = {};
|
||||||
|
this.use_with_hardware_wallet = false;
|
||||||
|
this.masterFingerprint = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {number} Timestamp (millisecsec) of when last transactions were fetched from the network
|
* @returns {number} Timestamp (millisecsec) of when last transactions were fetched from the network
|
||||||
*/
|
*/
|
||||||
getLastTxFetch() {
|
getLastTxFetch(): number {
|
||||||
return this._lastTxFetch;
|
return this._lastTxFetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
getID() {
|
getID(): string {
|
||||||
return createHash('sha256').update(this.getSecret()).digest().toString('hex');
|
const thisWithPassphrase = (this as unknown) as WalletWithPassphrase;
|
||||||
|
const passphrase = thisWithPassphrase.getPassphrase ? thisWithPassphrase.getPassphrase() : '';
|
||||||
|
const string2hash = this.getSecret() + passphrase;
|
||||||
|
return createHash('sha256').update(string2hash).digest().toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransactions() {
|
// TODO: return type is incomplete
|
||||||
|
getTransactions(): { received: number }[] {
|
||||||
throw new Error('not implemented');
|
throw new Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserHasSavedExport() {
|
getUserHasSavedExport(): boolean {
|
||||||
return this.userHasSavedExport;
|
return this.userHasSavedExport;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserHasSavedExport(value) {
|
setUserHasSavedExport(value: boolean): void {
|
||||||
this.userHasSavedExport = value;
|
this.userHasSavedExport = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getHideTransactionsInWalletsList() {
|
getHideTransactionsInWalletsList(): boolean {
|
||||||
return this._hideTransactionsInWalletsList;
|
return this._hideTransactionsInWalletsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
setHideTransactionsInWalletsList(value) {
|
setHideTransactionsInWalletsList(value: boolean): void {
|
||||||
this._hideTransactionsInWalletsList = value;
|
this._hideTransactionsInWalletsList = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,14 +116,14 @@ export class AbstractWallet {
|
||||||
*
|
*
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getLabel() {
|
getLabel(): string {
|
||||||
if (this.label.trim().length === 0) {
|
if (this.label.trim().length === 0) {
|
||||||
return 'Wallet';
|
return 'Wallet';
|
||||||
}
|
}
|
||||||
return this.label;
|
return this.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
getXpub() {
|
getXpub(): string | false {
|
||||||
return this._address;
|
return this._address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,11 +131,11 @@ export class AbstractWallet {
|
||||||
*
|
*
|
||||||
* @returns {number} Available to spend amount, int, in sats
|
* @returns {number} Available to spend amount, int, in sats
|
||||||
*/
|
*/
|
||||||
getBalance() {
|
getBalance(): number {
|
||||||
return this.balance + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
|
return this.balance + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreferredBalanceUnit() {
|
getPreferredBalanceUnit(): BitcoinUnit {
|
||||||
for (const value of Object.values(BitcoinUnit)) {
|
for (const value of Object.values(BitcoinUnit)) {
|
||||||
if (value === this.preferredBalanceUnit) {
|
if (value === this.preferredBalanceUnit) {
|
||||||
return this.preferredBalanceUnit;
|
return this.preferredBalanceUnit;
|
||||||
|
@ -100,47 +144,47 @@ export class AbstractWallet {
|
||||||
return BitcoinUnit.BTC;
|
return BitcoinUnit.BTC;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowReceive() {
|
allowReceive(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowSend() {
|
allowSend(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowRBF() {
|
allowRBF(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowHodlHodlTrading() {
|
allowHodlHodlTrading(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowPayJoin() {
|
allowPayJoin(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowCosignPsbt() {
|
allowCosignPsbt(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowSignVerifyMessage() {
|
allowSignVerifyMessage(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowMasterFingerprint() {
|
allowMasterFingerprint(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowXpub() {
|
allowXpub(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
weOwnAddress(address) {
|
weOwnAddress(address: string): boolean {
|
||||||
throw Error('not implemented');
|
throw Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
weOwnTransaction(txid) {
|
weOwnTransaction(txid: string): boolean {
|
||||||
throw Error('not implemented');
|
throw Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,20 +194,20 @@ export class AbstractWallet {
|
||||||
*
|
*
|
||||||
* @return {number} Satoshis
|
* @return {number} Satoshis
|
||||||
*/
|
*/
|
||||||
getUnconfirmedBalance() {
|
getUnconfirmedBalance(): number {
|
||||||
return this.unconfirmed_balance;
|
return this.unconfirmed_balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLabel(newLabel) {
|
setLabel(newLabel: string): this {
|
||||||
this.label = newLabel;
|
this.label = newLabel;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecret() {
|
getSecret(): string {
|
||||||
return this.secret;
|
return this.secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSecret(newSecret) {
|
setSecret(newSecret: string): this {
|
||||||
this.secret = newSecret.trim().replace('bitcoin:', '').replace('BITCOIN:', '');
|
this.secret = newSecret.trim().replace('bitcoin:', '').replace('BITCOIN:', '');
|
||||||
|
|
||||||
if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase();
|
if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase();
|
||||||
|
@ -172,10 +216,12 @@ export class AbstractWallet {
|
||||||
const re = /\[([^\]]+)\](.*)/;
|
const re = /\[([^\]]+)\](.*)/;
|
||||||
const m = this.secret.match(re);
|
const m = this.secret.match(re);
|
||||||
if (m && m.length === 3) {
|
if (m && m.length === 3) {
|
||||||
let hexFingerprint = m[1].split('/')[0];
|
let [hexFingerprint, ...derivationPathArray] = m[1].split('/');
|
||||||
|
const derivationPath = `m/${derivationPathArray.join('/').replace(/h/g, "'")}`;
|
||||||
if (hexFingerprint.length === 8) {
|
if (hexFingerprint.length === 8) {
|
||||||
hexFingerprint = Buffer.from(hexFingerprint, 'hex').reverse().toString('hex');
|
hexFingerprint = Buffer.from(hexFingerprint, 'hex').reverse().toString('hex');
|
||||||
this.masterFingerprint = parseInt(hexFingerprint, 16);
|
this.masterFingerprint = parseInt(hexFingerprint, 16);
|
||||||
|
this._derivationPath = derivationPath;
|
||||||
}
|
}
|
||||||
this.secret = m[2];
|
this.secret = m[2];
|
||||||
}
|
}
|
||||||
|
@ -193,7 +239,7 @@ export class AbstractWallet {
|
||||||
parsedSecret = JSON.parse(newSecret);
|
parsedSecret = JSON.parse(newSecret);
|
||||||
}
|
}
|
||||||
if (parsedSecret && parsedSecret.keystore && parsedSecret.keystore.xpub) {
|
if (parsedSecret && parsedSecret.keystore && parsedSecret.keystore.xpub) {
|
||||||
let masterFingerprint = false;
|
let masterFingerprint: number | false = false;
|
||||||
if (parsedSecret.keystore.ckcc_xfp) {
|
if (parsedSecret.keystore.ckcc_xfp) {
|
||||||
// It is a ColdCard Hardware Wallet
|
// It is a ColdCard Hardware Wallet
|
||||||
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);
|
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);
|
||||||
|
@ -203,42 +249,57 @@ export class AbstractWallet {
|
||||||
if (parsedSecret.keystore.label) {
|
if (parsedSecret.keystore.label) {
|
||||||
this.setLabel(parsedSecret.keystore.label);
|
this.setLabel(parsedSecret.keystore.label);
|
||||||
}
|
}
|
||||||
|
if (parsedSecret.keystore.derivation) {
|
||||||
|
this._derivationPath = parsedSecret.keystore.derivation;
|
||||||
|
}
|
||||||
this.secret = parsedSecret.keystore.xpub;
|
this.secret = parsedSecret.keystore.xpub;
|
||||||
this.masterFingerprint = masterFingerprint;
|
this.masterFingerprint = masterFingerprint;
|
||||||
|
|
||||||
if (parsedSecret.keystore.type === 'hardware') this.use_with_hardware_wallet = true;
|
if (parsedSecret.keystore.type === 'hardware') this.use_with_hardware_wallet = true;
|
||||||
}
|
}
|
||||||
// It is a Cobo Vault Hardware Wallet
|
// It is a Cobo Vault Hardware Wallet
|
||||||
if (parsedSecret && parsedSecret.ExtPubKey && parsedSecret.MasterFingerprint) {
|
if (parsedSecret && parsedSecret.ExtPubKey && parsedSecret.MasterFingerprint && parsedSecret.AccountKeyPath) {
|
||||||
this.secret = parsedSecret.ExtPubKey;
|
this.secret = parsedSecret.ExtPubKey;
|
||||||
const mfp = Buffer.from(parsedSecret.MasterFingerprint, 'hex').reverse().toString('hex');
|
const mfp = Buffer.from(parsedSecret.MasterFingerprint, 'hex').reverse().toString('hex');
|
||||||
this.masterFingerprint = parseInt(mfp, 16);
|
this.masterFingerprint = parseInt(mfp, 16);
|
||||||
this.setLabel('Cobo Vault ' + parsedSecret.MasterFingerprint);
|
this._derivationPath = `m/${parsedSecret.AccountKeyPath}`;
|
||||||
if (parsedSecret.CoboVaultFirmwareVersion) this.use_with_hardware_wallet = true;
|
if (parsedSecret.CoboVaultFirmwareVersion) this.use_with_hardware_wallet = true;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (!this._derivationPath) {
|
||||||
|
if (this.secret.startsWith('xpub')) {
|
||||||
|
this._derivationPath = "m/44'/0'/0'"; // Assume default BIP44 path for legacy wallets
|
||||||
|
} else if (this.secret.startsWith('ypub')) {
|
||||||
|
this._derivationPath = "m/49'/0'/0'"; // Assume default BIP49 path for segwit wrapped wallets
|
||||||
|
} else if (this.secret.startsWith('zpub')) {
|
||||||
|
this._derivationPath = "m/84'/0'/0'"; // Assume default BIP84 for native segwit wallets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLatestTransactionTime() {
|
getLatestTransactionTime(): number {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLatestTransactionTimeEpoch() {
|
getLatestTransactionTimeEpoch(): number {
|
||||||
if (this.getTransactions().length === 0) {
|
if (this.getTransactions().length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let max = 0;
|
let max = 0;
|
||||||
for (const tx of this.getTransactions()) {
|
for (const tx of this.getTransactions()) {
|
||||||
max = Math.max(new Date(tx.received) * 1, max);
|
max = Math.max(new Date(tx.received).getTime(), max);
|
||||||
}
|
}
|
||||||
return max;
|
return max;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
|
* TODO: be more precise on the type
|
||||||
*/
|
*/
|
||||||
createTx() {
|
createTx(): any {
|
||||||
throw Error('not implemented');
|
throw Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,28 +313,38 @@ export class AbstractWallet {
|
||||||
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
||||||
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
|
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
|
||||||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
||||||
|
*
|
||||||
|
* TODO: be more specific on the return type
|
||||||
*/
|
*/
|
||||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
createTransaction(
|
||||||
|
utxos: { vout: number; value: number; txId: string; address: string }[],
|
||||||
|
targets: { value: number; address: string },
|
||||||
|
feeRate: number,
|
||||||
|
changeAddress: string,
|
||||||
|
sequence: number,
|
||||||
|
skipSigning = false,
|
||||||
|
masterFingerprint: number,
|
||||||
|
): { outputs: any[]; tx: any; inputs: any[]; fee: number; psbt: any } {
|
||||||
throw Error('not implemented');
|
throw Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddress() {
|
getAddress(): string {
|
||||||
throw Error('not implemented');
|
throw Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddressAsync() {
|
getAddressAsync(): Promise<string> {
|
||||||
return new Promise(resolve => resolve(this.getAddress()));
|
return new Promise(resolve => resolve(this.getAddress()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChangeAddressAsync() {
|
async getChangeAddressAsync(): Promise<string> {
|
||||||
return new Promise(resolve => resolve(this.getAddress()));
|
return new Promise(resolve => resolve(this.getAddress()));
|
||||||
}
|
}
|
||||||
|
|
||||||
useWithHardwareWalletEnabled() {
|
useWithHardwareWalletEnabled(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async wasEverUsed() {
|
async wasEverUsed(): Promise<boolean> {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,7 +354,7 @@ export class AbstractWallet {
|
||||||
*
|
*
|
||||||
* @returns string[] Addresses
|
* @returns string[] Addresses
|
||||||
*/
|
*/
|
||||||
getAllExternalAddresses() {
|
getAllExternalAddresses(): string[] {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +364,7 @@ export class AbstractWallet {
|
||||||
* @param {String} zpub
|
* @param {String} zpub
|
||||||
* @returns {String} xpub
|
* @returns {String} xpub
|
||||||
*/
|
*/
|
||||||
static _zpubToXpub(zpub) {
|
static _zpubToXpub(zpub: string): string {
|
||||||
let data = b58.decode(zpub);
|
let data = b58.decode(zpub);
|
||||||
data = data.slice(4);
|
data = data.slice(4);
|
||||||
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
||||||
|
@ -306,7 +377,7 @@ export class AbstractWallet {
|
||||||
* @param {String} ypub - wallet ypub
|
* @param {String} ypub - wallet ypub
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
static _ypubToXpub(ypub) {
|
static _ypubToXpub(ypub: string): string {
|
||||||
let data = b58.decode(ypub);
|
let data = b58.decode(ypub);
|
||||||
if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!');
|
if (data.readUInt32BE() !== 0x049d7cb2) throw new Error('Not a valid ypub extended key!');
|
||||||
data = data.slice(4);
|
data = data.slice(4);
|
||||||
|
@ -315,7 +386,7 @@ export class AbstractWallet {
|
||||||
return b58.encode(data);
|
return b58.encode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareForSerialization() {}
|
prepareForSerialization(): void {}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get metadata (frozen, memo) for a specific UTXO
|
* Get metadata (frozen, memo) for a specific UTXO
|
||||||
|
@ -323,7 +394,7 @@ export class AbstractWallet {
|
||||||
* @param {String} txid - transaction id
|
* @param {String} txid - transaction id
|
||||||
* @param {number} vout - an index number of the output in transaction
|
* @param {number} vout - an index number of the output in transaction
|
||||||
*/
|
*/
|
||||||
getUTXOMetadata(txid, vout) {
|
getUTXOMetadata(txid: string, vout: number): UtxoMetadata {
|
||||||
return this._utxoMetadata[`${txid}:${vout}`] || {};
|
return this._utxoMetadata[`${txid}:${vout}`] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,7 +405,7 @@ export class AbstractWallet {
|
||||||
* @param {number} vout - an index number of the output in transaction
|
* @param {number} vout - an index number of the output in transaction
|
||||||
* @param {{memo: String, frozen: Boolean}} opts - options to attach to UTXO
|
* @param {{memo: String, frozen: Boolean}} opts - options to attach to UTXO
|
||||||
*/
|
*/
|
||||||
setUTXOMetadata(txid, vout, opts) {
|
setUTXOMetadata(txid: string, vout: number, opts: UtxoMetadata): void {
|
||||||
const meta = this._utxoMetadata[`${txid}:${vout}`] || {};
|
const meta = this._utxoMetadata[`${txid}:${vout}`] || {};
|
||||||
if ('memo' in opts) meta.memo = opts.memo;
|
if ('memo' in opts) meta.memo = opts.memo;
|
||||||
if ('frozen' in opts) meta.frozen = opts.frozen;
|
if ('frozen' in opts) meta.frozen = opts.frozen;
|
||||||
|
@ -344,7 +415,7 @@ export class AbstractWallet {
|
||||||
/**
|
/**
|
||||||
* @returns {string} Root derivation path for wallet if any
|
* @returns {string} Root derivation path for wallet if any
|
||||||
*/
|
*/
|
||||||
getDerivationPath() {
|
getDerivationPath(): string {
|
||||||
return this._derivationPath ?? '';
|
return this._derivationPath ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -21,7 +21,7 @@ export class HDAezeedWallet extends AbstractHDElectrumWallet {
|
||||||
|
|
||||||
setSecret(newSecret) {
|
setSecret(newSecret) {
|
||||||
this.secret = newSecret.trim();
|
this.secret = newSecret.trim();
|
||||||
this.secret = this.secret.replace(/[^a-zA-Z0-9:]/g, ' ').replace(/\s+/g, ' ');
|
this.secret = this.secret.replace(/[^a-zA-Z0-9]/g, ' ').replace(/\s+/g, ' ');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,14 +51,14 @@ export class HDAezeedWallet extends AbstractHDElectrumWallet {
|
||||||
return this._xpub;
|
return this._xpub;
|
||||||
}
|
}
|
||||||
|
|
||||||
validateMnemonic(): boolean {
|
validateMnemonic() {
|
||||||
throw new Error('Use validateMnemonicAsync()');
|
throw new Error('Use validateMnemonicAsync()');
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateMnemonicAsync() {
|
async validateMnemonicAsync() {
|
||||||
const [mnemonic3, password] = this.secret.split(':');
|
const passphrase = this.getPassphrase() || 'aezeed';
|
||||||
try {
|
try {
|
||||||
const cipherSeed1 = await CipherSeed.fromMnemonic(mnemonic3, password || 'aezeed');
|
const cipherSeed1 = await CipherSeed.fromMnemonic(this.secret, passphrase);
|
||||||
this._entropyHex = cipherSeed1.entropy.toString('hex'); // save cache
|
this._entropyHex = cipherSeed1.entropy.toString('hex'); // save cache
|
||||||
return !!cipherSeed1.entropy;
|
return !!cipherSeed1.entropy;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
@ -67,9 +67,9 @@ export class HDAezeedWallet extends AbstractHDElectrumWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
async mnemonicInvalidPassword() {
|
async mnemonicInvalidPassword() {
|
||||||
const [mnemonic3, password] = this.secret.split(':');
|
const passphrase = this.getPassphrase() || 'aezeed';
|
||||||
try {
|
try {
|
||||||
const cipherSeed1 = await CipherSeed.fromMnemonic(mnemonic3, password || 'aezeed');
|
const cipherSeed1 = await CipherSeed.fromMnemonic(this.secret, passphrase);
|
||||||
this._entropyHex = cipherSeed1.entropy.toString('hex'); // save cache
|
this._entropyHex = cipherSeed1.entropy.toString('hex'); // save cache
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return error.message === 'Invalid Password';
|
return error.message === 'Invalid Password';
|
||||||
|
|
|
@ -5,9 +5,6 @@ const mn = require('electrum-mnemonic');
|
||||||
const HDNode = require('bip32');
|
const HDNode = require('bip32');
|
||||||
|
|
||||||
const PREFIX = mn.PREFIXES.standard;
|
const PREFIX = mn.PREFIXES.standard;
|
||||||
const MNEMONIC_TO_SEED_OPTS = {
|
|
||||||
prefix: PREFIX,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ElectrumSeed means that instead of BIP39 seed format it works with the format invented by Electrum wallet. Otherwise
|
* ElectrumSeed means that instead of BIP39 seed format it works with the format invented by Electrum wallet. Otherwise
|
||||||
|
@ -32,7 +29,9 @@ export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
|
||||||
if (this._xpub) {
|
if (this._xpub) {
|
||||||
return this._xpub; // cache hit
|
return this._xpub; // cache hit
|
||||||
}
|
}
|
||||||
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
|
const args = { prefix: PREFIX };
|
||||||
|
if (this.passphrase) args.passphrase = this.passphrase;
|
||||||
|
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, args));
|
||||||
this._xpub = root.neutered().toBase58();
|
this._xpub = root.neutered().toBase58();
|
||||||
return this._xpub;
|
return this._xpub;
|
||||||
}
|
}
|
||||||
|
@ -63,7 +62,9 @@ export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet {
|
||||||
|
|
||||||
_getWIFByIndex(internal, index) {
|
_getWIFByIndex(internal, index) {
|
||||||
if (!this.secret) return false;
|
if (!this.secret) return false;
|
||||||
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
|
const args = { prefix: PREFIX };
|
||||||
|
if (this.passphrase) args.passphrase = this.passphrase;
|
||||||
|
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, args));
|
||||||
const path = `m/${internal ? 1 : 0}/${index}`;
|
const path = `m/${internal ? 1 : 0}/${index}`;
|
||||||
const child = root.derivePath(path);
|
const child = root.derivePath(path);
|
||||||
|
|
||||||
|
|
|
@ -162,4 +162,9 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet {
|
||||||
|
|
||||||
return psbt;
|
return psbt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getDerivationPathByAddress(address, BIP = 44) {
|
||||||
|
// only changing defaults for function arguments
|
||||||
|
return super._getDerivationPathByAddress(address, BIP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import b58 from 'bs58check';
|
||||||
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
|
import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
|
||||||
|
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
|
@ -5,9 +6,6 @@ const mn = require('electrum-mnemonic');
|
||||||
const HDNode = require('bip32');
|
const HDNode = require('bip32');
|
||||||
|
|
||||||
const PREFIX = mn.PREFIXES.segwit;
|
const PREFIX = mn.PREFIXES.segwit;
|
||||||
const MNEMONIC_TO_SEED_OPTS = {
|
|
||||||
prefix: PREFIX,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ElectrumSeed means that instead of BIP39 seed format it works with the format invented by Electrum wallet. Otherwise
|
* ElectrumSeed means that instead of BIP39 seed format it works with the format invented by Electrum wallet. Otherwise
|
||||||
|
@ -32,8 +30,17 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
|
||||||
if (this._xpub) {
|
if (this._xpub) {
|
||||||
return this._xpub; // cache hit
|
return this._xpub; // cache hit
|
||||||
}
|
}
|
||||||
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
|
const args = { prefix: PREFIX };
|
||||||
this._xpub = root.derivePath("m/0'").neutered().toBase58();
|
if (this.passphrase) args.passphrase = this.passphrase;
|
||||||
|
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, args));
|
||||||
|
const xpub = root.derivePath("m/0'").neutered().toBase58();
|
||||||
|
|
||||||
|
// bitcoinjs does not support zpub yet, so we just convert it from xpub
|
||||||
|
let data = b58.decode(xpub);
|
||||||
|
data = data.slice(4);
|
||||||
|
data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]);
|
||||||
|
this._xpub = b58.encode(data);
|
||||||
|
|
||||||
return this._xpub;
|
return this._xpub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +48,8 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
|
||||||
index = index * 1; // cast to int
|
index = index * 1; // cast to int
|
||||||
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
||||||
|
|
||||||
const node = bitcoin.bip32.fromBase58(this.getXpub());
|
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
||||||
|
const node = bitcoin.bip32.fromBase58(xpub);
|
||||||
const address = bitcoin.payments.p2wpkh({
|
const address = bitcoin.payments.p2wpkh({
|
||||||
pubkey: node.derive(1).derive(index).publicKey,
|
pubkey: node.derive(1).derive(index).publicKey,
|
||||||
}).address;
|
}).address;
|
||||||
|
@ -53,7 +61,8 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
|
||||||
index = index * 1; // cast to int
|
index = index * 1; // cast to int
|
||||||
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
||||||
|
|
||||||
const node = bitcoin.bip32.fromBase58(this.getXpub());
|
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
||||||
|
const node = bitcoin.bip32.fromBase58(xpub);
|
||||||
const address = bitcoin.payments.p2wpkh({
|
const address = bitcoin.payments.p2wpkh({
|
||||||
pubkey: node.derive(0).derive(index).publicKey,
|
pubkey: node.derive(0).derive(index).publicKey,
|
||||||
}).address;
|
}).address;
|
||||||
|
@ -63,7 +72,9 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
|
||||||
|
|
||||||
_getWIFByIndex(internal, index) {
|
_getWIFByIndex(internal, index) {
|
||||||
if (!this.secret) return false;
|
if (!this.secret) return false;
|
||||||
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, MNEMONIC_TO_SEED_OPTS));
|
const args = { prefix: PREFIX };
|
||||||
|
if (this.passphrase) args.passphrase = this.passphrase;
|
||||||
|
const root = bitcoin.bip32.fromSeed(mn.mnemonicToSeedSync(this.secret, args));
|
||||||
const path = `m/0'/${internal ? 1 : 0}/${index}`;
|
const path = `m/0'/${internal ? 1 : 0}/${index}`;
|
||||||
const child = root.derivePath(path);
|
const child = root.derivePath(path);
|
||||||
|
|
||||||
|
@ -74,13 +85,13 @@ export class HDSegwitElectrumSeedP2WPKHWallet extends HDSegwitBech32Wallet {
|
||||||
index = index * 1; // cast to int
|
index = index * 1; // cast to int
|
||||||
|
|
||||||
if (node === 0 && !this._node0) {
|
if (node === 0 && !this._node0) {
|
||||||
const xpub = this.getXpub();
|
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
||||||
const hdNode = HDNode.fromBase58(xpub);
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
this._node0 = hdNode.derive(node);
|
this._node0 = hdNode.derive(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node === 1 && !this._node1) {
|
if (node === 1 && !this._node1) {
|
||||||
const xpub = this.getXpub();
|
const xpub = this.constructor._zpubToXpub(this.getXpub());
|
||||||
const hdNode = HDNode.fromBase58(xpub);
|
const hdNode = HDNode.fromBase58(xpub);
|
||||||
this._node1 = hdNode.derive(node);
|
this._node1 = hdNode.derive(node);
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,4 +148,9 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
|
||||||
});
|
});
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getDerivationPathByAddress(address, BIP = 49) {
|
||||||
|
// only changing defaults for function arguments
|
||||||
|
return super._getDerivationPathByAddress(address, BIP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { AbstractWallet } from './abstract-wallet';
|
||||||
import { HDSegwitBech32Wallet } from '..';
|
import { HDSegwitBech32Wallet } from '..';
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
||||||
const coinSelectAccumulative = require('coinselect/accumulative');
|
const coinSelect = require('coinselect');
|
||||||
const coinSelectSplit = require('coinselect/split');
|
const coinSelectSplit = require('coinselect/split');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -337,7 +337,7 @@ export class LegacyWallet extends AbstractWallet {
|
||||||
coinselect(utxos, targets, feeRate, changeAddress) {
|
coinselect(utxos, targets, feeRate, changeAddress) {
|
||||||
if (!changeAddress) throw new Error('No change address provided');
|
if (!changeAddress) throw new Error('No change address provided');
|
||||||
|
|
||||||
let algo = coinSelectAccumulative;
|
let algo = coinSelect;
|
||||||
// if targets has output without a value, we want send MAX to it
|
// if targets has output without a value, we want send MAX to it
|
||||||
if (targets.some(i => !('value' in i))) {
|
if (targets.some(i => !('value' in i))) {
|
||||||
algo = coinSelectSplit;
|
algo = coinSelectSplit;
|
||||||
|
@ -465,7 +465,14 @@ export class LegacyWallet extends AbstractWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
weOwnAddress(address) {
|
weOwnAddress(address) {
|
||||||
return this.getAddress() === address || this._address === address;
|
if (!address) return false;
|
||||||
|
let cleanAddress = address;
|
||||||
|
|
||||||
|
if (this.segwitType === 'p2wpkh') {
|
||||||
|
cleanAddress = address.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getAddress() === cleanAddress || this._address === cleanAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
weOwnTransaction(txid) {
|
weOwnTransaction(txid) {
|
||||||
|
|
|
@ -2,12 +2,13 @@ import { LegacyWallet } from './legacy-wallet';
|
||||||
import Frisbee from 'frisbee';
|
import Frisbee from 'frisbee';
|
||||||
import bolt11 from 'bolt11';
|
import bolt11 from 'bolt11';
|
||||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||||
|
import { isTorCapable } from '../../blue_modules/environment';
|
||||||
const torrific = require('../../blue_modules/torrific');
|
const torrific = require('../../blue_modules/torrific');
|
||||||
|
|
||||||
export class LightningCustodianWallet extends LegacyWallet {
|
export class LightningCustodianWallet extends LegacyWallet {
|
||||||
static type = 'lightningCustodianWallet';
|
static type = 'lightningCustodianWallet';
|
||||||
static typeReadable = 'Lightning';
|
static typeReadable = 'Lightning';
|
||||||
static defaultBaseUri = 'https://lndhub.herokuapp.com/';
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.setBaseURI(); // no args to init with default value
|
this.setBaseURI(); // no args to init with default value
|
||||||
|
@ -30,11 +31,7 @@ export class LightningCustodianWallet extends LegacyWallet {
|
||||||
* @param URI
|
* @param URI
|
||||||
*/
|
*/
|
||||||
setBaseURI(URI) {
|
setBaseURI(URI) {
|
||||||
if (URI) {
|
this.baseURI = URI;
|
||||||
this.baseURI = URI;
|
|
||||||
} else {
|
|
||||||
this.baseURI = LightningCustodianWallet.defaultBaseUri;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseURI() {
|
getBaseURI() {
|
||||||
|
@ -54,9 +51,6 @@ export class LightningCustodianWallet extends LegacyWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecret() {
|
getSecret() {
|
||||||
if (this.baseURI === LightningCustodianWallet.defaultBaseUri) {
|
|
||||||
return this.secret;
|
|
||||||
}
|
|
||||||
return this.secret + '@' + this.baseURI;
|
return this.secret + '@' + this.baseURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +73,7 @@ export class LightningCustodianWallet extends LegacyWallet {
|
||||||
baseURI: this.baseURI,
|
baseURI: this.baseURI,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.baseURI.indexOf('.onion') !== -1) {
|
if (isTorCapable && this.baseURI && this.baseURI?.indexOf('.onion') !== -1) {
|
||||||
this._api = new torrific.Torsbee({
|
this._api = new torrific.Torsbee({
|
||||||
baseURI: this.baseURI,
|
baseURI: this.baseURI,
|
||||||
});
|
});
|
||||||
|
@ -377,7 +371,7 @@ export class LightningCustodianWallet extends LegacyWallet {
|
||||||
txs = txs.concat(this.pending_transactions_raw.slice(), this.transactions_raw.slice().reverse(), this.user_invoices_raw.slice()); // slice so array is cloned
|
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
|
// transforming to how wallets/list screen expects it
|
||||||
for (const tx of txs) {
|
for (const tx of txs) {
|
||||||
tx.fromWallet = this.getSecret();
|
tx.walletID = this.getID();
|
||||||
if (tx.amount) {
|
if (tx.amount) {
|
||||||
// pending tx
|
// pending tx
|
||||||
tx.amt = tx.amount * -100000000;
|
tx.amt = tx.amount * -100000000;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
|
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
|
||||||
import bip39 from 'bip39';
|
import * as bip39 from 'bip39';
|
||||||
import b58 from 'bs58check';
|
import b58 from 'bs58check';
|
||||||
import { decodeUR } from 'bc-ur';
|
import { decodeUR } from '../../blue_modules/ur';
|
||||||
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
||||||
const HDNode = require('bip32');
|
const HDNode = require('bip32');
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
|
@ -298,7 +298,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
||||||
if (mnemonic.startsWith(ELECTRUM_SEED_PREFIX)) {
|
if (mnemonic.startsWith(ELECTRUM_SEED_PREFIX)) {
|
||||||
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(mnemonic);
|
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(mnemonic);
|
||||||
} else {
|
} else {
|
||||||
seed = bip39.mnemonicToSeed(mnemonic);
|
seed = bip39.mnemonicToSeedSync(mnemonic);
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = bitcoin.bip32.fromSeed(seed);
|
const root = bitcoin.bip32.fromSeed(seed);
|
||||||
|
@ -593,8 +593,8 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
||||||
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
|
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
|
||||||
this.setWrappedSegwit();
|
this.setWrappedSegwit();
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
case MultisigHDWallet.FORMAT_P2WSH:
|
case MultisigHDWallet.FORMAT_P2WSH:
|
||||||
|
default:
|
||||||
this.setNativeSegwit();
|
this.setNativeSegwit();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -835,7 +835,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
||||||
// dont sign more than we need, otherwise there will be "Too many signatures" error
|
// dont sign more than we need, otherwise there will be "Too many signatures" error
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let seed = bip39.mnemonicToSeed(cosigner);
|
let seed = bip39.mnemonicToSeedSync(cosigner);
|
||||||
if (cosigner.startsWith(ELECTRUM_SEED_PREFIX)) {
|
if (cosigner.startsWith(ELECTRUM_SEED_PREFIX)) {
|
||||||
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner);
|
seed = MultisigHDWallet.convertElectrumMnemonicToSeed(cosigner);
|
||||||
}
|
}
|
||||||
|
@ -979,7 +979,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
||||||
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
|
for (const [cosignerIndex, cosigner] of this._cosigners.entries()) {
|
||||||
if (!MultisigHDWallet.isXpubString(cosigner)) {
|
if (!MultisigHDWallet.isXpubString(cosigner)) {
|
||||||
// ok this is a mnemonic, lets try to sign
|
// ok this is a mnemonic, lets try to sign
|
||||||
const seed = bip39.mnemonicToSeed(cosigner);
|
const seed = bip39.mnemonicToSeedSync(cosigner);
|
||||||
const hdRoot = bitcoin.bip32.fromSeed(seed);
|
const hdRoot = bitcoin.bip32.fromSeed(seed);
|
||||||
try {
|
try {
|
||||||
psbt.signInputHD(cc, hdRoot);
|
psbt.signInputHD(cc, hdRoot);
|
||||||
|
@ -995,7 +995,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
||||||
// correctly points to `/internal/index`, so we extract pubkey from our stored mnemonics+path and
|
// correctly points to `/internal/index`, so we extract pubkey from our stored mnemonics+path and
|
||||||
// match it to the one provided in PSBT's input, and if we have a match - we are in luck! we can sign
|
// match it to the one provided in PSBT's input, and if we have a match - we are in luck! we can sign
|
||||||
// with this private key.
|
// with this private key.
|
||||||
const seed = bip39.mnemonicToSeed(cosigner);
|
const seed = bip39.mnemonicToSeedSync(cosigner);
|
||||||
const root = HDNode.fromSeed(seed);
|
const root = HDNode.fromSeed(seed);
|
||||||
const splt = derivation.path.split('/');
|
const splt = derivation.path.split('/');
|
||||||
const internal = +splt[splt.length - 2];
|
const internal = +splt[splt.length - 2];
|
||||||
|
|
|
@ -4,34 +4,38 @@ export class PlaceholderWallet extends AbstractWallet {
|
||||||
static type = 'placeholder';
|
static type = 'placeholder';
|
||||||
static typeReadable = 'Placeholder';
|
static typeReadable = 'Placeholder';
|
||||||
|
|
||||||
|
_isFailure: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._isFailure = false;
|
this._isFailure = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSecret(newSecret) {
|
setSecret(newSecret: string): this {
|
||||||
// so TRY AGAIN when something goes wrong during import has more consistent prefilled text
|
// so TRY AGAIN when something goes wrong during import has more consistent prefilled text
|
||||||
this.secret = newSecret;
|
this.secret = newSecret;
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
allowSend() {
|
allowSend(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabel() {
|
getLabel(): string {
|
||||||
// no longer used in wallets carousel
|
// no longer used in wallets carousel
|
||||||
return this.getIsFailure() ? 'Wallet Import' : 'Importing Wallet...';
|
return this.getIsFailure() ? 'Wallet Import' : 'Importing Wallet...';
|
||||||
}
|
}
|
||||||
|
|
||||||
allowReceive() {
|
allowReceive(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getIsFailure() {
|
getIsFailure(): boolean {
|
||||||
return this._isFailure;
|
return this._isFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsFailure(value) {
|
setIsFailure(value: boolean): void {
|
||||||
this._isFailure = value;
|
this._isFailure = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,7 +8,7 @@ import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet';
|
||||||
// collection of SLIP39 functions
|
// collection of SLIP39 functions
|
||||||
const SLIP39Mixin = {
|
const SLIP39Mixin = {
|
||||||
_getSeed() {
|
_getSeed() {
|
||||||
const master = slip39.recoverSecret(this.secret);
|
const master = slip39.recoverSecret(this.secret, this.passphrase);
|
||||||
return Buffer.from(master);
|
return Buffer.from(master);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ const SLIP39Mixin = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getID() {
|
getID() {
|
||||||
const string2hash = this.secret.sort().join(',');
|
const string2hash = this.secret.sort().join(',') + (this.getPassphrase() || '');
|
||||||
return createHash('sha256').update(string2hash).digest().toString('hex');
|
return createHash('sha256').update(string2hash).digest().toString('hex');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -221,6 +221,8 @@ export class WatchOnlyWallet extends LegacyWallet {
|
||||||
throw new Error('Not initialized');
|
throw new Error('Not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (address && address.startsWith('BC1')) address = address.toLowerCase();
|
||||||
|
|
||||||
return this.getAddress() === address;
|
return this.getAddress() === address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,10 +66,15 @@ const AddressInput = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
style={[styles.scan, stylesHook.scan]}
|
style={[styles.scan, stylesHook.scan]}
|
||||||
|
accessibilityLabel={loc.send.details_scan}
|
||||||
|
accessibilityHint={loc.send.details_scan_hint}
|
||||||
>
|
>
|
||||||
<Image source={require('../img/scan-white.png')} />
|
<Image source={require('../img/scan-white.png')} accessible={false} />
|
||||||
<Text style={[styles.scanText, stylesHook.scanText]}>{loc.send.details_scan}</Text>
|
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
|
||||||
|
{loc.send.details_scan}
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
@ -240,7 +240,12 @@ class AmountInput extends Component {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{!disabled && amount !== BitcoinUnit.MAX && (
|
{!disabled && amount !== BitcoinUnit.MAX && (
|
||||||
<TouchableOpacity testID="changeAmountUnitButton" style={styles.changeAmountUnit} onPress={this.changeAmountUnit}>
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
|
testID="changeAmountUnitButton"
|
||||||
|
style={styles.changeAmountUnit}
|
||||||
|
onPress={this.changeAmountUnit}
|
||||||
|
>
|
||||||
<Image source={require('../img/round-compare-arrows-24-px.png')} />
|
<Image source={require('../img/round-compare-arrows-24-px.png')} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,21 +1,33 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { StyleSheet, useWindowDimensions } from 'react-native';
|
import { StyleSheet, Platform, useWindowDimensions, View } from 'react-native';
|
||||||
import Modal from 'react-native-modal';
|
import Modal from 'react-native-modal';
|
||||||
|
import { BlueButton, BlueSpacing40 } from '../BlueComponents';
|
||||||
|
import loc from '../loc';
|
||||||
|
import { useTheme } from '@react-navigation/native';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
root: {
|
root: {
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
},
|
},
|
||||||
|
hasDoneButton: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 24,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const BottomModal = ({ onBackButtonPress, onBackdropPress, onClose, windowHeight, windowWidth, ...props }) => {
|
const BottomModal = ({ onBackButtonPress, onBackdropPress, onClose, windowHeight, windowWidth, doneButton, ...props }) => {
|
||||||
const valueWindowHeight = useWindowDimensions().height;
|
const valueWindowHeight = useWindowDimensions().height;
|
||||||
const valueWindowWidth = useWindowDimensions().width;
|
const valueWindowWidth = useWindowDimensions().width;
|
||||||
const handleBackButtonPress = onBackButtonPress ?? onClose;
|
const handleBackButtonPress = onBackButtonPress ?? onClose;
|
||||||
const handleBackdropPress = onBackdropPress ?? onClose;
|
const handleBackdropPress = onBackdropPress ?? onClose;
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const stylesHook = StyleSheet.create({
|
||||||
|
hasDoneButton: {
|
||||||
|
backgroundColor: colors.elevated,
|
||||||
|
},
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
style={styles.root}
|
style={styles.root}
|
||||||
|
@ -26,8 +38,16 @@ const BottomModal = ({ onBackButtonPress, onBackdropPress, onClose, windowHeight
|
||||||
{...props}
|
{...props}
|
||||||
accessibilityViewIsModal
|
accessibilityViewIsModal
|
||||||
useNativeDriver
|
useNativeDriver
|
||||||
useNativeDriverForBackdrop
|
useNativeDriverForBackdrop={Platform.OS === 'android'}
|
||||||
/>
|
>
|
||||||
|
{props.children}
|
||||||
|
{doneButton && (
|
||||||
|
<View style={[styles.hasDoneButton, stylesHook.hasDoneButton]}>
|
||||||
|
<BlueButton title={loc.send.input_done} onPress={onClose} />
|
||||||
|
<BlueSpacing40 />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +56,7 @@ BottomModal.propTypes = {
|
||||||
onBackButtonPress: PropTypes.func,
|
onBackButtonPress: PropTypes.func,
|
||||||
onBackdropPress: PropTypes.func,
|
onBackdropPress: PropTypes.func,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
|
doneButton: PropTypes.bool,
|
||||||
windowHeight: PropTypes.number,
|
windowHeight: PropTypes.number,
|
||||||
windowWidth: PropTypes.number,
|
windowWidth: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,11 +35,11 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
const CoinsSelected = ({ number, onContainerPress, onClose }) => (
|
const CoinsSelected = ({ number, onContainerPress, onClose }) => (
|
||||||
<TouchableOpacity style={styles.root} onPress={onContainerPress}>
|
<TouchableOpacity accessibilityRole="button" style={styles.root} onPress={onContainerPress}>
|
||||||
<View style={styles.labelContainer}>
|
<View style={styles.labelContainer}>
|
||||||
<Text style={styles.labelText}>{loc.formatString(loc.cc.coins_selected, { number })}</Text>
|
<Text style={styles.labelText}>{loc.formatString(loc.cc.coins_selected, { number })}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={styles.buttonContainer} onPress={onClose}>
|
<TouchableOpacity accessibilityRole="button" style={styles.buttonContainer} onPress={onClose}>
|
||||||
<Avatar rounded containerStyle={[styles.ball]} icon={{ name: 'close', size: 22, type: 'ionicons', color: 'white' }} />
|
<Avatar rounded containerStyle={[styles.ball]} icon={{ name: 'close', size: 22, type: 'ionicons', color: 'white' }} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Text } from 'react-native-elements';
|
import { Text } from 'react-native-elements';
|
||||||
import { Dimensions, LayoutAnimation, StyleSheet, TouchableOpacity, View } from 'react-native';
|
import { Dimensions, LayoutAnimation, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||||
import { encodeUR } from 'bc-ur/dist';
|
import { encodeUR } from '../blue_modules/ur';
|
||||||
import QRCode from 'react-native-qrcode-svg';
|
import QRCode from 'react-native-qrcode-svg';
|
||||||
import { BlueCurrentTheme } from '../components/themes';
|
import { BlueCurrentTheme } from '../components/themes';
|
||||||
import { BlueSpacing20 } from '../BlueComponents';
|
import { BlueSpacing20 } from '../BlueComponents';
|
||||||
|
@ -105,6 +105,7 @@ export class DynamicQRCode extends Component {
|
||||||
return (
|
return (
|
||||||
<View style={animatedQRCodeStyle.container}>
|
<View style={animatedQRCodeStyle.container}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
testID="DynamicCode"
|
testID="DynamicCode"
|
||||||
style={animatedQRCodeStyle.qrcodeContainer}
|
style={animatedQRCodeStyle.qrcodeContainer}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
@ -136,18 +137,21 @@ export class DynamicQRCode extends Component {
|
||||||
<BlueSpacing20 />
|
<BlueSpacing20 />
|
||||||
<View style={animatedQRCodeStyle.controller}>
|
<View style={animatedQRCodeStyle.controller}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-start' }]}
|
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-start' }]}
|
||||||
onPress={this.moveToPreviousFragment}
|
onPress={this.moveToPreviousFragment}
|
||||||
>
|
>
|
||||||
<Text style={animatedQRCodeStyle.text}>{loc.send.dynamic_prev}</Text>
|
<Text style={animatedQRCodeStyle.text}>{loc.send.dynamic_prev}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={[animatedQRCodeStyle.button, { width: '50%' }]}
|
style={[animatedQRCodeStyle.button, { width: '50%' }]}
|
||||||
onPress={this.state.intervalHandler ? this.stopAutoMove : this.startAutoMove}
|
onPress={this.state.intervalHandler ? this.stopAutoMove : this.startAutoMove}
|
||||||
>
|
>
|
||||||
<Text style={animatedQRCodeStyle.text}>{this.state.intervalHandler ? loc.send.dynamic_stop : loc.send.dynamic_start}</Text>
|
<Text style={animatedQRCodeStyle.text}>{this.state.intervalHandler ? loc.send.dynamic_stop : loc.send.dynamic_start}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-end' }]}
|
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-end' }]}
|
||||||
onPress={this.moveToNextFragment}
|
onPress={this.moveToNextFragment}
|
||||||
>
|
>
|
||||||
|
|
|
@ -119,7 +119,7 @@ export const FButton = ({ text, icon, width, first, last, ...props }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={[bStyles.root, bStylesHook.root, style]} {...props}>
|
<TouchableOpacity accessibilityRole="button" style={[bStyles.root, bStylesHook.root, style]} {...props}>
|
||||||
<View style={bStyles.icon}>{icon}</View>
|
<View style={bStyles.icon}>{icon}</View>
|
||||||
<Text numberOfLines={1} style={[bStyles.text, props.disabled ? bStylesHook.textDisabled : bStylesHook.text]}>
|
<Text numberOfLines={1} style={[bStyles.text, props.disabled ? bStylesHook.textDisabled : bStylesHook.text]}>
|
||||||
{text}
|
{text}
|
||||||
|
|
|
@ -139,6 +139,7 @@ const MultipleStepsListItem = props => {
|
||||||
{props.button.buttonType === undefined ||
|
{props.button.buttonType === undefined ||
|
||||||
(props.button.buttonType === MultipleStepsListItemButtohType.full && (
|
(props.button.buttonType === MultipleStepsListItemButtohType.full && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
disabled={props.button.disabled}
|
disabled={props.button.disabled}
|
||||||
style={[styles.provideKeyButton, stylesHook.provideKeyButton, buttonOpacity]}
|
style={[styles.provideKeyButton, stylesHook.provideKeyButton, buttonOpacity]}
|
||||||
onPress={props.button.onPress}
|
onPress={props.button.onPress}
|
||||||
|
@ -152,6 +153,7 @@ const MultipleStepsListItem = props => {
|
||||||
{props.button.leftText}
|
{props.button.leftText}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
disabled={props.button.disabled}
|
disabled={props.button.disabled}
|
||||||
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
|
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
|
||||||
onPress={props.button.onPress}
|
onPress={props.button.onPress}
|
||||||
|
@ -166,7 +168,12 @@ const MultipleStepsListItem = props => {
|
||||||
)}
|
)}
|
||||||
{!showActivityIndicator && props.rightButton && checked && (
|
{!showActivityIndicator && props.rightButton && checked && (
|
||||||
<View style={styles.rightButtonContainer} accessibilityComponentType>
|
<View style={styles.rightButtonContainer} accessibilityComponentType>
|
||||||
<TouchableOpacity disabled={props.rightButton.disabled} style={styles.rightButton} onPress={props.rightButton.onPress}>
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
|
disabled={props.rightButton.disabled}
|
||||||
|
style={styles.rightButton}
|
||||||
|
onPress={props.rightButton.onPress}
|
||||||
|
>
|
||||||
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
|
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -29,6 +29,7 @@ export const SquareButton = forwardRef((props, ref) => {
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
accessibilityRole="button"
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
|
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
|
||||||
|
|
|
@ -35,7 +35,11 @@ const SquareEnumeratedWords = props => {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
component.push(
|
component.push(
|
||||||
<TouchableOpacity style={[styles.entryTextContainer, stylesHook.entryTextContainer]} key={`${secret}${index}`}>
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={[styles.entryTextContainer, stylesHook.entryTextContainer]}
|
||||||
|
key={`${secret}${index}`}
|
||||||
|
>
|
||||||
<Text textBreakStrategy="simple" style={[styles.entryText, stylesHook.entryText]}>
|
<Text textBreakStrategy="simple" style={[styles.entryText, stylesHook.entryText]}>
|
||||||
{secret}
|
{secret}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useRef, useCallback, useState, useImperativeHandle, forwardRef, useContext } from 'react';
|
import React, { useRef, useCallback, useImperativeHandle, forwardRef, useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
@ -12,25 +12,21 @@ import {
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
View,
|
View,
|
||||||
|
Dimensions,
|
||||||
|
FlatList,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useTheme } from '@react-navigation/native';
|
import { useTheme } from '@react-navigation/native';
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import Carousel from 'react-native-snap-carousel';
|
|
||||||
|
|
||||||
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
|
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
|
||||||
import { LightningCustodianWallet, MultisigHDWallet, PlaceholderWallet } from '../class';
|
import { LightningCustodianWallet, MultisigHDWallet, PlaceholderWallet } from '../class';
|
||||||
import WalletGradient from '../class/wallet-gradient';
|
import WalletGradient from '../class/wallet-gradient';
|
||||||
import { BluePrivateBalance } from '../BlueComponents';
|
import { BluePrivateBalance } from '../BlueComponents';
|
||||||
import { BlueStorageContext } from '../blue_modules/storage-context';
|
import { BlueStorageContext } from '../blue_modules/storage-context';
|
||||||
|
import { isHandset, isTablet, isDesktop } from '../blue_modules/environment';
|
||||||
|
|
||||||
const nStyles = StyleSheet.create({
|
const nStyles = StyleSheet.create({
|
||||||
root: {
|
root: {},
|
||||||
marginVertical: 17,
|
|
||||||
paddingRight: 10,
|
|
||||||
},
|
|
||||||
container: {
|
container: {
|
||||||
paddingHorizontal: 24,
|
|
||||||
paddingVertical: 16,
|
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
minHeight: Platform.OS === 'ios' ? 164 : 181,
|
minHeight: Platform.OS === 'ios' ? 164 : 181,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
@ -56,11 +52,72 @@ const nStyles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const PlaceholderWalletCarouselItem = props => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const { isImportingWallet } = useContext(BlueStorageContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback
|
||||||
|
onPressIn={() => {
|
||||||
|
if (isImportingWallet && isImportingWallet.getIsFailure()) {
|
||||||
|
props.onPressedIn();
|
||||||
|
} else {
|
||||||
|
props.onPressedOut();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPressOut={isImportingWallet && isImportingWallet.getIsFailure() ? props.onPressedOut : null}
|
||||||
|
onPress={isImportingWallet && isImportingWallet.getIsFailure() ? props.onPress : null}
|
||||||
|
>
|
||||||
|
<LinearGradient shadowColor={colors.shadowColor} colors={WalletGradient.gradientsFor(PlaceholderWallet.type)} style={iStyles.grad}>
|
||||||
|
<Image source={I18nManager.isRTL ? require('../img/btc-shape-rtl.png') : require('../img/btc-shape.png')} style={iStyles.image} />
|
||||||
|
<Text style={iStyles.br} />
|
||||||
|
<Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}>
|
||||||
|
{isImportingWallet.getIsFailure() ? loc.wallets.import_placeholder_fail : loc.wallets.import_placeholder_inprogress}
|
||||||
|
</Text>
|
||||||
|
{isImportingWallet.getIsFailure() ? (
|
||||||
|
<Text testID="ImportError" numberOfLines={0} style={[iStyles.importError, { color: colors.inverseForegroundColor }]}>
|
||||||
|
{loc.wallets.list_import_error}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<ActivityIndicator style={iStyles.activity} />
|
||||||
|
)}
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PlaceholderWalletCarouselItem.propTypes = { onPress: PropTypes.func, onPressedOut: PropTypes.func, onPressedIn: PropTypes.func };
|
||||||
|
|
||||||
const NewWalletPanel = ({ onPress }) => {
|
const NewWalletPanel = ({ onPress }) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
const { width } = useWindowDimensions();
|
||||||
|
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||||
|
const isLargeScreen = Platform.OS === 'android' ? isTablet() : width >= Dimensions.get('screen').width / 2 && (isTablet() || isDesktop);
|
||||||
|
const nStylesHooks = StyleSheet.create({
|
||||||
|
container: isLargeScreen
|
||||||
|
? {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
marginVertical: 16,
|
||||||
|
}
|
||||||
|
: { paddingVertical: 16, paddingHorizontal: 24 },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity testID="CreateAWallet" onPress={onPress} style={nStyles.root}>
|
<TouchableOpacity
|
||||||
<View style={[nStyles.container, { backgroundColor: WalletGradient.createWallet() }]}>
|
accessibilityRole="button"
|
||||||
|
testID="CreateAWallet"
|
||||||
|
onPress={onPress}
|
||||||
|
style={isLargeScreen ? {} : { width: itemWidth * 1.2 }}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
nStyles.container,
|
||||||
|
nStylesHooks.container,
|
||||||
|
{ backgroundColor: WalletGradient.createWallet() },
|
||||||
|
isLargeScreen ? {} : { width: itemWidth },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Text style={[nStyles.addAWAllet, { color: colors.foregroundColor }]}>{loc.wallets.list_create_a_wallet}</Text>
|
<Text style={[nStyles.addAWAllet, { color: colors.foregroundColor }]}>{loc.wallets.list_create_a_wallet}</Text>
|
||||||
<Text style={[nStyles.addLine, { color: colors.alternativeTextColor }]}>{loc.wallets.list_create_a_wallet_text}</Text>
|
<Text style={[nStyles.addLine, { color: colors.alternativeTextColor }]}>{loc.wallets.list_create_a_wallet_text}</Text>
|
||||||
<View style={nStyles.button}>
|
<View style={nStyles.button}>
|
||||||
|
@ -76,10 +133,8 @@ NewWalletPanel.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const iStyles = StyleSheet.create({
|
const iStyles = StyleSheet.create({
|
||||||
root: {
|
root: { paddingRight: 20 },
|
||||||
paddingRight: 10,
|
rootLargeDevice: { marginVertical: 20 },
|
||||||
marginVertical: 17,
|
|
||||||
},
|
|
||||||
grad: {
|
grad: {
|
||||||
padding: 15,
|
padding: 15,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
@ -131,8 +186,10 @@ const iStyles = StyleSheet.create({
|
||||||
const WalletCarouselItem = ({ item, index, onPress, handleLongPress, isSelectedWallet }) => {
|
const WalletCarouselItem = ({ item, index, onPress, handleLongPress, isSelectedWallet }) => {
|
||||||
const scaleValue = new Animated.Value(1.0);
|
const scaleValue = new Animated.Value(1.0);
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { walletTransactionUpdateStatus } = useContext(BlueStorageContext);
|
const { walletTransactionUpdateStatus, isImportingWallet } = useContext(BlueStorageContext);
|
||||||
|
const { width } = useWindowDimensions();
|
||||||
|
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||||
|
const isLargeScreen = Platform.OS === 'android' ? isTablet() : width >= Dimensions.get('screen').width / 2 && (isTablet() || isDesktop);
|
||||||
const onPressedIn = () => {
|
const onPressedIn = () => {
|
||||||
const props = { duration: 50 };
|
const props = { duration: 50 };
|
||||||
props.useNativeDriver = true;
|
props.useNativeDriver = true;
|
||||||
|
@ -148,7 +205,16 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress, isSelectedW
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!item)
|
if (!item)
|
||||||
return (
|
return isImportingWallet ? (
|
||||||
|
<Animated.View
|
||||||
|
style={[isLargeScreen ? iStyles.rootLargeDevice : { ...iStyles.root, width: itemWidth }, { transform: [{ scale: scaleValue }] }]}
|
||||||
|
shadowOpacity={25 / 100}
|
||||||
|
shadowOffset={{ width: 0, height: 3 }}
|
||||||
|
shadowRadius={8}
|
||||||
|
>
|
||||||
|
<PlaceholderWalletCarouselItem onPress={onPress} index={index} onPressedIn={onPressedIn} onPressedOut={onPressedOut} />
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
<NewWalletPanel
|
<NewWalletPanel
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onPressedOut();
|
onPressedOut();
|
||||||
|
@ -157,47 +223,6 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress, isSelectedW
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (item.type === PlaceholderWallet.type) {
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={[iStyles.root, { transform: [{ scale: scaleValue }] }]}
|
|
||||||
shadowOpacity={40 / 100}
|
|
||||||
shadowOffset={{ width: 0, height: 0 }}
|
|
||||||
shadowRadius={5}
|
|
||||||
>
|
|
||||||
<TouchableWithoutFeedback
|
|
||||||
onPressIn={item.getIsFailure() ? onPressedIn : null}
|
|
||||||
onPressOut={item.getIsFailure() ? onPressedOut : null}
|
|
||||||
onPress={() => {
|
|
||||||
if (item.getIsFailure()) {
|
|
||||||
onPressedOut();
|
|
||||||
onPress(index);
|
|
||||||
onPressedOut();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LinearGradient shadowColor={colors.shadowColor} colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
|
|
||||||
<Image
|
|
||||||
source={I18nManager.isRTL ? require('../img/btc-shape-rtl.png') : require('../img/btc-shape.png')}
|
|
||||||
style={iStyles.image}
|
|
||||||
/>
|
|
||||||
<Text style={iStyles.br} />
|
|
||||||
<Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}>
|
|
||||||
{item.getIsFailure() ? loc.wallets.import_placeholder_fail : loc.wallets.import_placeholder_inprogress}
|
|
||||||
</Text>
|
|
||||||
{item.getIsFailure() ? (
|
|
||||||
<Text testID="ImportError" numberOfLines={0} style={[iStyles.importError, { color: colors.inverseForegroundColor }]}>
|
|
||||||
{loc.wallets.list_import_error}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<ActivityIndicator style={iStyles.activity} />
|
|
||||||
)}
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const opacity = isSelectedWallet === false ? 0.5 : 1.0;
|
const opacity = isSelectedWallet === false ? 0.5 : 1.0;
|
||||||
let image;
|
let image;
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
|
@ -224,7 +249,10 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress, isSelectedW
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[iStyles.root, { opacity, transform: [{ scale: scaleValue }] }]}
|
style={[
|
||||||
|
isLargeScreen ? iStyles.rootLargeDevice : { ...iStyles.root, width: itemWidth },
|
||||||
|
{ opacity, transform: [{ scale: scaleValue }] },
|
||||||
|
]}
|
||||||
shadowOpacity={25 / 100}
|
shadowOpacity={25 / 100}
|
||||||
shadowOffset={{ width: 0, height: 3 }}
|
shadowOffset={{ width: 0, height: 3 }}
|
||||||
shadowRadius={8}
|
shadowRadius={8}
|
||||||
|
@ -286,19 +314,20 @@ const cStyles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
left: 16,
|
paddingTop: 16,
|
||||||
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
|
|
||||||
},
|
},
|
||||||
|
contentLargeScreen: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
separatorStyle: { width: 16, height: 20 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const WalletsCarousel = forwardRef((props, ref) => {
|
const WalletsCarousel = forwardRef((props, ref) => {
|
||||||
const carouselRef = useRef();
|
const { preferredFiatCurrency, language, isImportingWallet } = useContext(BlueStorageContext);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const { preferredFiatCurrency, language } = useContext(BlueStorageContext);
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item, index }) => (
|
({ item, index }) => (
|
||||||
<WalletCarouselItem
|
<WalletCarouselItem
|
||||||
isSelectedWallet={props.vertical && props.selectedWallet && item ? props.selectedWallet === item.getID() : undefined}
|
isSelectedWallet={!props.horizontal && props.selectedWallet && item ? props.selectedWallet === item.getID() : undefined}
|
||||||
item={item}
|
item={item}
|
||||||
index={index}
|
index={index}
|
||||||
handleLongPress={props.handleLongPress}
|
handleLongPress={props.handleLongPress}
|
||||||
|
@ -306,59 +335,67 @@ const WalletsCarousel = forwardRef((props, ref) => {
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[props.vertical, props.selectedWallet, props.handleLongPress, props.onPress, preferredFiatCurrency, language],
|
[props.horizontal, props.selectedWallet, props.handleLongPress, props.onPress, preferredFiatCurrency, language, isImportingWallet],
|
||||||
);
|
);
|
||||||
|
const flatListRef = useRef();
|
||||||
|
const ListHeaderComponent = () => <View style={cStyles.separatorStyle} />;
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
snapToItem: item => carouselRef?.current?.snapToItem(item),
|
scrollToItem: ({ item }) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
flatListRef?.current?.scrollToItem({ item, viewOffset: 16 });
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
scrollToIndex: ({ index }) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
flatListRef?.current?.scrollToIndex({ index, viewOffset: 16 });
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const onScrollToIndexFailed = error => {
|
||||||
|
console.log('onScrollToIndexFailed');
|
||||||
|
console.log(error);
|
||||||
|
flatListRef.current.scrollToOffset({ offset: error.averageItemLength * error.index, animated: true });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (props.data.length !== 0 && flatListRef.current !== null) {
|
||||||
|
flatListRef.current.scrollToIndex({ index: error.index, animated: true });
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
const sliderWidth = width * 1;
|
|
||||||
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
|
||||||
const sliderHeight = 190;
|
const sliderHeight = 190;
|
||||||
|
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||||
const onLayout = () => setLoading(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FlatList
|
||||||
{loading && (
|
ref={flatListRef}
|
||||||
<View
|
renderItem={renderItem}
|
||||||
style={[
|
extraData={props.data}
|
||||||
cStyles.loading,
|
keyExtractor={(_, index) => index.toString()}
|
||||||
{
|
showsVerticalScrollIndicator={false}
|
||||||
paddingVertical: sliderHeight / 2,
|
pagingEnabled
|
||||||
paddingHorizontal: sliderWidth / 2,
|
disableIntervalMomentum={isHandset}
|
||||||
},
|
snapToInterval={itemWidth} // Adjust to your content width
|
||||||
]}
|
decelerationRate="fast"
|
||||||
>
|
contentContainerStyle={props.horizontal ? cStyles.content : cStyles.contentLargeScreen}
|
||||||
<ActivityIndicator />
|
directionalLockEnabled
|
||||||
</View>
|
showsHorizontalScrollIndicator={false}
|
||||||
)}
|
initialNumToRender={10}
|
||||||
<Carousel
|
ListHeaderComponent={ListHeaderComponent}
|
||||||
ref={carouselRef}
|
style={props.horizontal ? { height: sliderHeight + 9 } : {}}
|
||||||
renderItem={renderItem}
|
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||||
sliderWidth={sliderWidth}
|
{...props}
|
||||||
sliderHeight={sliderHeight}
|
/>
|
||||||
itemWidth={itemWidth}
|
|
||||||
inactiveSlideScale={1}
|
|
||||||
inactiveSlideOpacity={I18nManager.isRTL ? 1.0 : 0.7}
|
|
||||||
activeSlideAlignment="start"
|
|
||||||
initialNumToRender={10}
|
|
||||||
inverted={I18nManager.isRTL && Platform.OS === 'android'}
|
|
||||||
onLayout={onLayout}
|
|
||||||
contentContainerCustomStyle={cStyles.content}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
WalletsCarousel.propTypes = {
|
WalletsCarousel.propTypes = {
|
||||||
vertical: PropTypes.bool,
|
horizontal: PropTypes.bool,
|
||||||
selectedWallet: PropTypes.string,
|
selectedWallet: PropTypes.string,
|
||||||
onPress: PropTypes.func.isRequired,
|
onPress: PropTypes.func.isRequired,
|
||||||
handleLongPress: PropTypes.func.isRequired,
|
handleLongPress: PropTypes.func.isRequired,
|
||||||
|
data: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WalletsCarousel;
|
export default WalletsCarousel;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { StyleSheet, Text, View } from 'react-native';
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
import { useTheme } from '@react-navigation/native';
|
import { useNavigation, useTheme } from '@react-navigation/native';
|
||||||
import { ListItem } from 'react-native-elements';
|
import { ListItem } from 'react-native-elements';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { AddressTypeBadge } from './AddressTypeBadge';
|
import { AddressTypeBadge } from './AddressTypeBadge';
|
||||||
|
@ -9,11 +9,13 @@ import TooltipMenu from '../TooltipMenu';
|
||||||
import Clipboard from '@react-native-clipboard/clipboard';
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
import Share from 'react-native-share';
|
import Share from 'react-native-share';
|
||||||
|
|
||||||
const AddressItem = ({ item, balanceUnit, onPress }) => {
|
const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const tooltip = useRef();
|
const tooltip = useRef();
|
||||||
const listItem = useRef();
|
const listItem = useRef();
|
||||||
|
|
||||||
|
const hasTransactions = item.transactions > 0;
|
||||||
|
|
||||||
const stylesHook = StyleSheet.create({
|
const stylesHook = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
borderBottomColor: colors.lightBorder,
|
borderBottomColor: colors.lightBorder,
|
||||||
|
@ -28,8 +30,33 @@ const AddressItem = ({ item, balanceUnit, onPress }) => {
|
||||||
balance: {
|
balance: {
|
||||||
color: colors.alternativeTextColor,
|
color: colors.alternativeTextColor,
|
||||||
},
|
},
|
||||||
|
address: {
|
||||||
|
color: hasTransactions ? colors.darkGray : colors.buttonTextColor,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { navigate } = useNavigation();
|
||||||
|
|
||||||
|
const navigateToReceive = () => {
|
||||||
|
navigate('ReceiveDetailsRoot', {
|
||||||
|
screen: 'ReceiveDetails',
|
||||||
|
params: {
|
||||||
|
walletID,
|
||||||
|
address: item.address,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToSignVerify = () => {
|
||||||
|
navigate('SignVerifyRoot', {
|
||||||
|
screen: 'SignVerify',
|
||||||
|
params: {
|
||||||
|
walletID,
|
||||||
|
address: item.address,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const showToolTipMenu = () => {
|
const showToolTipMenu = () => {
|
||||||
tooltip.current.showMenu();
|
tooltip.current.showMenu();
|
||||||
};
|
};
|
||||||
|
@ -44,30 +71,40 @@ const AddressItem = ({ item, balanceUnit, onPress }) => {
|
||||||
Share.open({ message: item.address }).catch(error => console.log(error));
|
Share.open({ message: item.address }).catch(error => console.log(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAvailableActions = () => {
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
id: 'copyToClipboard',
|
||||||
|
text: loc.transactions.details_copy,
|
||||||
|
onPress: handleCopyPress,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'share',
|
||||||
|
text: loc.receive.details_share,
|
||||||
|
onPress: handleSharePress,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allowSignVerifyMessage) {
|
||||||
|
actions.push({
|
||||||
|
id: 'signVerify',
|
||||||
|
text: loc.addresses.sign_title,
|
||||||
|
onPress: navigateToSignVerify,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<TooltipMenu
|
<TooltipMenu ref={tooltip} anchorRef={listItem} actions={getAvailableActions()} />
|
||||||
ref={tooltip}
|
|
||||||
anchorRef={listItem}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
id: 'copyToClipboard',
|
|
||||||
text: loc.transactions.details_copy,
|
|
||||||
onPress: handleCopyPress,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'share',
|
|
||||||
text: loc.receive.details_share,
|
|
||||||
onPress: handleSharePress,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<ListItem
|
<ListItem
|
||||||
ref={listItem}
|
ref={listItem}
|
||||||
key={`${item.key}`}
|
key={`${item.key}`}
|
||||||
button
|
button
|
||||||
onPress={onPress}
|
onPress={navigateToReceive}
|
||||||
containerStyle={stylesHook.container}
|
containerStyle={stylesHook.container}
|
||||||
onLongPress={showToolTipMenu}
|
onLongPress={showToolTipMenu}
|
||||||
>
|
>
|
||||||
|
@ -76,9 +113,16 @@ const AddressItem = ({ item, balanceUnit, onPress }) => {
|
||||||
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '}
|
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '}
|
||||||
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text>
|
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text>
|
||||||
</ListItem.Title>
|
</ListItem.Title>
|
||||||
<ListItem.Subtitle style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</ListItem.Subtitle>
|
<View style={styles.subtitle}>
|
||||||
|
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</Text>
|
||||||
|
</View>
|
||||||
</ListItem.Content>
|
</ListItem.Content>
|
||||||
<AddressTypeBadge isInternal={item.isInternal} />
|
<View style={styles.labels}>
|
||||||
|
<AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} />
|
||||||
|
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>
|
||||||
|
{loc.addresses.transactions}: {item.transactions}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -89,7 +133,7 @@ const AddressItem = ({ item, balanceUnit, onPress }) => {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
address: {
|
address: {
|
||||||
fontWeight: '600',
|
fontWeight: 'bold',
|
||||||
marginHorizontal: 40,
|
marginHorizontal: 40,
|
||||||
},
|
},
|
||||||
index: {
|
index: {
|
||||||
|
@ -99,6 +143,12 @@ const styles = StyleSheet.create({
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
marginLeft: 14,
|
marginLeft: 14,
|
||||||
},
|
},
|
||||||
|
subtitle: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
AddressItem.propTypes = {
|
AddressItem.propTypes = {
|
||||||
|
|
|
@ -2,34 +2,53 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTheme } from '@react-navigation/native';
|
import { useTheme } from '@react-navigation/native';
|
||||||
import { StyleSheet, View, Text } from 'react-native';
|
import { StyleSheet, View, Text } from 'react-native';
|
||||||
import loc from '../../loc';
|
import loc, { formatStringAddTwoWhiteSpaces } from '../../loc';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
|
alignSelf: 'flex-end',
|
||||||
},
|
},
|
||||||
badgeText: {
|
badgeText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const AddressTypeBadge = ({ isInternal }) => {
|
const AddressTypeBadge = ({ isInternal, hasTransactions }) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
const stylesHook = StyleSheet.create({
|
const stylesHook = StyleSheet.create({
|
||||||
changeBadge: { backgroundColor: colors.changeBackground },
|
changeBadge: { backgroundColor: colors.changeBackground },
|
||||||
receiveBadge: { backgroundColor: colors.receiveBackground },
|
receiveBadge: { backgroundColor: colors.receiveBackground },
|
||||||
|
usedBadge: { backgroundColor: colors.buttonDisabledBackgroundColor },
|
||||||
changeText: { color: colors.changeText },
|
changeText: { color: colors.changeText },
|
||||||
receiveText: { color: colors.receiveText },
|
receiveText: { color: colors.receiveText },
|
||||||
|
usedText: { color: colors.alternativeTextColor },
|
||||||
});
|
});
|
||||||
|
|
||||||
const badgeLabel = isInternal ? loc.addresses.type_change : loc.addresses.type_receive;
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
const badgeLabel = hasTransactions
|
||||||
|
? loc.addresses.type_used
|
||||||
|
: isInternal
|
||||||
|
? formatStringAddTwoWhiteSpaces(loc.addresses.type_change)
|
||||||
|
: formatStringAddTwoWhiteSpaces(loc.addresses.type_receive);
|
||||||
|
|
||||||
const badgeStyle = isInternal ? stylesHook.changeBadge : stylesHook.receiveBadge;
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
const badgeStyle = hasTransactions
|
||||||
|
? stylesHook.usedBadge
|
||||||
|
: isInternal
|
||||||
|
? stylesHook.changeBadge
|
||||||
|
: stylesHook.receiveBadge;
|
||||||
|
|
||||||
const textStyle = isInternal ? stylesHook.changeText : stylesHook.receiveText;
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
const textStyle = hasTransactions
|
||||||
|
? stylesHook.usedText
|
||||||
|
: isInternal
|
||||||
|
? stylesHook.changeText
|
||||||
|
: stylesHook.receiveText;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, badgeStyle]}>
|
<View style={[styles.container, badgeStyle]}>
|
||||||
|
@ -40,6 +59,7 @@ const AddressTypeBadge = ({ isInternal }) => {
|
||||||
|
|
||||||
AddressTypeBadge.propTypes = {
|
AddressTypeBadge.propTypes = {
|
||||||
isInternal: PropTypes.bool,
|
isInternal: PropTypes.bool,
|
||||||
|
hasTransactions: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { AddressTypeBadge };
|
export { AddressTypeBadge };
|
||||||
|
|
96
components/addresses/AddressTypeTabs.js
Normal file
96
components/addresses/AddressTypeTabs.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import { useTheme } from '@react-navigation/native';
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import loc from '../../loc';
|
||||||
|
|
||||||
|
export const TABS = {
|
||||||
|
EXTERNAL: 'receive',
|
||||||
|
INTERNAL: 'change',
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddressTypeTabs = ({ currentTab, setCurrentTab }) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const stylesHook = StyleSheet.create({
|
||||||
|
activeTab: {
|
||||||
|
backgroundColor: colors.modal,
|
||||||
|
},
|
||||||
|
activeText: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: colors.foregroundColor,
|
||||||
|
},
|
||||||
|
inactiveTab: {
|
||||||
|
fontWeight: 'normal',
|
||||||
|
color: colors.foregroundColor,
|
||||||
|
},
|
||||||
|
backTabs: {
|
||||||
|
backgroundColor: colors.buttonDisabledBackgroundColor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = Object.entries(TABS).map(([key, value]) => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
name: loc.addresses[`type_${value}`],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeToTab = tabKey => {
|
||||||
|
if (tabKey in TABS) {
|
||||||
|
setCurrentTab(TABS[tabKey]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
const tabsButtons = tabs.map(tab => {
|
||||||
|
const isActive = tab.value === currentTab;
|
||||||
|
|
||||||
|
const tabStyle = isActive ? stylesHook.activeTab : stylesHook.inactiveTab;
|
||||||
|
const textStyle = isActive ? stylesHook.activeText : stylesHook.inactiveTab;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={tab.key} onPress={() => changeToTab(tab.key)} style={[styles.tab, tabStyle]}>
|
||||||
|
<Text onPress={() => changeToTab(tab.key)} style={textStyle}>
|
||||||
|
{tab.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={[stylesHook.backTabs, styles.backTabs]}>
|
||||||
|
<View style={styles.tabs}>{tabsButtons}</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
backTabs: {
|
||||||
|
padding: 4,
|
||||||
|
marginVertical: 8,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { AddressTypeTabs };
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue