Skip to content

Commit

Permalink
Merge pull request #30 from celo-tools/1.0.1-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy authored Mar 8, 2021
2 parents f4f5b60 + 38eebaf commit b030701
Show file tree
Hide file tree
Showing 16 changed files with 127 additions and 51 deletions.
1 change: 0 additions & 1 deletion .github/workflows/bundle-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,5 @@ jobs:
name: celowallet-artifacts
path: |
dist-electron/*-mac*.dmg
dist-electron/*-mac*.zip
dist-electron/*-linux*.AppImage
dist-electron/*-win*.exe
1 change: 0 additions & 1 deletion electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ mac:
entitlementsInherit: electron/build/mac/entitlements.plist
target:
- dmg
- zip

linux:
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
Expand Down
2 changes: 1 addition & 1 deletion package-electron.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "celo-web-wallet",
"version": "1.0.0",
"version": "1.0.1",
"description": "A lightweight web and desktop wallet for the Celo network",
"main": "main.js",
"keywords": [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "celo-web-wallet",
"version": "1.0.0",
"version": "1.0.1",
"description": "A lightweight web and desktop wallet for the Celo network",
"keywords": [
"Celo",
Expand Down
3 changes: 2 additions & 1 deletion src/components/modal/useNavHintModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function useNavHintModal(
body: string,
navLabel: string,
navTarget: string,
navState?: Record<string, unknown>,
onClose?: () => void
) {
const navigate = useNavigate()
Expand All @@ -29,7 +30,7 @@ export function useNavHintModal(
color: Color.altGrey,
}
const onActionClick = (action: ModalAction) => {
if (action.key === 'nav') navigate(navTarget)
if (action.key === 'nav') navigate(navTarget, navState ? { state: navState } : undefined)
if (onClose) onClose()
closeModal()
}
Expand Down
2 changes: 1 addition & 1 deletion src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const ACCOUNT_UNLOCK_TIMEOUT = 600000 // 10 minutes
export const BALANCE_STALE_TIME = 15000 // 15 seconds
export const GAS_PRICE_STALE_TIME = 10000 // 10 seconds
export const EXCHANGE_RATE_STALE_TIME = 15000 // 15 seconds
export const ACCOUNT_STATUS_STALE_TIME = 86400000 // 1 day
export const ACCOUNT_STATUS_STALE_TIME = 43200000 // 12 hours
export const VALIDATOR_LIST_STALE_TIME = 86400000 // 1 day
export const VALIDATOR_VOTES_STALE_TIME = 300000 // 5 minutes
export const VALIDATOR_ACTIVATABLE_STALE_TIME = 43200000 // 12 hours
Expand Down
11 changes: 7 additions & 4 deletions src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { HeaderSection } from 'src/features/home/HeaderSection'
import { HeaderSectionEmpty } from 'src/features/home/HeaderSectionEmpty'
import { toggleHomeHeaderDismissed } from 'src/features/settings/settingsSlice'
import { PriceChartCelo } from 'src/features/tokenPrice/PriceChartCelo'
import { StakeActionType } from 'src/features/validators/types'
import { dismissActivatableReminder } from 'src/features/validators/validatorsSlice'
import { useAreBalancesEmpty } from 'src/features/wallet/utils'
import { Color } from 'src/styles/Color'
Expand Down Expand Up @@ -61,16 +62,18 @@ export function HomeScreen() {
// END PIN MIGRATION

// Detect if user has unactivated staking votes
const { status: hasActivatableVotes, reminderDismissed: votesReminderDismissed } = useSelector(
(state: RootState) => state.validators.hasActivatable
)
const showActivateModal = hasActivatableVotes && !votesReminderDismissed
const hasActivatable = useSelector((state: RootState) => state.validators.hasActivatable)
const showActivateModal =
hasActivatable.status &&
hasActivatable.groupAddresses.length &&
!hasActivatable.reminderDismissed
useNavHintModal(
showActivateModal,
'Activate Your Votes!',
'You have pending validator votes that are ready to be activated. They must be activated to start earning staking rewards.',
'Activate',
'/stake',
{ groupAddress: hasActivatable.groupAddresses[0], action: StakeActionType.Activate },
() => {
dispatch(dismissActivatableReminder())
}
Expand Down
22 changes: 14 additions & 8 deletions src/features/home/Tips.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const Tips = [
import { config } from 'src/config'

const Tips = [
[
'Transaction fees can be paid in any currency but they are smaller when paid with CELO.',
'Consider keeping some CELO in your account to pay for fees. It will be used by default.',
Expand All @@ -17,20 +19,24 @@ export const Tips = [
],
[
'Your wallet can be imported in many places at once.',
'For example, use your Account Key to load it into the Valora mobile app.',
],
[
'Using this wallet in a browser is only safe for small accounts or Ledger users.',
'For large accounts, downloading the Desktop App is strongly recommended.',
'For example, you can import your Account Key into the Valora mobile app.',
],
[
'You can lock CELO to participate in Celo network elections and governance.',
'Voting for validators that are elected will earn you free CELO rewards.',
],
]

const WebTips = [
...Tips,
[
'Using this wallet in a browser is only safe for small accounts or Ledger users.',
'For large accounts, downloading the Desktop App is strongly recommended.',
],
]

export function useDailyTip() {
// TODO save the starting date in storage so tips can be cycled through in order from the first
const tips = config.isElectron ? Tips : WebTips
const date = new Date().getDate()
return Tips[date % Tips.length]
return tips[date % tips.length]
}
2 changes: 1 addition & 1 deletion src/features/ledger/LedgerSigner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'src/features/ledger/buffer' // Must be the first import
import { CeloTransactionRequest, serializeCeloTransaction } from '@celo-tools/celo-ethers-wrapper'
import { TransportError, TransportStatusError } from '@ledgerhq/errors'
import { BigNumber, providers, Signer, utils } from 'ethers'
import { config } from 'src/config'
import { CELO_LEDGER_APP_MIN_VERSION } from 'src/consts'
import 'src/features/ledger/buffer' // Must be the first import
import { CeloLedgerApp } from 'src/features/ledger/CeloLedgerApp'
import { getLedgerTransport } from 'src/features/ledger/ledgerTransport'
import { getTokenData } from 'src/features/ledger/tokenData'
Expand Down
3 changes: 3 additions & 0 deletions src/features/lock/LockFormScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function LockFormScreen() {
handleSubmit,
setValues,
resetValues,
resetErrors,
} = useCustomForm<LockTokenForm>(getInitialValues(tx), onSubmit, validateForm)

// Keep form in sync with tx state
Expand All @@ -83,6 +84,7 @@ export function LockFormScreen() {
autoSetAmount = '0'
}
setValues({ ...values, [name]: value, amount: autoSetAmount })
resetErrors()
}

const onUseMax = () => {
Expand All @@ -96,6 +98,7 @@ export function LockFormScreen() {
maxAmount = fromWeiRounded(pendingFree, Currency.CELO, true)
}
setValues({ ...values, amount: maxAmount })
resetErrors()
}

const summaryData = useMemo(() => getSummaryChartData(balances), [balances])
Expand Down
60 changes: 49 additions & 11 deletions src/features/lock/lockToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import { getTotalUnlockedCelo } from 'src/features/lock/utils'
import { LockTokenTx, LockTokenType, TransactionType } from 'src/features/types'
import { GroupVotes } from 'src/features/validators/types'
import { getTotalNonvotingLocked } from 'src/features/validators/utils'
import { createAccountRegisterTx } from 'src/features/wallet/accountsContract'
import { createAccountRegisterTx, fetchAccountStatus } from 'src/features/wallet/accountsContract'
import { fetchBalancesActions, fetchBalancesIfStale } from 'src/features/wallet/fetchBalances'
import { Balances } from 'src/features/wallet/types'
import { setAccountIsRegistered } from 'src/features/wallet/walletSlice'
import {
areAmountsNearlyEqual,
BigNumberMin,
getAdjustedAmount,
validateAmount,
Expand All @@ -46,30 +47,43 @@ export function validate(
if (!amountInWei) {
errors = { ...errors, ...invalidInput('amount', 'Amount Missing') }
} else {
const { locked, pendingFree, pendingBlocked } = balances.lockedCelo
const adjustedBalances = { ...balances }
if (action === LockActionType.Lock) {
adjustedBalances.celo = getTotalUnlockedCelo(balances).toString()
} else if (action === LockActionType.Unlock) {
adjustedBalances.celo = balances.lockedCelo.locked
adjustedBalances.celo = locked
} else if (action === LockActionType.Withdraw) {
adjustedBalances.celo = balances.lockedCelo.pendingFree
adjustedBalances.celo = pendingFree
}
errors = {
...errors,
...validateAmount(amountInWei, Currency.CELO, adjustedBalances, undefined, MIN_LOCK_AMOUNT),
}

// Special case handling for withdraw which is confusing
if (
action === LockActionType.Withdraw &&
BigNumber.from(balances.lockedCelo.pendingFree).lte(0)
) {
if (action === LockActionType.Withdraw && BigNumber.from(pendingFree).lte(0)) {
errors = {
...errors,
...invalidInput('amount', 'No pending available to withdraw'),
}
}

// Special case handling for locking whole balance
if (action === LockActionType.Lock && !errors.amount) {
const remainingAfterPending = BigNumber.from(amountInWei).sub(pendingFree).sub(pendingBlocked)
if (
remainingAfterPending.gt(0) &&
(remainingAfterPending.gte(balances.celo) ||
areAmountsNearlyEqual(remainingAfterPending, balances.celo, Currency.CELO))
) {
errors = {
...errors,
...invalidInput('amount', 'Locking whole balance is not allowed'),
}
}
}

if (validateFee) {
errors = {
...errors,
Expand Down Expand Up @@ -112,11 +126,17 @@ function* lockToken(params: LockTokenParams) {
throw new Error('Fee estimates missing or do not match txPlan')
}

const { txPlan: txPlanAdjusted, feeEstimates: feeEstimatesAdjusted } = yield* call(
ensureAccountNotAlreadyRegistered,
txPlan,
feeEstimates
)

logger.info(`Executing ${action} for ${amountInWei} CELO`)
yield* call<TxPlanExecutor<LockTokenTxPlanItem>>(
executeTxPlan,
txPlan,
feeEstimates,
txPlanAdjusted,
feeEstimatesAdjusted,
createActionTx,
createPlaceholderTx,
'lockToken'
Expand Down Expand Up @@ -190,7 +210,7 @@ export function getLockActionTxPlan(
let amountRemaining = BigNumber.from(amountInWei)
const pwSorted = pendingWithdrawals.sort((a, b) => b.index - a.index)
for (const p of pwSorted) {
if (amountRemaining.lte(MIN_LOCK_AMOUNT)) break
if (amountRemaining.lt(MIN_LOCK_AMOUNT)) break
const txAmount = BigNumberMin(amountRemaining, BigNumber.from(p.value))
const adjustedAmount = getAdjustedAmount(txAmount, p.value, Currency.CELO)
txs.push({
Expand Down Expand Up @@ -278,15 +298,33 @@ async function createWithdrawCeloTx(
}

async function ensureAccountNotGovernanceVoting() {
const governance = getContract(CeloContract.Governance)
const address = getSigner().signer.address
const governance = getContract(CeloContract.Governance)
const isVoting: boolean = await governance.isVoting(address)
if (isVoting)
throw new Error(
'Account has voted for an active governance proposal. You must wait until the proposal is done.'
)
}

// This is necessary in case the isAccountRegistered in state is
// stale or incorrect, which is rare but does happen
function* ensureAccountNotAlreadyRegistered(txPlan: LockTokenTxPlan, feeEstimates: FeeEstimate[]) {
if (txPlan[0].type !== TransactionType.AccountRegistration) {
// Not trying to register, no adjustments needed
return { txPlan, feeEstimates }
}

// Force fetch latest account status
const { isRegistered } = yield* call(fetchAccountStatus, true)
if (isRegistered) {
// Remove the account registration tx from plan and estimates
return { txPlan: txPlan.slice(1), feeEstimates: feeEstimates.slice(1) }
} else {
return { txPlan, feeEstimates }
}
}

export const {
name: lockTokenSagaName,
wrappedSaga: lockTokenSaga,
Expand Down
44 changes: 33 additions & 11 deletions src/features/validators/StakeFormScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { TxFlowTransaction, TxFlowType } from 'src/features/txFlow/types'
import { getResultChartData, getSummaryChartData } from 'src/features/validators/barCharts'
import { validate } from 'src/features/validators/stakeToken'
import {
GroupVotes,
stakeActionLabel,
StakeActionType,
StakeTokenParams,
Expand Down Expand Up @@ -78,11 +79,16 @@ export function StakeFormScreen() {
handleSubmit,
setValues,
resetValues,
} = useCustomForm<StakeTokenForm>(getInitialValues(location, tx), onSubmit, validateForm)
resetErrors,
} = useCustomForm<StakeTokenForm>(
getInitialValues(location, tx, groupVotes),
onSubmit,
validateForm
)

// Keep form in sync with tx state
useEffect(() => {
const initialValues = getInitialValues(location, tx)
const initialValues = getInitialValues(location, tx, groupVotes)
resetValues(initialValues)
// Ensure we have the info needed otherwise send user back
if (!groups || !groups.length) {
Expand Down Expand Up @@ -114,6 +120,7 @@ export function StakeFormScreen() {
autoSetAmount = '0'
}
setValues({ ...values, [name]: value, amount: autoSetAmount })
resetErrors()
}

const onUseMax = () => {
Expand All @@ -124,6 +131,7 @@ export function StakeFormScreen() {
values.groupAddress
)
setValues({ ...values, amount: fromWeiRounded(maxAmount, Currency.CELO, true) })
resetErrors()
}

const onGoBack = () => {
Expand Down Expand Up @@ -270,17 +278,31 @@ function HelpModal() {
)
}

function getInitialValues(location: Location<any>, tx: TxFlowTransaction | null): StakeTokenForm {
const groupAddress = location?.state?.groupAddress
const initialGroup = groupAddress && utils.isAddress(groupAddress) ? groupAddress : ''
if (!tx || !tx.params || tx.type !== TxFlowType.Stake) {
return {
...initialValues,
groupAddress: initialGroup,
}
} else {
function getInitialValues(
location: Location<any>,
tx: TxFlowTransaction | null,
groupVotes: GroupVotes
): StakeTokenForm {
if (tx && tx.params && tx.type === TxFlowType.Stake) {
return amountFieldFromWei(tx.params)
}

const initialAction = location?.state?.action ?? initialValues.action
const groupAddress = location?.state?.groupAddress
const initialGroup =
groupAddress && utils.isAddress(groupAddress) ? groupAddress : initialValues.groupAddress

// Auto use pending when defaulting to activate
const initialAmount =
groupAddress && groupVotes[groupAddress] && initialAction === StakeActionType.Activate
? fromWeiRounded(groupVotes[groupAddress].pending, Currency.CELO, true)
: initialValues.amount

return {
groupAddress: initialGroup,
action: initialAction,
amount: initialAmount,
}
}

function getSelectOptions(groups: ValidatorGroup[]) {
Expand Down
7 changes: 6 additions & 1 deletion src/features/validators/fetchGroupVotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ async function checkHasActivatable(groupVotes: GroupVotes, accountAddress: strin
status: false,
lastUpdated: Date.now(),
reminderDismissed: false,
groupAddresses: [],
}
}

Expand All @@ -84,11 +85,15 @@ async function checkHasActivatable(groupVotes: GroupVotes, accountAddress: strin
'hasActivatablePendingVotes',
groupAddrsAndAccount
)
const status = hasActivatable.some((v) => !!v)
if (groupsWithPending.length !== hasActivatable.length)
throw new Error('Groups, activatable lists size mismatch')
const groupToActivate = groupsWithPending.filter((v, i) => !!hasActivatable[i])
const status = groupToActivate.length > 0

return {
status,
lastUpdated: Date.now(),
reminderDismissed: false,
groupAddresses: groupToActivate,
}
}
Loading

0 comments on commit b030701

Please sign in to comment.