Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fee estimation for Withdrawing CELO and pay for Withdrawing and Selling CELO fees in CELO #5345

Merged
merged 11 commits into from
Oct 20, 2020

Conversation

gnardini
Copy link
Contributor

@gnardini gnardini commented Oct 15, 2020

Description

Add a fee calculation for withdrawing CELO. This will be removed soonish when we do offline fee calculations (it's an epic in the upcoming milestone). This is meant as a quick fix in the meantime to not have a 0 hard-coded.
When withdrawing CELO or selling CELO. The fee is now paid for with CELO instead of cUSD.

I didn't add fee estimations for exchanging because it uses more than one transaction and made the calculation somewhat more complex, it will probably be improved doing offline estimations.

Tested

Tested it in an emulator, just looking at the estimate and paying attention to the exact CELO balance in the logs when doing different operations to see that the balance changes more than expected (meaning that gas was paid for in CELO) only when doing withdrawals and selling CELO.

Related issues


type Props = StackScreenProps<StackParamList, Screens.WithdrawCeloScreen>

const RANDOM_ADDRESS = '0xDCE9762d6C1fe89FF4f3857832131Ca18eE15C66'
Copy link
Contributor Author

@gnardini gnardini Oct 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have an address here yet to do the estimation, and estimating requires an address, so I used a random one. Could that cause any issues?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, we already do this here as well.

// TODO: Make it transparent for the user.
// TODO: Check for balance should be more than fee instead of zero.
const feeCurrency =
preferredFeeCurrency === CURRENCY_ENUM.DOLLAR && stableTokenBalance.isGreaterThan(0)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was working like this before and I was worried about changing it in case someone only has CELO and needs to pay for gas for something, but I would understand not wanting to have it anymore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this create a potential discrepancy between what we show users and what we actually do? For example, if someone has both cUSD and CELO, we will always show them the fee in CELO but then will always deduct their fee from their cUSD balance.

If my understanding is correct, I think we need to get rid of this. It looks like you've set the default currency = cUSD for the param in the relevant function calls but I'd definitely test it out to be safe.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, I totally misread this the first time. Sounds good!

Copy link
Contributor

@jeanregisser jeanregisser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! 👍

Comment on lines 142 to 145
const feeCurrencyAddress =
feeCurrency === CURRENCY_ENUM.DOLLAR
? yield call(getCurrencyAddress, CURRENCY_ENUM.DOLLAR)
: undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would be nice to add a small comment explaining it's normal that we pass undefined when the feeCurrency is CELO.

Comment on lines 60 to 78
useEffect(() => {
getSendFee(
accountAddress,
CURRENCY_ENUM.GOLD,
{
recipientAddress: RANDOM_ADDRESS,
amount: goldBalanceNumber,
comment: '',
},
false
)
.then((estimate: BigNumber) => {
setFeeEstimate(divideByWei(estimate))
})
.catch((error) => {
Logger.warn('WithdrawCeloScreen', `Error found while estimating gas fee: ${error}`)
})
}, [])

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async functions should be avoided in general in useEffect because they can trigger actions after the component has been unmounted.

useAsync from react-async-hook solves this.

Now we have CalculateFee which uses useAsync under the hood and mostly does what you want here.

I originally created it as a component because most components we had at the time, were class components.
But we could export a useSendFee hook from it now.

Added benefit is it provides loading/error states so we can display a spinner while it's calculating and it displays an error if it fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! I had run into this many times and always solved it in this inelegant way, the useAsync hook looks great.


type Props = StackScreenProps<StackParamList, Screens.WithdrawCeloScreen>

const RANDOM_ADDRESS = '0xDCE9762d6C1fe89FF4f3857832131Ca18eE15C66'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, we already do this here as well.

Copy link
Contributor

@nategraf nategraf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the parts that I am familiar with look good. I am less familiar with the react native components, so I am going to defer to @jeanregisser for final approval.

@@ -14,6 +14,7 @@ const SAMPLE_AMOUNT = new BigNumber(5.001)
const mockScreenProps = getMockStackScreenProps(Screens.WithdrawCeloReviewScreen, {
recipientAddress: SAMPLE_ADDRESS,
amount: SAMPLE_AMOUNT,
feeEstimate: new BigNumber(1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this 1 wei, and if so, is that displayed to the user at all? If the fee was actually 1 wei, it would likely not be significant enough to warrant mentioning (unless they are trying to withdraw 100% of their balance, in which case we might help craft a request that won't leave any dust behind.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing! The wei to CELO mapping happens in WithdrawCeloScreen, line 67. The converted fee is then passed to this review screen and this is a test of it where I put a != 0 number just to have something there.

Copy link
Contributor

@tarikbellamine tarikbellamine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! One comment I think needs to be addressed

function WithdrawCeloScreen({ navigation }: Props) {
const [accountAddress, setAccountAddress] = useState('')
const [celoInput, setCeloToTransfer] = useState('')

const goldBalance = useSelector((state) => state.goldToken.balance)
const goldBalanceNumber = new BigNumber(goldBalance || 0)
const goldBalanceNumber = useMemo(() => new BigNumber(goldBalance || 0), [goldBalance])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use useMemo here?

My understanding for the best use cases of this is for when there are expensive calculations involved, otherwise the storage costs are not worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, great catch, I'd like not to have it either, the problem is that we're creating a new BigNumber instance on each render and we pass this to the useSendFee hook, which uses it to see whether dependencies changed or not. If we pass different instances it assumes it changed and re-calculates the fee, which is undesired. The useMemo caches the instance of BigNumber so we use always the same unless the total balance changes for some reason.

Copy link
Contributor

@tarikbellamine tarikbellamine Oct 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's an interesting problem and a creative solution.

I don't feel strongly about doing this now but an alternative is to pass an int value instead and convert to BigNumber within the useSendFee function. If this would require a bunch of other changes, I'd understand if you'd rather not do it. But if that's the case, please leave a "to-do" so we fix this later.

As a side note, do you think we are having this issue in other places we are doing fee estimates?

Copy link
Contributor Author

@gnardini gnardini Oct 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed this comment somehow. I changed the amount to a string since I saw it was being transformed to string right after anyways.

I checked the other places but didn't find something like this happening

// TODO: Make it transparent for the user.
// TODO: Check for balance should be more than fee instead of zero.
const feeCurrency =
preferredFeeCurrency === CURRENCY_ENUM.DOLLAR && stableTokenBalance.isGreaterThan(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this create a potential discrepancy between what we show users and what we actually do? For example, if someone has both cUSD and CELO, we will always show them the fee in CELO but then will always deduct their fee from their cUSD balance.

If my understanding is correct, I think we need to get rid of this. It looks like you've set the default currency = cUSD for the param in the relevant function calls but I'd definitely test it out to be safe.

@gnardini
Copy link
Contributor Author

Doesn't this create a potential discrepancy between what we show users and what we actually do? For example, if someone has both cUSD and CELO, we will always show them the fee in CELO but then will always deduct their fee from their cUSD balance.

If my understanding is correct, I think we need to get rid of this. It looks like you've set the default currency = cUSD for the param in the relevant function calls but I'd definitely test it out to be safe.

Before this change we used cUSD if there was enough balance and CELO otherwise.

The new parameter preferredFeeCurrency is passed as GOLD for the two cases where we would prefer to pay for gas with CELO (withdrawing and selling CELO) and DOLLAR otherwise. In those two cases, the condition would evaluate to false and we would get a feeCurrency of GOLD so we'd pay for gas with CELO.

The condition is to maintain the logic we used to have for all other cases (where preferredFeeCurrency is DOLLAR). I just didn't want to risk forcing an operation to fail if the user has no cUSD if they have enough CELO to pay for gas. Maybe that's not what we want though, I'm just keeping what was already there for all cases except the two I mentioned.

Copy link
Contributor

@tarikbellamine tarikbellamine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Contributor

@jeanregisser jeanregisser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! 👍

</TextInputWithButtons>
)
}

const styles = StyleSheet.create({
maxAmount: fontStyles.small600,
loadingContainer: {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
loadingContainer: {},

@gnardini gnardini added the automerge Have PR merge automatically when checks pass label Oct 20, 2020
@mergify mergify bot merged commit e931005 into master Oct 20, 2020
@mergify mergify bot deleted the withdraw-fees branch October 20, 2020 19:26
@Lss-Ankit
Copy link

Hi @jeanregisser , @gnardini Verified task using latest Test flight build v1.4.0 (28) and Android play store (internal build) v1.4.0 (1004294317) and found the following:

  • CELO is cut as fee while withdrawing CELO and Selling CELO.
  • Observed that cUSD balance is not effected due to withdraw and Sell CELO.
  • Observed that Fee .001 is shown while withdrawing CELO but Fee is not cut as CELO 0.001 it slight different. we have it checked from https://explorer.celo.org.
  • Also, Observed that user is able to withdraw full amount by tapping on MAX button while withdrawing CELO.

Can you please let us know 3rd and 4th point are expected and Also, let us know if we need to verify anything else.

Thanks!!

@jeanregisser
Copy link
Contributor

All good thanks. The fee is probably rounded up for display.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge Have PR merge automatically when checks pass wallet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Exchange and Withdraw CELO transcations fail when moving full balance
5 participants