From 01aace0df0969b940d9deea21c7edfba669278fe Mon Sep 17 00:00:00 2001 From: Kieran Hall Date: Thu, 28 Nov 2024 10:04:37 +0100 Subject: [PATCH] explorer: Look-up transaction in mempool Resolves #2877 --- explorer/CHANGELOG.md | 12 +++-- .../lib/components/data-card/DataCard.svelte | 1 - .../TransactionDetails.svelte | 4 +- .../src/lib/dusk/components/banner/Banner.css | 46 +++++++++++++++++ .../lib/dusk/components/banner/Banner.svelte | 51 +++++++++++++++++++ .../lib/dusk/components/dusk.components.d.ts | 2 + explorer/src/lib/dusk/components/index.js | 1 + .../lib/services/__tests__/duskAPI.spec.js | 12 ++--- explorer/src/lib/services/duskAPI.js | 18 ++++++- explorer/src/lib/services/gql-queries.js | 10 +++- .../transactions/transaction/+page.svelte | 29 +++++++---- explorer/src/style/dusk/colors.css | 9 ++++ explorer/src/style/dusk/language.css | 24 +++++++-- 13 files changed, 186 insertions(+), 33 deletions(-) create mode 100644 explorer/src/lib/dusk/components/banner/Banner.css create mode 100644 explorer/src/lib/dusk/components/banner/Banner.svelte diff --git a/explorer/CHANGELOG.md b/explorer/CHANGELOG.md index ea6c5d33ce..6ad69fbaee 100644 --- a/explorer/CHANGELOG.md +++ b/explorer/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add decode feature for `memo` field [#2527] - Add top node info in StatisticsPanel [#2613] - Add Provisioners page [#2649] +- Check if transaction exists in mempool [#2877] ### Changed @@ -67,8 +68,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -[#2017]: https://github.com/dusk-network/rusk/issues/2017 [#1892]: https://github.com/dusk-network/rusk/issues/1892 +[#2017]: https://github.com/dusk-network/rusk/issues/2017 [#2025]: https://github.com/dusk-network/rusk/issues/2025 [#2034]: https://github.com/dusk-network/rusk/issues/2034 [#2036]: https://github.com/dusk-network/rusk/issues/2036 @@ -79,21 +80,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#2059]: https://github.com/dusk-network/rusk/issues/2059 [#2061]: https://github.com/dusk-network/rusk/issues/2061 [#2159]: https://github.com/dusk-network/rusk/issues/2159 +[#2166]: https://github.com/dusk-network/rusk/issues/2166 [#2220]: https://github.com/dusk-network/rusk/issues/2220 +[#2347]: https://github.com/dusk-network/rusk/issues/2347 [#2348]: https://github.com/dusk-network/rusk/issues/2348 [#2362]: https://github.com/dusk-network/rusk/issues/2362 [#2363]: https://github.com/dusk-network/rusk/issues/2363 -[#2363]: https://github.com/dusk-network/rusk/issues/2347 [#2364]: https://github.com/dusk-network/rusk/issues/2364 [#2389]: https://github.com/dusk-network/rusk/issues/2389 [#2527]: https://github.com/dusk-network/rusk/issues/2527 -[#2166]: https://github.com/dusk-network/rusk/issues/2166 [#2585]: https://github.com/dusk-network/rusk/issues/2585 -[#2640]: https://github.com/dusk-network/rusk/issues/2640 -[#2668]: https://github.com/dusk-network/rusk/issues/2668 [#2613]: https://github.com/dusk-network/rusk/issues/2613 +[#2640]: https://github.com/dusk-network/rusk/issues/2640 [#2649]: https://github.com/dusk-network/rusk/issues/2649 [#2662]: https://github.com/dusk-network/rusk/issues/2662 +[#2668]: https://github.com/dusk-network/rusk/issues/2668 +[#2877]: https://github.com/dusk-network/rusk/issues/2877 [#3038]: https://github.com/dusk-network/rusk/issues/3038 [#3064]: https://github.com/dusk-network/rusk/issues/3064 diff --git a/explorer/src/lib/components/data-card/DataCard.svelte b/explorer/src/lib/components/data-card/DataCard.svelte index a2089213e8..cba1fb3bd2 100644 --- a/explorer/src/lib/components/data-card/DataCard.svelte +++ b/explorer/src/lib/components/data-card/DataCard.svelte @@ -26,7 +26,6 @@ export let className = undefined; $: classes = makeClassName(["data-card", className]); - $: hasEmptyData = Array.isArray(data) && data.length === 0; diff --git a/explorer/src/lib/components/transaction-details/TransactionDetails.svelte b/explorer/src/lib/components/transaction-details/TransactionDetails.svelte index b498f3c09b..23a1ac4e20 100644 --- a/explorer/src/lib/components/transaction-details/TransactionDetails.svelte +++ b/explorer/src/lib/components/transaction-details/TransactionDetails.svelte @@ -57,8 +57,6 @@ /** @type {boolean} */ let isMemoDecoded = false; - $: classes = makeClassName(["transaction-details", className]); - onMount(() => { const resizeObserver = new ResizeObserver((entries) => { const entry = entries[0]; @@ -70,6 +68,8 @@ return () => resizeObserver.disconnect(); }); + + $: classes = makeClassName(["transaction-details", className]); + import { makeClassName } from "$lib/dusk/string"; + import { Icon } from "$lib/dusk/components"; + import { + mdiAlertCircleOutline, + mdiAlertDecagramOutline, + mdiAlertOutline, + } from "@mdi/js"; + + import "./Banner.css"; + + /** @type {string} */ + export let title; + + /** @type {String | Undefined} */ + export let className = undefined; + + /** @type {BannerVariant} */ + export let variant = "info"; + + function getBannerIconPath() { + switch (variant) { + case "warning": + return mdiAlertOutline; + case "error": + return mdiAlertDecagramOutline; + default: + return mdiAlertCircleOutline; + } + } + + $: classes = makeClassName([ + "dusk-banner", + `dusk-banner--${variant}`, + className, + ]); + + +
+ +
+ {title} + +

No banner content provided.

+
+
+
diff --git a/explorer/src/lib/dusk/components/dusk.components.d.ts b/explorer/src/lib/dusk/components/dusk.components.d.ts index 8fb8958889..57c0ceb351 100644 --- a/explorer/src/lib/dusk/components/dusk.components.d.ts +++ b/explorer/src/lib/dusk/components/dusk.components.d.ts @@ -1,5 +1,7 @@ type BadgeVariant = "neutral" | "success" | "warning" | "error" | "alt"; +type BannerVariant = "info" | "warning" | "error"; + type ButtonSize = "normal" | "small"; type ButtonVariant = "primary" | "secondary" | "tertiary"; diff --git a/explorer/src/lib/dusk/components/index.js b/explorer/src/lib/dusk/components/index.js index d3066574f1..2a0ff22a84 100644 --- a/explorer/src/lib/dusk/components/index.js +++ b/explorer/src/lib/dusk/components/index.js @@ -1,5 +1,6 @@ export { default as Anchor } from "./anchor/Anchor.svelte"; export { default as AnchorButton } from "./anchor-button/AnchorButton.svelte"; +export { default as Banner } from "./banner/Banner.svelte"; export { default as Badge } from "./badge/Badge.svelte"; export { default as Button } from "./button/Button.svelte"; export { default as Card } from "./card/Card.svelte"; diff --git a/explorer/src/lib/services/__tests__/duskAPI.spec.js b/explorer/src/lib/services/__tests__/duskAPI.spec.js index 0d8fcea5dc..1a791b1dd7 100644 --- a/explorer/src/lib/services/__tests__/duskAPI.spec.js +++ b/explorer/src/lib/services/__tests__/duskAPI.spec.js @@ -71,7 +71,7 @@ describe("duskAPI", () => { expect(fetchSpy.mock.calls[0][0]).toStrictEqual(gqlExpectedURL); expect(fetchSpy.mock.calls[0][1]).toMatchInlineSnapshot(` { - "body": "{"data":"\\n \\n\\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo\\n txType\\n }\\n}\\n\\nfragment BlockInfo on Block {\\n header {\\n hash,\\n gasLimit,\\n height,\\n prevBlockHash,\\n seed,\\n stateHash,\\n timestamp,\\n version\\n },\\n fees,\\n gasSpent,\\n reward,\\n transactions {...TransactionInfo}\\n}\\n\\n query($id: String!) { block(hash: $id) {...BlockInfo} }\\n ","topic":"gql"}", + "body": "{"data":"\\n \\n\\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo,\\n txType\\n }\\n}\\n\\nfragment BlockInfo on Block {\\n header {\\n hash,\\n gasLimit,\\n height,\\n prevBlockHash,\\n seed,\\n stateHash,\\n timestamp,\\n version\\n },\\n fees,\\n gasSpent,\\n reward,\\n transactions {...TransactionInfo}\\n}\\n\\n query($id: String!) { block(hash: $id) {...BlockInfo} }\\n ","topic":"gql"}", "headers": { "Accept": "application/json", "Accept-Charset": "utf-8", @@ -183,7 +183,7 @@ describe("duskAPI", () => { expect(fetchSpy.mock.calls[0][0]).toStrictEqual(gqlExpectedURL); expect(fetchSpy.mock.calls[0][1]).toMatchInlineSnapshot(` { - "body": "{"data":"\\n \\n\\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo\\n txType\\n }\\n}\\n\\nfragment BlockInfo on Block {\\n header {\\n hash,\\n gasLimit,\\n height,\\n prevBlockHash,\\n seed,\\n stateHash,\\n timestamp,\\n version\\n },\\n fees,\\n gasSpent,\\n reward,\\n transactions {...TransactionInfo}\\n}\\n\\n query($amount: Int!) { blocks(last: $amount) {...BlockInfo} }\\n ","topic":"gql"}", + "body": "{"data":"\\n \\n\\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo,\\n txType\\n }\\n}\\n\\nfragment BlockInfo on Block {\\n header {\\n hash,\\n gasLimit,\\n height,\\n prevBlockHash,\\n seed,\\n stateHash,\\n timestamp,\\n version\\n },\\n fees,\\n gasSpent,\\n reward,\\n transactions {...TransactionInfo}\\n}\\n\\n query($amount: Int!) { blocks(last: $amount) {...BlockInfo} }\\n ","topic":"gql"}", "headers": { "Accept": "application/json", "Accept-Charset": "utf-8", @@ -207,7 +207,7 @@ describe("duskAPI", () => { expect(fetchSpy.mock.calls[0][0]).toStrictEqual(gqlExpectedURL); expect(fetchSpy.mock.calls[0][1]).toMatchInlineSnapshot(` { - "body": "{"data":"\\n \\n\\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo\\n txType\\n }\\n}\\n\\nfragment BlockInfo on Block {\\n header {\\n hash,\\n gasLimit,\\n height,\\n prevBlockHash,\\n seed,\\n stateHash,\\n timestamp,\\n version\\n },\\n fees,\\n gasSpent,\\n reward,\\n transactions {...TransactionInfo}\\n}\\n\\n query($amount: Int!) {\\n blocks(last: $amount) {...BlockInfo},\\n transactions(last: $amount) {...TransactionInfo}\\n }\\n ","topic":"gql"}", + "body": "{"data":"\\n \\n\\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo,\\n txType\\n }\\n}\\n\\nfragment BlockInfo on Block {\\n header {\\n hash,\\n gasLimit,\\n height,\\n prevBlockHash,\\n seed,\\n stateHash,\\n timestamp,\\n version\\n },\\n fees,\\n gasSpent,\\n reward,\\n transactions {...TransactionInfo}\\n}\\n\\n query($amount: Int!) {\\n blocks(last: $amount) {...BlockInfo},\\n transactions(last: $amount) {...TransactionInfo}\\n }\\n ","topic":"gql"}", "headers": { "Accept": "application/json", "Accept-Charset": "utf-8", @@ -338,7 +338,7 @@ describe("duskAPI", () => { expect(fetchSpy.mock.calls[0][0]).toStrictEqual(gqlExpectedURL); expect(fetchSpy.mock.calls[0][1]).toMatchInlineSnapshot(` { - "body": "{"data":"\\n \\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo\\n txType\\n }\\n}\\n\\n query($id: String!) { tx(hash: $id) {...TransactionInfo} }\\n ","topic":"gql"}", + "body": "{"data":"\\n \\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo,\\n txType\\n }\\n}\\n\\n query($id: String!) { tx(hash: $id) {...TransactionInfo} }\\n ","topic":"gql"}", "headers": { "Accept": "application/json", "Accept-Charset": "utf-8", @@ -361,7 +361,7 @@ describe("duskAPI", () => { expect(fetchSpy.mock.calls[0][0]).toStrictEqual(gqlExpectedURL); expect(fetchSpy.mock.calls[0][1]).toMatchInlineSnapshot(` { - "body": "{"data":"query($id: String!) { tx(hash: $id) { tx {json} } }","topic":"gql"}", + "body": "{"data":"query($id: String!) { tx(hash: $id) { tx { json } } }","topic":"gql"}", "headers": { "Accept": "application/json", "Accept-Charset": "utf-8", @@ -383,7 +383,7 @@ describe("duskAPI", () => { expect(fetchSpy.mock.calls[0][0]).toStrictEqual(gqlExpectedURL); expect(fetchSpy.mock.calls[0][1]).toMatchInlineSnapshot(` { - "body": "{"data":"\\n \\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo\\n txType\\n }\\n}\\n\\n query($amount: Int!) { transactions(last: $amount) {...TransactionInfo} }\\n ","topic":"gql"}", + "body": "{"data":"\\n \\nfragment TransactionInfo on SpentTransaction {\\n\\tblockHash,\\n\\tblockHeight,\\n\\tblockTimestamp,\\n err,\\n\\tgasSpent,\\n\\tid,\\n tx {\\n callData {\\n contractId,\\n data,\\n fnName\\n },\\n gasLimit,\\n gasPrice,\\n id,\\n isDeploy,\\n memo,\\n txType\\n }\\n}\\n\\n query($amount: Int!) { transactions(last: $amount) {...TransactionInfo} }\\n ","topic":"gql"}", "headers": { "Accept": "application/json", "Accept-Charset": "utf-8", diff --git a/explorer/src/lib/services/duskAPI.js b/explorer/src/lib/services/duskAPI.js index 2f340fe89c..6df67ad4e6 100644 --- a/explorer/src/lib/services/duskAPI.js +++ b/explorer/src/lib/services/duskAPI.js @@ -230,12 +230,26 @@ const duskAPI = { /** * @param {string} id - * @returns {Promise} + * @returns {Promise} */ getTransaction(id) { return gqlGet(gqlQueries.getTransactionQueryInfo(id)) .then(getKey("tx")) - .then(transformTransaction); + .then((tx) => { + if (tx === null) { + return gqlGet(gqlQueries.getMempoolTx(id)) + .then(getKey("mempoolTx")) + .then((mempoolTx) => { + if (mempoolTx) { + return "This transaction is currently in the mempool and has not yet been confirmed. The transaction details will be displayed after confirmation."; + } else { + throw new Error("Transaction not found"); + } + }); + } else { + return transformTransaction(tx); + } + }); }, /** diff --git a/explorer/src/lib/services/gql-queries.js b/explorer/src/lib/services/gql-queries.js index 07d6f15317..5dbf3c4824 100644 --- a/explorer/src/lib/services/gql-queries.js +++ b/explorer/src/lib/services/gql-queries.js @@ -16,7 +16,7 @@ fragment TransactionInfo on SpentTransaction { gasPrice, id, isDeploy, - memo + memo, txType } } @@ -86,6 +86,12 @@ export const getLatestChainQueryInfo = (amount) => ({ variables: { amount }, }); +/** @param {string} id */ +export const getMempoolTx = (id) => ({ + query: "query($id: String!) { mempoolTx(hash: $id) { json } }", + variables: { id }, +}); + /** @param {string} id */ export const getTransactionQueryInfo = (id) => ({ query: ` @@ -106,7 +112,7 @@ export const getTransactionsQueryInfo = (amount) => ({ /** @param {string} id */ export const getTransactionDetailsQueryInfo = (id) => ({ - query: "query($id: String!) { tx(hash: $id) { tx {json} } }", + query: "query($id: String!) { tx(hash: $id) { tx { json } } }", variables: { id }, }); diff --git a/explorer/src/routes/transactions/transaction/+page.svelte b/explorer/src/routes/transactions/transaction/+page.svelte index 300408e53f..e4d7ee6277 100644 --- a/explorer/src/routes/transactions/transaction/+page.svelte +++ b/explorer/src/routes/transactions/transaction/+page.svelte @@ -2,16 +2,17 @@ import { onMount } from "svelte"; import { navigating, page } from "$app/stores"; import { TransactionDetails } from "$lib/components/"; + import { Banner } from "$lib/dusk/components/"; import { duskAPI } from "$lib/services"; import { marketDataStore } from "$lib/stores"; import { createDataStore } from "$lib/dusk/svelte-stores"; const dataStore = createDataStore(duskAPI.getTransaction); const payloadStore = createDataStore(duskAPI.getTransactionDetails); - const getTransaction = () => { - dataStore.getData($page.url.searchParams.get("id")); - payloadStore.getData($page.url.searchParams.get("id")); + const id = $page.url.searchParams.get("id"); + dataStore.getData(id); + payloadStore.getData(id); }; onMount(getTransaction); @@ -28,12 +29,18 @@
- + {#if typeof data === "string"} + + {data} + + {:else} + + {/if}
diff --git a/explorer/src/style/dusk/colors.css b/explorer/src/style/dusk/colors.css index 9359f710fe..0bd5a03ee2 100644 --- a/explorer/src/style/dusk/colors.css +++ b/explorer/src/style/dusk/colors.css @@ -27,4 +27,13 @@ --warning: #ffcf23; --error: #ed254e; --info: #71b1ff; + + --success-500: #16db93; + --success-700: #0f9363; + --warning-500: #ffcf23; + --warning-700: #d1a300; + --error-500: #ed254e; + --error-700: #8e112c; + --info-500: #71b1ff; + --info-700: #0863d1; } diff --git a/explorer/src/style/dusk/language.css b/explorer/src/style/dusk/language.css index 5aab2e6bb3..d7a8edd410 100644 --- a/explorer/src/style/dusk/language.css +++ b/explorer/src/style/dusk/language.css @@ -10,10 +10,14 @@ --secondary-color-variant-light: var(--cornflower-light); --surface-color: var(--magnolia); --danger-color: var(--error); - --error-color: var(--error); - --info-color: var(--info); - --success-color: var(--success); - --warning-color: var(--warning); + --error-color: var(--error-500); + --error-color-variant-dark: var(--error-700); + --info-color: var(--info-500); + --info-color-variant-dark: var(--info-700); + --success-color: var(--success-500); + --success-color-variant-dark: var(--success-700); + --warning-color: var(--warning-500); + --warning-color-variant-dark: var(--warning-700); --on-background-color: var(--smokey-black); --on-primary-color: var(--light-grey); @@ -100,6 +104,12 @@ /* Dividers */ --divider-color-primary: var(--taupe-grey); + + /* Banner */ + + --banner-info-color: var(--info-color-variant-dark); + --banner-warning-color: var(--warning-color-variant-dark); + --banner-error-color: var(--error-color-variant-dark); } :root.dark { @@ -134,4 +144,10 @@ --checkbox-control-border-color: var(--light-grey); --checkbox-control-checked-bg-color: var(--light-grey); + + /* Banner */ + + --banner-info-color: var(--info-color); + --banner-warning-color: var(--warning-color); + --banner-error-color: var(--error-color); }