Skip to content

Commit

Permalink
ADD: AOPP
Browse files Browse the repository at this point in the history
  • Loading branch information
limpbrains authored and Overtorment committed Apr 17, 2021
1 parent 59d3354 commit 897b945
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 15 deletions.
15 changes: 15 additions & 0 deletions Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import Marketplace from './screen/wallets/marketplace';
import ReorderWallets from './screen/wallets/reorderWallets';
import SelectWallet from './screen/wallets/selectWallet';
import ProvideEntropy from './screen/wallets/provideEntropy';
import AOPP from './screen/wallets/aopp';

import TransactionDetails from './screen/transactions/details';
import TransactionStatus from './screen/transactions/transactionStatus';
Expand Down Expand Up @@ -467,6 +468,19 @@ const ExportMultisigCoordinationSetupRoot = () => {
);
};

const AOPPStack = createStackNavigator();
const AOPPRoot = () => {
const theme = useTheme();

return (
<AOPPStack.Navigator screenOptions={defaultStackScreenOptions}>
<AOPPStack.Screen name="SelectWalletAOPP" component={SelectWallet} options={SelectWallet.navigationOptions(theme)} />
<AOPPStack.Screen name="AOPP" component={AOPP} options={AOPP.navigationOptions(theme)} />
<AOPPStack.Screen name="SignVerify" component={SignVerify} options={SignVerify.navigationOptions(theme)} />
</AOPPStack.Navigator>
);
};

const RootStack = createStackNavigator();
const Navigation = () => {
const theme = useTheme();
Expand Down Expand Up @@ -499,6 +513,7 @@ const Navigation = () => {
<RootStack.Screen name="SelectWallet" component={SelectWallet} options={{ headerLeft: null }} />
<RootStack.Screen name="ReceiveDetailsRoot" component={ReceiveDetailsStackRoot} options={{ headerShown: false }} />
<RootStack.Screen name="LappBrowserRoot" component={LappBrowserStackRoot} options={{ headerShown: false }} />
<RootStack.Screen name="AOPPRoot" component={AOPPRoot} options={{ headerShown: false }} />

<RootStack.Screen
name="ScanQRCodeRoot"
Expand Down
5 changes: 3 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@
<data android:scheme="bluewallet" />
<data android:scheme="lapp" />
<data android:scheme="blue" />
<data android:scheme="aopp" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="application/octet-stream"
android:host="*"
android:host="*"
android:pathPattern=".*\\.psbt"
/>
</intent-filter>
Expand All @@ -86,7 +87,7 @@
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="text/plain"
android:host="*"
android:host="*"
android:pathPattern=".*\\.psbt"
/>
</intent-filter>
Expand Down
72 changes: 72 additions & 0 deletions class/aopp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Frisbee from 'frisbee';
import url from 'url';

export default class AOPP {
static typeAny = 'any';
static typeP2wpkh = 'p2wpkh';
static typeP2sh = 'p2sh';
static typeP2pkh = 'p2pkh';

static getSegwitByAddressFormat(addressType) {
if (![AOPP.typeP2wpkh, AOPP.typeP2sh, AOPP.typeP2pkh].includes(addressType)) {
throw new Error('Work only for limited types');
}
switch (addressType) {
case 'p2wpkh':
return 'p2wpkh';
case 'p2sh':
return 'p2sh(p2wpkh)';
case 'p2pkh':
return undefined;
}
}

constructor(uri) {
this.uri = uri;
const { protocol, query } = url.parse(uri, true); // eslint-disable-line node/no-deprecated-api

if (protocol !== 'aopp:') throw new Error('Unsupported protocol');
if (query.v !== '0') throw new Error('Unsupported version');
if (!query.msg) throw new Error('Message required');
if (query.msg.lenth > 1024) throw new Error('Message is too big');
if (query.asset && query.asset !== 'btc') throw new Error('Unsupported asset');
if (query.format) {
if (![AOPP.typeAny, AOPP.typeP2wpkh, AOPP.typeP2sh, AOPP.typeP2pkh].includes(query.format)) {
throw new Error('Unsupported address format');
}
} else {
query.format = 'any';
}
if (!query.callback) throw new Error('Callback required');

this.v = Number(query.v);
this.msg = query.msg;
this.format = query.format;
this.callback = query.callback;

// parse callback url
const { hostname } = url.parse(this.callback, true); // eslint-disable-line node/no-deprecated-api
if (!hostname) throw new Error('Wrong callback');

this.callbackHostname = hostname;

this._api = new Frisbee({
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
}

async send({ address, signature }) {
const res = await this._api.post(this.callback, {
body: {
version: this.v,
address,
signature,
},
});

if (res.err) throw res.err;
}
}
13 changes: 11 additions & 2 deletions class/deeplink-schema-match.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class DeeplinkSchemaMatch {
lowercaseString.startsWith('lightning:') ||
lowercaseString.startsWith('blue:') ||
lowercaseString.startsWith('bluewallet:') ||
lowercaseString.startsWith('lapp:')
lowercaseString.startsWith('lapp:') ||
lowercaseString.startsWith('aopp:')
);
}

Expand Down Expand Up @@ -192,7 +193,15 @@ class DeeplinkSchemaMatch {
} else {
const urlObject = url.parse(event.url, true); // eslint-disable-line node/no-deprecated-api
(async () => {
if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
if (urlObject.protocol === 'aopp:') {
completionHandler([
'AOPPRoot',
{
screen: 'AOPP',
params: { uri: event.url },
},
]);
} else if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
switch (urlObject.host) {
case 'openlappbrowser': {
console.log('opening LAPP', urlObject.query.url);
Expand Down
4 changes: 2 additions & 2 deletions class/wallets/legacy-wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,12 +508,12 @@ export class LegacyWallet extends AbstractWallet {
* @param address {string}
* @returns {string} base64 encoded signature
*/
signMessage(message, address) {
signMessage(message, address, useSegwit = true) {
const wif = this._getWIFbyAddress(address);
if (wif === null) throw new Error('Invalid address');
const keyPair = bitcoin.ECPair.fromWIF(wif);
const privateKey = keyPair.privateKey;
const options = this.segwitType ? { segwitType: this.segwitType } : undefined;
const options = this.segwitType && useSegwit ? { segwitType: this.segwitType } : undefined;
const signature = bitcoinMessage.sign(message, privateKey, keyPair.compressed, options);
return signature.toString('base64');
}
Expand Down
33 changes: 33 additions & 0 deletions helpers/confirm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Alert } from 'react-native';
import loc from '../loc';

/**
* Helper function that throws un-cancellable dialog to confirm user's action.
* Promise resolves to TRUE if user confirms, FALSE otherwise
*
* @param title {string}
* @param text {string}
*
* @return {Promise<boolean>}
*/
module.exports = function (title = 'Are you sure?', text = '') {
return new Promise(resolve => {
Alert.alert(
title,
text,
[
{
text: loc._.yes,
onPress: () => resolve(true),
style: 'default',
},
{
text: loc._.cancel,
onPress: () => resolve(false),
style: 'cancel',
},
],
{ cancelable: false },
);
});
};
32 changes: 32 additions & 0 deletions helpers/select-wallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Helper function to select wallet.
* Navigates to selector screen, and then navigates back while resolving promise with selected wallet.
*
* @param navigateFunc {function} Function that does navigatino should be passed from outside
* @param currentScreenName {string} Current screen name, so we know to what screen to get back to
* @param chainType {string} One of `Chain.` constant to be used to filter wallet pannels to show
* @param availableWallets {array} Wallets to be present in selector. If set, overrides `chainType`
* @param noWalletExplanationText {string} Text that is displayed when there are no wallets to select from
*
* @returns {Promise<AbstractWallet>}
*/
module.exports = function (navigateFunc, currentScreenName, chainType, availableWallets, noWalletExplanationText = '') {
return new Promise((resolve, reject) => {
if (!currentScreenName) return reject(new Error('currentScreenName is not provided'));

const params = {};
if (chainType) params.chainType = chainType;
if (availableWallets) params.availableWallets = availableWallets;
if (noWalletExplanationText) params.noWalletExplanationText = noWalletExplanationText;

params.onWalletSelect = function (selectedWallet) {
if (!selectedWallet) return;

setTimeout(() => resolve(selectedWallet), 1);
console.warn('trying to navigate back to', currentScreenName);
navigateFunc(currentScreenName);
};

navigateFunc('SelectWallet', params);
});
};
1 change: 1 addition & 0 deletions ios/BlueWallet/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
<string>bluewallet</string>
<string>lapp</string>
<string>blue</string>
<string>aopp</string>
</array>
</dict>
</array>
Expand Down
8 changes: 8 additions & 0 deletions loc/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -565,15 +565,23 @@
"sign_title": "Sign/Verify message",
"sign_help": "Here you can create or verify a cryptographic signature based on a Bitcoin address",
"sign_sign": "Sign",
"sign_sign_submit": "Sign and Submit",
"sign_verify": "Verify",
"sign_signature_correct": "Verification Succeeded!",
"sign_signature_incorrect": "Verification Failed!",
"sign_placeholder_address": "Address",
"sign_placeholder_message": "Message",
"sign_placeholder_signature": "Signature",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "Do you want to send signed message to {hostname}?",
"address_balance": "Balance: {balance} sats",
"addresses_title": "Addresses",
"type_change": "Change",
"type_receive": "Receive"
},
"aopp": {
"title": "Select Address",
"send_success": "Signature sent successfully",
"send_error": "Signature sending error"
}
}
66 changes: 66 additions & 0 deletions screen/wallets/aopp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useEffect, useContext } from 'react';
import { ActivityIndicator, Alert, StyleSheet } from 'react-native';
import { useRoute, useTheme, useNavigation } from '@react-navigation/native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';

import { BlueStorageContext } from '../../blue_modules/storage-context';
import { SafeBlueArea } from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle';
import loc from '../../loc';
import AOPPClient from '../../class/aopp';
import selectWallet from '../../helpers/select-wallet';

const AOPP = () => {
const { colors } = useTheme();
const navigation = useNavigation();
const { uri } = useRoute().params;
const { name } = useRoute();
const { wallets } = useContext(BlueStorageContext);

useEffect(() => {
(async () => {
let aopp;
try {
aopp = new AOPPClient(uri);
} catch (e) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
Alert.alert(loc.errors.error, e.message);
return navigation.pop();
}

let availableWallets = wallets.filter(w => w.allowSignVerifyMessage());
if (aopp.format !== AOPPClient.typeAny) {
const segwitType = AOPPClient.getSegwitByAddressFormat(aopp.format);
availableWallets = availableWallets.filter(w => w.segwitType === segwitType);
}

const wallet = await selectWallet(navigation.navigate, name, false, availableWallets, 'Onchain wallet is required to sign a message');
if (!wallet) return navigation.pop();

const address = wallet.getAddressAsync ? await wallet.getAddressAsync() : wallet.getAddress();
navigation.navigate('SignVerify', {
walletID: wallet.getID(),
address,
message: aopp.msg,
aoppURI: uri,
});
})();
}, []); // eslint-disable-line react-hooks/exhaustive-deps

return (
<SafeBlueArea style={[styles.center, { backgroundColor: colors.elevated }]}>
<ActivityIndicator />
</SafeBlueArea>
);
};

const styles = StyleSheet.create({
center: {
justifyContent: 'center',
alignItems: 'center',
},
});

AOPP.navigationOptions = navigationStyle({}, opts => ({ ...opts, title: loc.aopp.title }));

export default AOPP;
7 changes: 3 additions & 4 deletions screen/wallets/selectWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import { useRoute, useTheme } from '@react-navigation/native';

import { SafeBlueArea, BlueText, BlueSpacing20, BluePrivateBalance } from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import WalletGradient from '../../class/wallet-gradient';
import loc, { formatBalance, transactionTimeToReadable } from '../../loc';
import { MultisigHDWallet } from '../../class';
import { MultisigHDWallet, LightningCustodianWallet } from '../../class';
import { BlueStorageContext } from '../../blue_modules/storage-context';

const SelectWallet = () => {
const { chainType, onWalletSelect, availableWallets } = useRoute().params;
const { chainType, onWalletSelect, availableWallets, noWalletExplanationText } = useRoute().params;
const [isLoading, setIsLoading] = useState(true);
const { wallets } = useContext(BlueStorageContext);
const { colors } = useTheme();
Expand Down Expand Up @@ -161,7 +160,7 @@ const SelectWallet = () => {
<View style={styles.noWallets}>
<BlueText style={styles.center}>{loc.wallets.select_no_bitcoin}</BlueText>
<BlueSpacing20 />
<BlueText style={styles.center}>{loc.wallets.select_no_bitcoin_exp}</BlueText>
<BlueText style={styles.center}>{noWalletExplanationText || loc.wallets.select_no_bitcoin_exp}</BlueText>
</View>
</SafeBlueArea>
);
Expand Down
Loading

0 comments on commit 897b945

Please sign in to comment.