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==