Commit 6cc59cfd authored by Luke Schoen's avatar Luke Schoen
Browse files

feat: Send transaction with data field and allow amount to be zero

* Update Send transaction UI so "amount" field can be 0, since some users may want to just send some data (i.e. https://github.com/chainx-org/ChainX/issues/66)
* Set the default amount value to be 0 and not required
* Update Send transaction UI to support "data" field in hex (with 0x prefix)
* Use a hard-coded Gas Limit of 200,000 when user provides hex "data" value
* Remove logic that doesn't make sense (i.e. `else if (amountBn.isZero()) {`)
* Update validation messages
* Add support for Blockscout link when user using Goerli testnet
* Update internationalisation
* Use custom `isHexString` function instead of the ethereumjs-util's version. The ethereumjs-util's version is used by MyCrypto and doesn't work properly.
* Tested on Goerli testnet by creating two accounts, requesting Goerli tokens, running with `--chain goerli` appended to the end (i.e. `"start": "cross-env ELECTRON_START_URL=http://localhost:3000 electron-webpack dev --chain goerli",` in fether/packages/fether-electron/package.json), and sending a transaction with the high gas price, the hex data. For example, for ChainX convert your ChainX Address (UTF8 chars) into Hex code (i.e. copy/paste it at https://www.browserling.com/tools/utf8-to-hex, for example "helloworld" becomes => `\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64`, and remove the `\x`, => `68656c6c6f20776f726c64` => then prepend 0x => `0x68656c6c6f20776f726c64` and add that to the "data" field), then send the transaction, and
when the transaction is successful go to the Blockscout link and you'll see under "Raw Input" ("Input Data" on Etherscan) that you can switch between viewing it in UTF8 or Hex, i.e. https://blockscout.com/eth/goerli/tx/0x7eaec61ce7753fd4c80aec4509c49942b53986585e4864e18134806bffb25f10/internal_transactions)
* TODO - update Parity Signer to show "data" field value if it doesn't already
parent 70b339b2
Pipeline #38913 passed with stage
in 2 minutes and 1 second
......@@ -37,7 +37,7 @@
"package": "electron-builder",
"prerelease": "./scripts/revertElectronBug.sh",
"release": "electron-builder",
"start": "cross-env ELECTRON_START_URL=http://localhost:3000 electron-webpack dev",
"start": "cross-env ELECTRON_START_URL=http://localhost:3000 electron-webpack dev --chain goerli",
"test": "jest --all --color --coverage"
},
"dependencies": {
......
......@@ -47,6 +47,7 @@
"bip39": "^2.5.0",
"debounce-promise": "^3.1.0",
"debug": "^4.1.0",
"ethjs-util": "^0.1.6",
"ethereumjs-tx": "^1.3.7",
"ethereumjs-util": "^6.0.0",
"ethereumjs-wallet": "^0.6.2",
......
......@@ -78,6 +78,7 @@ class SignedTxSummary extends Component {
<Form
key='txForm'
initialValues={{
data: tx.data,
from: address,
to: tx.to,
amount: `${tx.amount} ${token.symbol}`,
......@@ -104,6 +105,14 @@ class SignedTxSummary extends Component {
render={FetherForm.Field}
/>
<Field
className='form_field_value'
disabled
label={i18n.t(`${packageNS}:tx.form.field.data`)}
name='data'
render={FetherForm.Field}
/>
{values.to === values.from && (
<span>
<h3>
......
......@@ -17,7 +17,8 @@ class TxDetails extends Component {
if (
!estimatedTxFee ||
!values.gasPrice ||
!values.amount ||
// Allow estimating tx fee when the amount is zero
// !values.amount ||
!values.chainId ||
!values.ethBalance ||
!values.gas ||
......@@ -43,6 +44,17 @@ ${this.renderTotalAmount()}`;
}
const gasPriceBn = new BigNumber(values.gasPrice.toString());
// Temporarily solution since Fether has to have the simplest UI
// a Gas Price form field may be too overwhelming for beginner users
// but advanced users may want to send data that may require a
// higher Gas Limit. On
if (values.data) {
return i18n.t(`${packageNS}:tx.form.details.gas_limit`, {
gas_limit: new BigNumber(200000)
});
}
const gasLimitBn = estimatedTxFee
.div(gasPriceBn)
.div(10 ** 9)
......@@ -76,14 +88,14 @@ ${this.renderTotalAmount()}`;
const { estimatedTxFee, token, values } = this.props;
const currentChainIdBN = values.chainId;
if (!estimatedTxFee || !values.amount || !token.address) {
if (!estimatedTxFee || !token.address) {
return;
}
const totalAmount = `${fromWei(
estimatedTxFee.plus(
isNotErc20TokenAddress(token.address)
? toWei(values.amount.toString())
? values.amount && toWei(values.amount.toString())
: 0
),
'ether'
......
......@@ -5,6 +5,7 @@
import React, { Component } from 'react';
import BigNumber from 'bignumber.js';
// import { isHexString } from 'ethereumjs-util';
import { chainId$, transactionCountOf$ } from '@parity/light.js';
import { Clickable, Form as FetherForm, Header } from 'fether-ui';
import createDecorator from 'final-form-calculate';
......@@ -38,9 +39,50 @@ const DEFAULT_AMOUNT_MAX_CHARS = 9;
const MEDIUM_AMOUNT_MAX_CHARS = 14;
const MAX_GAS_PRICE = 40; // In Gwei
const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei
// Reference: https://ethereum.stackexchange.com/questions/1106/is-there-a-limit-for-transaction-size
const ESTIMATED_MAX_TOTAL_FEE = 0.002; // In ETH
const debug = Debug('TxForm');
// Luke's custom `isHexString`.
// See https://github.com/MyCryptoHQ/MyCrypto/issues/2592#issuecomment-496432227
function isHexString (value, length) {
if (typeof value !== 'string') {
return false;
}
// Check for hex prefix
if (value.substring(0, 2) !== '0x') {
return false;
}
// Extract hex value without the 0x prefix
// TODO - possible replace with `stripHexPrefix(value).
// See https://github.com/ethjs/ethjs-util/blob/master/dist/ethjs-util.js#L2120
const postfixValue = value.slice(2);
if (!postfixValue.length) {
console.log('No characters found after 0x prefix');
return false;
}
// Check if non-hex character exists after 0x prefix
const match = /[^0-9A-Fa-f]/.exec(postfixValue);
if (match) {
console.log('Non-hex character match found at ' + match.index);
return false;
}
// Check if the hex string is the expected length
if (length && postfixValue.length !== length) {
console.log(
`Hex value length is ${postfixValue.length} but should be ${length}`
);
return false;
}
return true;
}
@inject('parityStore', 'sendStore')
@withTokens
@withProps(({ match: { params: { tokenAddress } }, tokens }) => ({
......@@ -69,6 +111,20 @@ class TxForm extends Component {
};
decorator = createDecorator({
// FIXME - Why can't we estimate gas when we update the next line to
// include the `data` (i.e. `field: /data|to|amount/,`).
// If we do then it gives error in console:
// ```
// eth_estimateGas([{"data":"0x0000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaffffffffffff",
// ... "value":"0x0"}]): -32015: Transaction execution error.
// ```
//
// And in the UI's "Transaction Details" section it displays:
// ```
// Gas Limit: -1
// Fee: -0.000000004 ETH (gas limit * gas price)
// Total Amount: -4e-9 ETH
// ```
field: /to|amount/, // when the value of these fields change...
updates: {
// ...set field "gas"
......@@ -125,7 +181,8 @@ class TxForm extends Component {
isEstimatedTxFee = values => {
if (
values.amount &&
// Allow estimating tx fee when the amount is zero
// values.amount &&
values.gas &&
values.gasPrice &&
!isNaN(values.amount) &&
......@@ -242,6 +299,7 @@ class TxForm extends Component {
<Form
decorators={[this.decorator]}
initialValues={{
amount: 0,
chainId,
ethBalance,
from: address,
......@@ -303,7 +361,6 @@ class TxForm extends Component {
name='amount'
placeholder='0.00'
render={FetherForm.Field}
required
type='number'
>
<button
......@@ -322,6 +379,15 @@ class TxForm extends Component {
</button>
</Field>
<Field
as='textarea'
className='-sm'
label={i18n.t(`${packageNS}:tx.form.field.data`)}
name='data'
placeholder='0x...'
render={FetherForm.Field}
/>
<Field
centerText={`${values.gasPrice} GWEI`}
className='-range'
......@@ -356,6 +422,14 @@ class TxForm extends Component {
}}
</OnChange>
<OnChange name='data'>
{(value, previous) => {
if (this.state.maxSelected) {
mutators.recalculateMax();
}
}}
</OnChange>
{values.to === values.from && (
<span>
<h3>
......@@ -418,41 +492,27 @@ class TxForm extends Component {
return;
}
if (!values.amount) {
return {
amount: i18n.t(`${packageNS}:tx.form.validation.amount_invalid`)
};
}
const amountBn = new BigNumber(values.amount.toString());
if (amountBn.isNaN()) {
return {
amount: i18n.t(`${packageNS}:tx.form.validation.amount_invalid`)
};
} else if (amountBn.isZero()) {
if (this.state.maxSelected) {
return {
amount: i18n.t(
`${packageNS}:tx.form.validation.eth_balance_too_low_for_gas`,
{ chain_id: chainIdToString(currentChainIdBN) }
)
};
}
return {
amount: i18n.t(`${packageNS}:tx.form.validation.non_zero_amount`)
};
} else if (amountBn.isNegative()) {
return {
amount: i18n.t(`${packageNS}:tx.form.validation.positive_amount`)
};
} else if (
isNotErc20TokenAddress(token.address) &&
toWei(values.amount).lt(1)
) {
return {
amount: i18n.t(`${packageNS}:tx.form.validation.min_wei`)
};
// Question: Why do we require a minimum value in wei when users may often
// send transactions with a value of say 0 ETH and a "data" value instead.
// For example: https://github.com/chainx-org/ChainX/issues/66
// } else if (
// isNotErc20TokenAddress(token.address) &&
// toWei(values.amount).lt(1)
// ) {
// return {
// amount: i18n.t(`${packageNS}:tx.form.validation.min_wei`)
// };
} else if (amountBn.dp() > token.decimals) {
return {
amount: i18n.t(`${packageNS}:tx.form.validation.min_decimals`, {
......@@ -460,7 +520,19 @@ class TxForm extends Component {
token_decimals: token.decimals
})
};
} else if (balance && balance.lt(amountBn)) {
// Transaction without any "data"
} else if (!values.data && balance && balance.lt(amountBn)) {
return {
amount: i18n.t(
`${packageNS}:tx.form.validation.token_balance_too_low`,
{
token_symbol: token.symbol
}
)
};
// Transaction size may be large when user provides "data"
// so we need to ensure they have sufficient balance to cover worst-case fee
} else if (values.data && balance && balance.lt(ESTIMATED_MAX_TOTAL_FEE)) {
return {
amount: i18n.t(
`${packageNS}:tx.form.validation.token_balance_too_low`,
......@@ -484,6 +556,12 @@ class TxForm extends Component {
}
)
};
// "data" field should be a hex string (not a hash).
// See https://github.com/chainx-org/ChainX/issues/66#issuecomment-496446585
} else if (values.data && !isHexString(values.data)) {
return {
amount: i18n.t(`${packageNS}:tx.form.validation.data_invalid`)
};
}
return true;
};
......
......@@ -95,6 +95,14 @@ class Unlock extends Component {
defaultValue={`${tx.amount} ${token.symbol}`}
label={i18n.t(`${packageNS}:tx.form.field.amount`)}
/>
<FetherForm.Field
as='textarea'
className='form_field_value'
disabled
defaultValue={`${tx.data}`}
label={i18n.t(`${packageNS}:tx.form.field.data`)}
/>
</div>,
<Form
key='signerForm'
......
......@@ -139,6 +139,7 @@
"label_unlock": "Konto entsperren:",
"field": {
"to": "Zu",
"data": "Daten",
"amount": "Menge",
"tx_speed": "Transaktionsgeschwindigkeit",
"high": "Hoch",
......@@ -160,6 +161,7 @@
},
"validation": {
"amount_invalid": "Bitte geben Sie einen gültigen Betrag ein",
"data_invalid": "Bitte geben Sie eine gültige hex Eingabedatenfolge ein",
"eth_balance_too_low_for_gas": "{{chain_id}}-Bilanz zu niedrig, um für Gas zu zahlen.",
"eth_balance_too_low": "Sie haben nicht genug {{chain_id}}-Balance",
"fetching_chain_id": "Abrufen der Ketten-ID",
......
......@@ -139,6 +139,7 @@
"label_unlock": "Unlock account:",
"field": {
"to": "To",
"data": "Data",
"amount": "Amount",
"tx_speed": "Transaction Speed",
"high": "High",
......@@ -160,6 +161,7 @@
},
"validation": {
"amount_invalid": "Please enter a valid amount",
"data_invalid": "Please enter a valid input data hex string",
"eth_balance_too_low_for_gas": "{{chain_id}} balance too low to pay for gas.",
"eth_balance_too_low": "You don't have enough {{chain_id}} balance",
"fetching_chain_id": "Fetching chain ID",
......
......@@ -20,6 +20,7 @@ const baseUrlForChain = chainName => {
chainNameBlockscout = 'mainnet';
baseUrl = `https://blockscout.com/etc/${chainNameBlockscout}`;
break;
case 'goerli':
case 'kovan':
case 'ropsten':
chainNameBlockscout = chainName;
......
......@@ -93,6 +93,7 @@ export const txForErc20 = (tx, token) => {
)
],
options: {
data: tx.data,
from: tx.from,
gasPrice: toWei(tx.gasPrice, 'shannon') // shannon == gwei
}
......@@ -111,6 +112,7 @@ export const txForErc20 = (tx, token) => {
*/
export const txForEth = tx => {
const output = {
data: tx.data,
from: tx.from,
gasPrice: toWei(tx.gasPrice, 'shannon'), // shannon == gwei
to: tx.to,
......@@ -128,7 +130,16 @@ export const txForEth = tx => {
* EthereumTx object.
*/
const getEthereumTx = tx => {
const { amount, chainId, gas, gasPrice, to, token, transactionCount } = tx;
const {
amount,
chainId,
data,
gas,
gasPrice,
to,
token,
transactionCount
} = tx;
const txParams = {
nonce: '0x' + transactionCount.toNumber().toString(16),
......@@ -138,25 +149,27 @@ const getEthereumTx = tx => {
};
if (isNotErc20TokenAddress(token.address)) {
txParams.data = data;
txParams.to = to;
txParams.value = parseFloat(amount) * Math.pow(10, 18);
} else {
txParams.to = token.address;
txParams.data =
'0x' +
new Abi(eip20).functions
.find(f => f._name === 'transfer')
.encodeCall([
new Token('address', to),
new Token(
'uint',
'0x' +
new BigNumber(amount)
.multipliedBy(new BigNumber(10).pow(token.decimals))
.toNumber()
.toString(16)
)
]);
txParams.data = txParams.data
? data
: '0x' +
new Abi(eip20).functions
.find(f => f._name === 'transfer')
.encodeCall([
new Token('address', to),
new Token(
'uint',
'0x' +
new BigNumber(amount)
.multipliedBy(new BigNumber(10).pow(token.decimals))
.toNumber()
.toString(16)
)
]);
}
return new EthereumTx(txParams);
......
......@@ -6158,7 +6158,7 @@ ethereumjs-wallet@^0.6.2:
utf8 "^3.0.0"
uuid "^3.3.2"
 
ethjs-util@0.1.6, ethjs-util@^0.1.3:
ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536"
integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment