Merge branch 'master' into limpbrains-cc

This commit is contained in:
marcosrdz 2020-11-10 23:55:51 -05:00
commit d065bd586f
18 changed files with 663 additions and 326 deletions

View File

@ -784,6 +784,7 @@ export const BlueListItem = React.memo(props => {
topDivider={props.topDivider !== undefined ? props.topDivider : false}
testID={props.testID}
onPress={props.onPress}
disabled={props.disabled}
>
{props.leftAvatar && <Avatar>{props.leftAvatar}</Avatar>}
{props.leftIcon && <Avatar icon={props.leftIcon} />}

View File

@ -5,6 +5,7 @@ import Share from 'react-native-share';
import loc from '../loc';
import { getSystemName } from 'react-native-device-info';
import DocumentPicker from 'react-native-document-picker';
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
const isDesktop = getSystemName() === 'Mac OS X';
@ -83,7 +84,14 @@ const showFilePickerAndReadFile = async function () {
const res = await DocumentPicker.pick({
type:
Platform.OS === 'ios'
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', 'io.bluewallet.backup', DocumentPicker.types.plainText, 'public.json']
? [
'io.bluewallet.psbt',
'io.bluewallet.psbt.txn',
'io.bluewallet.backup',
DocumentPicker.types.plainText,
'public.json',
DocumentPicker.types.images,
]
: [DocumentPicker.types.allFiles],
});
@ -94,11 +102,24 @@ const showFilePickerAndReadFile = async function () {
if (res.uri.toLowerCase().endsWith('.psbt')) {
// this is either binary file from ElectrumDesktop OR string file with base64 string in there
file = await _readPsbtFileIntoBase64(uri);
return { data: file, uri: decodeURI(res.uri) };
} else {
file = await RNFS.readFile(uri);
if (res.type === DocumentPicker.types.images || res.type.startsWith('image/')) {
return new Promise(resolve => {
const uri = Platform.OS === 'ios' ? res.uri.toString().replace('file://', '') : res.path.toString();
LocalQRCode.decode(decodeURI(uri), (error, result) => {
if (!error) {
resolve({ data: result, uri: decodeURI(res.uri) });
} else {
resolve({ data: false, uri: false });
}
});
});
} else {
file = await RNFS.readFile(uri);
return { data: file, uri: decodeURI(res.uri) };
}
}
return { data: file, uri: decodeURI(res.uri) };
} catch (err) {
return { data: false, uri: false };
}

View File

@ -16,7 +16,6 @@ import {
HDSegwitElectrumSeedP2WPKHWallet,
MultisigHDWallet,
} from './';
import { AbstractHDElectrumWallet } from './wallets/abstract-hd-electrum-wallet';
import { Platform } from 'react-native';
const encryption = require('../blue_modules/encryption');
const Realm = require('realm');
@ -59,7 +58,7 @@ export class AppStorage {
} else {
return AsyncStorage.setItem(key, value);
}
}
};
/**
* Wrapper for storage call. Secure store works only in RN environment. AsyncStorage is
@ -68,13 +67,13 @@ export class AppStorage {
* @param key
* @returns {Promise<any>|*}
*/
getItem = (key) => {
getItem = key => {
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
return RNSecureKeyStore.get(key);
} else {
return AsyncStorage.getItem(key);
}
}
};
setResetOnAppUninstallTo = async value => {
if (Platform.OS === 'ios') {
@ -396,7 +395,7 @@ export class AppStorage {
const id = wallet.getID();
const walletToSave = wallet._hdWalletInstance ?? wallet;
if (walletToSave instanceof AbstractHDElectrumWallet) {
if (walletToSave._txs_by_external_index) {
realm.write(() => {
const j1 = JSON.stringify(walletToSave._txs_by_external_index);
const j2 = JSON.stringify(walletToSave._txs_by_internal_index);

View File

@ -1,5 +1,4 @@
import { LegacyWallet } from './legacy-wallet';
import Frisbee from 'frisbee';
const bip39 = require('bip39');
const BlueElectrum = require('../../blue_modules/BlueElectrum');
@ -180,99 +179,7 @@ export class AbstractHDWallet extends LegacyWallet {
* @returns {Promise<void>}
*/
async fetchTransactions() {
try {
const api = new Frisbee({ baseURI: 'https://blockchain.info' });
this.transactions = [];
let offset = 0;
while (1) {
const response = await api.get('/multiaddr?active=' + this.getXpub() + '&n=100&offset=' + offset);
if (response && response.body) {
if (response.body.txs && response.body.txs.length === 0) {
break;
}
let latestBlock = false;
if (response.body.info && response.body.info.latest_block) {
latestBlock = response.body.info.latest_block.height;
}
this._lastTxFetch = +new Date();
// processing TXs and adding to internal memory
if (response.body.txs) {
for (const tx of response.body.txs) {
let value = 0;
for (const input of tx.inputs) {
// ----- INPUTS
if (input.prev_out.xpub) {
// sent FROM US
value -= input.prev_out.value;
// setting internal caches to help ourselves in future...
const path = input.prev_out.xpub.path.split('/');
if (path[path.length - 2] === '1') {
// change address
this.next_free_change_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_change_address_index);
// setting to point to last maximum known change address + 1
}
if (path[path.length - 2] === '0') {
// main (aka external) address
this.next_free_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_address_index);
// setting to point to last maximum known main address + 1
}
// done with cache
}
}
for (const output of tx.out) {
// ----- OUTPUTS
if (output.xpub) {
// sent TO US (change)
value += output.value;
// setting internal caches to help ourselves in future...
const path = output.xpub.path.split('/');
if (path[path.length - 2] === '1') {
// change address
this.next_free_change_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_change_address_index);
// setting to point to last maximum known change address + 1
}
if (path[path.length - 2] === '0') {
// main (aka external) address
this.next_free_address_index = Math.max(path[path.length - 1] * 1 + 1, this.next_free_address_index);
// setting to point to last maximum known main address + 1
}
// done with cache
}
}
tx.value = value; // new BigNumber(value).div(100000000).toString() * 1;
if (!tx.confirmations && latestBlock) {
tx.confirmations = latestBlock - tx.block_height + 1;
}
this.transactions.push(tx);
}
if (response.body.txs.length < 100) {
// this fetch yilded less than page size, thus requesting next batch makes no sense
break;
}
} else {
break; // error ?
}
} else {
throw new Error('Could not fetch transactions from API: ' + response.err); // breaks here
}
offset += 100;
}
} catch (err) {
console.warn(err);
}
throw new Error('not implemented');
}
/**

View File

@ -23,7 +23,6 @@ export class AbstractWallet {
this.secret = ''; // private key or recovery phrase
this.balance = 0;
this.unconfirmed_balance = 0;
this.transactions = [];
this._address = false; // cache
this.utxo = [];
this._lastTxFetch = 0;
@ -41,7 +40,7 @@ export class AbstractWallet {
}
getTransactions() {
return this.transactions;
throw new Error('not implemented');
}
getUserHasSavedExport() {

View File

@ -26,6 +26,21 @@
6D641F2425525054003792DF /* WalletInformationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D641F2225525053003792DF /* WalletInformationView.swift */; };
6D641F3525526311003792DF /* SendReceiveButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D641F3425526311003792DF /* SendReceiveButtons.swift */; };
6D641F3625526311003792DF /* SendReceiveButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D641F3425526311003792DF /* SendReceiveButtons.swift */; };
6D6CA4B9255872E3009312A5 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D333B3A252FE1A3004D72DF /* WidgetKit.framework */; };
6D6CA4BA255872E3009312A5 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D333B3C252FE1A3004D72DF /* SwiftUI.framework */; };
6D6CA4BD255872E3009312A5 /* PriceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA4BC255872E3009312A5 /* PriceWidget.swift */; };
6D6CA4C3255872E7009312A5 /* PriceWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6D6CA4B8255872E3009312A5 /* PriceWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
6D6CA4D725587397009312A5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6D9A2E08254BA348007B5B82 /* Assets.xcassets */; };
6D6CA4E0255873BC009312A5 /* UserDefaultsGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA7047D254E24D5005FE5E2 /* UserDefaultsGroup.swift */; };
6D6CA5152558EBA4009312A5 /* WidgetAPI+Electrum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5142558EBA3009312A5 /* WidgetAPI+Electrum.swift */; };
6D6CA5162558EBA4009312A5 /* WidgetAPI+Electrum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5142558EBA3009312A5 /* WidgetAPI+Electrum.swift */; };
6D6CA5282558EC52009312A5 /* PriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5272558EC52009312A5 /* PriceView.swift */; };
6D6CA5292558EC52009312A5 /* PriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5272558EC52009312A5 /* PriceView.swift */; };
6D6CA5322558ED4D009312A5 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4C3A254FBF4800E9F9AA /* Colors.swift */; };
6D6CA5332558ED54009312A5 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4BFA254FBA0E00E9F9AA /* Models.swift */; };
6D6CA53C2558F316009312A5 /* WidgetAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9A2E6A254BAB1B007B5B82 /* WidgetAPI.swift */; };
6D6CA5452558F365009312A5 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9A2E6B254BAB1B007B5B82 /* WidgetDataStore.swift */; };
6D6CA54E2558F497009312A5 /* WidgetAPI+Electrum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5142558EBA3009312A5 /* WidgetAPI+Electrum.swift */; };
6D99465F2555A660000E52E8 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D333B3A252FE1A3004D72DF /* WidgetKit.framework */; };
6D9946602555A660000E52E8 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D333B3C252FE1A3004D72DF /* SwiftUI.framework */; };
6D9946632555A660000E52E8 /* MarketWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9946622555A660000E52E8 /* MarketWidget.swift */; };
@ -95,6 +110,13 @@
remoteGlobalIDString = 3271B0A8236E2E0700DA766F;
remoteInfo = TodayExtension;
};
6D6CA4C1255872E7009312A5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 6D6CA4B7255872E3009312A5;
remoteInfo = PriceWidgetExtension;
};
6D9946442555A583000E52E8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
@ -147,6 +169,7 @@
dstSubfolderSpec = 13;
files = (
6D9946692555A661000E52E8 /* MarketWidgetExtension.appex in Embed App Extensions */,
6D6CA4C3255872E7009312A5 /* PriceWidgetExtension.appex in Embed App Extensions */,
6D9A2E0D254BA348007B5B82 /* WalletInformationAndMarketWidgetExtension.appex in Embed App Extensions */,
6DEB4AB8254FB59E00E9F9AA /* WalletInformationWidgetExtension.appex in Embed App Extensions */,
3271B0B5236E2E0700DA766F /* BlueWallet - Bitcoin Price.appex in Embed App Extensions */,
@ -272,6 +295,12 @@
6D641F17255226DA003792DF /* MarketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketView.swift; sourceTree = "<group>"; };
6D641F2225525053003792DF /* WalletInformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletInformationView.swift; sourceTree = "<group>"; };
6D641F3425526311003792DF /* SendReceiveButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendReceiveButtons.swift; sourceTree = "<group>"; };
6D6CA4B8255872E3009312A5 /* PriceWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PriceWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
6D6CA4BC255872E3009312A5 /* PriceWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceWidget.swift; sourceTree = "<group>"; };
6D6CA4C0255872E7009312A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6D6CA5142558EBA3009312A5 /* WidgetAPI+Electrum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WidgetAPI+Electrum.swift"; sourceTree = "<group>"; };
6D6CA5272558EC52009312A5 /* PriceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceView.swift; sourceTree = "<group>"; };
6D6CA6192558F6AB009312A5 /* PriceWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PriceWidgetExtension.entitlements; sourceTree = "<group>"; };
6D99465E2555A660000E52E8 /* MarketWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MarketWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
6D9946622555A660000E52E8 /* MarketWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWidget.swift; sourceTree = "<group>"; };
6D9946662555A661000E52E8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -383,6 +412,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
6D6CA4B5255872E3009312A5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6D6CA4BA255872E3009312A5 /* SwiftUI.framework in Frameworks */,
6D6CA4B9255872E3009312A5 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6D99465B2555A660000E52E8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -518,6 +556,15 @@
name = Resources;
sourceTree = "<group>";
};
6D6CA4BB255872E3009312A5 /* PriceWidget */ = {
isa = PBXGroup;
children = (
6D6CA4BC255872E3009312A5 /* PriceWidget.swift */,
6D6CA4C0255872E7009312A5 /* Info.plist */,
);
path = PriceWidget;
sourceTree = "<group>";
};
6D9946612555A660000E52E8 /* MarketWidget */ = {
isa = PBXGroup;
children = (
@ -551,6 +598,7 @@
children = (
6DEB4BC1254FB98300E9F9AA /* Shared */,
6D9946612555A660000E52E8 /* MarketWidget */,
6D6CA4BB255872E3009312A5 /* PriceWidget */,
6D9A2E05254BA347007B5B82 /* WalletInformationAndMarketWidget */,
6DEB4AB0254FB59C00E9F9AA /* WalletInformationWidget */,
);
@ -563,6 +611,7 @@
children = (
6DEB4DD82552260200E9F9AA /* Views */,
6D9A2E6A254BAB1B007B5B82 /* WidgetAPI.swift */,
6D6CA5142558EBA3009312A5 /* WidgetAPI+Electrum.swift */,
6D9A2E6B254BAB1B007B5B82 /* WidgetDataStore.swift */,
6DA7047D254E24D5005FE5E2 /* UserDefaultsGroup.swift */,
6D9A2E08254BA348007B5B82 /* Assets.xcassets */,
@ -578,6 +627,7 @@
6D641F17255226DA003792DF /* MarketView.swift */,
6D641F2225525053003792DF /* WalletInformationView.swift */,
6D641F3425526311003792DF /* SendReceiveButtons.swift */,
6D6CA5272558EC52009312A5 /* PriceView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -585,6 +635,7 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
6D6CA6192558F6AB009312A5 /* PriceWidgetExtension.entitlements */,
6D9947152555AB9E000E52E8 /* MarketWidgetExtension.entitlements */,
6DEB4ACE254FB5D800E9F9AA /* WalletInformationWidgetExtension.entitlements */,
13B07FAE1A68108700A75B9A /* BlueWallet */,
@ -614,6 +665,7 @@
6D9A2E02254BA347007B5B82 /* WalletInformationAndMarketWidgetExtension.appex */,
6DEB4AAD254FB59B00E9F9AA /* WalletInformationWidgetExtension.appex */,
6D99465E2555A660000E52E8 /* MarketWidgetExtension.appex */,
6D6CA4B8255872E3009312A5 /* PriceWidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -736,6 +788,7 @@
6DEB4AB7254FB59E00E9F9AA /* PBXTargetDependency */,
6D9946452555A583000E52E8 /* PBXTargetDependency */,
6D9946682555A661000E52E8 /* PBXTargetDependency */,
6D6CA4C2255872E7009312A5 /* PBXTargetDependency */,
);
name = BlueWallet;
productName = "Hello World";
@ -759,6 +812,23 @@
productReference = 3271B0A9236E2E0700DA766F /* BlueWallet - Bitcoin Price.appex */;
productType = "com.apple.product-type.app-extension";
};
6D6CA4B7255872E3009312A5 /* PriceWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 6D6CA4C6255872E7009312A5 /* Build configuration list for PBXNativeTarget "PriceWidgetExtension" */;
buildPhases = (
6D6CA4B4255872E3009312A5 /* Sources */,
6D6CA4B5255872E3009312A5 /* Frameworks */,
6D6CA4B6255872E3009312A5 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = PriceWidgetExtension;
productName = PriceWidgetExtension;
productReference = 6D6CA4B8255872E3009312A5 /* PriceWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
6D99465D2555A660000E52E8 /* MarketWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 6D99466A2555A661000E52E8 /* Build configuration list for PBXNativeTarget "MarketWidgetExtension" */;
@ -875,6 +945,9 @@
CreatedOnToolsVersion = 11.2;
LastSwiftMigration = 1130;
};
6D6CA4B7255872E3009312A5 = {
CreatedOnToolsVersion = 12.1;
};
6D99465D2555A660000E52E8 = {
CreatedOnToolsVersion = 12.1;
};
@ -948,6 +1021,7 @@
6D99465D2555A660000E52E8 /* MarketWidgetExtension */,
6DEB4AAC254FB59B00E9F9AA /* WalletInformationWidgetExtension */,
6D9A2E01254BA347007B5B82 /* WalletInformationAndMarketWidgetExtension */,
6D6CA4B7255872E3009312A5 /* PriceWidgetExtension */,
);
};
/* End PBXProject section */
@ -970,6 +1044,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
6D6CA4B6255872E3009312A5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6D6CA4D725587397009312A5 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6D99465C2555A660000E52E8 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -1170,6 +1252,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
6D6CA4B4255872E3009312A5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6D6CA5452558F365009312A5 /* WidgetDataStore.swift in Sources */,
6D6CA4E0255873BC009312A5 /* UserDefaultsGroup.swift in Sources */,
6D6CA5322558ED4D009312A5 /* Colors.swift in Sources */,
6D6CA4BD255872E3009312A5 /* PriceWidget.swift in Sources */,
6D6CA5292558EC52009312A5 /* PriceView.swift in Sources */,
6D6CA53C2558F316009312A5 /* WidgetAPI.swift in Sources */,
6D6CA5332558ED54009312A5 /* Models.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6D99465A2555A660000E52E8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -1183,6 +1279,7 @@
6D99467B2555A68A000E52E8 /* MarketView.swift in Sources */,
6D9946872555A695000E52E8 /* UserDefaultsGroup.swift in Sources */,
6D9946632555A660000E52E8 /* MarketWidget.swift in Sources */,
6D6CA5152558EBA4009312A5 /* WidgetAPI+Electrum.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1193,12 +1290,14 @@
6D641F2325525054003792DF /* WalletInformationView.swift in Sources */,
6D9A2E6D254BAB1B007B5B82 /* WidgetAPI.swift in Sources */,
6DEB4C3B254FBF4800E9F9AA /* Colors.swift in Sources */,
6D6CA5282558EC52009312A5 /* PriceView.swift in Sources */,
6D9A2E6F254BAB1B007B5B82 /* WidgetDataStore.swift in Sources */,
6DA7047E254E24D5005FE5E2 /* UserDefaultsGroup.swift in Sources */,
6D9A2E07254BA347007B5B82 /* WalletInformationAndMarketWidget.swift in Sources */,
6D641F3525526311003792DF /* SendReceiveButtons.swift in Sources */,
6DEB4BFB254FBA0E00E9F9AA /* Models.swift in Sources */,
6D641F18255226DA003792DF /* MarketView.swift in Sources */,
6D6CA5162558EBA4009312A5 /* WidgetAPI+Electrum.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1215,6 +1314,7 @@
6D641F3625526311003792DF /* SendReceiveButtons.swift in Sources */,
6DEB4BFC254FBA0E00E9F9AA /* Models.swift in Sources */,
6D641F19255226DA003792DF /* MarketView.swift in Sources */,
6D6CA54E2558F497009312A5 /* WidgetAPI+Electrum.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1249,6 +1349,11 @@
target = 3271B0A8236E2E0700DA766F /* TodayExtension */;
targetProxy = 3271B0B3236E2E0700DA766F /* PBXContainerItemProxy */;
};
6D6CA4C2255872E7009312A5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 6D6CA4B7255872E3009312A5 /* PriceWidgetExtension */;
targetProxy = 6D6CA4C1255872E7009312A5 /* PBXContainerItemProxy */;
};
6D9946452555A583000E52E8 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 6D99465D2555A660000E52E8 /* MarketWidgetExtension */;
@ -1506,6 +1611,85 @@
};
name = Release;
};
6D6CA4C4255872E7009312A5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = PriceWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 290;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "$(SRCROOT)/WalletInformationWidget/Widgets/PriceWidget/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.6.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.PriceWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = PriceWidget;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
6D6CA4C5255872E7009312A5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = PriceWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 290;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "$(SRCROOT)/WalletInformationWidget/Widgets/PriceWidget/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.6.5;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.PriceWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = PriceWidget;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
6D99466B2555A661000E52E8 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 367FA8CEB35BC9431019D98A /* Pods-MarketWidgetExtension.debug.xcconfig */;
@ -2025,6 +2209,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
6D6CA4C6255872E7009312A5 /* Build configuration list for PBXNativeTarget "PriceWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6D6CA4C4255872E7009312A5 /* Debug */,
6D6CA4C5255872E7009312A5 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
6D99466A2555A661000E52E8 /* Build configuration list for PBXNativeTarget "MarketWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -390,7 +390,7 @@ PODS:
- React
- RNVectorIcons (6.6.0):
- React
- RNWatch (1.0.2):
- RNWatch (1.0.3):
- React
- Sentry (5.2.2):
- Sentry/Core (= 5.2.2)
@ -739,7 +739,7 @@ SPEC CHECKSUMS:
RNShare: 7a7277f3c313652422d9de072ac50714dff5e8a4
RNSVG: ce9d996113475209013317e48b05c21ee988d42e
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
RNWatch: d56d00be49131ee454bb5a4a574f18506c8949e4
RNWatch: e4c5d19506c94506860032fb68aedd5991beb985
Sentry: 8fa58a051237554f22507fb483b9a1de0171a2dc
SwiftSocket: c8d482e867ae4d3eb4c769e9382e123c1f1f833b
ToolTipMenu: 4d89d95ddffd7539230bdbe02ee51bbde362e37e

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.bluewallet.bluewallet</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PriceWidget</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,97 @@
//
// PriceWidget.swift
// PriceWidget
//
// Created by Marcos Rodriguez on 11/8/20.
// Copyright © 2020 BlueWallet. All rights reserved.
//
import WidgetKit
import SwiftUI
var marketData: [MarketDataTimeline: MarketData?] = [ .Current: nil, .Previous: nil]
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), currentMarketData: nil)
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry: SimpleEntry
if (context.isPreview) {
entry = SimpleEntry(date: Date(), currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"))
} else {
entry = SimpleEntry(date: Date(), currentMarketData: emptyMarketData)
}
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
if WidgetAPI.getUserPreferredCurrency() != WidgetAPI.getLastSelectedCurrency() {
marketData[.Current] = nil
marketData[.Previous] = nil
WidgetAPI.saveNewSelectedCurrency()
}
var entryMarketData = marketData[.Current] ?? emptyMarketData
WidgetAPI.fetchPrice(currency: WidgetAPI.getUserPreferredCurrency()) { (data, error) in
if let data = data, let formattedRate = data.formattedRate {
let currentMarketData = MarketData(nextBlock: "", sats: "", price: formattedRate, rate: data.rateDouble, dateString: data.lastUpdate)
if let cachedMarketData = marketData[.Current], currentMarketData.dateString != cachedMarketData?.dateString {
marketData[.Previous] = marketData[.Current]
marketData[.Current] = currentMarketData
entryMarketData = currentMarketData
entries.append(SimpleEntry(date:Date(), currentMarketData: entryMarketData))
} else {
entries.append(SimpleEntry(date:Date(), currentMarketData: currentMarketData))
}
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let currentMarketData: MarketData?
var previousMarketData: MarketData? {
return marketData[.Previous] as? MarketData
}
}
struct PriceWidgetEntryView : View {
var entry: Provider.Entry
var priceView: some View {
PriceView(currentMarketData: entry.currentMarketData, previousMarketData: marketData[.Previous] ?? emptyMarketData).padding()
}
var body: some View {
priceView.background(Color.widgetBackground)
}
}
@main
struct PriceWidget: Widget {
let kind: String = "PriceWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
PriceWidgetEntryView(entry: entry)
}
.configurationDisplayName("Price")
.description("View the current price of Bitcoin.").supportedFamilies([.systemSmall])
}
}
struct PriceWidget_Previews: PreviewProvider {
static var previews: some View {
PriceWidgetEntryView(entry: SimpleEntry(date: Date(), currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00")))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

View File

@ -8,7 +8,7 @@
import Foundation
struct MarketData {
struct MarketData:Codable {
var nextBlock: String
var sats: String
var price: String
@ -16,6 +16,19 @@ struct MarketData {
var formattedNextBlock: String {
return nextBlock == "..." ? "..." : #"\#(nextBlock) sat/b"#
}
var dateString: String = ""
var formattedDate: String? {
let isoDateFormatter = ISO8601DateFormatter()
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.timeStyle = .short
if let date = isoDateFormatter.date(from: dateString) {
return dateFormatter.string(from: date)
}
return nil
}
}
struct WalletData {
@ -30,3 +43,8 @@ struct WalletData {
let emptyMarketData = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
let emptyWalletData = WalletData(balance: 0, latestTransactionTime: Int(Date().timeIntervalSince1970))
enum MarketDataTimeline: String {
case Previous = "previous"
case Current = "current"
}

View File

@ -32,13 +32,16 @@ class UserDefaultsGroup {
static private let suite = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
static func getElectrumSettings() -> UserDefaultsElectrumSettings {
guard let electrumSettingsHost = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsHost.rawValue), let electrumSettingsTCPPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue), let electrumSettingsSSLPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) else {
guard let electrumSettingsHost = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsHost.rawValue) else {
return UserDefaultsElectrumSettings(host: "electrum1.bluewallet.io", port: 50001, sslPort: 443)
}
let electrumSettingsTCPPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? "50001"
let electrumSettingsSSLPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? "443"
let host = electrumSettingsHost
let sslPort = Int32(electrumSettingsSSLPort) ?? 443
let port = Int32(electrumSettingsTCPPort) ?? 50001
let sslPort = Int32(electrumSettingsSSLPort)
let port = Int32(electrumSettingsTCPPort)
return UserDefaultsElectrumSettings(host: host, port: port, sslPort: sslPort)
}

View File

@ -0,0 +1,48 @@
//
// PriceView.swift
// BlueWallet
//
// Created by Marcos Rodriguez on 11/8/20.
// Copyright © 2020 BlueWallet. All rights reserved.
//
import SwiftUI
import WidgetKit
struct PriceView: View {
var currentMarketData: MarketData? = emptyMarketData
var previousMarketData: MarketData? = emptyMarketData
var body: some View {
VStack(alignment: .trailing, spacing: /*@START_MENU_TOKEN@*/nil/*@END_MENU_TOKEN@*/, content: {
Text("Last Updated").font(Font.system(size: 11, weight: .regular, design: .default)).foregroundColor(.textColorLightGray)
HStack(alignment: .lastTextBaseline, spacing: /*@START_MENU_TOKEN@*/nil/*@END_MENU_TOKEN@*/, content: {
Text(currentMarketData?.formattedDate ?? "").lineLimit(1).foregroundColor(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01).transition(.opacity)
})
Spacer()
VStack(alignment: .trailing, spacing: 16, content: {
HStack(alignment: .lastTextBaseline, spacing: /*@START_MENU_TOKEN@*/nil/*@END_MENU_TOKEN@*/, content: {
Text(currentMarketData?.price ?? "").lineLimit(1).foregroundColor(.textColor).font(Font.system(size:28, weight: .bold, design: .default)).minimumScaleFactor(0.01).transition(.opacity)
})
if let previousMarketDataPrice = previousMarketData?.price, let currentMarketDataRate = currentMarketData?.rate, let previousMarketDataRate = previousMarketData?.rate, previousMarketDataRate > 0, currentMarketDataRate != previousMarketDataRate {
HStack(alignment: .lastTextBaseline, spacing: /*@START_MENU_TOKEN@*/nil/*@END_MENU_TOKEN@*/, content: {
Image(systemName: currentMarketDataRate > previousMarketDataRate ? "arrow.up" : "arrow.down")
Text("from").lineLimit(1).foregroundColor(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01)
Text(previousMarketDataPrice).lineLimit(1).foregroundColor(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01)
}).transition(.slide)
}
})
}).frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .trailing)
}
}
struct PriceView_Previews: PreviewProvider {
static var previews: some View {
PriceView().previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

View File

@ -0,0 +1,89 @@
//
// WidgetAPI+Electrum.swift
// BlueWallet
//
// Created by Marcos Rodriguez on 11/8/20.
// Copyright © 2020 BlueWallet. All rights reserved.
//
import SwiftSocket
struct APIError: LocalizedError {
var errorDescription: String = "Failed to fetch Electrum data..."
}
extension WidgetAPI {
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
guard let host = userElectrumSettings.host, let _ = userElectrumSettings.sslPort, let port = userElectrumSettings.port else {
print("No valid UserDefaultsElectrumSettings found");
return
}
DispatchQueue.global(qos: .background).async {
let client = TCPClient(address: host, port: port)
let send = "{\"id\": 1, \"method\": \"blockchain.estimatefee\", \"params\": [1]}\n"
switch client.connect(timeout: 1) {
case .success:
switch client.send(string: send) {
case .success:
guard let data = client.read(1024*10, timeout: 1) else {
client.close()
completion(nil, APIError())
return
}
let characterSet = Set("0123456789.")
if let response = String(bytes: data, encoding: .utf8), let nextBlockResponse = response.components(separatedBy: #"result":"#).last?.components(separatedBy: ",").first, let nextBlockResponseDouble = Double(nextBlockResponse.filter({characterSet.contains($0)}).trimmingCharacters(in: .whitespacesAndNewlines)) {
print("Successfully obtained response from Electrum sever")
print(userElectrumSettings)
let marketData = MarketData(nextBlock: String(format: "%.0f", (nextBlockResponseDouble / 1024) * 100000000), sats: "0", price: "0", rate: 0)
client.close()
completion(MarketData(nextBlock: String(format: "%.0f", (nextBlockResponseDouble / 1024) * 100000000), sats: "0", price: "0", rate: 0), nil)
completion(marketData, nil)
} else {
client.close()
completion(nil, APIError())
}
case .failure(let error):
print(error)
client.close()
completion(nil, APIError())
}
case .failure(let error):
print(error)
client.close()
if userElectrumSettings.host == DefaultElectrumPeers.last?.host {
completion(nil, APIError())
} else if let currentIndex = DefaultElectrumPeers.firstIndex(where: {$0.host == userElectrumSettings.host}) {
fetchNextBlockFee(completion: completion, userElectrumSettings: DefaultElectrumPeers[DefaultElectrumPeers.index(after: currentIndex)])
} else {
if let first = DefaultElectrumPeers.first {
fetchNextBlockFee(completion: completion, userElectrumSettings: first)
}
}
}
}
}
static func fetchMarketData(currency: String, completion: @escaping ((MarketData?, Error?) -> Void)) {
var marketDataEntry = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
WidgetAPI.fetchPrice(currency: currency, completion: { (result, error) in
if let result = result {
marketDataEntry.rate = result.rateDouble
marketDataEntry.price = result.formattedRate ?? "!"
}
WidgetAPI.fetchNextBlockFee { (marketData, error) in
if let nextBlock = marketData?.nextBlock {
marketDataEntry.nextBlock = nextBlock
} else {
marketDataEntry.nextBlock = "!"
}
if let rateDouble = result?.rateDouble {
marketDataEntry.sats = numberFormatter.string(from: NSNumber(value: Double(10 / rateDouble) * 10000000)) ?? "!"
}
completion(marketDataEntry, nil)
}
})
}
}

View File

@ -7,11 +7,7 @@
//
import Foundation
import SwiftSocket
struct APIError: LocalizedError {
var errorDescription: String = "Failed to fetch Electrum data..."
}
var numberFormatter: NumberFormatter {
let formatter = NumberFormatter()
@ -24,71 +20,6 @@ var numberFormatter: NumberFormatter {
class WidgetAPI {
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
guard let host = userElectrumSettings.host, let _ = userElectrumSettings.sslPort, let port = userElectrumSettings.port else {
print("No valid UserDefaultsElectrumSettings found");
return
}
DispatchQueue.global(qos: .background).async {
let client = TCPClient(address: host, port: port)
let send = "{\"id\": 1, \"method\": \"blockchain.estimatefee\", \"params\": [1]}\n"
switch client.connect(timeout: 1) {
case .success:
switch client.send(string: send) {
case .success:
guard let data = client.read(1024*10, timeout: 1) else {
client.close()
completion(nil, APIError())
return
}
if let response = String(bytes: data, encoding: .utf8), let nextBlockResponse = response.components(separatedBy: #"result":"#).last?.components(separatedBy: ",").first, let nextBlockResponseDouble = Double(nextBlockResponse.trimmingCharacters(in: .whitespacesAndNewlines)) {
print("Successfully obtained response from Electrum sever")
print(userElectrumSettings)
client.close()
completion(MarketData(nextBlock: String(format: "%.0f", (nextBlockResponseDouble / 1024) * 100000000), sats: "0", price: "0", rate: 0), nil)
}
case .failure(let error):
print(error)
client.close()
completion(nil, APIError())
}
case .failure(let error):
print(error)
client.close()
if userElectrumSettings.host == DefaultElectrumPeers.last?.host {
completion(nil, APIError())
} else if let currentIndex = DefaultElectrumPeers.firstIndex(where: {$0.host == userElectrumSettings.host}) {
fetchNextBlockFee(completion: completion, userElectrumSettings: DefaultElectrumPeers[DefaultElectrumPeers.index(after: currentIndex)])
} else {
if let first = DefaultElectrumPeers.first {
fetchNextBlockFee(completion: completion, userElectrumSettings: first)
}
}
}
}
}
static func fetchMarketData(currency: String, completion: @escaping ((MarketData?, Error?) -> Void)) {
var marketDataEntry = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
WidgetAPI.fetchPrice(currency: currency, completion: { (result, error) in
if let result = result {
marketDataEntry.rate = result.rateDouble
marketDataEntry.price = result.formattedRate ?? "!"
}
WidgetAPI.fetchNextBlockFee { (marketData, error) in
if let nextBlock = marketData?.nextBlock {
marketDataEntry.nextBlock = nextBlock
} else {
marketDataEntry.nextBlock = "!"
}
if let rateDouble = result?.rateDouble {
marketDataEntry.sats = numberFormatter.string(from: NSNumber(value: Double(10 / rateDouble) * 10000000)) ?? "!"
}
completion(marketDataEntry, nil)
}
})
}
static func fetchPrice(currency: String, completion: @escaping ((WidgetDataStore?, Error?) -> Void)) {
guard let url = URL(string: "https://api.coindesk.com/v1/bpi/currentPrice/\(currency).json") else {return}
@ -100,11 +31,12 @@ class WidgetAPI {
completion(nil, error)
return }
guard let bpi = json?["bpi"] as? Dictionary<String, Any>, let preferredCurrency = bpi[currency] as? Dictionary<String, Any>, let rateString = preferredCurrency["rate"] as? String, let rateDouble = preferredCurrency["rate_float"] as? Double else {
guard let bpi = json?["bpi"] as? Dictionary<String, Any>, let preferredCurrency = bpi[currency] as? Dictionary<String, Any>, let rateString = preferredCurrency["rate"] as? String, let rateDouble = preferredCurrency["rate_float"] as? Double, let time = json?["time"] as? Dictionary<String, Any>, let lastUpdatedString = time["updatedISO"] as? String
else {
print(error?.localizedDescription ?? "Response Error")
completion(nil, error)
return }
let latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: "", rateDouble: rateDouble)
let latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
completion(latestRateDataStore, nil)
}.resume()
}

27
package-lock.json generated
View File

@ -6957,9 +6957,9 @@
}
},
"amplitude-js": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.2.2.tgz",
"integrity": "sha512-Y1/kw/NaxMdqwBnkbjPywpjPbSmuVuszFLQ9tw56P6YraljvbMC93afHQvLC/3zG5SImDnykbg/8HxrWFDhsLg==",
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.3.0.tgz",
"integrity": "sha512-FI3ziFNfV4kqpLYHLo6t+E7cbZZ1n8VRCNt214Z1CuDbEzPcc/TenmkCwPoYJA5FQibamqZL9qiKtdMTZhSsUg==",
"requires": {
"@amplitude/ua-parser-js": "0.7.24",
"blueimp-md5": "^2.10.0",
@ -8523,13 +8523,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true
"dev": true,
"optional": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@ -11427,6 +11429,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
"integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
"dev": true,
"optional": true,
"requires": {
"is-glob": "^2.0.0"
},
@ -11435,13 +11438,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true
"dev": true,
"optional": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@ -17866,7 +17871,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true
"dev": true,
"optional": true
},
"is-glob": {
"version": "2.0.1",
@ -19215,9 +19221,9 @@
}
},
"react-native-watch-connectivity": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-1.0.2.tgz",
"integrity": "sha512-uNx6Zhx++LpRRyQddukBcMyMtWnleP97OME9HEkW91rEuwTviBK12+1sn/YGBgGLrmdEULAoPO91w619w2Efbw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-1.0.3.tgz",
"integrity": "sha512-YITROIsVJw2mw5bm2Xr0waob16EdWJKpIO1B7AJWEpoWwSRu6bS/x1Z0QOhIFDWhRJFNvTQwnnflCq6PjflXNQ==",
"requires": {
"lodash.sortby": "^4.7.0"
}
@ -19390,7 +19396,8 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
"dev": true,
"optional": true
},
"string_decoder": {
"version": "1.1.1",

View File

@ -77,7 +77,7 @@
"@react-navigation/stack": "5.9.3",
"@remobile/react-native-qrcode-local-image": "git+https://github.com/BlueWallet/react-native-qrcode-local-image.git",
"@sentry/react-native": "1.9.0",
"amplitude-js": "7.2.2",
"amplitude-js": "7.3.0",
"assert": "1.5.0",
"base-x": "3.0.8",
"bc-bech32": "file:blue_modules/bc-bech32",
@ -156,7 +156,7 @@
"react-native-tcp-socket": "3.7.1",
"react-native-tooltip": "git+https://github.com/BlueWallet/react-native-tooltip.git#d369e7ece09e4dec73873f1cfeac83e9d35294a6",
"react-native-vector-icons": "6.6.0",
"react-native-watch-connectivity": "1.0.2",
"react-native-watch-connectivity": "1.0.3",
"react-native-webview": "10.9.2",
"react-native-widget-center": "git+https://github.com/BlueWallet/react-native-widget-center.git#e2e9a9038b76d096bf929a87105a97a0a7095001",
"react-test-render": "1.1.2",

View File

@ -14,8 +14,9 @@ import {
StyleSheet,
Dimensions,
Platform,
ScrollView,
Text,
LayoutAnimation,
FlatList,
} from 'react-native';
import { Icon } from 'react-native-elements';
import AsyncStorage from '@react-native-community/async-storage';
@ -65,7 +66,6 @@ const styles = StyleSheet.create({
backgroundColor: BlueCurrentTheme.colors.elevated,
},
scrollViewContent: {
flexWrap: 'wrap',
flexDirection: 'row',
},
modalContent: {
@ -214,6 +214,7 @@ const styles = StyleSheet.create({
export default class SendDetails extends Component {
static contextType = BlueStorageContext;
state = { isLoading: true };
scrollView = React.createRef();
constructor(props, context) {
super(props);
@ -255,8 +256,8 @@ export default class SendDetails extends Component {
},
feeUnit: fromWallet.getPreferredBalanceUnit(),
amountUnit: fromWallet.preferredBalanceUnit, // default for whole screen
renderWalletSelectionOrCoinsSelectedHidden: false,
width: Dimensions.get('window').width - 320,
renderWalletSelectionButtonHidden: false,
width: Dimensions.get('window').width,
utxo: null,
};
}
@ -453,15 +454,8 @@ export default class SendDetails extends Component {
}
}
if (error) {
if (index === 0) {
this.scrollView.scrollTo();
} else if (index === this.state.addresses.length - 1) {
this.scrollView.scrollToEnd();
} else {
const page = Math.round(this.state.width * (this.state.addresses.length - 2));
this.scrollView.scrollTo({ x: page, y: 0, animated: true });
}
this.setState({ isLoading: false, recipientsScrollIndex: index });
this.scrollView.current.scrollToIndex({ index });
this.setState({ isLoading: false });
alert(error);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return;
@ -708,7 +702,8 @@ export default class SendDetails extends Component {
const feeSatoshi = new BigNumber(element.amount).multipliedBy(100000000);
return element.address.length > 0 && feeSatoshi > 0;
}) || this.state.addresses[0];
this.setState({ addresses: [firstTransaction], recipientsScrollIndex: 0 }, () => changeWallet());
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({ addresses: [firstTransaction] }, () => changeWallet());
},
style: 'default',
},
@ -730,7 +725,8 @@ export default class SendDetails extends Component {
return element.amount === BitcoinUnit.MAX;
}) || this.state.addresses[0];
firstTransaction.amount = 0;
this.setState({ addresses: [firstTransaction], recipientsScrollIndex: 0 }, () => changeWallet());
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({ addresses: [firstTransaction] }, () => changeWallet());
},
style: 'default',
},
@ -977,14 +973,15 @@ export default class SendDetails extends Component {
handleAddRecipient = () => {
const { addresses } = this.state;
addresses.push(new BitcoinTransaction());
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut, () => this.scrollView.current.scrollToEnd());
this.setState(
{
addresses,
isAdvancedTransactionOptionsVisible: false,
},
() => {
this.scrollView.scrollToEnd();
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
this.scrollView.current.scrollToEnd();
if (this.state.addresses.length > 1) this.scrollView.current.flashScrollIndicators();
// after adding recipient it automatically scrolls to the last one
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
},
@ -994,13 +991,14 @@ export default class SendDetails extends Component {
handleRemoveRecipient = () => {
const { addresses } = this.state;
addresses.splice(this.state.recipientsScrollIndex, 1);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState(
{
addresses,
isAdvancedTransactionOptionsVisible: false,
},
() => {
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
if (this.state.addresses.length > 1) this.scrollView.current.flashScrollIndicators();
// after deletion it automatically scrolls to the last one
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
},
@ -1109,6 +1107,16 @@ export default class SendDetails extends Component {
this.setState({ isTransactionReplaceable: value });
};
scrollViewCurrentIndex = () => {
Keyboard.dismiss();
const offset = this.scrollView.current.contentOffset;
if (offset) {
const page = Math.round(offset.x / Dimensions.get('window').width);
return page;
}
return 0;
};
renderCreateButton = () => {
return (
<View style={styles.createButton}>
@ -1159,110 +1167,84 @@ export default class SendDetails extends Component {
);
};
handlePageChange = e => {
Keyboard.dismiss();
const offset = e.nativeEvent.contentOffset;
if (offset) {
const page = Math.round(offset.x / this.state.width);
if (this.state.recipientsScrollIndex !== page) {
this.setState({ recipientsScrollIndex: page });
}
}
};
renderBitcoinTransactionInfoFields = ({ item, index }) => {
return (
<View style={{ width: this.state.width }}>
<BlueBitcoinAmount
isLoading={this.state.isLoading}
amount={item.amount ? item.amount.toString() : null}
onAmountUnitChange={unit => {
const units = this.state.units;
units[index] = unit;
scrollViewCurrentIndex = () => {
Keyboard.dismiss();
const offset = this.scrollView.contentOffset;
if (offset) {
const page = Math.round(offset.x / this.state.width);
return page;
}
return 0;
};
const addresses = this.state.addresses;
const item = addresses[index];
renderBitcoinTransactionInfoFields = () => {
const rows = [];
switch (unit) {
case BitcoinUnit.SATS:
item.amountSats = parseInt(item.amount);
break;
case BitcoinUnit.BTC:
item.amountSats = currency.btcToSatoshi(item.amount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
// also accounting for cached fiat->sat conversion to avoid rounding error
item.amountSats =
BlueBitcoinAmount.getCachedSatoshis(item.amount) || currency.btcToSatoshi(currency.fiatToBTC(item.amount));
break;
}
for (const [index, item] of this.state.addresses.entries()) {
rows.push(
<View key={index} style={{ width: this.state.width }}>
<BlueBitcoinAmount
isLoading={this.state.isLoading}
amount={item.amount ? item.amount.toString() : null}
onAmountUnitChange={unit => {
const units = this.state.units;
units[index] = unit;
const addresses = this.state.addresses;
const item = addresses[index];
switch (unit) {
case BitcoinUnit.SATS:
item.amountSats = parseInt(item.amount);
break;
case BitcoinUnit.BTC:
item.amountSats = currency.btcToSatoshi(item.amount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
// also accounting for cached fiat->sat conversion to avoid rounding error
item.amountSats =
BlueBitcoinAmount.getCachedSatoshis(item.amount) || currency.btcToSatoshi(currency.fiatToBTC(item.amount));
break;
}
addresses[index] = item;
this.setState({ units, addresses });
}}
onChangeText={text => {
item.amount = text;
switch (this.state.units[index] || this.state.amountUnit) {
case BitcoinUnit.BTC:
item.amountSats = currency.btcToSatoshi(item.amount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
item.amountSats = currency.btcToSatoshi(currency.fiatToBTC(item.amount));
break;
default:
case BitcoinUnit.SATS:
item.amountSats = parseInt(text);
break;
}
const addresses = this.state.addresses;
addresses[index] = item;
this.setState({ addresses }, this.reCalcTx);
}}
unit={this.state.units[index] || this.state.amountUnit}
inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null}
/>
<BlueAddressInput
onChangeText={async text => {
text = text.trim();
const transactions = this.state.addresses;
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(text);
item.address = address || text;
item.amount = amount || item.amount;
transactions[index] = item;
this.setState({
addresses: transactions,
memo: memo || this.state.memo,
isLoading: false,
payjoinUrl,
});
this.reCalcTx();
}}
onBarScanned={this.processAddressData}
address={item.address}
isLoading={this.state.isLoading}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
launchedBy={this.props.route.name}
/>
{this.state.addresses.length > 1 && (
<BlueText style={styles.of}>{loc.formatString(loc._.of, { number: index + 1, total: this.state.addresses.length })}</BlueText>
)}
</View>,
);
}
return rows;
addresses[index] = item;
this.setState({ units, addresses });
}}
onChangeText={text => {
item.amount = text;
switch (this.state.units[index] || this.state.amountUnit) {
case BitcoinUnit.BTC:
item.amountSats = currency.btcToSatoshi(item.amount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
item.amountSats = currency.btcToSatoshi(currency.fiatToBTC(item.amount));
break;
default:
case BitcoinUnit.SATS:
item.amountSats = parseInt(text);
break;
}
const addresses = this.state.addresses;
addresses[index] = item;
this.setState({ addresses }, this.reCalcTx);
}}
unit={this.state.units[index] || this.state.amountUnit}
inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null}
/>
<BlueAddressInput
onChangeText={async text => {
text = text.trim();
const transactions = this.state.addresses;
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(text);
item.address = address || text;
item.amount = amount || item.amount;
transactions[index] = item;
this.setState({
addresses: transactions,
memo: memo || this.state.memo,
isLoading: false,
payjoinUrl,
});
this.reCalcTx();
}}
onBarScanned={this.processAddressData}
address={item.address}
isLoading={this.state.isLoading}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
launchedBy={this.props.route.name}
/>
{this.state.addresses.length > 1 && (
<BlueText style={styles.of}>{loc.formatString(loc._.of, { number: index + 1, total: this.state.addresses.length })}</BlueText>
)}
</View>
);
};
onUseAllPressed = () => {
@ -1275,13 +1257,13 @@ export default class SendDetails extends Component {
text: loc._.ok,
onPress: async () => {
Keyboard.dismiss();
const recipient = this.state.addresses[this.state.recipientsScrollIndex];
const recipient = this.state.addresses[this.scrollViewCurrentIndex()];
recipient.amount = BitcoinUnit.MAX;
recipient.amountSats = BitcoinUnit.MAX;
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({
addresses: [recipient],
units: [BitcoinUnit.BTC],
recipientsScrollIndex: 0,
isAdvancedTransactionOptionsVisible: false,
});
},
@ -1308,6 +1290,8 @@ export default class SendDetails extends Component {
this.setState({ width: e.nativeEvent.layout.width });
};
keyExtractor = (_item, index) => `${index}`;
render() {
const { fromWallet, utxo } = this.state;
if (this.state.isLoading || typeof fromWallet === 'undefined') {
@ -1328,20 +1312,20 @@ export default class SendDetails extends Component {
<StatusBar barStyle="light-content" />
<View>
<KeyboardAvoidingView behavior="position">
<ScrollView
pagingEnabled
horizontal
contentContainerStyle={styles.scrollViewContent}
ref={ref => (this.scrollView = ref)}
<FlatList
keyboardShouldPersistTaps="always"
onContentSizeChange={() => this.scrollView.scrollToEnd()}
onLayout={() => this.scrollView.scrollToEnd()}
onMomentumScrollEnd={this.handlePageChange}
scrollEnabled={this.state.addresses.length > 1}
extraData={this.state.addresses}
data={this.state.addresses}
renderItem={this.renderBitcoinTransactionInfoFields}
keyExtractor={this.keyExtractor}
ref={this.scrollView}
horizontal
pagingEnabled
onMomentumScrollBegin={Keyboard.dismiss}
scrollIndicatorInsets={{ top: 0, left: 8, bottom: 0, right: 8 }}
>
{this.renderBitcoinTransactionInfoFields()}
</ScrollView>
contentContainerStyle={styles.scrollViewContent}
/>
<View hide={!this.state.showMemoRow} style={styles.memo}>
<TextInput
onChangeText={text => this.setState({ memo: text })}