diff options
47 files changed, 1208 insertions, 373 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index d23de5fa0..400633c8c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -17,6 +17,9 @@ "confirmClear": { "message": "Are you sure you want to clear approved websites?" }, + "contractInteraction": { + "message": "Contract Interaction" + }, "clearApprovalDataSuccess": { "message": "Approved website data cleared successfully." }, @@ -185,6 +188,9 @@ "cancellationGasFee": { "message": "Cancellation Gas Fee" }, + "cancelled": { + "message": "Cancelled" + }, "cancelN": { "message": "Cancel all $1 transactions" }, @@ -1177,6 +1183,12 @@ "speedUpSubtitle": { "message": "Increase your gas price to attempt to overwrite and speed up your transaction" }, + "speedUpCancellation": { + "message": "Speed up this cancellation" + }, + "speedUpTransaction": { + "message": "Speed up this transaction" + }, "status": { "message": "Status" }, @@ -1263,29 +1275,38 @@ "message": "transaction" }, "transactionConfirmed": { - "message": "Transaction confirmed on $2." + "message": "Transaction confirmed at $2." }, "transactionCreated": { - "message": "Transaction created with a value of $1 on $2." + "message": "Transaction created with a value of $1 at $2." }, "transactionWithNonce": { "message": "Transaction $1" }, "transactionDropped": { - "message": "Transaction dropped on $2." + "message": "Transaction dropped at $2." }, "transactionSubmitted": { - "message": "Transaction submitted on $2." + "message": "Transaction submitted with gas fee of $1 at $2." + }, + "transactionResubmitted": { + "message": "Transaction resubmitted with gas fee increased to $1 at $2" }, "transactionUpdated": { - "message": "Transaction updated on $2." + "message": "Transaction updated at $2." }, "transactionUpdatedGas": { - "message": "Transaction updated with a gas price of $1 on $2." + "message": "Transaction updated with a gas fee of $1 at $2." }, "transactionErrored": { "message": "Transaction encountered an error." }, + "transactionCancelAttempted": { + "message": "Transaction cancel attempted with gas fee of $1 at $2" + }, + "transactionCancelSuccess": { + "message": "Transaction successfully cancelled at $2" + }, "transactions": { "message": "transactions" }, @@ -1350,9 +1371,6 @@ "unknown": { "message": "Unknown" }, - "unknownFunction": { - "message": "Unknown Function" - }, "unknownNetwork": { "message": "Unknown Private Network" }, diff --git a/app/images/icons/cancelled.svg b/app/images/icons/cancelled.svg new file mode 100755 index 000000000..ae4846dde --- /dev/null +++ b/app/images/icons/cancelled.svg @@ -0,0 +1,3 @@ +<svg width="7" height="8" viewBox="0 0 7 8" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M1.97959 1.19291C1.66717 0.880488 1.16063 0.880488 0.848215 1.19291C0.535796 1.50533 0.535796 2.01186 0.848215 2.32428L2.52394 4L0.848215 5.67572C0.535796 5.98814 0.535796 6.49467 0.848215 6.80709C1.16063 7.11951 1.66717 7.11951 1.97959 6.80709L3.65531 5.13137L5.33122 6.80728C5.64364 7.1197 6.15017 7.1197 6.46259 6.80728C6.77501 6.49486 6.77501 5.98833 6.46259 5.67591L4.78668 4L6.46259 2.32409C6.77501 2.01167 6.77501 1.50514 6.46259 1.19272C6.15017 0.880297 5.64364 0.880297 5.33122 1.19272L3.65531 2.86863L1.97959 1.19291Z" fill="#F9FBFF"/> +</svg> diff --git a/app/images/icons/confirm.svg b/app/images/icons/confirm.svg new file mode 100644 index 000000000..3263bf03e --- /dev/null +++ b/app/images/icons/confirm.svg @@ -0,0 +1,3 @@ +<svg width="7" height="5" viewBox="0 0 7 5" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.97989 0.212475C6.27337 0.495775 6.27337 0.955095 5.97989 1.23839L2.16061 4.92513L0.220114 3.05198C-0.0733712 2.76868 -0.0733712 2.30936 0.220114 2.02606C0.513599 1.74276 0.989432 1.74276 1.28292 2.02606L2.16061 2.87329L4.91708 0.212475C5.21057 -0.070825 5.6864 -0.070825 5.97989 0.212475Z" fill="white"/> +</svg> diff --git a/app/images/icons/error.svg b/app/images/icons/error.svg new file mode 100644 index 000000000..bf5abf946 --- /dev/null +++ b/app/images/icons/error.svg @@ -0,0 +1,4 @@ +<svg width="2" height="8" viewBox="0 0 2 8" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="2" height="5" rx="1" fill="white"/> +<rect y="6" width="2" height="2" rx="1" fill="white"/> +</svg> diff --git a/app/images/icons/new.svg b/app/images/icons/new.svg new file mode 100755 index 000000000..f56c43e08 --- /dev/null +++ b/app/images/icons/new.svg @@ -0,0 +1,3 @@ +<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M1.76923 4.2003C1.3274 4.2003 0.969231 4.55847 0.969231 5.0003C0.969231 5.44213 1.3274 5.8003 1.76923 5.8003H4.20048V8.23077C4.20048 8.6726 4.55865 9.03077 5.00048 9.03077C5.44231 9.03077 5.80048 8.6726 5.80048 8.23077V5.8003H8.23077C8.6726 5.8003 9.03077 5.44213 9.03077 5.0003C9.03077 4.55847 8.6726 4.2003 8.23077 4.2003L5.80048 4.2003L5.80048 1.76923C5.80048 1.3274 5.44231 0.969229 5.00048 0.969229C4.55865 0.969229 4.20048 1.3274 4.20048 1.76923V4.2003H1.76923Z" fill="#F9FBFF"/> +</svg> diff --git a/app/images/icons/retry.svg b/app/images/icons/retry.svg new file mode 100755 index 000000000..ddaa198ca --- /dev/null +++ b/app/images/icons/retry.svg @@ -0,0 +1,7 @@ +<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-1-inside-1" fill="white"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M0.778067 3.8208C0.871457 2.12275 2.27987 0.769234 4 0.769234C4.96355 0.769234 5.83056 1.19264 6.42308 1.86404V1.25385C6.42056 1.07527 6.56316 0.928879 6.74111 0.926355C6.82882 0.925093 6.91274 0.959168 6.97458 1.02101C7.03642 1.08285 7.0705 1.16677 7.06923 1.25385V2.86923H6.57641C6.53919 2.87554 6.50196 2.87554 6.46536 2.86923H5.45385C5.33711 2.87113 5.22921 2.80992 5.17053 2.70896C5.11121 2.60863 5.11121 2.48369 5.17053 2.38336C5.22921 2.2824 5.33711 2.22119 5.45385 2.22308H5.87536C5.40526 1.72648 4.74081 1.41539 4 1.41539C2.61746 1.41539 1.49805 2.49378 1.42296 3.85613C1.41854 3.97224 1.35103 4.07699 1.24754 4.12999C1.14405 4.183 1.01974 4.17605 0.92257 4.11232C0.825394 4.04796 0.770495 3.9369 0.778067 3.8208ZM6.57705 4.14387C6.58399 3.96529 6.73417 3.82647 6.91274 3.83404C6.99982 3.83783 7.08185 3.87632 7.14054 3.94132C7.19859 4.00631 7.22825 4.09213 7.22194 4.17921C7.12855 5.87725 5.72014 7.23077 4 7.23077C3.03645 7.23077 2.16944 6.80736 1.57693 6.13597V6.74615C1.57882 6.86289 1.51761 6.97079 1.41665 7.02948C1.31632 7.08879 1.19138 7.08879 1.09105 7.02948C0.990088 6.97079 0.928879 6.86289 0.930771 6.74615V5.13077H1.42927C1.46335 5.12572 1.49742 5.12572 1.53149 5.13077H2.54616C2.66289 5.12888 2.7708 5.19008 2.82948 5.29104C2.88879 5.39138 2.88879 5.51632 2.82948 5.61665C2.7708 5.71761 2.66289 5.77882 2.54616 5.77692H2.12464C2.59538 6.27353 3.2592 6.58461 4 6.58461C5.38254 6.58461 6.50196 5.50622 6.57705 4.14387Z"/> +</mask> +<path fill-rule="evenodd" clip-rule="evenodd" d="M0.778067 3.8208C0.871457 2.12275 2.27987 0.769234 4 0.769234C4.96355 0.769234 5.83056 1.19264 6.42308 1.86404V1.25385C6.42056 1.07527 6.56316 0.928879 6.74111 0.926355C6.82882 0.925093 6.91274 0.959168 6.97458 1.02101C7.03642 1.08285 7.0705 1.16677 7.06923 1.25385V2.86923H6.57641C6.53919 2.87554 6.50196 2.87554 6.46536 2.86923H5.45385C5.33711 2.87113 5.22921 2.80992 5.17053 2.70896C5.11121 2.60863 5.11121 2.48369 5.17053 2.38336C5.22921 2.2824 5.33711 2.22119 5.45385 2.22308H5.87536C5.40526 1.72648 4.74081 1.41539 4 1.41539C2.61746 1.41539 1.49805 2.49378 1.42296 3.85613C1.41854 3.97224 1.35103 4.07699 1.24754 4.12999C1.14405 4.183 1.01974 4.17605 0.92257 4.11232C0.825394 4.04796 0.770495 3.9369 0.778067 3.8208ZM6.57705 4.14387C6.58399 3.96529 6.73417 3.82647 6.91274 3.83404C6.99982 3.83783 7.08185 3.87632 7.14054 3.94132C7.19859 4.00631 7.22825 4.09213 7.22194 4.17921C7.12855 5.87725 5.72014 7.23077 4 7.23077C3.03645 7.23077 2.16944 6.80736 1.57693 6.13597V6.74615C1.57882 6.86289 1.51761 6.97079 1.41665 7.02948C1.31632 7.08879 1.19138 7.08879 1.09105 7.02948C0.990088 6.97079 0.928879 6.86289 0.930771 6.74615V5.13077H1.42927C1.46335 5.12572 1.49742 5.12572 1.53149 5.13077H2.54616C2.66289 5.12888 2.7708 5.19008 2.82948 5.29104C2.88879 5.39138 2.88879 5.51632 2.82948 5.61665C2.7708 5.71761 2.66289 5.77882 2.54616 5.77692H2.12464C2.59538 6.27353 3.2592 6.58461 4 6.58461C5.38254 6.58461 6.50196 5.50622 6.57705 4.14387Z" fill="#F9FBFF"/> +<path d="M0.778067 3.8208L1.776 3.88588L1.77656 3.87571L0.778067 3.8208ZM6.42308 1.86404L5.6733 2.52573L7.42308 4.50843V1.86404H6.42308ZM6.42308 1.25385H7.42318L7.42298 1.23972L6.42308 1.25385ZM6.74111 0.926355L6.75529 1.92625L6.75549 1.92625L6.74111 0.926355ZM6.97458 1.02101L6.26747 1.72811L6.26748 1.72812L6.97458 1.02101ZM7.06923 1.25385L6.06934 1.23935L6.06923 1.2466V1.25385H7.06923ZM7.06923 2.86923V3.86923H8.06923V2.86923H7.06923ZM6.57641 2.86923V1.86923H6.49227L6.40931 1.88329L6.57641 2.86923ZM6.46536 2.86923L6.63526 1.88377L6.55093 1.86923H6.46536V2.86923ZM5.45385 2.86923V1.86923H5.44574L5.43763 1.86937L5.45385 2.86923ZM5.17053 2.70896L6.03511 2.20642L6.03134 2.20005L5.17053 2.70896ZM5.17053 2.38336L6.03137 2.89228L6.03509 2.88588L5.17053 2.38336ZM5.45385 2.22308L5.43763 3.22295L5.44574 3.22308H5.45385V2.22308ZM5.87536 2.22308V3.22308H8.19899L6.60158 1.53562L5.87536 2.22308ZM1.42296 3.85613L0.424475 3.8011L0.424006 3.8096L0.423682 3.81811L1.42296 3.85613ZM1.24754 4.12999L0.791666 3.23995L0.791661 3.23995L1.24754 4.12999ZM0.92257 4.11232L0.370365 4.94604L0.374142 4.94852L0.92257 4.11232ZM6.91274 3.83404L6.95618 2.83499L6.95511 2.83494L6.91274 3.83404ZM6.57705 4.14387L7.57553 4.1989L7.57598 4.19081L7.57629 4.18271L6.57705 4.14387ZM7.14054 3.94132L7.88636 3.27515L7.88276 3.27116L7.14054 3.94132ZM7.22194 4.17921L6.22455 4.10692L6.22392 4.1156L6.22345 4.12429L7.22194 4.17921ZM1.57693 6.13597L2.3267 5.47428L0.576926 3.49157V6.13597H1.57693ZM1.57693 6.74615H0.576926V6.75426L0.577057 6.76238L1.57693 6.74615ZM1.41665 7.02948L0.91411 6.16489L0.907735 6.16866L1.41665 7.02948ZM1.09105 7.02948L1.59998 6.16864L1.59358 6.16492L1.09105 7.02948ZM0.930771 6.74615L1.93064 6.76236L1.93077 6.75426V6.74615H0.930771ZM0.930771 5.13077V4.13077H-0.0692286V5.13077H0.930771ZM1.42927 5.13077V6.13077H1.50294L1.57582 6.11997L1.42927 5.13077ZM1.53149 5.13077L1.38494 6.11997L1.45782 6.13077H1.53149V5.13077ZM2.54616 5.13077V6.13077H2.55426L2.56237 6.13064L2.54616 5.13077ZM2.82948 5.29104L1.96489 5.79358L1.96866 5.79996L2.82948 5.29104ZM2.82948 5.61665L1.96864 5.10772L1.96492 5.11412L2.82948 5.61665ZM2.54616 5.77692L2.56237 4.77705L2.55426 4.77692H2.54616V5.77692ZM2.12464 5.77692V4.77692H-0.201132L1.39888 6.46487L2.12464 5.77692ZM4 -0.230766C1.74426 -0.230766 -0.098134 1.54236 -0.220424 3.76588L1.77656 3.87571C1.84105 2.70314 2.81549 1.76923 4 1.76923V-0.230766ZM7.17286 1.20234C6.39879 0.325232 5.26272 -0.230766 4 -0.230766V1.76923C4.66439 1.76923 5.26233 2.06005 5.6733 2.52573L7.17286 1.20234ZM5.42308 1.25385V1.86404H7.42308V1.25385H5.42308ZM6.72693 -0.0735444C5.99673 -0.0631869 5.41285 0.537233 5.42318 1.26798L7.42298 1.23972C7.42826 1.61331 7.12959 1.92095 6.75529 1.92625L6.72693 -0.0735444ZM7.68169 0.313905C7.42857 0.0607823 7.08431 -0.0786862 6.72672 -0.0735415L6.75549 1.92625C6.57333 1.92887 6.39691 1.85755 6.26747 1.72811L7.68169 0.313905ZM8.06913 1.26835C8.07432 0.910271 7.93407 0.566273 7.68168 0.313893L6.26748 1.72812C6.13878 1.59942 6.06667 1.42327 6.06934 1.23935L8.06913 1.26835ZM8.06923 2.86923V1.25385H6.06923V2.86923H8.06923ZM6.57641 3.86923H7.06923V1.86923H6.57641V3.86923ZM6.29545 3.85469C6.44544 3.88055 6.59664 3.88007 6.74352 3.85517L6.40931 1.88329C6.48173 1.87102 6.55847 1.87053 6.63526 1.88377L6.29545 3.85469ZM5.45385 3.86923H6.46536V1.86923H5.45385V3.86923ZM4.30596 3.21148C4.54571 3.62395 4.98964 3.87689 5.47006 3.8691L5.43763 1.86937C5.68458 1.86536 5.91271 1.99589 6.03509 2.20643L4.30596 3.21148ZM4.30971 1.87445C4.06482 2.28867 4.06482 2.80364 4.30971 3.21787L6.03134 2.20005C6.1576 2.41361 6.1576 2.6787 6.03134 2.89227L4.30971 1.87445ZM5.47006 1.22321C4.98964 1.21542 4.54571 1.46836 4.30596 1.88083L6.03509 2.88588C5.91271 3.09643 5.68458 3.22695 5.43763 3.22295L5.47006 1.22321ZM5.87536 1.22308H5.45385V3.22308H5.87536V1.22308ZM4 2.41539C4.45384 2.41539 4.85932 2.60438 5.14914 2.91054L6.60158 1.53562C5.9512 0.84857 5.02778 0.415388 4 0.415388V2.41539ZM2.42144 3.91117C2.46762 3.07348 3.15379 2.41539 4 2.41539V0.415388C2.08113 0.415388 0.528483 1.91409 0.424475 3.8011L2.42144 3.91117ZM1.70341 5.02004C2.12497 4.80412 2.40391 4.37587 2.42224 3.89415L0.423682 3.81811C0.433175 3.5686 0.577079 3.34986 0.791666 3.23995L1.70341 5.02004ZM0.374142 4.94852C0.770549 5.20851 1.27824 5.2378 1.70342 5.02003L0.791661 3.23995C1.00986 3.12819 1.26894 3.1436 1.471 3.27612L0.374142 4.94852ZM-0.219813 3.75572C-0.250677 4.22897 -0.0263505 4.68327 0.370371 4.94603L1.47477 3.27861C1.67714 3.41265 1.79167 3.64484 1.77595 3.88588L-0.219813 3.75572ZM6.95511 2.83494C6.22187 2.80385 5.60619 3.37471 5.5778 4.10503L7.57629 4.18271C7.56179 4.55588 7.24647 4.84909 6.87038 4.83315L6.95511 2.83494ZM7.88276 3.27116C7.64442 3.0072 7.31059 2.8504 6.95618 2.83499L6.86931 4.8331C6.68906 4.82526 6.51928 4.74545 6.39832 4.61147L7.88276 3.27116ZM8.21932 4.2515C8.24543 3.89125 8.12269 3.53976 7.88635 3.27516L6.39472 4.60747C6.27449 4.47286 6.21107 4.293 6.22455 4.10692L8.21932 4.2515ZM4 8.23077C6.25575 8.23077 8.09814 6.45764 8.22043 4.23412L6.22345 4.12429C6.15896 5.29686 5.18452 6.23077 4 6.23077V8.23077ZM0.82715 6.79766C1.60122 7.67477 2.73729 8.23077 4 8.23077V6.23077C3.33561 6.23077 2.73767 5.93995 2.3267 5.47428L0.82715 6.79766ZM2.57693 6.74615V6.13597H0.576926V6.74615H2.57693ZM1.91918 7.89404C2.33164 7.6543 2.58459 7.21037 2.57679 6.72993L0.577057 6.76238C0.573051 6.51541 0.703586 6.28729 0.914124 6.16491L1.91918 7.89404ZM0.582134 7.89029C0.996363 8.13519 1.51133 8.13519 1.92556 7.89029L0.907735 6.16866C1.1213 6.0424 1.38639 6.0424 1.59996 6.16866L0.582134 7.89029ZM-0.0690972 6.72995C-0.076884 7.21037 0.17606 7.65429 0.588515 7.89403L1.59358 6.16492C1.80412 6.28729 1.93464 6.51541 1.93064 6.76236L-0.0690972 6.72995ZM-0.0692286 5.13077V6.74615H1.93077V5.13077H-0.0692286ZM1.42927 4.13077H0.930771V6.13077H1.42927V4.13077ZM1.67805 4.14157C1.5468 4.12212 1.41396 4.12212 1.28272 4.14157L1.57582 6.11997C1.51273 6.12932 1.44804 6.12932 1.38494 6.11997L1.67805 4.14157ZM2.54616 4.13077H1.53149V6.13077H2.54616V4.13077ZM3.69404 4.78852C3.4543 4.37605 3.01037 4.12311 2.52994 4.1309L2.56237 6.13064C2.31542 6.13464 2.08729 6.00411 1.96492 5.79357L3.69404 4.78852ZM3.6903 6.12556C3.93519 5.71133 3.93519 5.19636 3.6903 4.78213L1.96866 5.79996C1.8424 5.58639 1.8424 5.3213 1.96866 5.10773L3.6903 6.12556ZM2.52994 6.77679C3.01036 6.78458 3.4543 6.53164 3.69404 6.11917L1.96492 5.11412C2.08729 4.90358 2.31542 4.77305 2.56237 4.77705L2.52994 6.77679ZM2.12464 6.77692H2.54616V4.77692H2.12464V6.77692ZM4 5.58461C3.54654 5.58461 3.14132 5.39588 2.8504 5.08897L1.39888 6.46487C2.04943 7.15117 2.97185 7.58461 4 7.58461V5.58461ZM5.57856 4.08883C5.53239 4.92653 4.84621 5.58461 4 5.58461V7.58461C5.91888 7.58461 7.47152 6.08591 7.57553 4.1989L5.57856 4.08883Z" fill="white" mask="url(#path-1-inside-1)"/> +</svg> diff --git a/app/images/icons/submitted.svg b/app/images/icons/submitted.svg new file mode 100755 index 000000000..b5ced8777 --- /dev/null +++ b/app/images/icons/submitted.svg @@ -0,0 +1,3 @@ +<svg width="7" height="6" viewBox="0 0 7 6" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.24834 0.0498428C5.69016 0.0498428 6.04834 0.408016 6.04834 0.849844L6.04834 4.84984C6.04834 5.29167 5.69016 5.64984 5.24834 5.64984C4.80651 5.64984 4.44834 5.29167 4.44834 4.84984V2.68278L1.56558 5.56553C1.25316 5.87795 0.746632 5.87795 0.434212 5.56553C0.121793 5.25311 0.121793 4.74658 0.434212 4.43416L3.21853 1.64984L1.24834 1.64984C0.806507 1.64984 0.448335 1.29167 0.448335 0.849844C0.448335 0.408016 0.806507 0.0498428 1.24834 0.0498428L5.24834 0.0498428Z" fill="#F9FBFF"/> +</svg> diff --git a/app/scripts/controllers/transactions/enums.js b/app/scripts/controllers/transactions/enums.js index be6f16e0d..d41400b9f 100644 --- a/app/scripts/controllers/transactions/enums.js +++ b/app/scripts/controllers/transactions/enums.js @@ -3,10 +3,12 @@ const TRANSACTION_TYPE_RETRY = 'retry' const TRANSACTION_TYPE_STANDARD = 'standard' const TRANSACTION_STATUS_APPROVED = 'approved' +const TRANSACTION_STATUS_CONFIRMED = 'confirmed' module.exports = { TRANSACTION_TYPE_CANCEL, TRANSACTION_TYPE_RETRY, TRANSACTION_TYPE_STANDARD, TRANSACTION_STATUS_APPROVED, + TRANSACTION_STATUS_CONFIRMED, } diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index f530fbd22..2ce736beb 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -230,13 +230,15 @@ class TransactionController extends EventEmitter { to allow the user to resign the transaction with a higher gas values @param originalTxId {number} - the id of the txMeta that you want to attempt to retry + @param gasPrice {string=} - Optional gas price to be increased to use as the retry + transaction's gas price @return {txMeta} */ - async retryTransaction (originalTxId) { + async retryTransaction (originalTxId, gasPrice) { const originalTxMeta = this.txStateManager.getTx(originalTxId) const { txParams } = originalTxMeta - const lastGasPrice = originalTxMeta.txParams.gasPrice + const lastGasPrice = gasPrice || originalTxMeta.txParams.gasPrice const suggestedGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(this.getGasPrice()), 16) const lastGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(lastGasPrice), 16) // essentially lastGasPrice * 1.1 but diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index d382b1ad0..c7e9cfcc7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1144,8 +1144,8 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string} txId - The ID of the transaction to speed up. * @param {Function} cb - The callback function called with a full state update. */ - async retryTransaction (txId, cb) { - await this.txController.retryTransaction(txId) + async retryTransaction (txId, gasPrice, cb) { + await this.txController.retryTransaction(txId, gasPrice) const state = await this.getState() return state } @@ -1158,9 +1158,13 @@ module.exports = class MetamaskController extends EventEmitter { * @returns {object} MetaMask state */ async createCancelTransaction (originalTxId, customGasPrice, cb) { - await this.txController.createCancelTransaction(originalTxId, customGasPrice) - const state = await this.getState() - return state + try { + await this.txController.createCancelTransaction(originalTxId, customGasPrice) + const state = await this.getState() + return state + } catch (error) { + throw error + } } async createSpeedUpTransaction (originalTxId, customGasPrice, cb) { diff --git a/test/integration/lib/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js index 9c2ad7cf4..041a1af34 100644 --- a/test/integration/lib/confirm-sig-requests.js +++ b/test/integration/lib/confirm-sig-requests.js @@ -21,8 +21,8 @@ async function runConfirmSigRequestsTest (assert, done) { const pendingRequestItem = $.find('.transaction-list-item .transaction-list-item__grid') - if (pendingRequestItem[0]) { - pendingRequestItem[0].click() + if (pendingRequestItem[2]) { + pendingRequestItem[2].click() } await timeout(1000) diff --git a/test/integration/lib/tx-list-items.js b/test/integration/lib/tx-list-items.js index ed4f82074..ff196fac8 100644 --- a/test/integration/lib/tx-list-items.js +++ b/test/integration/lib/tx-list-items.js @@ -30,35 +30,25 @@ async function runTxListItemsTest (assert, done) { metamaskLogo[0].click() const txListItems = await queryAsync($, '.transaction-list-item') - assert.equal(txListItems.length, 8, 'all tx list items are rendered') + assert.equal(txListItems.length, 7, 'all tx list items are rendered') - const retryTxGrid = await findAsync($(txListItems[2]), '.transaction-list-item__grid') - retryTxGrid[0].click() - const retryTxDetails = await findAsync($, '.transaction-list-item-details') - const headerButtons = await findAsync($(retryTxDetails[0]), '.transaction-list-item-details__header-button') - assert.equal(headerButtons[0].textContent, 'speed up') - - const approvedTx = txListItems[2] + const approvedTx = txListItems[0] const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status') assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label') - const unapprovedMsg = txListItems[0] + const unapprovedMsg = txListItems[1] const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action') assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description') - const failedTx = txListItems[4] - const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status') - assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label') - - const shapeShiftTx = txListItems[5] + const shapeShiftTx = txListItems[4] const shapeShiftTxStatus = await findAsync($(shapeShiftTx), '.flex-column div:eq(1)') assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status') + const rejectedTx = txListItems[5] + const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status') + assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label') + const confirmedTokenTx = txListItems[6] const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status') assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address') - - const rejectedTx = txListItems[7] - const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status') - assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label') } diff --git a/ui/app/actions.js b/ui/app/actions.js index cd24aed0a..fa175177e 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1793,13 +1793,13 @@ function markAccountsFound () { return callBackgroundThenUpdate(background.markAccountsFound) } -function retryTransaction (txId) { +function retryTransaction (txId, gasPrice) { log.debug(`background.retryTransaction`) let newTxId - return (dispatch) => { + return dispatch => { return new Promise((resolve, reject) => { - background.retryTransaction(txId, (err, newState) => { + background.retryTransaction(txId, gasPrice, (err, newState) => { if (err) { dispatch(actions.displayWarning(err.message)) reject(err) diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js b/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js index eede8b1ee..10931a001 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js +++ b/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js @@ -28,31 +28,29 @@ const mapStateToProps = (state, ownProps) => { transactionId, transactionStatus, originalGasPrice, + defaultNewGasPrice, newGasFee, } } const mapDispatchToProps = dispatch => { return { - createCancelTransaction: txId => dispatch(createCancelTransaction(txId)), + createCancelTransaction: (txId, customGasPrice) => { + return dispatch(createCancelTransaction(txId, customGasPrice)) + }, showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })), } } const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { transactionId, ...restStateProps } = stateProps - const { - createCancelTransaction: dispatchCreateCancelTransaction, - ...restDispatchProps - } = dispatchProps + const { transactionId, defaultNewGasPrice, ...restStateProps } = stateProps + const { createCancelTransaction, ...restDispatchProps } = dispatchProps return { ...restStateProps, ...restDispatchProps, ...ownProps, - createCancelTransaction: newGasPrice => { - return dispatchCreateCancelTransaction(transactionId, newGasPrice) - }, + createCancelTransaction: () => createCancelTransaction(transactionId, defaultNewGasPrice), } } diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index e3abde233..6bc415781 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -426,7 +426,7 @@ export default class ConfirmTransactionBase extends Component { toName={toName} toAddress={toAddress} showEdit={onEdit && !isTxReprice} - action={action || name || this.context.t('unknownFunction')} + action={action || name || this.context.t('contractInteraction')} title={title} titleComponent={this.renderTitleComponent()} subtitle={subtitle} diff --git a/ui/app/components/sender-to-recipient/index.scss b/ui/app/components/sender-to-recipient/index.scss index 0ab0413be..b21e4e1bb 100644 --- a/ui/app/components/sender-to-recipient/index.scss +++ b/ui/app/components/sender-to-recipient/index.scss @@ -1,12 +1,13 @@ .sender-to-recipient { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + position: relative; + flex: 0 0 auto; + &--default { - width: 100%; - display: flex; - flex-direction: row; - justify-content: center; border-bottom: 1px solid $geyser; - position: relative; - flex: 0 0 auto; height: 42px; .sender-to-recipient { @@ -74,13 +75,6 @@ } &--cards { - width: 100%; - display: flex; - flex-direction: row; - justify-content: center; - position: relative; - flex: 0 0 auto; - .sender-to-recipient { &__party { display: flex; @@ -117,4 +111,39 @@ } } } + + &--flat { + .sender-to-recipient { + &__party { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + flex: 1; + padding: 6px; + cursor: pointer; + min-width: 0; + color: $dusty-gray; + } + + &__tooltip-wrapper { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: .6875rem; + } + + &__arrow-container { + display: flex; + justify-content: center; + align-items: center; + } + } + } } diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js index e71bd7406..89a1a9c08 100644 --- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js @@ -4,12 +4,13 @@ import classnames from 'classnames' import Identicon from '../identicon' import Tooltip from '../tooltip-v2' import copyToClipboard from 'copy-to-clipboard' -import { DEFAULT_VARIANT, CARDS_VARIANT } from './sender-to-recipient.constants' +import { DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT } from './sender-to-recipient.constants' import { checksumAddress } from '../../util' const variantHash = { [DEFAULT_VARIANT]: 'sender-to-recipient--default', [CARDS_VARIANT]: 'sender-to-recipient--cards', + [FLAT_VARIANT]: 'sender-to-recipient--flat', } export default class SenderToRecipient extends PureComponent { @@ -19,7 +20,7 @@ export default class SenderToRecipient extends PureComponent { recipientName: PropTypes.string, recipientAddress: PropTypes.string, t: PropTypes.func, - variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT]), + variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]), addressOnly: PropTypes.bool, assetImage: PropTypes.string, } @@ -128,16 +129,9 @@ export default class SenderToRecipient extends PureComponent { } renderArrow () { - return this.props.variant === CARDS_VARIANT + return this.props.variant === DEFAULT_VARIANT ? ( <div className="sender-to-recipient__arrow-container"> - <img - height={20} - src="./images/caret-right.svg" - /> - </div> - ) : ( - <div className="sender-to-recipient__arrow-container"> <div className="sender-to-recipient__arrow-circle"> <img height={15} @@ -146,6 +140,13 @@ export default class SenderToRecipient extends PureComponent { /> </div> </div> + ) : ( + <div className="sender-to-recipient__arrow-container"> + <img + height={20} + src="./images/caret-right.svg" + /> + </div> ) } @@ -154,7 +155,7 @@ export default class SenderToRecipient extends PureComponent { const checksummedSenderAddress = checksumAddress(senderAddress) return ( - <div className={classnames(variantHash[variant])}> + <div className={classnames('sender-to-recipient', variantHash[variant])}> <div className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')} onClick={() => { diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js index 166228932..f53a5115d 100644 --- a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js +++ b/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js @@ -1,3 +1,4 @@ // Component design variants export const DEFAULT_VARIANT = 'DEFAULT_VARIANT' export const CARDS_VARIANT = 'CARDS_VARIANT' +export const FLAT_VARIANT = 'FLAT_VARIANT' diff --git a/ui/app/components/transaction-activity-log/index.scss b/ui/app/components/transaction-activity-log/index.scss index 27f3006b3..00c17e6aa 100644 --- a/ui/app/components/transaction-activity-log/index.scss +++ b/ui/app/components/transaction-activity-log/index.scss @@ -1,7 +1,8 @@ .transaction-activity-log { - &__card { - background: $white; - height: 100%; + &__title { + border-bottom: 1px solid #d8d8d8; + padding-bottom: 4px; + text-transform: capitalize; } &__activities-container { @@ -21,8 +22,8 @@ left: 0; top: 0; height: 100%; - width: 6px; - border-right: 1px solid $scorpion; + width: 7px; + border-right: 1px solid #909090; } &:first-child::after { @@ -40,22 +41,25 @@ } &__activity-icon { - width: 13px; - height: 13px; + width: 15px; + height: 15px; margin-right: 6px; border-radius: 50%; - background: $scorpion; + background: #909090; flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; } &__activity-text { - color: $scorpion; + color: $dusty-gray; font-size: .75rem; + cursor: pointer; - @media screen and (min-width: $break-large) { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + &:hover { + color: $black; } } @@ -64,6 +68,16 @@ font-weight: 500; } + &__entry-container { + min-width: 0; + } + + &__action-link { + font-size: .75rem; + cursor: pointer; + color: $curious-blue; + } + b { font-weight: 500; } diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js index 8687dbbc7..a2946e53d 100644 --- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js +++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js @@ -2,34 +2,100 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import TransactionActivityLog from '../transaction-activity-log.component' -import Card from '../../card' describe('TransactionActivityLog Component', () => { it('should render properly', () => { - const transaction = { - history: [], - id: 1, - status: 'confirmed', - txParams: { - from: '0x1', - gas: '0x5208', - gasPrice: '0x3b9aca00', - nonce: '0xa4', - to: '0x2', + const activities = [ + { + eventKey: 'transactionCreated', + hash: '0xe46c7f9b39af2fbf1c53e66f72f80343ab54c2c6dba902d51fb98ada08fe1a63', + id: 2005383477493174, + timestamp: 1543957986150, value: '0x2386f26fc10000', + }, { + eventKey: 'transactionSubmitted', + hash: '0xe46c7f9b39af2fbf1c53e66f72f80343ab54c2c6dba902d51fb98ada08fe1a63', + id: 2005383477493174, + timestamp: 1543957987853, + value: '0x1319718a5000', + }, { + eventKey: 'transactionResubmitted', + hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87', + id: 2005383477493175, + timestamp: 1543957991563, + value: '0x1502634b5800', + }, { + eventKey: 'transactionConfirmed', + hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87', + id: 2005383477493175, + timestamp: 1543958029960, + value: '0x1502634b5800', }, - } + ] const wrapper = shallow( <TransactionActivityLog - transaction={transaction} + activities={activities} className="test-class" + inlineRetryIndex={-1} + inlineCancelIndex={-1} + nativeCurrency="ETH" + onCancel={() => {}} + onRetry={() => {}} + primaryTransactionStatus="confirmed" />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } ) assert.ok(wrapper.hasClass('transaction-activity-log')) assert.ok(wrapper.hasClass('test-class')) - assert.equal(wrapper.find(Card).length, 1) + }) + + it('should render inline retry and cancel buttons', () => { + const activities = [ + { + eventKey: 'transactionCreated', + hash: '0xa', + id: 1, + timestamp: 1, + value: '0x1', + }, { + eventKey: 'transactionSubmitted', + hash: '0xa', + id: 1, + timestamp: 2, + value: '0x1', + }, { + eventKey: 'transactionResubmitted', + hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87', + id: 2, + timestamp: 3, + value: '0x1', + }, { + eventKey: 'transactionCancelAttempted', + hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87', + id: 3, + timestamp: 4, + value: '0x1', + }, + ] + + const wrapper = shallow( + <TransactionActivityLog + activities={activities} + className="test-class" + inlineRetryIndex={2} + inlineCancelIndex={3} + nativeCurrency="ETH" + onCancel={() => {}} + onRetry={() => {}} + primaryTransactionStatus="pending" + />, + { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } + ) + + assert.ok(wrapper.hasClass('transaction-activity-log')) + assert.ok(wrapper.hasClass('test-class')) + assert.equal(wrapper.find('.transaction-activity-log__action-link').length, 2) }) }) diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js index 586500408..d014b8886 100644 --- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js +++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js @@ -1,5 +1,130 @@ import assert from 'assert' -import { getActivities } from '../transaction-activity-log.util' +import { combineTransactionHistories, getActivities } from '../transaction-activity-log.util' + +describe('combineTransactionHistories', () => { + it('should return no activites for an empty list of transactions', () => { + assert.deepEqual(combineTransactionHistories([]), []) + }) + + it('should return activities for an array of transactions', () => { + const transactions = [ + { + estimatedGas: '0x5208', + hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3', + history: [ + { + 'id': 6400627574331058, + 'time': 1543958845581, + 'status': 'unapproved', + 'metamaskNetworkId': '3', + 'loadingDefaults': true, + 'txParams': { + 'from': '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + 'to': '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + 'value': '0x2386f26fc10000', + 'gas': '0x5208', + 'gasPrice': '0x3b9aca00', + }, + 'type': 'standard', + }, + [{ 'op': 'replace', 'path': '/status', 'value': 'approved', 'note': 'txStateManager: setting status to approved', 'timestamp': 1543958847813 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'submitted', 'note': 'txStateManager: setting status to submitted', 'timestamp': 1543958848147 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'dropped', 'note': 'txStateManager: setting status to dropped', 'timestamp': 1543958897181 }, { 'op': 'add', 'path': '/replacedBy', 'value': '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33' }], + ], + id: 6400627574331058, + loadingDefaults: false, + metamaskNetworkId: '3', + status: 'dropped', + submittedTime: 1543958848135, + time: 1543958845581, + txParams: { + from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0x32', + to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + value: '0x2386f26fc10000', + }, + type: 'standard', + }, { + hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33', + history: [ + { + 'id': 6400627574331060, + 'time': 1543958857697, + 'status': 'unapproved', + 'metamaskNetworkId': '3', + 'loadingDefaults': false, + 'txParams': { + 'from': '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + 'to': '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + 'value': '0x2386f26fc10000', + 'gas': '0x5208', + 'gasPrice': '0x3b9aca00', + 'nonce': '0x32', + }, + 'lastGasPrice': '0x4190ab00', + 'type': 'retry', + }, + [{ 'op': 'replace', 'path': '/txParams/gasPrice', 'value': '0x481f2280', 'note': 'confTx: user approved transaction', 'timestamp': 1543958859470 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'approved', 'note': 'txStateManager: setting status to approved', 'timestamp': 1543958859485 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'signed', 'note': 'transactions#publishTransaction', 'timestamp': 1543958859889 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'submitted', 'note': 'txStateManager: setting status to submitted', 'timestamp': 1543958860061 }], [{ 'op': 'add', 'path': '/firstRetryBlockNumber', 'value': '0x45a0fd', 'note': 'transactions/pending-tx-tracker#event: tx:block-update', 'timestamp': 1543958896466 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'confirmed', 'timestamp': 1543958897165 }], + ], + id: 6400627574331060, + lastGasPrice: '0x4190ab00', + loadingDefaults: false, + metamaskNetworkId: '3', + status: 'confirmed', + submittedTime: 1543958860054, + time: 1543958857697, + txParams: { + from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + gas: '0x5208', + gasPrice: '0x481f2280', + nonce: '0x32', + to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + value: '0x2386f26fc10000', + }, + txReceipt: { + status: '0x1', + }, + type: 'retry', + }, + ] + + const expected = [ + { + id: 6400627574331058, + hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3', + eventKey: 'transactionCreated', + timestamp: 1543958845581, + value: '0x2386f26fc10000', + }, { + id: 6400627574331058, + hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3', + eventKey: 'transactionSubmitted', + timestamp: 1543958848147, + value: '0x1319718a5000', + }, { + id: 6400627574331060, + hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33', + eventKey: 'transactionResubmitted', + timestamp: 1543958860061, + value: '0x171c3a061400', + }, { + id: 6400627574331060, + hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33', + eventKey: 'transactionConfirmed', + timestamp: 1543958897165, + value: '0x171c3a061400', + }, + ] + + assert.deepEqual(combineTransactionHistories(transactions), expected) + }) +}) describe('getActivities', () => { it('should return no activities for an empty history', () => { @@ -178,6 +303,7 @@ describe('getActivities', () => { to: '0x2', value: '0x2386f26fc10000', }, + hash: '0xabc', } const expectedResult = [ @@ -185,24 +311,25 @@ describe('getActivities', () => { 'eventKey': 'transactionCreated', 'timestamp': 1535507561452, 'value': '0x2386f26fc10000', - }, - { - 'eventKey': 'transactionUpdatedGas', - 'timestamp': 1535664571504, - 'value': '0x77359400', + 'id': 1, + 'hash': '0xabc', }, { 'eventKey': 'transactionSubmitted', 'timestamp': 1535507564665, - 'value': undefined, + 'value': '0x2632e314a000', + 'id': 1, + 'hash': '0xabc', }, { 'eventKey': 'transactionConfirmed', 'timestamp': 1535507615993, - 'value': undefined, + 'value': '0x2632e314a000', + 'id': 1, + 'hash': '0xabc', }, ] - assert.deepEqual(getActivities(transaction), expectedResult) + assert.deepEqual(getActivities(transaction, true), expectedResult) }) }) diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log-icon/index.js b/ui/app/components/transaction-activity-log/transaction-activity-log-icon/index.js new file mode 100644 index 000000000..86b12360a --- /dev/null +++ b/ui/app/components/transaction-activity-log/transaction-activity-log-icon/index.js @@ -0,0 +1 @@ +export { default } from './transaction-activity-log-icon.component' diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js new file mode 100644 index 000000000..871716002 --- /dev/null +++ b/ui/app/components/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js @@ -0,0 +1,55 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +import { + TRANSACTION_CREATED_EVENT, + TRANSACTION_SUBMITTED_EVENT, + TRANSACTION_RESUBMITTED_EVENT, + TRANSACTION_CONFIRMED_EVENT, + TRANSACTION_DROPPED_EVENT, + TRANSACTION_ERRORED_EVENT, + TRANSACTION_CANCEL_ATTEMPTED_EVENT, + TRANSACTION_CANCEL_SUCCESS_EVENT, +} from '../transaction-activity-log.constants' + +const imageHash = { + [TRANSACTION_CREATED_EVENT]: '/images/icons/new.svg', + [TRANSACTION_SUBMITTED_EVENT]: '/images/icons/submitted.svg', + [TRANSACTION_RESUBMITTED_EVENT]: '/images/icons/retry.svg', + [TRANSACTION_CONFIRMED_EVENT]: '/images/icons/confirm.svg', + [TRANSACTION_DROPPED_EVENT]: '/images/icons/cancelled.svg', + [TRANSACTION_ERRORED_EVENT]: '/images/icons/error.svg', + [TRANSACTION_CANCEL_ATTEMPTED_EVENT]: '/images/icons/cancelled.svg', + [TRANSACTION_CANCEL_SUCCESS_EVENT]: '/images/icons/cancelled.svg', +} + +export default class TransactionActivityLogIcon extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + className: PropTypes.string, + eventKey: PropTypes.oneOf(Object.keys(imageHash)), + } + + render () { + const { className, eventKey } = this.props + const imagePath = imageHash[eventKey] + + return ( + <div className={classnames('transaction-activity-log-icon', className)}> + { + imagePath && ( + <img + src={imagePath} + height={9} + width={9} + /> + ) + } + </div> + ) + } +} diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js index 58d932a0f..d6f90860a 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js @@ -1,10 +1,11 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { getActivities } from './transaction-activity-log.util' -import Card from '../card' import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util' import { formatDate } from '../../util' +import TransactionActivityLogIcon from './transaction-activity-log-icon' +import { CONFIRMED_STATUS } from './transaction-activity-log.constants' +import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' export default class TransactionActivityLog extends PureComponent { static contextTypes = { @@ -12,41 +13,64 @@ export default class TransactionActivityLog extends PureComponent { } static propTypes = { - transaction: PropTypes.object, + activities: PropTypes.array, className: PropTypes.string, conversionRate: PropTypes.number, + inlineRetryIndex: PropTypes.number, + inlineCancelIndex: PropTypes.number, nativeCurrency: PropTypes.string, + onCancel: PropTypes.func, + onRetry: PropTypes.func, + primaryTransaction: PropTypes.object, } - state = { - activities: [], - } + handleActivityClick = hash => { + const { primaryTransaction } = this.props + const { metamaskNetworkId } = primaryTransaction + + const prefix = prefixForNetwork(metamaskNetworkId) + const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` - componentDidMount () { - this.setActivites() + global.platform.openWindow({ url: etherscanUrl }) } - componentDidUpdate (prevProps) { - const { - transaction: { history: prevHistory = [], txReceipt: { status: prevStatus } = {} } = {}, - } = prevProps - const { - transaction: { history = [], txReceipt: { status } = {} } = {}, - } = this.props + renderInlineRetry (index, activity) { + const { t } = this.context + const { inlineRetryIndex, primaryTransaction = {}, onRetry } = this.props + const { status } = primaryTransaction + const { id } = activity - if (prevHistory.length !== history.length || prevStatus !== status) { - this.setActivites() - } + return status !== CONFIRMED_STATUS && index === inlineRetryIndex + ? ( + <div + className="transaction-activity-log__action-link" + onClick={() => onRetry(id)} + > + { t('speedUpTransaction') } + </div> + ) : null } - setActivites () { - const activities = getActivities(this.props.transaction) - this.setState({ activities }) + renderInlineCancel (index, activity) { + const { t } = this.context + const { inlineCancelIndex, primaryTransaction = {}, onCancel } = this.props + const { status } = primaryTransaction + const { id } = activity + + return status !== CONFIRMED_STATUS && index === inlineCancelIndex + ? ( + <div + className="transaction-activity-log__action-link" + onClick={() => onCancel(id)} + > + { t('speedUpCancellation') } + </div> + ) : null } renderActivity (activity, index) { const { conversionRate, nativeCurrency } = this.props - const { eventKey, value, timestamp } = activity + const { eventKey, value, timestamp, hash } = activity const ethValue = index === 0 ? `${getValueFromWeiHex({ value, @@ -55,8 +79,13 @@ export default class TransactionActivityLog extends PureComponent { conversionRate, numberOfDecimals: 6, })} ${nativeCurrency}` - : getEthConversionFromWeiHex({ value, fromCurrency: nativeCurrency, conversionRate }) - const formattedTimestamp = formatDate(timestamp) + : getEthConversionFromWeiHex({ + value, + fromCurrency: nativeCurrency, + conversionRate, + numberOfDecimals: 3, + }) + const formattedTimestamp = formatDate(timestamp, '14:30 on 3/16/2014') const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp]) return ( @@ -64,12 +93,20 @@ export default class TransactionActivityLog extends PureComponent { key={index} className="transaction-activity-log__activity" > - <div className="transaction-activity-log__activity-icon" /> - <div - className="transaction-activity-log__activity-text" - title={activityText} - > - { activityText } + <TransactionActivityLogIcon + className="transaction-activity-log__activity-icon" + eventKey={eventKey} + /> + <div className="transaction-activity-log__entry-container"> + <div + className="transaction-activity-log__activity-text" + title={activityText} + onClick={() => this.handleActivityClick(hash)} + > + { activityText } + </div> + { this.renderInlineRetry(index, activity) } + { this.renderInlineCancel(index, activity) } </div> </div> ) @@ -77,19 +114,16 @@ export default class TransactionActivityLog extends PureComponent { render () { const { t } = this.context - const { className } = this.props - const { activities } = this.state + const { className, activities } = this.props return ( <div className={classnames('transaction-activity-log', className)}> - <Card - title={t('activityLog')} - className="transaction-activity-log__card" - > - <div className="transaction-activity-log__activities-container"> - { activities.map((activity, index) => this.renderActivity(activity, index)) } - </div> - </Card> + <div className="transaction-activity-log__title"> + { t('activityLog') } + </div> + <div className="transaction-activity-log__activities-container"> + { activities.map((activity, index) => this.renderActivity(activity, index)) } + </div> </div> ) } diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.constants.js b/ui/app/components/transaction-activity-log/transaction-activity-log.constants.js new file mode 100644 index 000000000..72e63d85c --- /dev/null +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.constants.js @@ -0,0 +1,13 @@ +export const TRANSACTION_CREATED_EVENT = 'transactionCreated' +export const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted' +export const TRANSACTION_RESUBMITTED_EVENT = 'transactionResubmitted' +export const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed' +export const TRANSACTION_DROPPED_EVENT = 'transactionDropped' +export const TRANSACTION_UPDATED_EVENT = 'transactionUpdated' +export const TRANSACTION_ERRORED_EVENT = 'transactionErrored' +export const TRANSACTION_CANCEL_ATTEMPTED_EVENT = 'transactionCancelAttempted' +export const TRANSACTION_CANCEL_SUCCESS_EVENT = 'transactionCancelSuccess' + +export const SUBMITTED_STATUS = 'submitted' +export const CONFIRMED_STATUS = 'confirmed' +export const DROPPED_STATUS = 'dropped' diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js index 622f77df1..e43229708 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js @@ -1,6 +1,14 @@ import { connect } from 'react-redux' +import R from 'ramda' import TransactionActivityLog from './transaction-activity-log.component' import { conversionRateSelector, getNativeCurrency } from '../../selectors' +import { combineTransactionHistories } from './transaction-activity-log.util' +import { + TRANSACTION_RESUBMITTED_EVENT, + TRANSACTION_CANCEL_ATTEMPTED_EVENT, +} from './transaction-activity-log.constants' + +const matchesEventKey = matchEventKey => ({ eventKey }) => eventKey === matchEventKey const mapStateToProps = state => { return { @@ -9,4 +17,28 @@ const mapStateToProps = state => { } } -export default connect(mapStateToProps)(TransactionActivityLog) +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { + transactionGroup: { + transactions = [], + primaryTransaction, + } = {}, + ...restOwnProps + } = ownProps + + const activities = combineTransactionHistories(transactions) + const inlineRetryIndex = R.findLastIndex(matchesEventKey(TRANSACTION_RESUBMITTED_EVENT))(activities) + const inlineCancelIndex = R.findLastIndex(matchesEventKey(TRANSACTION_CANCEL_ATTEMPTED_EVENT))(activities) + + return { + ...stateProps, + ...dispatchProps, + ...restOwnProps, + activities, + inlineRetryIndex, + inlineCancelIndex, + primaryTransaction, + } +} + +export default connect(mapStateToProps, null, mergeProps)(TransactionActivityLog) diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js index 16597ae1a..6206a4678 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js @@ -1,28 +1,39 @@ +import { getHexGasTotal } from '../../helpers/confirm-transaction/util' + // path constants const STATUS_PATH = '/status' const GAS_PRICE_PATH = '/txParams/gasPrice' - -// status constants -const UNAPPROVED_STATUS = 'unapproved' -const SUBMITTED_STATUS = 'submitted' -const CONFIRMED_STATUS = 'confirmed' -const DROPPED_STATUS = 'dropped' +const GAS_LIMIT_PATH = '/txParams/gas' // op constants const REPLACE_OP = 'replace' -// event constants -const TRANSACTION_CREATED_EVENT = 'transactionCreated' -const TRANSACTION_UPDATED_GAS_EVENT = 'transactionUpdatedGas' -const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted' -const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed' -const TRANSACTION_DROPPED_EVENT = 'transactionDropped' -const TRANSACTION_UPDATED_EVENT = 'transactionUpdated' -const TRANSACTION_ERRORED_EVENT = 'transactionErrored' +import { + // event constants + TRANSACTION_CREATED_EVENT, + TRANSACTION_SUBMITTED_EVENT, + TRANSACTION_RESUBMITTED_EVENT, + TRANSACTION_CONFIRMED_EVENT, + TRANSACTION_DROPPED_EVENT, + TRANSACTION_UPDATED_EVENT, + TRANSACTION_ERRORED_EVENT, + TRANSACTION_CANCEL_ATTEMPTED_EVENT, + TRANSACTION_CANCEL_SUCCESS_EVENT, + // status constants + SUBMITTED_STATUS, + CONFIRMED_STATUS, + DROPPED_STATUS, +} from './transaction-activity-log.constants' + +import { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_TYPE_RETRY, +} from '../../../../app/scripts/controllers/transactions/enums' const eventPathsHash = { [STATUS_PATH]: true, [GAS_PRICE_PATH]: true, + [GAS_LIMIT_PATH]: true, } const statusHash = { @@ -31,22 +42,39 @@ const statusHash = { [DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT, } -function eventCreator (eventKey, timestamp, value) { - return { - eventKey, - timestamp, - value, - } -} - -export function getActivities (transaction) { - const { history = [], txReceipt: { status } = {} } = transaction - - const historyActivities = history.reduce((acc, base) => { +/** + * @name getActivities + * @param {Object} transaction - txMeta object + * @param {boolean} isFirstTransaction - True if the transaction is the first created transaction + * in the list of transactions with the same nonce. If so, we use this transaction to create the + * transactionCreated activity. + * @returns {Array} + */ +export function getActivities (transaction, isFirstTransaction = false) { + const { id, hash, history = [], txReceipt: { status } = {}, type } = transaction + + let cachedGasLimit = '0x0' + let cachedGasPrice = '0x0' + + const historyActivities = history.reduce((acc, base, index) => { // First history item should be transaction creation - if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) { - const { time, txParams: { value } = {} } = base - return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value)) + if (index === 0 && !Array.isArray(base) && base.txParams) { + const { time: timestamp, txParams: { value, gas = '0x0', gasPrice = '0x0' } = {} } = base + // The cached gas limit and gas price are used to display the gas fee in the activity log. We + // need to cache these values because the status update history events don't provide us with + // the latest gas limit and gas price. + cachedGasLimit = gas + cachedGasPrice = gasPrice + + if (isFirstTransaction) { + return acc.concat({ + id, + hash, + eventKey: TRANSACTION_CREATED_EVENT, + timestamp, + value, + }) + } // An entry in the history may be an array of more sub-entries. } else if (Array.isArray(base)) { const events = [] @@ -60,20 +88,69 @@ export function getActivities (transaction) { if (path in eventPathsHash && op === REPLACE_OP) { switch (path) { case STATUS_PATH: { + const gasFee = getHexGasTotal({ gasLimit: cachedGasLimit, gasPrice: cachedGasPrice }) + if (value in statusHash) { - events.push(eventCreator(statusHash[value], timestamp)) + let eventKey = statusHash[value] + + // If the status is 'submitted', we need to determine whether the event is a + // transaction retry or a cancellation attempt. + if (value === SUBMITTED_STATUS) { + if (type === TRANSACTION_TYPE_RETRY) { + eventKey = TRANSACTION_RESUBMITTED_EVENT + } else if (type === TRANSACTION_TYPE_CANCEL) { + eventKey = TRANSACTION_CANCEL_ATTEMPTED_EVENT + } + } else if (value === CONFIRMED_STATUS) { + if (type === TRANSACTION_TYPE_CANCEL) { + eventKey = TRANSACTION_CANCEL_SUCCESS_EVENT + } + } + + events.push({ + id, + hash, + eventKey, + timestamp, + value: gasFee, + }) } break } - case GAS_PRICE_PATH: { - events.push(eventCreator(TRANSACTION_UPDATED_GAS_EVENT, timestamp, value)) + // If the gas price or gas limit has been changed, we update the gasFee of the + // previously submitted event. These events happen when the gas limit and gas price is + // changed at the confirm screen. + case GAS_PRICE_PATH: + case GAS_LIMIT_PATH: { + const lastEvent = events[events.length - 1] || {} + const { lastEventKey } = lastEvent + + if (path === GAS_LIMIT_PATH) { + cachedGasLimit = value + } else if (path === GAS_PRICE_PATH) { + cachedGasPrice = value + } + + if (lastEventKey === TRANSACTION_SUBMITTED_EVENT || + lastEventKey === TRANSACTION_RESUBMITTED_EVENT) { + lastEvent.value = getHexGasTotal({ + gasLimit: cachedGasLimit, + gasPrice: cachedGasPrice, + }) + } + break } default: { - events.push(eventCreator(TRANSACTION_UPDATED_EVENT, timestamp)) + events.push({ + id, + hash, + eventKey: TRANSACTION_UPDATED_EVENT, + timestamp, + }) } } } @@ -88,6 +165,60 @@ export function getActivities (transaction) { // If txReceipt.status is '0x0', that means that an on-chain error occured for the transaction, // so we add an error entry to the Activity Log. return status === '0x0' - ? historyActivities.concat(eventCreator(TRANSACTION_ERRORED_EVENT)) + ? historyActivities.concat({ id, hash, eventKey: TRANSACTION_ERRORED_EVENT }) : historyActivities } + +/** + * @description Removes "Transaction dropped" activities from a list of sorted activities if one of + * the transactions has been confirmed. Typically, if multiple transactions have the same nonce, + * once one transaction is confirmed, the rest are dropped. In this case, we don't want to show + * multiple "Transaction dropped" activities, and instead want to show a single "Transaction + * confirmed". + * @param {Array} activities - List of sorted activities generated from the getActivities function. + * @returns {Array} + */ +function filterSortedActivities (activities) { + const filteredActivities = [] + const hasConfirmedActivity = Boolean(activities.find(({ eventKey }) => ( + eventKey === TRANSACTION_CONFIRMED_EVENT || eventKey === TRANSACTION_CANCEL_SUCCESS_EVENT + ))) + let addedDroppedActivity = false + + activities.forEach(activity => { + if (activity.eventKey === TRANSACTION_DROPPED_EVENT) { + if (!hasConfirmedActivity && !addedDroppedActivity) { + filteredActivities.push(activity) + addedDroppedActivity = true + } + } else { + filteredActivities.push(activity) + } + }) + + return filteredActivities +} + +/** + * Combines the histories of an array of transactions into a single array. + * @param {Array} transactions - Array of txMeta transaction objects. + * @returns {Array} + */ +export function combineTransactionHistories (transactions = []) { + if (!transactions.length) { + return [] + } + + const activities = [] + + transactions.forEach((transaction, index) => { + // The first transaction should be the transaction with the earliest submittedTime. We show the + // 'created' and 'submitted' activities here. All subsequent transactions will use 'resubmitted' + // instead. + const transactionActivities = getActivities(transaction, index === 0) + activities.push(...transactionActivities) + }) + + const sortedActivities = activities.sort((a, b) => a.timestamp - b.timestamp) + return filterSortedActivities(sortedActivities) +} diff --git a/ui/app/components/transaction-breakdown/index.scss b/ui/app/components/transaction-breakdown/index.scss index 1bb108943..b56cbdd7f 100644 --- a/ui/app/components/transaction-breakdown/index.scss +++ b/ui/app/components/transaction-breakdown/index.scss @@ -1,9 +1,10 @@ @import './transaction-breakdown-row/index'; .transaction-breakdown { - &__card { - background: $white; - height: 100%; + &__title { + border-bottom: 1px solid #d8d8d8; + padding-bottom: 4px; + text-transform: capitalize; } &__row-title { diff --git a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js b/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js index d18cd420c..4512b84f0 100644 --- a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js +++ b/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js @@ -2,8 +2,6 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import TransactionBreakdown from '../transaction-breakdown.component' -import TransactionBreakdownRow from '../transaction-breakdown-row' -import Card from '../../card' describe('TransactionBreakdown Component', () => { it('should render properly', () => { @@ -31,7 +29,5 @@ describe('TransactionBreakdown Component', () => { assert.ok(wrapper.hasClass('transaction-breakdown')) assert.ok(wrapper.hasClass('test-class')) - assert.equal(wrapper.find(Card).length, 1) - assert.equal(wrapper.find(Card).find(TransactionBreakdownRow).length, 4) }) }) diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js index 3a7647873..141e16e17 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js +++ b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js @@ -2,7 +2,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import TransactionBreakdownRow from './transaction-breakdown-row' -import Card from '../card' import CurrencyDisplay from '../currency-display' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import HexToDecimal from '../hex-to-decimal' @@ -37,63 +36,61 @@ export default class TransactionBreakdown extends PureComponent { return ( <div className={classnames('transaction-breakdown', className)}> - <Card - title={t('transaction')} - className="transaction-breakdown__card" + <div className="transaction-breakdown__title"> + { t('transaction') } + </div> + <TransactionBreakdownRow title={t('amount')}> + <UserPreferencedCurrencyDisplay + className="transaction-breakdown__value" + type={PRIMARY} + value={value} + /> + </TransactionBreakdownRow> + <TransactionBreakdownRow + title={`${t('gasLimit')} (${t('units')})`} + className="transaction-breakdown__row-title" > - <TransactionBreakdownRow title={t('amount')}> + <HexToDecimal + className="transaction-breakdown__value" + value={gas} + /> + </TransactionBreakdownRow> + { + typeof gasUsed === 'string' && ( + <TransactionBreakdownRow + title={`${t('gasUsed')} (${t('units')})`} + className="transaction-breakdown__row-title" + > + <HexToDecimal + className="transaction-breakdown__value" + value={gasUsed} + /> + </TransactionBreakdownRow> + ) + } + <TransactionBreakdownRow title={t('gasPrice')}> + <CurrencyDisplay + className="transaction-breakdown__value" + currency={nativeCurrency} + denomination={GWEI} + value={gasPrice} + hideLabel + /> + </TransactionBreakdownRow> + <TransactionBreakdownRow title={t('total')}> + <div> <UserPreferencedCurrencyDisplay - className="transaction-breakdown__value" + className="transaction-breakdown__value transaction-breakdown__value--eth-total" type={PRIMARY} - value={value} - /> - </TransactionBreakdownRow> - <TransactionBreakdownRow - title={`${t('gasLimit')} (${t('units')})`} - className="transaction-breakdown__row-title" - > - <HexToDecimal - className="transaction-breakdown__value" - value={gas} + value={totalInHex} /> - </TransactionBreakdownRow> - { - typeof gasUsed === 'string' && ( - <TransactionBreakdownRow - title={`${t('gasUsed')} (${t('units')})`} - className="transaction-breakdown__row-title" - > - <HexToDecimal - className="transaction-breakdown__value" - value={gasUsed} - /> - </TransactionBreakdownRow> - ) - } - <TransactionBreakdownRow title={t('gasPrice')}> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-breakdown__value" - currency={nativeCurrency} - denomination={GWEI} - value={gasPrice} - hideLabel + type={SECONDARY} + value={totalInHex} /> - </TransactionBreakdownRow> - <TransactionBreakdownRow title={t('total')}> - <div> - <UserPreferencedCurrencyDisplay - className="transaction-breakdown__value transaction-breakdown__value--eth-total" - type={PRIMARY} - value={totalInHex} - /> - <UserPreferencedCurrencyDisplay - className="transaction-breakdown__value" - type={SECONDARY} - value={totalInHex} - /> - </div> - </TransactionBreakdownRow> - </Card> + </div> + </TransactionBreakdownRow> </div> ) } diff --git a/ui/app/components/transaction-list-item-details/index.scss b/ui/app/components/transaction-list-item-details/index.scss index 54cf834cc..2e3a06f84 100644 --- a/ui/app/components/transaction-list-item-details/index.scss +++ b/ui/app/components/transaction-list-item-details/index.scss @@ -1,11 +1,16 @@ .transaction-list-item-details { &__header { - margin-bottom: 8px; + margin: 8px 16px; display: flex; justify-content: space-between; align-items: center; } + &__body { + background: #fafbfc; + padding: 8px 16px; + } + &__header-buttons { display: flex; flex-direction: row; @@ -45,5 +50,9 @@ &__transaction-activity-log { flex: 2; min-width: 0; + + @media screen and (min-width: $break-large) { + padding-left: 12px; + } } } diff --git a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js b/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js index f2bbe8789..62fc64db9 100644 --- a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js +++ b/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js @@ -23,9 +23,15 @@ describe('TransactionListItemDetails Component', () => { }, } + const transactionGroup = { + transactions: [transaction], + primaryTransaction: transaction, + initialTransaction: transaction, + } + const wrapper = shallow( <TransactionListItemDetails - transaction={transaction} + transactionGroup={transactionGroup} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } ) @@ -52,9 +58,18 @@ describe('TransactionListItemDetails Component', () => { }, } + const transactionGroup = { + transactions: [transaction], + primaryTransaction: transaction, + initialTransaction: transaction, + nonce: '0xa4', + hasRetried: false, + hasCancelled: false, + } + const wrapper = shallow( <TransactionListItemDetails - transaction={transaction} + transactionGroup={transactionGroup} showRetry={true} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js index a79213ace..cc2c45290 100644 --- a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import SenderToRecipient from '../sender-to-recipient' -import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants' +import { FLAT_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants' import TransactionActivityLog from '../transaction-activity-log' import TransactionBreakdown from '../transaction-breakdown' import Button from '../button' @@ -18,42 +18,43 @@ export default class TransactionListItemDetails extends PureComponent { onRetry: PropTypes.func, showCancel: PropTypes.bool, showRetry: PropTypes.bool, - transaction: PropTypes.object, + transactionGroup: PropTypes.object, } handleEtherscanClick = () => { - const { hash, metamaskNetworkId } = this.props.transaction + const { transactionGroup: { primaryTransaction } } = this.props + const { hash, metamaskNetworkId } = primaryTransaction const prefix = prefixForNetwork(metamaskNetworkId) const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` global.platform.openWindow({ url: etherscanUrl }) - this.setState({ showTransactionDetails: true }) } handleCancel = event => { - const { onCancel } = this.props + const { transactionGroup: { initialTransaction: { id } = {} } = {}, onCancel } = this.props event.stopPropagation() - onCancel() + onCancel(id) } handleRetry = event => { - const { onRetry } = this.props + const { transactionGroup: { initialTransaction: { id } = {} } = {}, onRetry } = this.props event.stopPropagation() - onRetry() + onRetry(id) } render () { const { t } = this.context - const { transaction, showCancel, showRetry } = this.props + const { transactionGroup, showCancel, showRetry, onCancel, onRetry } = this.props + const { primaryTransaction: transaction } = transactionGroup const { txParams: { to, from } = {} } = transaction return ( <div className="transaction-list-item-details"> <div className="transaction-list-item-details__header"> - <div>Details</div> + <div>{ t('details') }</div> <div className="transaction-list-item-details__header-buttons"> { showRetry && ( @@ -88,23 +89,27 @@ export default class TransactionListItemDetails extends PureComponent { </Tooltip> </div> </div> - <div className="transaction-list-item-details__sender-to-recipient-container"> - <SenderToRecipient - variant={CARDS_VARIANT} - addressOnly - recipientAddress={to} - senderAddress={from} - /> - </div> - <div className="transaction-list-item-details__cards-container"> - <TransactionBreakdown - transaction={transaction} - className="transaction-list-item-details__transaction-breakdown" - /> - <TransactionActivityLog - transaction={transaction} - className="transaction-list-item-details__transaction-activity-log" - /> + <div className="transaction-list-item-details__body"> + <div className="transaction-list-item-details__sender-to-recipient-container"> + <SenderToRecipient + variant={FLAT_VARIANT} + addressOnly + recipientAddress={to} + senderAddress={from} + /> + </div> + <div className="transaction-list-item-details__cards-container"> + <TransactionBreakdown + transaction={transaction} + className="transaction-list-item-details__transaction-breakdown" + /> + <TransactionActivityLog + transactionGroup={transactionGroup} + className="transaction-list-item-details__transaction-activity-log" + onCancel={onCancel} + onRetry={onRetry} + /> + </div> </div> </div> ) diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/transaction-list-item/index.scss index 449974734..9e73a546c 100644 --- a/ui/app/components/transaction-list-item/index.scss +++ b/ui/app/components/transaction-list-item/index.scss @@ -117,12 +117,6 @@ } } - &__details-container { - padding: 8px 16px 16px; - background: #f3f4f7; - width: 100%; - } - &__expander { max-height: 0px; width: 100%; diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js index 5334484db..ecd8b4cef 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js @@ -18,6 +18,7 @@ export default class TransactionListItem extends PureComponent { history: PropTypes.object, methodData: PropTypes.object, nonceAndDate: PropTypes.string, + primaryTransaction: PropTypes.object, retryTransaction: PropTypes.func, setSelectedToken: PropTypes.func, showCancelModal: PropTypes.func, @@ -26,6 +27,7 @@ export default class TransactionListItem extends PureComponent { token: PropTypes.object, tokenData: PropTypes.object, transaction: PropTypes.object, + transactionGroup: PropTypes.object, value: PropTypes.string, fetchBasicGasAndTimeEstimates: PropTypes.func, fetchGasEstimates: PropTypes.func, @@ -51,36 +53,48 @@ export default class TransactionListItem extends PureComponent { this.setState({ showTransactionDetails: !showTransactionDetails }) } - handleCancel = () => { - const { transaction: { id, txParams: { gasPrice } } = {}, showCancelModal } = this.props - showCancelModal(id, gasPrice) + handleCancel = id => { + const { + primaryTransaction: { txParams: { gasPrice } } = {}, + transaction: { id: initialTransactionId }, + showCancelModal, + } = this.props + + const cancelId = id || initialTransactionId + showCancelModal(cancelId, gasPrice) } - handleRetry = () => { + /** + * @name handleRetry + * @description Resubmits a transaction. Retrying a transaction within a list of transactions with + * the same nonce requires keeping the original value while increasing the gas price of the latest + * transaction. + * @param {number} id - Transaction id + */ + handleRetry = id => { const { - transaction: { txParams: { to } = {} }, + primaryTransaction: { txParams: { gasPrice } } = {}, + transaction: { txParams: { to } = {}, id: initialTransactionId }, methodData: { name } = {}, setSelectedToken, + retryTransaction, + fetchBasicGasAndTimeEstimates, + fetchGasEstimates, } = this.props if (name === TOKEN_METHOD_TRANSFER) { setSelectedToken(to) } - return this.resubmit() - } + const retryId = id || initialTransactionId - resubmit () { - const { transaction, retryTransaction, fetchBasicGasAndTimeEstimates, fetchGasEstimates } = this.props - fetchBasicGasAndTimeEstimates().then(basicEstimates => { - fetchGasEstimates(basicEstimates.blockTime) - }).then(() => { - retryTransaction(transaction) - }) + return fetchBasicGasAndTimeEstimates() + .then(basicEstimates => fetchGasEstimates(basicEstimates.blockTime)) + .then(retryTransaction(retryId, gasPrice)) } renderPrimaryCurrency () { - const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props + const { token, primaryTransaction: { txParams: { data } = {} } = {}, value } = this.props return token ? ( @@ -118,12 +132,14 @@ export default class TransactionListItem extends PureComponent { render () { const { assetImages, + transaction, methodData, nonceAndDate, + primaryTransaction, showCancel, showRetry, tokenData, - transaction, + transactionGroup, } = this.props const { txParams = {} } = transaction const { showTransactionDetails } = this.state @@ -156,11 +172,11 @@ export default class TransactionListItem extends PureComponent { </div> <TransactionStatus className="transaction-list-item__status" - statusKey={getStatusKey(transaction)} + statusKey={getStatusKey(primaryTransaction)} title={( - (transaction.err && transaction.err.rpc) - ? transaction.err.rpc.message - : transaction.err && transaction.err.message + (primaryTransaction.err && primaryTransaction.err.rpc) + ? primaryTransaction.err.rpc.message + : primaryTransaction.err && primaryTransaction.err.message )} /> { this.renderPrimaryCurrency() } @@ -173,7 +189,7 @@ export default class TransactionListItem extends PureComponent { showTransactionDetails && ( <div className="transaction-list-item__details-container"> <TransactionListItemDetails - transaction={transaction} + transactionGroup={transactionGroup} onRetry={this.handleRetry} showRetry={showRetry && methodData.done} onCancel={this.handleCancel} diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js index 61ecb04d0..73d9d8250 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js @@ -6,6 +6,7 @@ import TransactionListItem from './transaction-list-item.component' import { setSelectedToken, showModal, showSidebar } from '../../actions' import { hexToDecimal } from '../../helpers/conversions.util' import { getTokenData } from '../../helpers/transactions.util' +import { increaseLastGasPrice } from '../../helpers/confirm-transaction/util' import { formatDate } from '../../util' import { fetchBasicGasAndTimeEstimates, @@ -14,26 +15,13 @@ import { setCustomGasLimit, } from '../../ducks/gas.duck' -const mapStateToProps = (state, ownProps) => { - const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps - - const tokenData = data && getTokenData(data) - const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time) - - return { - value, - nonceAndDate, - tokenData, - } -} - const mapDispatchToProps = dispatch => { return { fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)), - retryTransaction: (transaction) => { - dispatch(setCustomGasPrice(transaction.txParams.gasPrice)) + retryTransaction: (transaction, gasPrice) => { + dispatch(setCustomGasPrice(gasPrice || transaction.txParams.gasPrice)) dispatch(setCustomGasLimit(transaction.txParams.gas)) dispatch(showSidebar({ transitionName: 'sidebar-left', @@ -47,8 +35,35 @@ const mapDispatchToProps = dispatch => { } } +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps + const { retryTransaction, ...restDispatchProps } = dispatchProps + const { txParams: { nonce, data } = {}, time } = initialTransaction + const { txParams: { value } = {} } = primaryTransaction + + const tokenData = data && getTokenData(data) + const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time) + + return { + ...stateProps, + ...restDispatchProps, + ...ownProps, + value, + nonceAndDate, + tokenData, + transaction: initialTransaction, + primaryTransaction, + retryTransaction: (transactionId, gasPrice) => { + const { transactionGroup: { transactions = [] } } = ownProps + const transaction = transactions.find(tx => tx.id === transactionId) || {} + const increasedGasPrice = increaseLastGasPrice(gasPrice) + retryTransaction(transaction, increasedGasPrice) + }, + } +} + export default compose( withRouter, - connect(mapStateToProps, mapDispatchToProps), + connect(null, mapDispatchToProps, mergeProps), withMethodData, )(TransactionListItem) diff --git a/ui/app/components/transaction-list/transaction-list.component.js b/ui/app/components/transaction-list/transaction-list.component.js index eef60186d..c1e3b3d1c 100644 --- a/ui/app/components/transaction-list/transaction-list.component.js +++ b/ui/app/components/transaction-list/transaction-list.component.js @@ -12,13 +12,11 @@ export default class TransactionList extends PureComponent { static defaultProps = { pendingTransactions: [], completedTransactions: [], - transactionToRetry: {}, } static propTypes = { pendingTransactions: PropTypes.array, completedTransactions: PropTypes.array, - transactionToRetry: PropTypes.object, selectedToken: PropTypes.object, updateNetworkNonce: PropTypes.func, assetImages: PropTypes.object, @@ -37,26 +35,34 @@ export default class TransactionList extends PureComponent { } } - shouldShowRetry = transaction => { - const { transactionToRetry } = this.props - const { id, submittedTime } = transaction - return id === transactionToRetry.id && Date.now() - submittedTime > 30000 + shouldShowRetry = (transactionGroup, isEarliestNonce) => { + const { transactions = [], hasRetried } = transactionGroup + const [earliestTransaction = {}] = transactions + const { submittedTime } = earliestTransaction + return Date.now() - submittedTime > 30000 && isEarliestNonce && !hasRetried + } + + shouldShowCancel (transactionGroup) { + const { hasCancelled } = transactionGroup + return !hasCancelled } renderTransactions () { const { t } = this.context const { pendingTransactions = [], completedTransactions = [] } = this.props + const pendingLength = pendingTransactions.length + return ( <div className="transaction-list__transactions"> { - pendingTransactions.length > 0 && ( + pendingLength > 0 && ( <div className="transaction-list__pending-transactions"> <div className="transaction-list__header"> { `${t('queue')} (${pendingTransactions.length})` } </div> { - pendingTransactions.map((transaction, index) => ( - this.renderTransaction(transaction, index, true) + pendingTransactions.map((transactionGroup, index) => ( + this.renderTransaction(transactionGroup, index, true, index === pendingLength - 1) )) } </div> @@ -68,8 +74,8 @@ export default class TransactionList extends PureComponent { </div> { completedTransactions.length > 0 - ? completedTransactions.map((transaction, index) => ( - this.renderTransaction(transaction, index) + ? completedTransactions.map((transactionGroup, index) => ( + this.renderTransaction(transactionGroup, index) )) : this.renderEmpty() } @@ -78,21 +84,22 @@ export default class TransactionList extends PureComponent { ) } - renderTransaction (transaction, index, showCancel) { + renderTransaction (transactionGroup, index, isPendingTx = false, isEarliestNonce = false) { const { selectedToken, assetImages } = this.props + const { transactions = [] } = transactionGroup - return transaction.key === TRANSACTION_TYPE_SHAPESHIFT + return transactions[0].key === TRANSACTION_TYPE_SHAPESHIFT ? ( <ShapeShiftTransactionListItem - { ...transaction } + { ...transactions[0] } key={`shapeshift${index}`} /> ) : ( <TransactionListItem - transaction={transaction} - key={transaction.id} - showRetry={this.shouldShowRetry(transaction)} - showCancel={showCancel} + transactionGroup={transactionGroup} + key={`${transactionGroup.nonce}:${index}`} + showRetry={isPendingTx && this.shouldShowRetry(transactionGroup, isEarliestNonce)} + showCancel={isPendingTx && this.shouldShowCancel(transactionGroup)} token={selectedToken} assetImages={assetImages} /> diff --git a/ui/app/components/transaction-list/transaction-list.container.js b/ui/app/components/transaction-list/transaction-list.container.js index 2e946c67d..e70ca15c5 100644 --- a/ui/app/components/transaction-list/transaction-list.container.js +++ b/ui/app/components/transaction-list/transaction-list.container.js @@ -3,24 +3,17 @@ import { withRouter } from 'react-router-dom' import { compose } from 'recompose' import TransactionList from './transaction-list.component' import { - pendingTransactionsSelector, - submittedPendingTransactionsSelector, - completedTransactionsSelector, + nonceSortedCompletedTransactionsSelector, + nonceSortedPendingTransactionsSelector, } from '../../selectors/transactions' import { getSelectedAddress, getAssetImages } from '../../selectors' import { selectedTokenSelector } from '../../selectors/tokens' -import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util' import { updateNetworkNonce } from '../../actions' const mapStateToProps = state => { - const pendingTransactions = pendingTransactionsSelector(state) - const submittedPendingTransactions = submittedPendingTransactionsSelector(state) - const networkNonce = state.appState.networkNonce - return { - completedTransactions: completedTransactionsSelector(state), - pendingTransactions, - transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce), + completedTransactions: nonceSortedCompletedTransactionsSelector(state), + pendingTransactions: nonceSortedPendingTransactionsSelector(state), selectedToken: selectedTokenSelector(state), selectedAddress: getSelectedAddress(state), assetImages: getAssetImages(state), diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/transaction-status/index.scss index 26a1f5d38..e7daafeef 100644 --- a/ui/app/components/transaction-status/index.scss +++ b/ui/app/components/transaction-status/index.scss @@ -1,6 +1,6 @@ .transaction-status { height: 26px; - width: 81px; + width: 84px; border-radius: 4px; background-color: #f0f0f0; color: #5e6064; @@ -12,22 +12,34 @@ @media screen and (max-width: $break-small) { height: 16px; - width: 70px; + width: 72px; font-size: .5rem; } &--confirmed { background-color: #eafad7; color: #609a1c; + + .transaction-status__transaction-count { + border: 1px solid #609a1c; + } } &--approved, &--submitted { background-color: #FFF2DB; color: #CA810A; + + .transaction-status__transaction-count { + border: 1px solid #CA810A; + } } &--failed { background: lighten($monzo, 56%); color: $monzo; + + .transaction-status__transaction-count { + border: 1px solid $monzo; + } } } diff --git a/ui/app/components/transaction-status/tests/transaction-status.component.test.js b/ui/app/components/transaction-status/tests/transaction-status.component.test.js index 9e3bffe4f..f4ddc9206 100644 --- a/ui/app/components/transaction-status/tests/transaction-status.component.test.js +++ b/ui/app/components/transaction-status/tests/transaction-status.component.test.js @@ -15,9 +15,8 @@ describe('TransactionStatus Component', () => { ) assert.ok(wrapper) - const tooltipProps = wrapper.find(Tooltip).props() - assert.equal(tooltipProps.children, 'APPROVED') - assert.equal(tooltipProps.title, 'test-title') + assert.equal(wrapper.text(), 'APPROVED') + assert.equal(wrapper.find(Tooltip).props().title, 'test-title') }) it('should render SUBMITTED properly', () => { @@ -29,7 +28,6 @@ describe('TransactionStatus Component', () => { ) assert.ok(wrapper) - const tooltipProps = wrapper.find(Tooltip).props() - assert.equal(tooltipProps.children, 'PENDING') + assert.equal(wrapper.text(), 'PENDING') }) }) diff --git a/ui/app/components/transaction-status/transaction-status.component.js b/ui/app/components/transaction-status/transaction-status.component.js index 0d47d7868..28544d2cd 100644 --- a/ui/app/components/transaction-status/transaction-status.component.js +++ b/ui/app/components/transaction-status/transaction-status.component.js @@ -11,6 +11,7 @@ import { CONFIRMED_STATUS, FAILED_STATUS, DROPPED_STATUS, + CANCELLED_STATUS, } from '../../constants/transactions' const statusToClassNameHash = { @@ -22,6 +23,7 @@ const statusToClassNameHash = { [CONFIRMED_STATUS]: 'transaction-status--confirmed', [FAILED_STATUS]: 'transaction-status--failed', [DROPPED_STATUS]: 'transaction-status--dropped', + [CANCELLED_STATUS]: 'transaction-status--failed', } const statusToTextHash = { @@ -49,7 +51,10 @@ export default class TransactionStatus extends PureComponent { return ( <div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}> - <Tooltip position="top" title={title}> + <Tooltip + position="top" + title={title} + > { statusText } </Tooltip> </div> diff --git a/ui/app/constants/transactions.js b/ui/app/constants/transactions.js index 2dc061091..d0a819b9b 100644 --- a/ui/app/constants/transactions.js +++ b/ui/app/constants/transactions.js @@ -6,6 +6,7 @@ export const SUBMITTED_STATUS = 'submitted' export const CONFIRMED_STATUS = 'confirmed' export const FAILED_STATUS = 'failed' export const DROPPED_STATUS = 'dropped' +export const CANCELLED_STATUS = 'cancelled' export const TOKEN_METHOD_TRANSFER = 'transfer' export const TOKEN_METHOD_APPROVE = 'approve' @@ -17,7 +18,7 @@ export const APPROVE_ACTION_KEY = 'approve' export const SEND_TOKEN_ACTION_KEY = 'sentTokens' export const TRANSFER_FROM_ACTION_KEY = 'transferFrom' export const SIGNATURE_REQUEST_KEY = 'signatureRequest' -export const UNKNOWN_FUNCTION_KEY = 'unknownFunction' +export const CONTRACT_INTERACTION_KEY = 'contractInteraction' export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt' export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift' diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index 8db24cc83..83c236d81 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -4,6 +4,9 @@ import { loadLocalStorageData, saveLocalStorageData, } from '../../lib/local-storage-helpers' +import { + decGWEIToHexWEI, +} from '../helpers/conversions.util' // Actions const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED' @@ -403,6 +406,17 @@ export function fetchGasEstimates (blockTime) { } } +export function setCustomGasPriceForRetry (newPrice) { + return (dispatch) => { + if (newPrice !== '0x0') { + dispatch(setCustomGasPrice(newPrice)) + } else { + const { fast } = loadLocalStorageData('BASIC_PRICE_ESTIMATES') + dispatch(setCustomGasPrice(decGWEIToHexWEI(fast))) + } + } +} + export function setBasicGasEstimateData (basicGasEstimateData) { return { type: SET_BASIC_GAS_ESTIMATE_DATA, diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js index 2f4b1d095..0f1ed70a3 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/transactions.util.js @@ -2,6 +2,10 @@ import ethUtil from 'ethereumjs-util' import MethodRegistry from 'eth-method-registry' import abi from 'human-standard-token-abi' import abiDecoder from 'abi-decoder' +import { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_STATUS_CONFIRMED, +} from '../../../app/scripts/controllers/transactions/enums' import { TOKEN_METHOD_TRANSFER, @@ -13,7 +17,7 @@ import { SEND_TOKEN_ACTION_KEY, TRANSFER_FROM_ACTION_KEY, SIGNATURE_REQUEST_KEY, - UNKNOWN_FUNCTION_KEY, + CONTRACT_INTERACTION_KEY, CANCEL_ATTEMPT_ACTION_KEY, } from '../constants/transactions' @@ -87,7 +91,7 @@ export async function getTransactionActionKey (transaction, methodData) { const methodName = name && name.toLowerCase() if (!methodName) { - return UNKNOWN_FUNCTION_KEY + return CONTRACT_INTERACTION_KEY } switch (methodName) { @@ -148,12 +152,16 @@ export function sumHexes (...args) { * @returns {string} */ export function getStatusKey (transaction) { - const { txReceipt: { status } = {} } = transaction + const { txReceipt: { status: receiptStatus } = {}, type, status } = transaction // There was an on-chain failure - if (status === '0x0') { + if (receiptStatus === '0x0') { return 'failed' } + if (status === TRANSACTION_STATUS_CONFIRMED && type === TRANSACTION_TYPE_CANCEL) { + return 'cancelled' + } + return transaction.status } diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js index 479002794..301e8d11f 100644 --- a/ui/app/selectors/transactions.js +++ b/ui/app/selectors/transactions.js @@ -1,16 +1,44 @@ import { createSelector } from 'reselect' -import { valuesFor } from '../util' import { UNAPPROVED_STATUS, APPROVED_STATUS, SUBMITTED_STATUS, + CONFIRMED_STATUS, } from '../constants/transactions' +import { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_TYPE_RETRY, +} from '../../../app/scripts/controllers/transactions/enums' +import { hexToDecimal } from '../helpers/conversions.util' import { selectedTokenAddressSelector } from './tokens' +import txHelper from '../../lib/tx-helper' export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList +export const unapprovedPersonalMsgsSelector = state => state.metamask.unapprovedPersonalMsgs +export const unapprovedTypedMessagesSelector = state => state.metamask.unapprovedTypedMessages +export const networkSelector = state => state.metamask.network + +export const unapprovedMessagesSelector = createSelector( + unapprovedMsgsSelector, + unapprovedPersonalMsgsSelector, + unapprovedTypedMessagesSelector, + networkSelector, + ( + unapprovedMsgs = {}, + unapprovedPersonalMsgs = {}, + unapprovedTypedMessages = {}, + network + ) => txHelper( + {}, + unapprovedMsgs, + unapprovedPersonalMsgs, + unapprovedTypedMessages, + network + ) || [] +) const pendingStatusHash = { [UNAPPROVED_STATUS]: true, @@ -18,14 +46,18 @@ const pendingStatusHash = { [SUBMITTED_STATUS]: true, } +const priorityStatusHash = { + ...pendingStatusHash, + [CONFIRMED_STATUS]: true, +} + export const transactionsSelector = createSelector( selectedTokenAddressSelector, - unapprovedMsgsSelector, + unapprovedMessagesSelector, shapeShiftTxListSelector, selectedAddressTxListSelector, - (selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => { - const unapprovedMsgsList = valuesFor(unapprovedMsgs) - const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList) + (selectedTokenAddress, unapprovedMessages = [], shapeShiftTxList = [], transactions = []) => { + const txsToRender = transactions.concat(unapprovedMessages, shapeShiftTxList) return selectedTokenAddress ? txsToRender @@ -36,23 +68,199 @@ export const transactionsSelector = createSelector( } ) -export const pendingTransactionsSelector = createSelector( +/** + * @name insertOrderedNonce + * @private + * @description Inserts (mutates) a nonce into an array of ordered nonces, sorted in ascending + * order. + * @param {string[]} nonces - Array of nonce strings in hex + * @param {string} nonceToInsert - Nonce string in hex to be inserted into the array of nonces. + * @returns {string[]} + */ +const insertOrderedNonce = (nonces, nonceToInsert) => { + let insertIndex = nonces.length + + for (let i = 0; i < nonces.length; i++) { + const nonce = nonces[i] + + if (Number(hexToDecimal(nonce)) < Number(hexToDecimal(nonceToInsert))) { + insertIndex = i + break + } + } + + nonces.splice(insertIndex, 0, nonceToInsert) +} + +/** + * @name insertTransactionByTime + * @private + * @description Inserts (mutates) a transaction object into an array of ordered transactions, sorted + * in ascending order by time. + * @param {Object[]} transactions - Array of transaction objects. + * @param {Object} transaction - Transaction object to be inserted into the array of transactions. + * @returns {Object[]} + */ +const insertTransactionByTime = (transactions, transaction) => { + const { time } = transaction + + let insertIndex = transactions.length + + for (let i = 0; i < transactions.length; i++) { + const tx = transactions[i] + + if (tx.time > time) { + insertIndex = i + break + } + } + + transactions.splice(insertIndex, 0, transaction) +} + +/** + * Contains transactions and properties associated with those transactions of the same nonce. + * @typedef {Object} transactionGroup + * @property {string} nonce - The nonce that the transactions within this transactionGroup share. + * @property {Object[]} transactions - An array of transaction (txMeta) objects. + * @property {Object} initialTransaction - The transaction (txMeta) with the lowest "time". + * @property {Object} primaryTransaction - Either the latest transaction or the confirmed + * transaction. + * @property {boolean} hasRetried - True if a transaction in the group was a retry transaction. + * @property {boolean} hasCancelled - True if a transaction in the group was a cancel transaction. + */ + +/** + * @name insertTransactionGroupByTime + * @private + * @description Inserts (mutates) a transactionGroup object into an array of ordered + * transactionGroups, sorted in ascending order by nonce. + * @param {transactionGroup[]} transactionGroups - Array of transactionGroup objects. + * @param {transactionGroup} transactionGroup - transactionGroup object to be inserted into the + * array of transactionGroups. + * @returns {transactionGroup[]} + */ +const insertTransactionGroupByTime = (transactionGroups, transactionGroup) => { + const { primaryTransaction: { time } = {} } = transactionGroup + + let insertIndex = transactionGroups.length + + for (let i = 0; i < transactionGroups.length; i++) { + const txGroup = transactionGroups[i] + + if (txGroup.time > time) { + insertIndex = i + break + } + } + + transactionGroups.splice(insertIndex, 0, transactionGroup) +} + +/** + * @name nonceSortedTransactionsSelector + * @description Returns an array of transactionGroups sorted by nonce in ascending order. + * @returns {transactionGroup[]} + */ +export const nonceSortedTransactionsSelector = createSelector( transactionsSelector, + (transactions = []) => { + const unapprovedTransactionGroups = [] + const orderedNonces = [] + const nonceToTransactionsMap = {} + + transactions.forEach(transaction => { + const { txParams: { nonce } = {}, status, type, time: txTime } = transaction + + if (typeof nonce === 'undefined') { + const transactionGroup = { + transactions: [transaction], + initialTransaction: transaction, + primaryTransaction: transaction, + hasRetried: false, + hasCancelled: false, + } + + insertTransactionGroupByTime(unapprovedTransactionGroups, transactionGroup) + } else if (nonce in nonceToTransactionsMap) { + const nonceProps = nonceToTransactionsMap[nonce] + insertTransactionByTime(nonceProps.transactions, transaction) + + if (status in priorityStatusHash) { + const { primaryTransaction: { time: primaryTxTime = 0 } = {} } = nonceProps + + if (status === CONFIRMED_STATUS || txTime > primaryTxTime) { + nonceProps.primaryTransaction = transaction + } + } + + const { initialTransaction: { time: initialTxTime = 0 } = {} } = nonceProps + + // Used to display the transaction action, since we don't want to overwrite the action if + // it was replaced with a cancel attempt transaction. + if (txTime < initialTxTime) { + nonceProps.initialTransaction = transaction + } + + if (type === TRANSACTION_TYPE_RETRY) { + nonceProps.hasRetried = true + } + + if (type === TRANSACTION_TYPE_CANCEL) { + nonceProps.hasCancelled = true + } + } else { + nonceToTransactionsMap[nonce] = { + nonce, + transactions: [transaction], + initialTransaction: transaction, + primaryTransaction: transaction, + hasRetried: transaction.type === TRANSACTION_TYPE_RETRY, + hasCancelled: transaction.type === TRANSACTION_TYPE_CANCEL, + } + + insertOrderedNonce(orderedNonces, nonce) + } + }) + + const orderedTransactionGroups = orderedNonces.map(nonce => nonceToTransactionsMap[nonce]) + return unapprovedTransactionGroups.concat(orderedTransactionGroups) + } +) + +/** + * @name nonceSortedPendingTransactionsSelector + * @description Returns an array of transactionGroups where transactions are still pending sorted by + * nonce in descending order. + * @returns {transactionGroup[]} + */ +export const nonceSortedPendingTransactionsSelector = createSelector( + nonceSortedTransactionsSelector, (transactions = []) => ( - transactions.filter(transaction => transaction.status in pendingStatusHash).reverse() + transactions + .filter(({ primaryTransaction }) => primaryTransaction.status in pendingStatusHash) + .reverse() ) ) -export const submittedPendingTransactionsSelector = createSelector( - transactionsSelector, +/** + * @name nonceSortedCompletedTransactionsSelector + * @description Returns an array of transactionGroups where transactions are confirmed sorted by + * nonce in descending order. + * @returns {transactionGroup[]} + */ +export const nonceSortedCompletedTransactionsSelector = createSelector( + nonceSortedTransactionsSelector, (transactions = []) => ( - transactions.filter(transaction => transaction.status === SUBMITTED_STATUS) + transactions.filter(({ primaryTransaction }) => { + return !(primaryTransaction.status in pendingStatusHash) + }) ) ) -export const completedTransactionsSelector = createSelector( +export const submittedPendingTransactionsSelector = createSelector( transactionsSelector, (transactions = []) => ( - transactions.filter(transaction => !(transaction.status in pendingStatusHash)) + transactions.filter(transaction => transaction.status === SUBMITTED_STATUS) ) ) diff --git a/ui/app/util.js b/ui/app/util.js index b19a028cc..28f027e26 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -8,8 +8,8 @@ const GWEI_FACTOR = new ethUtil.BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) // formatData :: ( date: <Unix Timestamp> ) -> String -function formatDate (date) { - return vreme.format(new Date(date), '3/16/2014 at 14:30') +function formatDate (date, format = '3/16/2014 at 14:30') { + return vreme.format(new Date(date), format) } var valueTable = { diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index 0a6f55a63..260dbaa39 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -21,7 +21,7 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMes allValues = allValues.concat(typedValues) allValues = allValues.sort((a, b) => { - return a.time > b.time + return a.time - b.time }) return allValues |