diff --git a/__tests__/__snapshots__/coinid-public-test.js.snap b/__tests__/__snapshots__/coinid-public-test.js.snap
index e3319a1..59cc03a 100644
--- a/__tests__/__snapshots__/coinid-public-test.js.snap
+++ b/__tests__/__snapshots__/coinid-public-test.js.snap
@@ -1,7 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`bitcoin COINiDPublic can create message signing data 1`] = `"MSG/BTC:49-*0-*0-*0*5+3GO7JP:49-*0-*0-*0*5:This%20is%20a%20test%20message"`;
+
exports[`bitcoin COINiDPublic can create transaction data 1`] = `"TX/BTC:49-*0-*0-*0*5+3GO7JP:49-*0-*0-*0*5+49-*0-*0-*1*2:0100000002A06364D80225EA2181D41AD469B2D5560E594F2C783BBB41CAAA652B493A104C020000000000000000A06364D80225EA2181D41AD469B2D5560E594F2C783BBB41CAAA652B493A104C01000000000000000005400D03000000000017A914A89F3493631D141E847A15CB3BA68234CC6ACC2E87B08F0600000000001976A9147E6257D509CCDD623F4AB33416F17A44A0A5F7BC88AC10270000000000001600147E6257D509CCDD623F4AB33416F17A44A0A5F7BC307500000000000017A91461F05801F6931D95CD0180917B6CACF84C767E818738FA01000000000017A914CD05CE4F0D4B88D2E00A9FE7CF3AA8FEEAF773628700000000:4:369796+430000"`;
+exports[`bitcoin COINiDPublic fails to generate message with invalid address 1`] = `undefined`;
+
+exports[`bitcoin COINiDPublic fails to verify invalid signature 1`] = `undefined`;
+
exports[`bitcoin COINiDPublic generates a correct P2PKH addresses 1`] = `
Array [
"16FoVqrkVAAUWK7RVduXt1RKZRAuCXXoxi",
@@ -57,8 +63,14 @@ exports[`bitcoin COINiDPublic throws error when address not valid 1`] = `"3H4cAG
exports[`bitcoin COINiDPublic throws error when having insufficient funds 1`] = `undefined`;
+exports[`groestlcoin COINiDPublic can create message signing data 1`] = `"MSG/GRS:49-*17-*0-*0*5+3LXPYO:49-*17-*0-*0*5:This%20is%20a%20test%20message"`;
+
exports[`groestlcoin COINiDPublic can create transaction data 1`] = `"TX/GRS:49-*17-*0-*0*3+3HMJUS:49-*17-*0-*0*3+49-*17-*0-*1*1:0100000002A06364D80225EA2181D41AD469B2D5560E594F2C783BBB41CAAA652B493A104C020000000000000000A06364D80225EA2181D41AD469B2D5560E594F2C783BBB41CAAA652B493A104C01000000000000000005400D03000000000017A914C50D589AE0965FBB3484D51AF1271A6BC2851E8987B08F0600000000001976A914EF20271534095EA2A69064620164F248997D64CB88AC1027000000000000160014EF20271534095EA2A69064620164F248997D64CB307500000000000017A914609796DC6E5DFFB0948A3D268ACE1FCFE2B4A96B8738FA01000000000017A914263482828E92FF4510191F10A3FA2CDBC1D047118700000000:4:369796+430000"`;
+exports[`groestlcoin COINiDPublic fails to generate message with invalid address 1`] = `undefined`;
+
+exports[`groestlcoin COINiDPublic fails to verify invalid signature 1`] = `undefined`;
+
exports[`groestlcoin COINiDPublic generates a correct P2PKH addresses 1`] = `
Array [
"FXKSZ871pjUFXdGX3Jo1usNkusbKQSr5Nk",
@@ -114,8 +126,14 @@ exports[`groestlcoin COINiDPublic throws error when address not valid 1`] = `"3K
exports[`groestlcoin COINiDPublic throws error when having insufficient funds 1`] = `undefined`;
+exports[`groestlcoin-testnet COINiDPublic can create message signing data 1`] = `"MSG/tGRS:49-*1-*0-*0*5+2N7UPZ:49-*1-*0-*0*5:This%20is%20a%20test%20message"`;
+
exports[`groestlcoin-testnet COINiDPublic can create transaction data 1`] = `"TX/TGRS:49-*1-*0-*0*2+2NA5BU:49-*1-*0-*0*2+49-*1-*0-*1*1:0100000002A06364D80225EA2181D41AD469B2D5560E594F2C783BBB41CAAA652B493A104C020000000000000000A06364D80225EA2181D41AD469B2D5560E594F2C783BBB41CAAA652B493A104C01000000000000000005400D03000000000017A914D7EF101DAA78E7656CA3C49B30B854E8C4D7A2BF87B08F0600000000001976A9147E8EB917EAB0DA74EE21769044B8E36562BF5D3288AC10270000000000001600147E8EB917EAB0DA74EE21769044B8E36562BF5D32307500000000000017A914E1BCEA2765A83B1D688CF113A93F78E2D119EA2A8738FA01000000000017A9142E95CFDD61B2F735E6BEDF5291B6F0873E4F1D008700000000:4:369796+430000"`;
+exports[`groestlcoin-testnet COINiDPublic fails to generate message with invalid address 1`] = `undefined`;
+
+exports[`groestlcoin-testnet COINiDPublic fails to verify invalid signature 1`] = `undefined`;
+
exports[`groestlcoin-testnet COINiDPublic generates a correct P2PKH addresses 1`] = `
Array [
"mkwr5sX93GcwRDb38NXXcLb9j9YqpDycMz",
@@ -171,8 +189,14 @@ exports[`groestlcoin-testnet COINiDPublic throws error when address not valid 1`
exports[`groestlcoin-testnet COINiDPublic throws error when having insufficient funds 1`] = `undefined`;
+exports[`myriad COINiDPublic can create message signing data 1`] = `"MSG/XMY:49-*90-*0-*0*5+4OV111:49-*90-*0-*0*5:This%20is%20a%20test%20message"`;
+
exports[`myriad COINiDPublic can create transaction data 1`] = `"TX/XMY:49-*90-*0-*0*2+4FY57D:49-*90-*0-*0*2:01000000010D4E4F1E87B9F1D14727A9472640F74F4A18BA448CB063ED3574D21FCF74673800000000000000000004400D0300000000001976A9149E9DCD04CF375691FDFA2D19A52AE404B9EF03DE88ACB08F06000000000017A914594F6D51146D61F84C7027A466069E47DE69712C871027000000000000160014293DBC2B6B7CED3D7F363496A4E00B332F209B94E9645E2C0100000017A914556AA879860E42C377B13639C27350469972A71B8700000000:3:5039991221"`;
+exports[`myriad COINiDPublic fails to generate message with invalid address 1`] = `undefined`;
+
+exports[`myriad COINiDPublic fails to verify invalid signature 1`] = `undefined`;
+
exports[`myriad COINiDPublic generates a correct P2PKH addresses 1`] = `
Array [
"MBzgcG2PyPS3xk9FZcRoa6GcEHSW5MoobK",
@@ -228,8 +252,14 @@ exports[`myriad COINiDPublic throws error when address not valid 1`] = `"MNMer2w
exports[`myriad COINiDPublic throws error when having insufficient funds 1`] = `undefined`;
+exports[`testnet COINiDPublic can create message signing data 1`] = `"MSG/tBTC:49-*1-*0-*0*5+2N7UPZ:49-*1-*0-*0*5:This%20is%20a%20test%20message"`;
+
exports[`testnet COINiDPublic can create transaction data 1`] = `"TX/TBTC:49-*1-*0-*1*0+2MTRGW:49-*1-*0-*1*0+49-*1-*0-*0*5:0100000002A06364D80225EA2181D41AD469B2D5560E594F2C783BBB41CAAA652B493A104C020000000000000000A06364D80225EA2181D41AD469B2D5560E594F2C783BBB41CAAA652B493A104C01000000000000000005400D03000000000017A91466A8A442F12B2B8A9C45D3D4CB1CCBDCA952D9A387B08F06000000000017A914D170EE37EA45FBB9DC81598C8445237EC47E8A678710270000000000001976A914F12F2C6E408B3CDFF1991B8783D1EB428F57814B88AC307500000000000016001408C3E3704FC510BEE9F2F05686CBF2A9F541E23F38FA01000000000017A9142E95CFDD61B2F735E6BEDF5291B6F0873E4F1D008700000000:4:369796+430000"`;
+exports[`testnet COINiDPublic fails to generate message with invalid address 1`] = `undefined`;
+
+exports[`testnet COINiDPublic fails to verify invalid signature 1`] = `undefined`;
+
exports[`testnet COINiDPublic generates a correct P2PKH addresses 1`] = `
Array [
"mkwr5sX93GcwRDb38NXXcLb9j9YqowH5DE",
diff --git a/__tests__/coinid-public-test.js b/__tests__/coinid-public-test.js
index a08f6f9..b969133 100644
--- a/__tests__/coinid-public-test.js
+++ b/__tests__/coinid-public-test.js
@@ -12,6 +12,10 @@ coinArray.forEach(coin => {
correctPayments,
insufficientPayments,
wrongAddressPayments,
+ messageSignature,
+ messageAddress,
+ message,
+ anotherMessage,
} = require(`./data/${coin}.json`);
var i = 0;
@@ -85,6 +89,34 @@ coinArray.forEach(coin => {
expect(coinid.P2WPKH.getAllAddresses()).toMatchSnapshot();
});
+ it('can create message signing data', () => {
+ const address = coinid.P2SHP2WPKH.getReceiveAddress();
+ const message = 'This is a test message';
+ const messageData = coinid.P2SHP2WPKH.buildMsgCoinIdData(address, message);
+
+ expect(messageData).toMatchSnapshot();
+ });
+
+ it('fails to generate message with invalid address', () => {
+ const address = coinid.P2PKH.getReceiveAddress();
+ const message = 'This is a test message that will fail';
+
+ expect(() => {
+ coinid.P2SHP2WPKH.buildMsgCoinIdData(address, message);
+ }).toThrowErrorMatchingSnapshot();
+ });
+
+ it('can verify message', () => {
+ const verify = coinid.P2PKH.verifyMessage(message, messageAddress, messageSignature);
+ expect(verify).toEqual(true);
+ });
+
+ it('fails to verify invalid signature', () => {
+ expect(() => {
+ coinid.P2PKH.verifyMessage(anotherMessage, messageAddress, messageSignature);
+ }).toThrowErrorMatchingSnapshot();
+ });
+
});
});
diff --git a/__tests__/data/bitcoin.json b/__tests__/data/bitcoin.json
index b986666..3f3abe4 100644
--- a/__tests__/data/bitcoin.json
+++ b/__tests__/data/bitcoin.json
@@ -45,5 +45,9 @@
"wrongAddressPayments": [
{ "amount": 0.002, "address": "3H4cAGZ7qPd4HcgSQzad9oX4AbSVxs9hMS5", "note": "" },
{ "amount": 0.0043, "address": "1CXFxAnFzeCa61ChFB7iHnFe2NekG74YZE", "note": "" }
- ]
+ ],
+ "message": "This is a test message",
+ "anotherMessage": "This is another test message",
+ "messageAddress": "3Go7JpcBu7XdYa93EUfvTD4hFbECWRr1Ew",
+ "messageSignature": "IGqfHR/PVMDAafnYx0nvyeFabcBpwf120CCpzIfvJlhBA7pceh6zUTYtYF8q2Gb61naZuItvVRMsCMgGvAtErIw="
}
diff --git a/__tests__/data/groestlcoin-testnet.json b/__tests__/data/groestlcoin-testnet.json
index e2bba0a..c50722c 100644
--- a/__tests__/data/groestlcoin-testnet.json
+++ b/__tests__/data/groestlcoin-testnet.json
@@ -45,5 +45,9 @@
"wrongAddressPayments": [
{ "amount": 0.002, "address": "2NCCvyeutPLBED5fVbPtydrvGX2dUjvaBvUP", "note": "" },
{ "amount": 0.0043, "address": "ms48Qo1Kb8qQEb5A7UNSLf16pTmdsSWju1", "note": "" }
- ]
+ ],
+ "message": "This is a test message",
+ "anotherMessage": "This is another test message",
+ "messageAddress": "2N7UPz9tH9hkmAoKnR1VhkgBq9TyyDoc3xX",
+ "messageSignature": "IBCqq4n0DQ8lJQ/VZk+69VcxJQ32KEs0b1dnEjbiRm0AfsVwmmppVcYWCW3m/nOEID8FWpJVyKihbvfJnopEpb4="
}
diff --git a/__tests__/data/groestlcoin.json b/__tests__/data/groestlcoin.json
index 68abcca..524e154 100644
--- a/__tests__/data/groestlcoin.json
+++ b/__tests__/data/groestlcoin.json
@@ -45,5 +45,9 @@
"wrongAddressPayments": [
{ "amount": 0.002, "address": "3KKew2dzLeMDUMchRrVse1e1SVYW3odQM3w", "note": "" },
{ "amount": 0.0043, "address": "Fry6TmG62wfuFP4JKhjuKNjKv8wmjiTVou", "note": "" }
- ]
+ ],
+ "message": "This is a test message",
+ "anotherMessage": "This is another test message",
+ "messageAddress": "3LXpYowXikyPUATLPCyYASzzomZUKfuKk4",
+ "messageSignature": "IBBoUR8KwpwrB9EohYzV9f/M+nRXyCEi8kGTGJSKOZPMfubRI0/QDcLzAkPJ3BfGEndJVYv+TkGh40WAMerEAvc="
}
diff --git a/__tests__/data/myriad.json b/__tests__/data/myriad.json
index 7011c47..34cd5fd 100644
--- a/__tests__/data/myriad.json
+++ b/__tests__/data/myriad.json
@@ -30,5 +30,9 @@
{ "amount": 0.002, "address": "MNMer2wBZNHXpqXJKfwqFzHxNhUNa3PFvPG", "note": "" },
{ "amount": 0.0043, "address": "4mBf9jhXHGNP2PY22uoGo4FQL5dErYxcJr", "note": "" },
{ "amount": 0.0001, "address": "my1q9y7mc2mt0nkn6lekxjt2fcqtxvhjpxu5thhhqj", "note": "" }
- ]
+ ],
+ "message": "This is a test message",
+ "anotherMessage": "This is another test message",
+ "messageAddress": "4oV111FHy1kR739yXdVv2fp9WhvrhfN4eT",
+ "messageSignature": "IFK83t6NlHfb0SPHhdnTdmL0hBFbJf+TEQ4ZVzFT4Kz6GHfTJyrQtOwfIO6ipBMdpd+sO9in21K2NNCST2Vtz9Y="
}
diff --git a/__tests__/data/testnet.json b/__tests__/data/testnet.json
index e8a47c9..da87914 100644
--- a/__tests__/data/testnet.json
+++ b/__tests__/data/testnet.json
@@ -45,5 +45,9 @@
"wrongAddressPayments": [
{ "amount": 0.002, "address": "2N2c2wfH4LcxhRjGXxggzmDL2iXsVBQEjwsK", "note": "" },
{ "amount": 0.0043, "address": "2NCLeVvnAoXZou3LrZv9bJMcykSrXnjcRR1", "note": "" }
- ]
+ ],
+ "message": "This is a test message",
+ "anotherMessage": "This is another test message",
+ "messageAddress": "2N7UPz9tH9hkmAoKnR1VhkgBq9TyyHVL68t",
+ "messageSignature": "H1ox+mmOF5Wh0LK8RgRtL5YSoSHt3l03qjEEMVLlJzCkEvUzfbEBClNji+OeJ65NepKHDB3vm99DHGqa5vTi/8w="
}
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 50d8676..e001aff 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -116,8 +116,8 @@ android {
applicationId "org.coinid.wallet.tbtc"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 315
- versionName "1.6.0"
+ versionCode 320
+ versionName "1.7.0"
renderscriptTargetApi 23
renderscriptSupportModeEnabled true
}
diff --git a/ios/COINiDWallet.xcodeproj/project.pbxproj b/ios/COINiDWallet.xcodeproj/project.pbxproj
index 9095e24..bbdc4f1 100644
--- a/ios/COINiDWallet.xcodeproj/project.pbxproj
+++ b/ios/COINiDWallet.xcodeproj/project.pbxproj
@@ -1703,7 +1703,7 @@
buildSettings = {
APP_RETURN_SCHEME = "coinid-tbtc";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CURRENT_PROJECT_VERSION = 312;
+ CURRENT_PROJECT_VERSION = 317;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = GC88SQF2BV;
HEADER_SEARCH_PATHS = (
@@ -1748,7 +1748,7 @@
buildSettings = {
APP_RETURN_SCHEME = "coinid-tbtc";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CURRENT_PROJECT_VERSION = 312;
+ CURRENT_PROJECT_VERSION = 317;
DEVELOPMENT_TEAM = GC88SQF2BV;
HEADER_SEARCH_PATHS = (
"$(inherited)",
diff --git a/ios/COINiDWallet/Info.plist b/ios/COINiDWallet/Info.plist
index 531a15e..53cdc86 100644
--- a/ios/COINiDWallet/Info.plist
+++ b/ios/COINiDWallet/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.6.0
+ 1.7.0
CFBundleSignature
????
CFBundleURLTypes
@@ -32,7 +32,7 @@
CFBundleVersion
- 312
+ 317
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/package.json b/package.json
index 3fef588..4f9004d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "COINiDWallet",
- "version": "1.6.0",
+ "version": "1.7.0",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
@@ -21,7 +21,7 @@
"bip21": "^2.0.2",
"bip32-utils": "^0.11.1",
"bitcoinjs-lib": "https://github.com/wlc-/bitcoinjs-lib",
- "bitcoinjs-message": "^2.0.0",
+ "bitcoinjs-message": "https://github.com/COINiD/bitcoinjs-message.git#coinid-version",
"buffer": "^4.9.1",
"buffer-reverse": "^1.0.1",
"coinid-address-functions": "https://github.com/wlc-/coinid-address-functions.git",
diff --git a/src/actionmenus/VerifyMessageActionMenu.js b/src/actionmenus/VerifyMessageActionMenu.js
new file mode 100644
index 0000000..5908a4a
--- /dev/null
+++ b/src/actionmenus/VerifyMessageActionMenu.js
@@ -0,0 +1,39 @@
+import ActionMenuRouter from './ActionMenuRouter';
+
+class VerifyMessageActionMenu {
+ constructor({ showActionSheetWithOptions, ...params }) {
+ this.params = params;
+ this.actionRouter = new ActionMenuRouter({ showFn: showActionSheetWithOptions });
+ }
+
+ getRootMenu = () => {
+ const { onParseClipboard } = this.params;
+ return [
+ {
+ name: 'Parse clipboard data',
+ callback: () => setTimeout(onParseClipboard, 100),
+ },
+ {
+ name: 'Cancel',
+ isCancel: true,
+ },
+ ];
+ };
+
+ getActionRoutes = () => {
+ const actionRoutes = {
+ root: {
+ menu: this.getRootMenu(),
+ },
+ };
+
+ return actionRoutes;
+ };
+
+ show = () => {
+ this.actionRouter.setRoutes(this.getActionRoutes());
+ this.actionRouter.goTo('root');
+ };
+}
+
+export default VerifyMessageActionMenu;
diff --git a/src/dialogs/SignMessage.js b/src/dialogs/SignMessage.js
new file mode 100644
index 0000000..8d5260f
--- /dev/null
+++ b/src/dialogs/SignMessage.js
@@ -0,0 +1,197 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import {
+ Alert, StyleSheet, View, TextInput, Platform, Clipboard,
+} from 'react-native';
+import Share from 'react-native-share';
+import {
+ Button, CancelButton, Text, COINiDTransport,
+} from '../components';
+
+import WalletContext from '../contexts/WalletContext';
+
+import styleMerge from '../utils/styleMerge';
+import parentStyles from './styles/common';
+
+const styles = styleMerge(
+ parentStyles('light'),
+ StyleSheet.create({
+ container: {
+ paddingTop: 8,
+ },
+ }),
+);
+
+export default class SignMessage extends PureComponent {
+ static contextType = WalletContext;
+
+ static propTypes = {
+ dialogRef: PropTypes.shape({}).isRequired,
+ };
+
+ constructor(props, context) {
+ super(props);
+
+ const { coinid } = context;
+ this.coinid = coinid;
+ this.state = { address: this.coinid.getReceiveAddress(), message: '' };
+ }
+
+ _getTransportData = () => {
+ const { address, message } = this.state;
+ try {
+ const valData = this.coinid.buildMsgCoinIdData(address, message);
+ return Promise.resolve(valData);
+ } catch (err) {
+ Alert.alert('Validation Error', 'Make sure address belongs to this wallet.');
+ }
+ };
+
+ _handleReturnData = (data) => {
+ const { address, message } = this.state;
+ const { dialogCloseAndClear, showStatus } = this.context;
+ const signature = data
+ .split('/')
+ .slice(1)
+ .join('/');
+ const coinTitle = this.coinid.coinTitle.toUpperCase();
+
+ const lines = [];
+ lines.push(`-----BEGIN ${coinTitle} SIGNED MESSAGE-----`);
+ lines.push(`${message}`);
+ lines.push('-----BEGIN SIGNATURE-----');
+ lines.push(`${address}`);
+ lines.push(`${signature}`);
+ lines.push(`-----END ${coinTitle} SIGNED MESSAGE-----`);
+ const signedMessage = lines.join('\n');
+
+ Clipboard.setString(signedMessage);
+ dialogCloseAndClear();
+ showStatus('Signed message copied to clipboard', {
+ linkIcon: Platform.OS === 'ios' ? 'share-apple' : 'share-google',
+ linkIconType: 'evilicon',
+ onLinkPress: () => this._share(signedMessage),
+ });
+ };
+
+ _share = (signedMessage) => {
+ const options = {
+ title: 'Share via',
+ message: signedMessage,
+ };
+
+ Share.open(options)
+ .then(() => {
+ if (Platform.OS === 'ios') {
+ this.showStatus('QR code shared successfully');
+ }
+ })
+ .catch(() => {});
+ };
+
+ _renderTransportContent = ({
+ isSigning, signingText, cancel, submit,
+ }) => {
+ let disableButton = false;
+ if (isSigning) {
+ disableButton = true;
+ }
+
+ const { dialogRef } = this.props;
+ const { message, address } = this.state;
+
+ return (
+
+ {
+ this.refContHeight = e.nativeEvent.layout.height;
+ }}
+ >
+ {
+ dialogRef._setKeyboardOffset(this.refAddressBottom - this.refContHeight + 8);
+ }}
+ onLayout={(e) => {
+ this.refAddressBottom = e.nativeEvent.layout.y + e.nativeEvent.layout.height;
+ }}
+ >
+ Address
+
+ {
+ this.setState({ address: newAddress });
+ }}
+ ref={(c) => {
+ this.addressRef = c;
+ }}
+ underlineColorAndroid="transparent"
+ />
+
+
+
+ {
+ dialogRef._setKeyboardOffset(this.refMessageBottom - this.refContHeight + 8);
+ }}
+ onLayout={(e) => {
+ this.refMessageBottom = e.nativeEvent.layout.y + e.nativeEvent.layout.height;
+ }}
+ >
+ Message
+
+ {
+ this.setState({ message: newMessage });
+ }}
+ ref={(c) => {
+ this.messageRef = c;
+ }}
+ underlineColorAndroid="transparent"
+ />
+
+
+
+
+
+ Cancel
+
+
+
+ );
+ };
+
+ render() {
+ return (
+
+ {arg => this._renderTransportContent(arg)}
+
+ );
+ }
+}
diff --git a/src/dialogs/VerifyMessage.js b/src/dialogs/VerifyMessage.js
new file mode 100644
index 0000000..e6af9e5
--- /dev/null
+++ b/src/dialogs/VerifyMessage.js
@@ -0,0 +1,203 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import {
+ Alert, StyleSheet, View, TextInput, Platform, Clipboard,
+} from 'react-native';
+import { Button, Text } from '../components';
+import VerifyMessageActionMenu from '../actionmenus/VerifyMessageActionMenu';
+
+import WalletContext from '../contexts/WalletContext';
+
+import styleMerge from '../utils/styleMerge';
+import parentStyles from './styles/common';
+
+const styles = styleMerge(
+ parentStyles('light'),
+ StyleSheet.create({
+ container: {
+ paddingTop: 8,
+ },
+ }),
+);
+
+export default class SignMessage extends PureComponent {
+ static contextType = WalletContext;
+
+ static propTypes = {
+ dialogRef: PropTypes.shape({}).isRequired,
+ setMoreOptionsFunc: PropTypes.func.isRequired,
+ };
+
+ constructor(props, context) {
+ super(props);
+
+ const { setMoreOptionsFunc } = props;
+
+ const {
+ coinid,
+ globalContext: { showActionSheetWithOptions },
+ } = context;
+ this.coinid = coinid;
+
+ setMoreOptionsFunc(this._onMoreOptions);
+
+ this.state = {
+ address: '',
+ message: '',
+ signature: '',
+ showActionSheetWithOptions,
+ };
+ }
+
+ _onMoreOptions = () => {
+ const { showActionSheetWithOptions } = this.state;
+
+ const actionMenu = new VerifyMessageActionMenu({
+ showActionSheetWithOptions,
+ onParseClipboard: this._parseClipboard,
+ });
+
+ actionMenu.show();
+ };
+
+ _parseClipboard = async () => {
+ const coinTitle = this.coinid.coinTitle.toUpperCase();
+
+ const re = new RegExp(
+ `-----BEGIN ${coinTitle} SIGNED MESSAGE-----\n(.*?)\n-----BEGIN SIGNATURE-----\n(?!-----BEGIN SIGNATURE-----)([^\n]*)\n([^\n]*)\n-----END ${coinTitle} SIGNED MESSAGE-----`,
+ 's',
+ );
+
+ try {
+ const clipboardData = await Clipboard.getString();
+ const [, message, address, signature] = re.exec(clipboardData);
+
+ this.setState({
+ message,
+ address,
+ signature,
+ });
+ } catch (err) {
+ Alert.alert(
+ 'Parsing error',
+ 'Could not parse clipboard data, make sure it is formatted correctly.',
+ );
+ }
+ };
+
+ _verifyMessage = () => {
+ const { message, address, signature } = this.state;
+ const { dialogCloseAndClear } = this.context;
+
+ try {
+ const verify = this.coinid.verifyMessage(message, address, signature);
+
+ if (verify) {
+ Alert.alert('Verify message', `Message verified to be from ${address}`);
+ dialogCloseAndClear();
+ }
+ } catch (err) {
+ Alert.alert('Verification error', `${err}`);
+ }
+ };
+
+ render() {
+ const { dialogRef } = this.props;
+ const { message, address, signature } = this.state;
+
+ return (
+
+ {
+ this.refContHeight = e.nativeEvent.layout.height;
+ }}
+ >
+ {
+ dialogRef._setKeyboardOffset(this.refAddressBottom - this.refContHeight + 8);
+ }}
+ onLayout={(e) => {
+ this.refAddressBottom = e.nativeEvent.layout.y + e.nativeEvent.layout.height;
+ }}
+ >
+ Address
+
+ {
+ this.setState({ address: newAddress });
+ }}
+ underlineColorAndroid="transparent"
+ />
+
+
+
+ {
+ dialogRef._setKeyboardOffset(this.refMessageBottom - this.refContHeight + 8);
+ }}
+ onLayout={(e) => {
+ this.refMessageBottom = e.nativeEvent.layout.y + e.nativeEvent.layout.height;
+ }}
+ >
+ Message
+
+ {
+ this.setState({ message: newMessage });
+ }}
+ ref={(c) => {
+ this.messageRef = c;
+ }}
+ underlineColorAndroid="transparent"
+ />
+
+
+
+ {
+ dialogRef._setKeyboardOffset(this.refSignatureBottom - this.refContHeight + 8);
+ }}
+ onLayout={(e) => {
+ this.refSignatureBottom = e.nativeEvent.layout.y + e.nativeEvent.layout.height;
+ }}
+ >
+ Signature
+
+ {
+ this.setState({ signature: newSignature });
+ }}
+ underlineColorAndroid="transparent"
+ />
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/libs/coinid-public/index.js b/src/libs/coinid-public/index.js
index 3d4275f..e301932 100644
--- a/src/libs/coinid-public/index.js
+++ b/src/libs/coinid-public/index.js
@@ -25,6 +25,8 @@ import {
} from './utils';
const bitcoin = require('bitcoinjs-lib');
+const bitcoinMessage = require('bitcoinjs-message');
+
const bip32utils = require('./bip32-utils-extension');
const Blockchain = require('./blockchain');
@@ -810,6 +812,20 @@ class COINiDPublic extends EventEmitter {
}
};
+ verifyMessage = (message, address, signature) => {
+ try {
+ const verify = bitcoinMessage.verify(message, address, signature, this.network);
+
+ if (!verify) {
+ throw 'Message could not be verified with the supplied signature and address';
+ }
+
+ return true;
+ } catch (err) {
+ throw err;
+ }
+ };
+
saveAll = () => Promise.all([
this.storage.set('account', this.account),
this.storage.set('pubKeyData', this.pubKeyData),
diff --git a/src/routes/dialogs.js b/src/routes/dialogs.js
index dd3b584..45e69b9 100644
--- a/src/routes/dialogs.js
+++ b/src/routes/dialogs.js
@@ -11,6 +11,8 @@ import Receive from '../dialogs/Receive';
import TransactionDetails from '../dialogs/TransactionDetails';
import Send from '../dialogs/Send';
import Sign from '../dialogs/Sign';
+import SignMessage from '../dialogs/SignMessage';
+import VerifyMessage from '../dialogs/VerifyMessage';
export const dialogRoutes = {
COINiDNotFound: {
@@ -93,4 +95,21 @@ export const dialogRoutes = {
avoidKeyboard: true,
},
},
+ SignMessage: {
+ DialogComponent: SignMessage,
+ defaultProps: {
+ title: 'Sign message',
+ verticalPosition: 'flex-end',
+ avoidKeyboard: true,
+ },
+ },
+ VerifyMessage: {
+ DialogComponent: VerifyMessage,
+ defaultProps: {
+ title: 'Verify message',
+ verticalPosition: 'flex-end',
+ avoidKeyboard: true,
+ showMoreOptions: true,
+ },
+ },
};
diff --git a/src/routes/settings.js b/src/routes/settings.js
index 381ea55..f9e7632 100644
--- a/src/routes/settings.js
+++ b/src/routes/settings.js
@@ -24,6 +24,10 @@ export const settingRoutes = {
screen: SettingsRoute,
title: 'Remove account',
},
+ SignMessage: {
+ screen: SettingsRoute,
+ title: 'Sign message',
+ },
About: {
screen: SettingsRoute,
title: 'About',
diff --git a/src/screens/Home.js b/src/screens/Home.js
index 8dee908..0b644a6 100644
--- a/src/screens/Home.js
+++ b/src/screens/Home.js
@@ -14,7 +14,6 @@ import { Text } from '../components';
import { Wallet } from '.';
import StatusBoxContext from '../contexts/StatusBoxContext';
import DialogBoxContext from '../contexts/DialogBoxContext';
-
import COINiDPublic from '../libs/coinid-public';
import storageHelper from '../utils/storageHelper';
@@ -133,6 +132,15 @@ class Home extends PureComponent {
theme: 'light',
dotColor: colors.getHot(),
settingHelper: this.settingHelper,
+ snapTo: () => {
+ this._snapToItem(0);
+ },
+ openSignMessage: () => {
+ this._openSignMessage(0);
+ },
+ openVerifyMessage: () => {
+ this._openVerifyMessage(0);
+ },
},
{
coinid: this.coldCOINiD,
@@ -141,6 +149,15 @@ class Home extends PureComponent {
theme: 'dark',
dotColor: colors.getCold(),
settingHelper: this.settingHelper,
+ snapTo: () => {
+ this._snapToItem(1);
+ },
+ openSignMessage: () => {
+ this._openSignMessage(1);
+ },
+ openVerifyMessage: () => {
+ this._openVerifyMessage(1);
+ },
},
];
@@ -234,7 +251,7 @@ class Home extends PureComponent {
_renderItem = ({ item, index }) => {
const { navigation } = this.props;
- const { hideSensitive } = this.state;
+ const { slides, hideSensitive } = this.state;
return (
@@ -264,6 +281,18 @@ class Home extends PureComponent {
});
};
+ _openSignMessage = (index) => {
+ if (this.walletComponents[index] && this.walletComponents[index]._openSignMessage) {
+ this.walletComponents[index]._openSignMessage();
+ }
+ };
+
+ _openVerifyMessage = (index) => {
+ if (this.walletComponents[index] && this.walletComponents[index]._openVerifyMessage) {
+ this.walletComponents[index]._openVerifyMessage();
+ }
+ };
+
_onSnapToItem = (index) => {
this.pagination.setActiveDotIndex(index);
this._updateActiveTitle(index);
@@ -285,6 +314,10 @@ class Home extends PureComponent {
}
};
+ _snapToItem = (index) => {
+ this.carusel.snapToItem(index);
+ };
+
_onWalletReset = (index) => {
this.walletComponents[index]._checkAccount();
this.carusel.snapToItem(index);
diff --git a/src/screens/InstalledWallet.js b/src/screens/InstalledWallet.js
index e902bdf..a8d257f 100644
--- a/src/screens/InstalledWallet.js
+++ b/src/screens/InstalledWallet.js
@@ -9,7 +9,6 @@ import { getBottomSpace } from 'react-native-iphone-x-helper';
import {
BatchSummary, ConnectionStatus, Balance, TransactionList, Text,
} from '../components';
-import { Sign } from '../dialogs';
import projectSettings from '../config/settings';
import { colors } from '../config/styling';
@@ -222,6 +221,16 @@ class InstalledWallet extends PureComponent {
);
};
+ _openSignMessage = () => {
+ const { dialogNavigate } = this.context;
+ dialogNavigate('SignMessage', {}, this.context);
+ };
+
+ _openVerifyMessage = () => {
+ const { dialogNavigate } = this.context;
+ dialogNavigate('VerifyMessage', {}, this.context);
+ };
+
_openSend = () => {
const { dialogNavigate } = this.context;
const { balance } = this.state;
diff --git a/src/screens/Wallet.js b/src/screens/Wallet.js
index 13236c1..c6d69cf 100644
--- a/src/screens/Wallet.js
+++ b/src/screens/Wallet.js
@@ -77,6 +77,9 @@ class Wallet extends PureComponent {
return (
{
+ this.walletRef = c;
+ }}
settingHelper={this.settingHelper}
navigation={navigation}
hideSensitive={hideSensitive}
@@ -97,6 +100,18 @@ class Wallet extends PureComponent {
}
};
+ _openSignMessage = () => {
+ if (this.walletRef && this.walletRef._openSignMessage) {
+ this.walletRef._openSignMessage();
+ }
+ };
+
+ _openVerifyMessage = () => {
+ if (this.walletRef && this.walletRef._openVerifyMessage) {
+ this.walletRef._openVerifyMessage();
+ }
+ };
+
_checkAccount = (hasBeenSetup) => {
this.coinid
.getAccount()
diff --git a/src/settingstree/Home.js b/src/settingstree/Home.js
index 7beee5e..36047ec 100644
--- a/src/settingstree/Home.js
+++ b/src/settingstree/Home.js
@@ -51,7 +51,14 @@ const getPasscodeTimingTitle = (state) => {
const Home = (state) => {
const {
- gotoRoute, hasCOINiD, hasHotWallet, hasAnyWallets, settingHelper, settings,
+ gotoRoute,
+ hasCOINiD,
+ hasHotWallet,
+ hasAnyWallets,
+ settingHelper,
+ settings,
+ activeWallets,
+ goBack,
} = state;
return [
@@ -89,6 +96,42 @@ const Home = (state) => {
onPress: () => gotoRoute('PreferredCurrency'),
rightTitle: `${getPreferredCurrencyTitle(state)}`,
},
+ ],
+ },
+ {
+ items: [
+ {
+ title: 'Sign message',
+ onPress: () => {
+ if (activeWallets.length === 1) {
+ const [{ snapTo, openSignMessage }] = activeWallets;
+
+ goBack();
+ snapTo();
+ openSignMessage();
+ } else {
+ gotoRoute('SignMessage');
+ }
+ },
+ disabled: !activeWallets.length,
+ },
+ {
+ title: 'Verify message',
+ onPress: () => {
+ if (activeWallets.length > 0) {
+ const [{ snapTo, openVerifyMessage }] = activeWallets;
+
+ goBack();
+ snapTo();
+ openVerifyMessage();
+ }
+ },
+ disabled: !activeWallets.length,
+ },
+ ],
+ },
+ {
+ items: [
{
title: 'Remove account',
onPress: () => gotoRoute('Reset'),
diff --git a/src/settingstree/SignMessage.js b/src/settingstree/SignMessage.js
new file mode 100644
index 0000000..2cf60f7
--- /dev/null
+++ b/src/settingstree/SignMessage.js
@@ -0,0 +1,35 @@
+const SignMessage = (state) => {
+ const { activeWallets, goBack } = state;
+
+ const items = activeWallets.map(({ title, snapTo, openSignMessage }) => ({
+ title: `Sign message with ${title.toLowerCase()} wallet account`,
+ onPress: () => {
+ goBack();
+ snapTo();
+ openSignMessage();
+ },
+ hideChevron: true,
+ }));
+
+ if (items.length > 0) {
+ return [
+ {
+ items,
+ listHint: 'Select which account you want to use to sign a message.',
+ },
+ ];
+ }
+
+ return [
+ {
+ items: {
+ title: 'No wallets installed to sign with...',
+ onPress: () => {},
+ hideChevron: true,
+ },
+ listHint: 'You have not installed any wallets.',
+ },
+ ];
+};
+
+export default SignMessage;
diff --git a/src/settingstree/index.js b/src/settingstree/index.js
index 8579e56..602b45c 100644
--- a/src/settingstree/index.js
+++ b/src/settingstree/index.js
@@ -3,6 +3,7 @@ import Passcode from './Passcode';
import OfflineTransport from './OfflineTransport';
import PreferredCurrency from './PreferredCurrency';
import Reset from './Reset';
+import SignMessage from './SignMessage';
import About from './About';
const SettingsTree = state => ({
@@ -11,6 +12,7 @@ const SettingsTree = state => ({
OfflineTransport: OfflineTransport(state),
PreferredCurrency: PreferredCurrency(state),
Reset: Reset(state),
+ SignMessage: SignMessage(state),
About: About(state),
});
diff --git a/yarn.lock b/yarn.lock
index cc9f736..f9ccef2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1504,7 +1504,7 @@ bitcoin-ops@^1.3.0:
"bitcoinjs-lib@https://github.com/wlc-/bitcoinjs-lib":
version "3.3.0"
- resolved "https://github.com/wlc-/bitcoinjs-lib#5b8af6472d7e24f93aa70a1db79bd360e5077a55"
+ resolved "https://github.com/wlc-/bitcoinjs-lib#12fd8b3159da10f907e6f19b25dade5ec04f2da7"
dependencies:
bech32 "0.0.3"
bigi "^1.4.0"
@@ -1523,14 +1523,12 @@ bitcoin-ops@^1.3.0:
varuint-bitcoin "^1.0.4"
wif "https://github.com/COINiD/wif.git"
-bitcoinjs-message@^2.0.0:
+"bitcoinjs-message@https://github.com/COINiD/bitcoinjs-message.git#coinid-version":
version "2.0.0"
- resolved "https://registry.yarnpkg.com/bitcoinjs-message/-/bitcoinjs-message-2.0.0.tgz#e285d223607dabf2b33a6ee486a223b59d1b1548"
- integrity sha512-H5pJC7/eSqVjREiEOZ4jifX+7zXYP3Y28GIOIqg9hrgE7Vj8Eva9+HnVqnxwA1rJPOwZKuw0vo6k0UxgVc6q1A==
+ resolved "https://github.com/COINiD/bitcoinjs-message.git#45ca80585b5ef75d90143171ecb7445f8c355605"
dependencies:
- bs58check "^2.0.2"
+ bitcoinjs-lib "https://github.com/wlc-/bitcoinjs-lib"
buffer-equals "^1.0.3"
- create-hash "^1.1.2"
secp256k1 "^3.0.1"
varuint-bitcoin "^1.0.1"
@@ -1672,7 +1670,7 @@ bs58@^4.0.0:
dependencies:
base-x "^3.0.2"
-bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.0.2:
+bs58check@<3.0.0, bs58check@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==
@@ -1929,7 +1927,7 @@ code-point-at@^1.0.0:
"coinid-address-functions@https://github.com/wlc-/coinid-address-functions.git":
version "1.0.0"
- resolved "https://github.com/wlc-/coinid-address-functions.git#f97e21ff3bc8b78b98754455eaff885c0196784f"
+ resolved "https://github.com/wlc-/coinid-address-functions.git#def0ce0020c4cdf15b46ba5358451086e4dc6fd6"
"coinid-address-types@https://github.com/wlc-/coinid-address-types.git":
version "1.0.0"
@@ -5600,7 +5598,12 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
-nan@^2.2.1, nan@^2.9.2:
+nan@^2.2.1:
+ version "2.13.2"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
+ integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
+
+nan@^2.9.2:
version "2.13.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd"
integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA==