Skip to content

Commit 897b945

Browse files
limpbrainsOvertorment
authored andcommitted
ADD: AOPP
1 parent 59d3354 commit 897b945

File tree

14 files changed

+364
-15
lines changed

14 files changed

+364
-15
lines changed

Navigation.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import Marketplace from './screen/wallets/marketplace';
4747
import ReorderWallets from './screen/wallets/reorderWallets';
4848
import SelectWallet from './screen/wallets/selectWallet';
4949
import ProvideEntropy from './screen/wallets/provideEntropy';
50+
import AOPP from './screen/wallets/aopp';
5051

5152
import TransactionDetails from './screen/transactions/details';
5253
import TransactionStatus from './screen/transactions/transactionStatus';
@@ -467,6 +468,19 @@ const ExportMultisigCoordinationSetupRoot = () => {
467468
);
468469
};
469470

471+
const AOPPStack = createStackNavigator();
472+
const AOPPRoot = () => {
473+
const theme = useTheme();
474+
475+
return (
476+
<AOPPStack.Navigator screenOptions={defaultStackScreenOptions}>
477+
<AOPPStack.Screen name="SelectWalletAOPP" component={SelectWallet} options={SelectWallet.navigationOptions(theme)} />
478+
<AOPPStack.Screen name="AOPP" component={AOPP} options={AOPP.navigationOptions(theme)} />
479+
<AOPPStack.Screen name="SignVerify" component={SignVerify} options={SignVerify.navigationOptions(theme)} />
480+
</AOPPStack.Navigator>
481+
);
482+
};
483+
470484
const RootStack = createStackNavigator();
471485
const Navigation = () => {
472486
const theme = useTheme();
@@ -499,6 +513,7 @@ const Navigation = () => {
499513
<RootStack.Screen name="SelectWallet" component={SelectWallet} options={{ headerLeft: null }} />
500514
<RootStack.Screen name="ReceiveDetailsRoot" component={ReceiveDetailsStackRoot} options={{ headerShown: false }} />
501515
<RootStack.Screen name="LappBrowserRoot" component={LappBrowserStackRoot} options={{ headerShown: false }} />
516+
<RootStack.Screen name="AOPPRoot" component={AOPPRoot} options={{ headerShown: false }} />
502517

503518
<RootStack.Screen
504519
name="ScanQRCodeRoot"

android/app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,15 @@
6969
<data android:scheme="bluewallet" />
7070
<data android:scheme="lapp" />
7171
<data android:scheme="blue" />
72+
<data android:scheme="aopp" />
7273
</intent-filter>
7374
<intent-filter>
7475
<action android:name="android.intent.action.VIEW" />
7576
<action android:name="android.intent.action.EDIT" />
7677
<category android:name="android.intent.category.DEFAULT" />
7778
<data
7879
android:mimeType="application/octet-stream"
79-
android:host="*"
80+
android:host="*"
8081
android:pathPattern=".*\\.psbt"
8182
/>
8283
</intent-filter>
@@ -86,7 +87,7 @@
8687
<category android:name="android.intent.category.DEFAULT" />
8788
<data
8889
android:mimeType="text/plain"
89-
android:host="*"
90+
android:host="*"
9091
android:pathPattern=".*\\.psbt"
9192
/>
9293
</intent-filter>

class/aopp.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Frisbee from 'frisbee';
2+
import url from 'url';
3+
4+
export default class AOPP {
5+
static typeAny = 'any';
6+
static typeP2wpkh = 'p2wpkh';
7+
static typeP2sh = 'p2sh';
8+
static typeP2pkh = 'p2pkh';
9+
10+
static getSegwitByAddressFormat(addressType) {
11+
if (![AOPP.typeP2wpkh, AOPP.typeP2sh, AOPP.typeP2pkh].includes(addressType)) {
12+
throw new Error('Work only for limited types');
13+
}
14+
switch (addressType) {
15+
case 'p2wpkh':
16+
return 'p2wpkh';
17+
case 'p2sh':
18+
return 'p2sh(p2wpkh)';
19+
case 'p2pkh':
20+
return undefined;
21+
}
22+
}
23+
24+
constructor(uri) {
25+
this.uri = uri;
26+
const { protocol, query } = url.parse(uri, true); // eslint-disable-line node/no-deprecated-api
27+
28+
if (protocol !== 'aopp:') throw new Error('Unsupported protocol');
29+
if (query.v !== '0') throw new Error('Unsupported version');
30+
if (!query.msg) throw new Error('Message required');
31+
if (query.msg.lenth > 1024) throw new Error('Message is too big');
32+
if (query.asset && query.asset !== 'btc') throw new Error('Unsupported asset');
33+
if (query.format) {
34+
if (![AOPP.typeAny, AOPP.typeP2wpkh, AOPP.typeP2sh, AOPP.typeP2pkh].includes(query.format)) {
35+
throw new Error('Unsupported address format');
36+
}
37+
} else {
38+
query.format = 'any';
39+
}
40+
if (!query.callback) throw new Error('Callback required');
41+
42+
this.v = Number(query.v);
43+
this.msg = query.msg;
44+
this.format = query.format;
45+
this.callback = query.callback;
46+
47+
// parse callback url
48+
const { hostname } = url.parse(this.callback, true); // eslint-disable-line node/no-deprecated-api
49+
if (!hostname) throw new Error('Wrong callback');
50+
51+
this.callbackHostname = hostname;
52+
53+
this._api = new Frisbee({
54+
headers: {
55+
Accept: 'application/json',
56+
'Content-Type': 'application/json',
57+
},
58+
});
59+
}
60+
61+
async send({ address, signature }) {
62+
const res = await this._api.post(this.callback, {
63+
body: {
64+
version: this.v,
65+
address,
66+
signature,
67+
},
68+
});
69+
70+
if (res.err) throw res.err;
71+
}
72+
}

class/deeplink-schema-match.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class DeeplinkSchemaMatch {
1717
lowercaseString.startsWith('lightning:') ||
1818
lowercaseString.startsWith('blue:') ||
1919
lowercaseString.startsWith('bluewallet:') ||
20-
lowercaseString.startsWith('lapp:')
20+
lowercaseString.startsWith('lapp:') ||
21+
lowercaseString.startsWith('aopp:')
2122
);
2223
}
2324

@@ -192,7 +193,15 @@ class DeeplinkSchemaMatch {
192193
} else {
193194
const urlObject = url.parse(event.url, true); // eslint-disable-line node/no-deprecated-api
194195
(async () => {
195-
if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
196+
if (urlObject.protocol === 'aopp:') {
197+
completionHandler([
198+
'AOPPRoot',
199+
{
200+
screen: 'AOPP',
201+
params: { uri: event.url },
202+
},
203+
]);
204+
} else if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') {
196205
switch (urlObject.host) {
197206
case 'openlappbrowser': {
198207
console.log('opening LAPP', urlObject.query.url);

class/wallets/legacy-wallet.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,12 +508,12 @@ export class LegacyWallet extends AbstractWallet {
508508
* @param address {string}
509509
* @returns {string} base64 encoded signature
510510
*/
511-
signMessage(message, address) {
511+
signMessage(message, address, useSegwit = true) {
512512
const wif = this._getWIFbyAddress(address);
513513
if (wif === null) throw new Error('Invalid address');
514514
const keyPair = bitcoin.ECPair.fromWIF(wif);
515515
const privateKey = keyPair.privateKey;
516-
const options = this.segwitType ? { segwitType: this.segwitType } : undefined;
516+
const options = this.segwitType && useSegwit ? { segwitType: this.segwitType } : undefined;
517517
const signature = bitcoinMessage.sign(message, privateKey, keyPair.compressed, options);
518518
return signature.toString('base64');
519519
}

helpers/confirm.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Alert } from 'react-native';
2+
import loc from '../loc';
3+
4+
/**
5+
* Helper function that throws un-cancellable dialog to confirm user's action.
6+
* Promise resolves to TRUE if user confirms, FALSE otherwise
7+
*
8+
* @param title {string}
9+
* @param text {string}
10+
*
11+
* @return {Promise<boolean>}
12+
*/
13+
module.exports = function (title = 'Are you sure?', text = '') {
14+
return new Promise(resolve => {
15+
Alert.alert(
16+
title,
17+
text,
18+
[
19+
{
20+
text: loc._.yes,
21+
onPress: () => resolve(true),
22+
style: 'default',
23+
},
24+
{
25+
text: loc._.cancel,
26+
onPress: () => resolve(false),
27+
style: 'cancel',
28+
},
29+
],
30+
{ cancelable: false },
31+
);
32+
});
33+
};

helpers/select-wallet.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Helper function to select wallet.
3+
* Navigates to selector screen, and then navigates back while resolving promise with selected wallet.
4+
*
5+
* @param navigateFunc {function} Function that does navigatino should be passed from outside
6+
* @param currentScreenName {string} Current screen name, so we know to what screen to get back to
7+
* @param chainType {string} One of `Chain.` constant to be used to filter wallet pannels to show
8+
* @param availableWallets {array} Wallets to be present in selector. If set, overrides `chainType`
9+
* @param noWalletExplanationText {string} Text that is displayed when there are no wallets to select from
10+
*
11+
* @returns {Promise<AbstractWallet>}
12+
*/
13+
module.exports = function (navigateFunc, currentScreenName, chainType, availableWallets, noWalletExplanationText = '') {
14+
return new Promise((resolve, reject) => {
15+
if (!currentScreenName) return reject(new Error('currentScreenName is not provided'));
16+
17+
const params = {};
18+
if (chainType) params.chainType = chainType;
19+
if (availableWallets) params.availableWallets = availableWallets;
20+
if (noWalletExplanationText) params.noWalletExplanationText = noWalletExplanationText;
21+
22+
params.onWalletSelect = function (selectedWallet) {
23+
if (!selectedWallet) return;
24+
25+
setTimeout(() => resolve(selectedWallet), 1);
26+
console.warn('trying to navigate back to', currentScreenName);
27+
navigateFunc(currentScreenName);
28+
};
29+
30+
navigateFunc('SelectWallet', params);
31+
});
32+
};

ios/BlueWallet/Info.plist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
<string>bluewallet</string>
110110
<string>lapp</string>
111111
<string>blue</string>
112+
<string>aopp</string>
112113
</array>
113114
</dict>
114115
</array>

loc/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,15 +565,23 @@
565565
"sign_title": "Sign/Verify message",
566566
"sign_help": "Here you can create or verify a cryptographic signature based on a Bitcoin address",
567567
"sign_sign": "Sign",
568+
"sign_sign_submit": "Sign and Submit",
568569
"sign_verify": "Verify",
569570
"sign_signature_correct": "Verification Succeeded!",
570571
"sign_signature_incorrect": "Verification Failed!",
571572
"sign_placeholder_address": "Address",
572573
"sign_placeholder_message": "Message",
573574
"sign_placeholder_signature": "Signature",
575+
"sign_aopp_title": "AOPP",
576+
"sign_aopp_confirm": "Do you want to send signed message to {hostname}?",
574577
"address_balance": "Balance: {balance} sats",
575578
"addresses_title": "Addresses",
576579
"type_change": "Change",
577580
"type_receive": "Receive"
581+
},
582+
"aopp": {
583+
"title": "Select Address",
584+
"send_success": "Signature sent successfully",
585+
"send_error": "Signature sending error"
578586
}
579587
}

screen/wallets/aopp.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { useEffect, useContext } from 'react';
2+
import { ActivityIndicator, Alert, StyleSheet } from 'react-native';
3+
import { useRoute, useTheme, useNavigation } from '@react-navigation/native';
4+
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
5+
6+
import { BlueStorageContext } from '../../blue_modules/storage-context';
7+
import { SafeBlueArea } from '../../BlueComponents';
8+
import navigationStyle from '../../components/navigationStyle';
9+
import loc from '../../loc';
10+
import AOPPClient from '../../class/aopp';
11+
import selectWallet from '../../helpers/select-wallet';
12+
13+
const AOPP = () => {
14+
const { colors } = useTheme();
15+
const navigation = useNavigation();
16+
const { uri } = useRoute().params;
17+
const { name } = useRoute();
18+
const { wallets } = useContext(BlueStorageContext);
19+
20+
useEffect(() => {
21+
(async () => {
22+
let aopp;
23+
try {
24+
aopp = new AOPPClient(uri);
25+
} catch (e) {
26+
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
27+
Alert.alert(loc.errors.error, e.message);
28+
return navigation.pop();
29+
}
30+
31+
let availableWallets = wallets.filter(w => w.allowSignVerifyMessage());
32+
if (aopp.format !== AOPPClient.typeAny) {
33+
const segwitType = AOPPClient.getSegwitByAddressFormat(aopp.format);
34+
availableWallets = availableWallets.filter(w => w.segwitType === segwitType);
35+
}
36+
37+
const wallet = await selectWallet(navigation.navigate, name, false, availableWallets, 'Onchain wallet is required to sign a message');
38+
if (!wallet) return navigation.pop();
39+
40+
const address = wallet.getAddressAsync ? await wallet.getAddressAsync() : wallet.getAddress();
41+
navigation.navigate('SignVerify', {
42+
walletID: wallet.getID(),
43+
address,
44+
message: aopp.msg,
45+
aoppURI: uri,
46+
});
47+
})();
48+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
49+
50+
return (
51+
<SafeBlueArea style={[styles.center, { backgroundColor: colors.elevated }]}>
52+
<ActivityIndicator />
53+
</SafeBlueArea>
54+
);
55+
};
56+
57+
const styles = StyleSheet.create({
58+
center: {
59+
justifyContent: 'center',
60+
alignItems: 'center',
61+
},
62+
});
63+
64+
AOPP.navigationOptions = navigationStyle({}, opts => ({ ...opts, title: loc.aopp.title }));
65+
66+
export default AOPP;

0 commit comments

Comments
 (0)