Skip to content

Commit

Permalink
use staking key for voting rewards (#1276)
Browse files Browse the repository at this point in the history
* use staking key for voting rewards

* add warning dialog for wallets not delegating

* add unit test for Catalyst registration metadata [ch8755]

* bump Cardano libs

* improve typing and allow for repeatable registration metadata
  • Loading branch information
v-almonacid committed May 5, 2021
1 parent 2177abe commit 3219ac9
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 42 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@cardano-foundation/ledgerjs-hw-app-cardano": "2.2.1",
"@emurgo/cip14-js": "2.0.0",
"@emurgo/cip4-js": "1.0.5",
"@emurgo/react-native-haskell-shelley": "1.1.3",
"@emurgo/react-native-haskell-shelley": "1.1.4",
"@ledgerhq/react-native-hw-transport-ble": "5.41.0",
"@react-native-async-storage/async-storage": "1.13.3",
"@react-native-community/masked-view": "0.1.10",
Expand Down
28 changes: 19 additions & 9 deletions src/actions/voting.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// @flow
import type {Dispatch} from 'redux'
import cryptoRandomString from 'crypto-random-string'
import {PrivateKey} from '@emurgo/react-native-haskell-shelley'

import {encryptWithPassword} from '../crypto/catalystCipher'
import {generatePrivateKeyForCatalyst} from '../crypto/shelley/catalystUtils'
import walletManager from '../crypto/walletManager'
import {Logger} from '../utils/logging'
import type {RawUtxo} from '../api/types'
import {CONFIG} from '../config/config'

import type {RawUtxo} from '../api/types'
import type {State} from '../state'

const _setCatalystKeys = (voting) => ({
Expand All @@ -30,23 +30,30 @@ export const generateVotingKeys = () => async (
_getState: () => State,
) => {
Logger.debug('voting actions::generateVotingKeys')
const pin = cryptoRandomString({length: 4, type: 'numeric'})
let pin
if (CONFIG.DEBUG.PREFILL_FORMS) {
if (!__DEV__) throw new Error('using debug data in non-dev env')
pin = CONFIG.DEBUG.CATALYST_PIN
} else {
pin = cryptoRandomString({length: 4, type: 'numeric'})
}

const pinArray = pin.split('').map(Number)

const passBuff = Buffer.from(pinArray)
const rootKey = await generatePrivateKeyForCatalyst()
const key = await encryptWithPassword(
const catalystEncryptedPrivateKey = await encryptWithPassword(
passBuff,
await (await rootKey.to_raw_key()).as_bytes(),
)

dispatch(
await dispatch(
_setCatalystKeys({
pin: pinArray,
encryptedKey: key,
catalystPrivateKey: await PrivateKey.from_extended_bytes(
encryptedKey: catalystEncryptedPrivateKey,
catalystPrivateKey: Buffer.from(
await (await rootKey.to_raw_key()).as_bytes(),
),
).toString('hex'),
}),
)
}
Expand All @@ -55,13 +62,16 @@ export const generateVotingTransaction = (
decryptedKey: string,
utxos: Array<RawUtxo>,
) => async (dispatch: Dispatch<any>, getState: () => State) => {
const catalystPrivateKey = getState().voting.catalystPrivateKey
Logger.debug('voting actions::generateVotingTransaction')
const catalystPrivateKey: ?string = getState().voting.catalystPrivateKey
const serverTime: Date | void = getState().serverStatus.serverTime

if (catalystPrivateKey) {
const signRequest = await walletManager.createVotingRegTx(
utxos,
catalystPrivateKey,
decryptedKey,
serverTime,
)
dispatch(_setUnSignedTx(signRequest))
} else {
Expand Down
49 changes: 44 additions & 5 deletions src/components/Catalyst/Step1.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Step 1 for the Catalyst registration - landing
*/

import React, {useEffect} from 'react'
import React, {useEffect, useState} from 'react'
import {
View,
ScrollView,
Expand All @@ -19,12 +19,14 @@ import {connect} from 'react-redux'
import {generateVotingKeys} from '../../actions/voting'
import {fetchUTXOs} from '../../actions/utxo'
import {Text, Button, ProgressStep} from '../UiKit'
import StandardModal from '../Common/StandardModal'
import {withTitle} from '../../utils/renderUtils'
import {CATALYST_ROUTES} from '../../RoutesList'
import globalMessages, {confirmationMessages} from '../../i18n/global-messages'
import AppDownload from '../../assets/img/pic-catalyst-step1.png'
import playstoreBadge from '../../assets/img/google-play-badge.png'
import appstoreBadge from '../../assets/img/app-store-badge.png'
import {isDelegatingSelector} from '../../selectors'

import styles from './styles/Step1.style'

Expand All @@ -39,6 +41,13 @@ const messages = defineMessages({
defaultMessage:
'!!!Before you begin, make sure to download the Catalyst Voting App.',
},
stakingKeyNotRegistered: {
id: 'components.catalyst.step1.stakingKeyNotRegistered',
defaultMessage:
'!!!Catalyst voting rewards are sent to delegation accounts and your ' +
'wallet does not seem to have a registered delegation certificate. If ' +
'you want to receive voting rewards, you need to delegate your funds first.',
},
tip: {
id: 'components.catalyst.step1.tip',
defaultMessage:
Expand All @@ -47,7 +56,21 @@ const messages = defineMessages({
},
})

const Step1 = ({intl, generateVotingKeys, navigation, fetchUTXOs}) => {
const WarningModalBody = ({intl}) => (
<View>
<Text>{intl.formatMessage(messages.stakingKeyNotRegistered)}</Text>
</View>
)

const Step1 = ({
intl,
generateVotingKeys,
navigation,
fetchUTXOs,
isDelegating,
}) => {
const [showModal, setShowModal] = useState<boolean>(!isDelegating)

useEffect(() => {
fetchUTXOs()
generateVotingKeys()
Expand Down Expand Up @@ -98,27 +121,43 @@ const Step1 = ({intl, generateVotingKeys, navigation, fetchUTXOs}) => {
)}
/>
</View>
<StandardModal
visible={showModal}
title={intl.formatMessage(globalMessages.attention)}
children={<WarningModalBody intl={intl} />}
onRequestClose={() => setShowModal(false)}
primaryButton={{
label: intl.formatMessage(
confirmationMessages.commonButtons.iUnderstandButton,
),
onPress: () => setShowModal(false),
}}
showCloseIcon
/>
</SafeAreaView>
)
}

type ExternalProps = {|
type Props = {|
navigation: Navigation,
route: Object, // TODO(navigation): type
intl: IntlShape,
generateVotingKeys: () => void,
fetchUTXOs: () => Promise<void>,
isDelegating: boolean,
|}

export default injectIntl(
connect(
(_state) => ({}),
(state) => ({
isDelegating: isDelegatingSelector(state),
}),
{
generateVotingKeys,
fetchUTXOs,
},
)(
withTitle((Step1: ComponentType<ExternalProps>), ({intl}) =>
withTitle((Step1: ComponentType<Props>), ({intl}) =>
intl.formatMessage(globalMessages.votingTitle),
),
),
Expand Down
1 change: 1 addition & 0 deletions src/components/Catalyst/Step2.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {Text, Button, ProgressStep} from '../UiKit'
import {withTitle} from '../../utils/renderUtils'
import {CATALYST_ROUTES} from '../../RoutesList'
import globalMessages, {confirmationMessages} from '../../i18n/global-messages'

import styles from './styles/Step2.style'

import type {ComponentType} from 'react'
Expand Down
10 changes: 9 additions & 1 deletion src/components/Catalyst/Step5.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ const Step5 = ({
const [fees, setFees] = useState(null)

useEffect(() => {
if (unSignedTx == null) {
// note: should never happen
throw new Error('Could not retrieve transaction data')
}
unSignedTx.fee().then((o) => {
setFees(o.getDefault())
})
Expand All @@ -102,6 +106,10 @@ const Step5 = ({
const submitTrx = async (decryptedKey) => {
setSendingTransaction(true)
try {
if (unSignedTx == null) {
// note: should never happen
throw new Error('Could not retrieve transaction data')
}
await submitTransaction(unSignedTx, decryptedKey)
} catch (error) {
// error is used where submitTx is used
Expand Down Expand Up @@ -240,7 +248,7 @@ type ExternalProps = {|
intl: IntlShape,
isEasyConfirmationEnabled: boolean,
submitTransaction: (ISignRequest<any>, string) => void,
unSignedTx: ISignRequest<any>,
unSignedTx: ?ISignRequest<any>,
defaultAsset: DefaultAsset,
|}

Expand Down
2 changes: 2 additions & 0 deletions src/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export const CONFIG = {
PUB_KEY:
'42cfdc53da2220ba52ce62f8e20ab9bb99857a3fceacf43d676d7987ad909b53' +
'ed75534e0d0ee8fce835eb2e7c67c5caec18a9c894388d9a046380edebbfc46d',
CATALYST_PIN: '1234',
CATALYST_NONCE: 1234,
},
E2E: {
// WARNING: NEVER change these flags here, use .env.e2e
Expand Down
1 change: 1 addition & 0 deletions src/crypto/WalletInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export interface WalletInterface {
utxos: Array<RawUtxo>,
catalystPrivateKey: string,
decryptedKey: string,
serverTime: Date | void,
): Promise<ISignRequest<T>>;

createWithdrawalTx<T>(
Expand Down
20 changes: 15 additions & 5 deletions src/crypto/shelley/ShelleyWallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
make_vkey_witness,
/* eslint-enable camelcase */
PublicKey,
PrivateKey,
RewardAddress,
StakeCredential,
Transaction,
Expand Down Expand Up @@ -608,33 +609,42 @@ export default class ShelleyWallet extends Wallet implements WalletInterface {

async createVotingRegTx(
utxos: Array<RawUtxo>,
catalystPrivateKey: string,
catalystKey: string,
decryptedKey: string,
serverTime: Date | void,
): Promise<ISignRequest<TransactionBuilder>> {
Logger.debug('ShelleyWallet::createVotingRegTx called')
const timeToSlotFn = await genTimeToSlot(getCardanoBaseConfig())
const absSlotNumber = new BigNumber(timeToSlotFn({time: new Date()}).slot)
const time = serverTime !== undefined ? serverTime : new Date()
const absSlotNumber = new BigNumber(timeToSlotFn({time}).slot)

const changeAddr = await this._getAddressedChangeAddress()
const addressedUtxos = this.asAddressedUtxo(utxos)

const masterKey = await Bip32PrivateKey.from_bytes(
Buffer.from(decryptedKey, 'hex'),
)

const catalystPrivateKey = await PrivateKey.from_extended_bytes(
Buffer.from(catalystKey, 'hex'),
)

const accountPvrKey: Bip32PrivateKey = await (await (await masterKey.derive(
this._getPurpose(),
)).derive(CONFIG.NUMBERS.COIN_TYPES.CARDANO)).derive(
0 + CONFIG.NUMBERS.HARD_DERIVATION_START,
)

const stakePrivateKey = await (await (await accountPvrKey.derive(
const stakePrivateKey: PrivateKey = await (await (await accountPvrKey.derive(
CONFIG.NUMBERS.CHAIN_DERIVATIONS.CHIMERIC_ACCOUNT,
)).derive(CONFIG.NUMBERS.STAKING_KEY_INDEX)).to_raw_key()

const rewardAddress = await this.getRewardAddress()

const metadata = await catalystUtils.generateRegistration({
stakePrivateKey,
catalystPrivateKey,
receiverAddress: Buffer.from(changeAddr.address, 'hex'),
rewardAddress,
absSlotNumber: absSlotNumber.toNumber(),
})

Expand Down Expand Up @@ -663,7 +673,7 @@ export default class ShelleyWallet extends Wallet implements WalletInterface {
[], // no delegations
[], // no withdrawals
false,
metadata, // metadata
metadata,
)

const signRequest = new HaskellShelleyTxSignRequest(
Expand Down

0 comments on commit 3219ac9

Please sign in to comment.