From 5666fe5d34b17edcf26e43952b8629fd412650a7 Mon Sep 17 00:00:00 2001 From: void-57 Date: Tue, 13 Jan 2026 22:14:40 +0530 Subject: [PATCH 1/5] Add transaction details and transaction history features Implemented transaction details view for Solana transactions, including sender, receiver, value, fee, slot, and status. Added transaction history fetching, pagination, and filtering (all/sent/received). Improved UI for transaction navigation and error handling. --- css/main.css | 467 +++- index.html | 4284 ++++++++++++++++++++++++++----------- scripts/components.min.js | 492 ++++- 3 files changed, 3882 insertions(+), 1361 deletions(-) diff --git a/css/main.css b/css/main.css index a3c5ac2..9bb1a39 100644 --- a/css/main.css +++ b/css/main.css @@ -28,7 +28,7 @@ body { background-color: rgba(var(--foreground-color), 1); } -body[data-theme=dark] { +body[data-theme="dark"] { --accent-color: #92a2ff; --accent-color-rgb: 160, 182, 255; --secondary-color: #d60739; @@ -39,7 +39,7 @@ body[data-theme=dark] { --green: #00e676; --yellow: rgb(255, 213, 5); } -body[data-theme=dark] ::-webkit-calendar-picker-indicator { +body[data-theme="dark"] ::-webkit-calendar-picker-indicator { filter: invert(1); } @@ -63,7 +63,7 @@ strong { img { -o-object-fit: cover; - object-fit: cover; + object-fit: cover; } a:where([class]) { @@ -90,7 +90,7 @@ a:any-link:focus-visible { outline: rgba(var(--text-color), 1) 0.1rem solid; } -input[type=datetime-local] { +input[type="datetime-local"] { width: 100%; padding: 0.8rem 0.6rem; border: none; @@ -101,7 +101,7 @@ input[type=datetime-local] { color: inherit; background-color: rgba(var(--text-color), 0.06); } -input[type=datetime-local]:focus { +input[type="datetime-local"]:focus { outline: none; box-shadow: 0 0 0 0.1rem var(--accent-color); } @@ -109,8 +109,8 @@ input[type=datetime-local]:focus { button, .button { -webkit-user-select: none; - -moz-user-select: none; - user-select: none; + -moz-user-select: none; + user-select: none; position: relative; display: inline-flex; border: none; @@ -224,8 +224,8 @@ a:any-link:focus-visible { details summary { display: flex; -webkit-user-select: none; - -moz-user-select: none; - user-select: none; + -moz-user-select: none; + user-select: none; cursor: pointer; align-items: center; gap: 1rem; @@ -264,8 +264,8 @@ sm-chip { --padding: 0.5rem 0.8rem; --background: rgba(var(--text-color), 0.06); -webkit-user-select: none; - -moz-user-select: none; - user-select: none; + -moz-user-select: none; + user-select: none; font-weight: 500; } sm-chip[selected] { @@ -604,7 +604,11 @@ ul { position: absolute; border-radius: 50%; transform: scale(0); - background: radial-gradient(circle, rgba(var(--text-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%); + background: radial-gradient( + circle, + rgba(var(--text-color), 0.3) 0%, + rgba(0, 0, 0, 0) 50% + ); pointer-events: none; } @@ -699,17 +703,17 @@ ul { justify-self: flex-start; } -ul[type=circle], -menu[type=circle] { +ul[type="circle"], +menu[type="circle"] { padding: 1.5rem 2.5rem; list-style: circle; } -ul[type=circle] li, -menu[type=circle] li { +ul[type="circle"] li, +menu[type="circle"] li { margin-bottom: 1rem; } -ul[type=circle] li:last-of-type, -menu[type=circle] li:last-of-type { +ul[type="circle"] li:last-of-type, +menu[type="circle"] li:last-of-type { margin-bottom: 0; } ul, @@ -773,13 +777,13 @@ menu { #meta_mask_status_button .icon-wrapper > * { grid-area: 1/1; } -#meta_mask_status_button[data-status=connected] { +#meta_mask_status_button[data-status="connected"] { pointer-events: none; } -#meta_mask_status_button[data-status=connected] .icon-wrapper::after { +#meta_mask_status_button[data-status="connected"] .icon-wrapper::after { background-color: var(--green); } -#meta_mask_status_button[data-status=disconnected] .icon-wrapper::after { +#meta_mask_status_button[data-status="disconnected"] .icon-wrapper::after { background-color: var(--danger-color); } @@ -847,7 +851,8 @@ main { transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .nav-item__title { - transition: opacity 0.2s, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); + transition: opacity 0.2s, + transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .nav-item--active { color: var(--accent-color); @@ -870,17 +875,17 @@ main { overflow: auto; grid-area: pages; } -#page_container[data-page=home] > :nth-child(2) { +#page_container[data-page="home"] > :nth-child(2) { flex: 1; } -#page_container[data-page=send] { +#page_container[data-page="send"] { align-items: flex-start; } -#page_container[data-page=send] > * { +#page_container[data-page="send"] > * { padding: 1rem; margin: 0 auto; } -#page_container[data-page=create] { +#page_container[data-page="create"] { margin: 0 auto; padding: 4vw 1rem; gap: 2rem; @@ -951,6 +956,19 @@ aside h4 { padding-bottom: 0.5rem; } +#address_transactions { + width: 100%; + max-width: 32rem; +} + +.transaction { + width: 100%; +} + +#transactions_list { + width: 100%; +} + #error_section { display: grid; height: 100%; @@ -978,6 +996,7 @@ aside h4 { position: relative; margin-bottom: 2rem; } + .transaction__phase:not(:last-of-type)::after { content: ""; position: absolute; @@ -1004,7 +1023,7 @@ aside h4 { width: 4rem; border-radius: 5rem; -webkit-animation: popup 1s; - animation: popup 1s; + animation: popup 1s; padding: 1rem; } .user-action-result__icon.pending { @@ -1068,17 +1087,24 @@ aside h4 { padding-bottom: 1rem; border-bottom: solid thin rgba(var(--text-color), 0.3); } - +.create-buttons { + display: flex; + max-width: 400px; + gap: 1rem; +} @media only screen and (max-width: 640px) { .hide-on-small { display: none; } - #page_container[data-page=home] { + #page_container[data-page="home"] { flex-direction: column; } - #page_container[data-page=home] > :first-child { + #page_container[data-page="home"] > :first-child { order: 1; } + .create-buttons { + display: grid; + } } @media only screen and (min-width: 640px) { sm-popup { @@ -1166,9 +1192,384 @@ aside h4 { } @media (prefers-reduced-motion) { ::view-transition-group(*), -::view-transition-old(*), -::view-transition-new(*) { + ::view-transition-old(*), + ::view-transition-new(*) { -webkit-animation: none !important; - animation: none !important; + animation: none !important; } +} + +.tx-details-container { + max-width: 800px; + margin: 1rem auto; + padding: 1rem; + font-family: inherit; +} + +/* Header styling */ +.tx-header { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(var(--text-color), 0.1); +} + +.tx-title { + font-size: 1.5rem; + font-weight: 600; + color: rgba(var(--text-color), 0.95); + margin: 0; +} + +.tx-card { + background-color: rgba(var(--foreground-color), 1); + border-radius: 0.75rem; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(var(--text-color), 0.1); + margin-top: 1rem; +} + +.tx-status-header { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.25rem; + background-color: rgba(var(--text-color), 0.03); + border-bottom: 1px solid rgba(var(--text-color), 0.1); +} + +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; +} + +.status-indicator.confirmed { + background-color: var(--color-success); + box-shadow: 0 0 0 4px rgba(var(--color-success-rgb), 0.2); +} + +.status-indicator.pending { + background-color: var(--color-warning); + box-shadow: 0 0 0 4px rgba(var(--color-warning-rgb), 0.2); +} + +.status-details { + flex-grow: 1; +} + +.status-title { + font-size: 1.15rem; + font-weight: 600; + color: rgba(var(--text-color), 0.95); + margin: 0; +} + +.status-subtext { + font-size: 0.85rem; + color: rgba(var(--text-color), 0.7); + margin: 0.25rem 0 0; +} + +.tx-info-grid { + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.tx-address-section { + display: flex; + align-items: stretch; + gap: 1rem; + background: rgba(var(--text-color), 0.02); + padding: 1rem; + border-radius: 0.5rem; +} + +.address-card { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.address-label { + font-size: 0.75rem; + font-weight: 500; + color: rgba(var(--text-color), 0.6); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.address-value { + font-family: "Roboto Mono", monospace; + font-size: 0.85rem; + color: rgba(var(--text-color), 0.9); + word-break: break-all; + background: rgba(var(--text-color), 0.05); + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; +} + +.tx-arrow { + font-size: 1.5rem; + color: rgba(var(--text-color), 0.4); + display: flex; + align-items: center; + padding: 0 0.5rem; +} + +.tx-hash-section { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.section-label { + font-size: 0.75rem; + font-weight: 500; + color: rgba(var(--text-color), 0.6); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.hash-value { + background-color: rgba(var(--text-color), 0.05); + padding: 0.75rem; + border-radius: 0.25rem; + font-family: "Roboto Mono", monospace; + font-size: 0.85rem; + color: rgba(var(--text-color), 0.9); + word-break: break-all; +} + +.tx-metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; +} + +.metric-card { + background-color: rgba(var(--text-color), 0.03); + padding: 1rem; + border-radius: 0.5rem; + text-align: center; + border: 1px solid rgba(var(--text-color), 0.08); + transition: background-color 0.2s ease, transform 0.2s ease; +} + +.metric-card:hover { + background-color: rgba(var(--text-color), 0.06); + transform: translateY(-2px); +} + +.metric-label { + font-size: 0.75rem; + font-weight: 500; + color: rgba(var(--text-color), 0.6); + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.5px; + display: block; +} + +.metric-value { + font-size: 0.95rem; + font-weight: 500; + color: rgba(var(--text-color), 0.95); + word-break: break-word; +} + +.tx-actions { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 1.25rem; + border-top: 1px solid rgba(var(--text-color), 0.1); + background-color: rgba(var(--text-color), 0.03); +} + +.tx-actions .button { + flex: 1; +} + +@media (max-width: 768px) { + .tx-address-section { + flex-direction: column; + gap: 0.75rem; + align-items: stretch; + } + + .tx-arrow { + transform: rotate(90deg); + margin: 0.5rem auto; + padding: 0; + } + + .tx-metrics-grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 576px) { + .tx-header { + padding-bottom: 0.75rem; + } + + .tx-status-header { + padding: 1rem; + gap: 0.75rem; + } + + .status-title { + font-size: 1.1rem; + } + + .tx-info-grid { + padding: 1rem; + gap: 1.25rem; + } + + .tx-address-section { + padding: 0.75rem; + } + + .tx-metrics-grid { + grid-template-columns: 1fr; + } + + .tx-actions { + flex-direction: column; + padding: 1rem; + } +} + +/* Valuation toggle styles */ +.valuation-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; + color: rgba(var(--text-color), 0.8); + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.toggle-switch { + position: relative; + display: inline-block; + width: 3rem; + height: 1.5rem; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(var(--text-color), 0.2); + transition: 0.4s; + border-radius: 1.5rem; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 1.1rem; + width: 1.1rem; + left: 0.2rem; + bottom: 0.2rem; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} + +input:checked + .toggle-slider { + background-color: var(--accent-color); +} + +input:checked + .toggle-slider:before { + transform: translateX(1.5rem); +} + +.transaction-controls { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; + padding: 0.5rem; + background-color: rgba(var(--text-color), 0.03); + border-radius: 0.5rem; + border: 1px solid rgba(var(--text-color), 0.08); +} + +.filter-control { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.filter-control label { + font-size: 0.9rem; + color: rgba(var(--text-color), 0.8); + font-weight: 500; +} + +.filter-control select { + padding: 0.4rem 0.6rem; + border-radius: 0.4rem; + border: 1px solid rgba(var(--text-color), 0.1); + background-color: rgba(var(--foreground-color), 1); + color: rgba(var(--text-color), 0.9); + font-size: 0.9rem; + cursor: pointer; + outline: none; +} +.margin-left-auto + .margin-left-auto { + display: none !important; +} + +.filter-control select:focus { + border-color: var(--accent-color); +} + +/* Loading state for transaction details */ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + gap: 1rem; +} + +.loading-text { + color: rgba(var(--text-color), 0.7); + font-size: 0.9rem; +} + +#sol_balance_wrapper { + background-color: rgba(var(--text-color), 0.06); + padding: max(1rem, 1.5vw); + border-radius: 0.5rem; + width: 100%; +} +#sol_balance_wrapper li:not(:last-of-type) { + border-bottom: solid thin rgba(var(--text-color), 0.3); + padding-bottom: 0.5rem; } \ No newline at end of file diff --git a/index.html b/index.html index ea1e7b5..30cf3ef 100644 --- a/index.html +++ b/index.html @@ -1,828 +1,1082 @@ - - - - Solana Wallet - - - - - - - - -

-

-
- - + + + + Solana Wallet + + + + + + + + + +

+

+
+ + +
+
+ +
+
+ - - -
-
-
- -
-
- - +
- -
-
- - -
-
-

Did you forget your Solana Address?

-

- If you have your Solana Seed, enter it here and recover your Solana - Address. -

-
- - - - - - - -
- - - - - - - + + + + + + + + + + + + + - + - - - - - - - - - - - + + + + + + + + + + - + - + ); + } + + + \ No newline at end of file diff --git a/scripts/components.min.js b/scripts/components.min.js index 2d0e22a..f2a9560 100644 --- a/scripts/components.min.js +++ b/scripts/components.min.js @@ -28,8 +28,11 @@ const smChips = document.createElement("template"); get value() { return this._value; } - set value(t) { - this.setSelectedOption(t); + set value(val) { + this.setSelectedOption(val); + } + get isValid() { + return void 0 !== this._value; } scrollLeft() { this.chipsWrapper.scrollBy({ @@ -43,18 +46,18 @@ const smChips = document.createElement("template"); behavior: "smooth", }); } - setSelectedOption(t) { - this._value !== t && - ((this._value = t), - this.assignedElements.forEach((e) => { - e.value == t - ? (e.setAttribute("selected", ""), - e.scrollIntoView({ + setSelectedOption(value) { + this._value !== value && + ((this._value = value), + this.assignedElements.forEach((elem) => { + elem.value == value + ? (elem.setAttribute("selected", ""), + elem.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", })) - : e.removeAttribute("selected"); + : elem.removeAttribute("selected"); })); } fireEvent() { @@ -68,54 +71,51 @@ const smChips = document.createElement("template"); } connectedCallback() { this.setAttribute("role", "listbox"); - const t = this.shadowRoot.querySelector("slot"); - t.addEventListener("slotchange", (e) => { - n.disconnect(), - i.disconnect(), + const slot = this.shadowRoot.querySelector("slot"); + slot.addEventListener("slotchange", (e) => { + firstOptionObserver.disconnect(), + lastOptionObserver.disconnect(), this.observeSelf.disconnect(), clearTimeout(this.slotChangeTimeout), (this.slotChangeTimeout = setTimeout(() => { - (this.assignedElements = t.assignedElements()), - this.assignedElements.forEach((t) => { - t.hasAttribute("selected") && (this._value = t.value); - }), + (this.assignedElements = slot.assignedElements()), this.observeSelf.observe(this); }, 0)); - }); - const e = new ResizeObserver((t) => { - t.forEach((t) => { - if (t.contentBoxSize) { - const e = Array.isArray(t.contentBoxSize) - ? t.contentBoxSize[0] - : t.contentBoxSize; - this.scrollDistance = 0.6 * e.inlineSize; - } else this.scrollDistance = 0.6 * t.contentRect.width; - }); - }); - e.observe(this), + }), + new ResizeObserver((entries) => { + entries.forEach((entry) => { + if (entry.contentBoxSize) { + const contentBoxSize = Array.isArray(entry.contentBoxSize) + ? entry.contentBoxSize[0] + : entry.contentBoxSize; + this.scrollDistance = 0.6 * contentBoxSize.inlineSize; + } else this.scrollDistance = 0.6 * entry.contentRect.width; + }); + }).observe(this), (this.observeSelf = new IntersectionObserver( - (t, e) => { - t.forEach((t) => { - t.isIntersecting && + (entries, observer) => { + entries.forEach((entry) => { + entry.isIntersecting && !this.hasAttribute("multiline") && this.assignedElements.length > 0 && - (n.observe(this.assignedElements[0]), - i.observe( + (firstOptionObserver.observe(this.assignedElements[0]), + lastOptionObserver.observe( this.assignedElements[this.assignedElements.length - 1] ), - e.unobserve(this)); + observer.unobserve(this)); }); }, { threshold: 1 } )), - this.chipsWrapper.addEventListener("option-clicked", (t) => { - this._value !== t.target.value && - (this.setSelectedOption(t.target.value), this.fireEvent()); + this.chipsWrapper.addEventListener("option-clicked", (e) => { + e.stopPropagation(), + this._value !== e.detail.value && + (this.setSelectedOption(e.detail.value), this.fireEvent()); }); - const n = new IntersectionObserver( - (t) => { - t.forEach((t) => { - t.isIntersecting + const firstOptionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + entry.isIntersecting ? (this.navButtonLeft.classList.add("hide"), this.coverLeft.classList.add("hide")) : (this.navButtonLeft.classList.remove("hide"), @@ -124,10 +124,10 @@ const smChips = document.createElement("template"); }, { threshold: 1, root: this } ), - i = new IntersectionObserver( - (t) => { - t.forEach((t) => { - t.isIntersecting + lastOptionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + entry.isIntersecting ? (this.navButtonRight.classList.add("hide"), this.coverRight.classList.add("hide")) : (this.navButtonRight.classList.remove("hide"), @@ -156,11 +156,14 @@ const smChip = document.createElement("template"); this.attachShadow({ mode: "open" }).append( smChip.content.cloneNode(!0) ), - (this._value = void 0), + (this._value = this.getAttribute("value")), (this.radioButton = this.shadowRoot.querySelector("input")), (this.fireEvent = this.fireEvent.bind(this)), (this.handleKeyDown = this.handleKeyDown.bind(this)); } + static get observedAttributes() { + return ["selected"]; + } get value() { return this._value; } @@ -173,16 +176,25 @@ const smChip = document.createElement("template"); }) ); } - handleKeyDown(t) { - ("Enter" !== t.key && "Space" !== t.key) || this.fireEvent(); + handleKeyDown(e) { + ("Enter" !== e.key && "Space" !== e.key) || this.fireEvent(); } connectedCallback() { this.setAttribute("role", "option"), this.setAttribute("tabindex", "0"), - (this._value = this.getAttribute("value")), + this.hasAttribute("value") || + console.error("sm-chip must have a value attribute"), + this.hasAttribute("selected") && this.fireEvent(), this.addEventListener("click", this.fireEvent), this.addEventListener("keydown", this.handleKeyDown); } + attributeChangedCallback(name, oldValue, newValue) { + "selected" === name + ? this.hasAttribute("selected") + ? (this.fireEvent(), this.setAttribute("aria-selected", "true")) + : this.removeAttribute("aria-selected") + : "value" === name && (this._value = newValue); + } disconnectedCallback() { this.removeEventListener("click", this.fireEvent), this.removeEventListener("keydown", this.handleKeyDown); @@ -354,11 +366,6 @@ const smForm = document.createElement("template"); this.resetButton.addEventListener("click", this.reset), this._checkValidity(); }; - checkIfSupported = (elem) => - 1 === elem.nodeType && - (elem.tagName.includes("-") || - "input" === elem.tagName || - elem.querySelector(this.supportedElements)); connectedCallback() { const updateFormDecedents = this.debounce(this.elementsChanged, 100); this.addEventListener("input", this.debounce(this._checkValidity, 100)), @@ -372,11 +379,15 @@ const smForm = document.createElement("template"); (this.mutationObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { (("childList" === mutation.type && - [...mutation.addedNodes].some((node) => - this.checkIfSupported(node) + [...mutation.addedNodes].some( + (node) => + 1 === node.nodeType && + node.querySelector(this.supportedElements) )) || - [...mutation.removedNodes].some((node) => - this.checkIfSupported(node) + [...mutation.removedNodes].some( + (node) => + 1 === node.nodeType && + node.querySelector(this.supportedElements) )) && updateFormDecedents(); }); @@ -384,8 +395,7 @@ const smForm = document.createElement("template"); this.mutationObserver.observe(this, { childList: !0, subtree: !0 }); } attributeChangedCallback(name, oldValue, newValue) { - "skip-submit" === name && - (this.skipSubmit = this.hasAttribute("skip-submit")); + "skip-submit" === name && (this.skipSubmit = null !== newValue); } disconnectedCallback() { this.removeEventListener( @@ -535,7 +545,7 @@ const smInput = document.createElement("template"); let _validity = { isValid: !0, errorText: "" }; return ( this.validationFunction && - (_validity = this.validationFunction(this.input.value)), + (_validity = this.validationFunction(this.input.value, this)), _isValid && _validity.isValid ? (this.setAttribute("valid", ""), this.removeAttribute("invalid"), @@ -1493,6 +1503,362 @@ const popupStack = new Stack(), } } ); +const smSwitch = document.createElement("template"); +(smSwitch.innerHTML = + '\t'), + customElements.define( + "sm-switch", + class extends HTMLElement { + constructor() { + super(), + this.attachShadow({ mode: "open" }).append( + smSwitch.content.cloneNode(!0) + ), + (this.switch = this.shadowRoot.querySelector(".switch")), + (this.input = this.shadowRoot.querySelector("input")), + (this.isChecked = !1), + (this.isDisabled = !1); + } + static get observedAttributes() { + return ["disabled", "checked"]; + } + get disabled() { + return this.isDisabled; + } + set disabled(val) { + val + ? this.setAttribute("disabled", "") + : this.removeAttribute("disabled"); + } + get checked() { + return this.isChecked; + } + set checked(value) { + value + ? this.setAttribute("checked", "") + : this.removeAttribute("checked"); + } + get value() { + return this.isChecked; + } + reset() {} + dispatch = () => { + this.dispatchEvent( + new CustomEvent("change", { + bubbles: !0, + composed: !0, + detail: { value: this.isChecked }, + }) + ); + }; + connectedCallback() { + this.addEventListener("keydown", (e) => { + " " !== e.key || + this.isDisabled || + (e.preventDefault(), this.input.click()); + }), + this.input.addEventListener("click", (e) => { + this.input.checked ? (this.checked = !0) : (this.checked = !1), + this.dispatch(); + }); + } + attributeChangedCallback(name, oldValue, newValue) { + oldValue !== newValue && + ("disabled" === name + ? this.hasAttribute("disabled") + ? ((this.disabled = !0), (this.inert = !0)) + : ((this.disabled = !1), (this.inert = !1)) + : "checked" === name && + (this.hasAttribute("checked") + ? ((this.isChecked = !0), (this.input.checked = !0)) + : ((this.isChecked = !1), (this.input.checked = !1)))); + } + } + ); +const smSelect = document.createElement("template"); +(smSelect.innerHTML = + '
'), + customElements.define( + "sm-select", + class extends HTMLElement { + constructor() { + super(), + this.attachShadow({ mode: "open" }).append( + smSelect.content.cloneNode(!0) + ), + (this.focusIn = this.focusIn.bind(this)), + (this.reset = this.reset.bind(this)), + (this.open = this.open.bind(this)), + (this.collapse = this.collapse.bind(this)), + (this.toggle = this.toggle.bind(this)), + (this.handleOptionsNavigation = + this.handleOptionsNavigation.bind(this)), + (this.handleOptionSelection = this.handleOptionSelection.bind(this)), + (this.handleKeydown = this.handleKeydown.bind(this)), + (this.handleClickOutside = this.handleClickOutside.bind(this)), + (this.selectOption = this.selectOption.bind(this)), + (this.debounce = this.debounce.bind(this)), + (this.elementsChanged = this.elementsChanged.bind(this)), + (this.availableOptions = []), + this.previousOption, + (this.isOpen = !1), + (this.label = ""), + (this.defaultSelected = ""), + (this.isUnderViewport = !1), + (this.animationOptions = { + duration: 300, + fill: "forwards", + easing: "ease", + }), + (this.optionList = this.shadowRoot.querySelector(".options")), + (this.selection = this.shadowRoot.querySelector(".selection")), + (this.selectedOptionText = this.shadowRoot.querySelector( + ".selected-option-text" + )); + } + static get observedAttributes() { + return ["disabled", "label", "readonly"]; + } + get value() { + return this.getAttribute("value"); + } + set value(t) { + const e = this.availableOptions.find( + (e) => e.getAttribute("value") === t + ); + e + ? (this.setAttribute("value", t), this.selectOption(e)) + : console.warn(`There is no option with ${t} as value`); + } + debounce(t, e) { + let n = null; + return (...i) => { + window.clearTimeout(n), + (n = window.setTimeout(() => { + t.apply(null, i); + }, e)); + }; + } + reset(t = !0) { + if ( + this.availableOptions[0] && + this.previousOption !== this.availableOptions[0] + ) { + const e = + this.availableOptions.find((t) => t.hasAttribute("selected")) || + this.availableOptions[0]; + (this.value = e.getAttribute("value")), t && this.fireEvent(); + } + } + selectOption(t) { + this.previousOption !== t && + (this.querySelectorAll("[selected").forEach((t) => + t.removeAttribute("selected") + ), + (this.selectedOptionText.textContent = `${this.label}${t.textContent}`), + t.setAttribute("selected", ""), + (this.previousOption = t)); + } + focusIn() { + this.selection.focus(); + } + open() { + this.availableOptions.forEach((t) => t.setAttribute("tabindex", 0)), + this.optionList.classList.remove("hidden"), + (this.isUnderViewport = + this.getBoundingClientRect().bottom + + this.optionList.getBoundingClientRect().height > + window.innerHeight), + this.isUnderViewport + ? this.setAttribute("isUnder", "") + : this.removeAttribute("isUnder"), + this.optionList.animate( + [ + { + transform: `translateY(${ + this.isUnderViewport ? "" : "-" + }0.5rem)`, + opacity: 0, + }, + { transform: "translateY(0)", opacity: 1 }, + ], + this.animationOptions + ), + this.setAttribute("open", ""), + (this.style.zIndex = 1e3), + ( + this.availableOptions.find((t) => t.hasAttribute("selected")) || + this.availableOptions[0] + ).focus(), + document.addEventListener("mousedown", this.handleClickOutside), + (this.isOpen = !0); + } + collapse() { + this.removeAttribute("open"), + (this.optionList.animate( + [ + { transform: "translateY(0)", opacity: 1 }, + { + transform: `translateY(${ + this.isUnderViewport ? "" : "-" + }0.5rem)`, + opacity: 0, + }, + ], + this.animationOptions + ).onfinish = () => { + this.availableOptions.forEach((t) => t.removeAttribute("tabindex")), + document.removeEventListener( + "mousedown", + this.handleClickOutside + ), + this.optionList.classList.add("hidden"), + (this.isOpen = !1), + (this.style.zIndex = "auto"); + }); + } + toggle() { + this.isOpen || this.hasAttribute("disabled") + ? this.collapse() + : this.open(); + } + fireEvent() { + this.dispatchEvent( + new CustomEvent("change", { + bubbles: !0, + composed: !0, + detail: { value: this.value }, + }) + ); + } + handleOptionsNavigation(t) { + "ArrowUp" === t.key + ? (t.preventDefault(), + document.activeElement.previousElementSibling + ? document.activeElement.previousElementSibling.focus() + : this.availableOptions[this.availableOptions.length - 1].focus()) + : "ArrowDown" === t.key && + (t.preventDefault(), + document.activeElement.nextElementSibling + ? document.activeElement.nextElementSibling.focus() + : this.availableOptions[0].focus()); + } + handleOptionSelection(t) { + this.previousOption !== document.activeElement && + ((this.value = document.activeElement.getAttribute("value")), + this.fireEvent()); + } + handleClick(t) { + t.target === this + ? this.toggle() + : (this.handleOptionSelection(), this.collapse()); + } + handleKeydown(t) { + t.target === this + ? this.isOpen && "ArrowDown" === t.key + ? (t.preventDefault(), + ( + this.availableOptions.find((t) => t.hasAttribute("selected")) || + this.availableOptions[0] + ).focus(), + this.handleOptionSelection(t)) + : " " === t.key && (t.preventDefault(), this.toggle()) + : (this.handleOptionsNavigation(t), + this.handleOptionSelection(t), + ["Enter", " ", "Escape", "Tab"].includes(t.key) && + (t.preventDefault(), this.collapse(), this.focusIn())); + } + handleClickOutside(t) { + this.isOpen && !this.contains(t.target) && this.collapse(); + } + elementsChanged() { + (this.availableOptions = [...this.querySelectorAll("sm-option")]), + this.reset(!1), + (this.defaultSelected = this.value); + } + connectedCallback() { + this.setAttribute("role", "listbox"), + this.hasAttribute("disabled") || + this.hasAttribute("readonly") || + (this.selection.setAttribute("tabindex", "0"), + this.addEventListener("click", this.handleClick), + this.addEventListener("keydown", this.handleKeydown)); + const t = this.debounce(this.elementsChanged, 100); + this.shadowRoot.querySelector("slot").addEventListener("slotchange", t), + (this.mutationObserver = new MutationObserver((e) => { + let n = !1; + if ( + (e.forEach((e) => { + switch (e.type) { + case "childList": + t(); + break; + case "attributes": + n = !0; + } + }), + n) + ) { + const t = + this.availableOptions.find((t) => t.hasAttribute("selected")) || + this.availableOptions[0]; + (this.selectedOptionText.textContent = `${this.label}${t.textContent}`), + this.setAttribute("value", t.getAttribute("value")); + } + })), + this.mutationObserver.observe(this, { + subtree: !0, + childList: !0, + attributeFilter: ["selected"], + }), + new IntersectionObserver((t, e) => { + t.forEach((t) => { + if (t.isIntersecting) { + this.selection.getBoundingClientRect().left < + window.innerWidth / 2 + ? this.setAttribute("align-select", "left") + : this.setAttribute("align-select", "right"); + } + }); + }).observe(this); + } + disconnectedCallback() { + this.removeEventListener("click", this.handleClick), + this.removeEventListener("keydown", this.handleKeydown); + } + attributeChangedCallback(t) { + "disabled" === t || "readonly" === t + ? this.hasAttribute("disabled") || this.hasAttribute("readonly") + ? (this.selection.removeAttribute("tabindex"), + this.removeEventListener("click", this.handleClick), + this.removeEventListener("keydown", this.handleKeydown)) + : (this.selection.setAttribute("tabindex", "0"), + this.addEventListener("click", this.handleClick), + this.addEventListener("keydown", this.handleKeydown)) + : "label" === t && + (this.label = this.hasAttribute("label") + ? `${this.getAttribute("label")} ` + : ""); + } + } + ); +const smOption = document.createElement("template"); +(smOption.innerHTML = + '
'), + customElements.define( + "sm-option", + class extends HTMLElement { + constructor() { + super(), + this.attachShadow({ mode: "open" }).append( + smOption.content.cloneNode(!0) + ); + } + connectedCallback() { + this.setAttribute("role", "option"); + } + } + ); const spinner = document.createElement("template"); spinner.innerHTML = ''; From a5ebc25567180a3d70f66351def09021dd20d4b2 Mon Sep 17 00:00:00 2001 From: void-57 Date: Tue, 13 Jan 2026 23:10:06 +0530 Subject: [PATCH 2/5] Calculate transaction fee and adjust transfer amount accordingly --- index.html | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index 30cf3ef..39944a5 100644 --- a/index.html +++ b/index.html @@ -2453,17 +2453,20 @@ } console.log("Transaction details:", tx); - // Extract transaction information - const value = - tx.meta?.postBalances && tx.meta?.preBalances - ? Math.abs(tx.meta.postBalances[0] - tx.meta.preBalances[0]) / - solanaWeb3.LAMPORTS_PER_SOL - : 0; - + // Calculate fee first const fee = tx.meta?.fee ? tx.meta.fee / solanaWeb3.LAMPORTS_PER_SOL : 0; + // Extract transaction information + // Sender's balance change includes both transfer amount and fee + // So we subtract the fee to get just the transfer amount + const value = + tx.meta?.postBalances && tx.meta?.preBalances + ? (Math.abs(tx.meta.postBalances[0] - tx.meta.preBalances[0]) / + solanaWeb3.LAMPORTS_PER_SOL) - fee + : 0; + const timestamp = tx.blockTime ? getFormattedTime(tx.blockTime) : "Pending"; From 774abf7bd60ee99bdc21ffdf213f8476916fc1ed Mon Sep 17 00:00:00 2001 From: void-57 Date: Wed, 14 Jan 2026 01:40:01 +0530 Subject: [PATCH 3/5] Enhance transaction detail extraction to identify sender, receiver, and transaction types (transfer, swap, nft_mint) --- index.html | 155 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 104 insertions(+), 51 deletions(-) diff --git a/index.html b/index.html index 39944a5..354dffe 100644 --- a/index.html +++ b/index.html @@ -2578,11 +2578,111 @@ status === "confirmed" ? `Included in Slot #${tx.slot}` : ""; } - // Fill in transaction details + + // Extract sender and receiver from transaction + let sender = "Unknown"; + let receiver = "Unknown"; + let transactionType = "transfer"; // transfer, swap, nft_mint, other + + const accountKeys = tx.transaction?.message?.accountKeys || []; + const instructions = tx.transaction?.message?.instructions || []; + const logMessages = tx.meta?.logMessages || []; + + // Check if this is an NFT mint by looking at log messages + const isNFTMint = logMessages.some(log => + log.includes("MintToCollectionV1") || + log.includes("MintV1") || + log.includes("Bubblegum") || + (log.includes("Instruction: Mint") && !log.includes("MintTo") && !log.includes("JUP")) + ); + + if (isNFTMint) { + transactionType = "nft_mint"; + sender = "NFT Mint"; + receiver = accountKeys[0]?.toString() || "Unknown"; + } + // Check if this is a token swap by looking at token balance changes + else if (tx.meta?.preTokenBalances && tx.meta?.postTokenBalances) { + const preTokens = tx.meta.preTokenBalances; + const postTokens = tx.meta.postTokenBalances; + + // Find tokens that decreased (sent) + let tokenSent = null; + let tokenReceived = null; + + for (const preTok of preTokens) { + const postTok = postTokens.find(p => p.accountIndex === preTok.accountIndex); + if (postTok) { + const preAmount = parseFloat(preTok.uiTokenAmount.amount); + const postAmount = parseFloat(postTok.uiTokenAmount.amount); + + // Token amount decreased significantly (sent/swapped) + if (preAmount > postAmount && (preAmount - postAmount) > 0.000001) { + tokenSent = { + mint: preTok.mint, + amount: preTok.uiTokenAmount.uiAmountString, + symbol: preTok.mint === "So11111111111111111111111111111111111111112" ? "SOL" : preTok.mint + }; + } + + // Token amount increased significantly (received) + if (postAmount > preAmount && (postAmount - preAmount) > 0.000001) { + tokenReceived = { + mint: postTok.mint, + amount: postTok.uiTokenAmount.uiAmountString, + symbol: postTok.mint === "So11111111111111111111111111111111111111112" ? "SOL" : postTok.mint + }; + } + } + } + + // If we found a token swap, use that instead of FROM/TO + if (tokenSent && tokenReceived) { + transactionType = "swap"; + sender = `${tokenSent.symbol}`; + receiver = `${tokenReceived.symbol}`; + } + } + + // If not a swap or NFT mint, try to find the transfer instruction + if (transactionType === "transfer") { + for (const ix of instructions) { + if (ix.accounts && ix.accounts.length >= 2) { + if (ix.data) { + try { + const dataBytes = bs58.decode(ix.data); + if (dataBytes[0] === 2) { + sender = accountKeys[ix.accounts[0]]?.toString() || "Unknown"; + receiver = accountKeys[ix.accounts[1]]?.toString() || "Unknown"; + break; + } + } catch (e) { + // If decode fails, continue + } + } + } + } + + // Fallback: use balance changes to determine sender/receiver + if (sender === "Unknown" && tx.meta?.preBalances && tx.meta?.postBalances) { + for (let i = 0; i < accountKeys.length; i++) { + const balanceChange = tx.meta.postBalances[i] - tx.meta.preBalances[i]; + if (balanceChange < -100000 && sender === "Unknown") { + sender = accountKeys[i]?.toString() || "Unknown"; + } + if (balanceChange > 100000 && receiver === "Unknown") { + receiver = accountKeys[i]?.toString() || "Unknown"; + } + } + } + + // Final fallback: use first account as sender + if (sender === "Unknown" && accountKeys[0]) { + sender = accountKeys[0]?.toString() || "Unknown"; + } + } + if (getRef("tx_from")) { - const sender = - tx.transaction?.message?.accountKeys?.[0]?.toString() || - "Unknown"; getRef("tx_from").value = sender; getRef("tx_from").addEventListener("click", function (e) { if ( @@ -2601,9 +2701,6 @@ } if (getRef("tx_to")) { - const receiver = - tx.transaction?.message?.accountKeys?.[1]?.toString() || - "Unknown"; getRef("tx_to").value = receiver; getRef("tx_to").addEventListener("click", function (e) { if ( @@ -2682,49 +2779,6 @@ if (currentCurrency !== "sol" && !isHistoricApiAvailable) { testHistoricalAPIAndRenderToggle(); } - - let sender = "Unknown"; - let receiver = "Unknown"; - - const message = tx.transaction?.message; - const accountKeys = - message?.staticAccountKeys || message?.accountKeys || []; - const instructions = - message?.compiledInstructions || message?.instructions || []; - - // Try to extract from parsed instructions (for parsed transactions) - if (instructions && instructions.length > 0) { - // Try to find a transfer instruction (SPL or System) - for (const ix of instructions) { - // For parsed transactions (if available) - if (ix.parsed && ix.parsed.info) { - if (ix.parsed.info.source && ix.parsed.info.destination) { - sender = ix.parsed.info.source; - receiver = ix.parsed.info.destination; - break; - } - if (ix.parsed.info.authority && ix.parsed.info.destination) { - sender = ix.parsed.info.authority; - receiver = ix.parsed.info.destination; - break; - } - } - // For raw instructions (like in your prompt) - if (ix.accountKeyIndexes && ix.accountKeyIndexes.length >= 2) { - sender = accountKeys[ix.accountKeyIndexes[0]] || "Unknown"; - receiver = accountKeys[ix.accountKeyIndexes[1]] || "Unknown"; - break; - } - } - } - - // Fallback: just use first two account keys - if (sender === "Unknown" && accountKeys[0]) sender = accountKeys[0]; - if (receiver === "Unknown" && accountKeys[1]) - receiver = accountKeys[1]; - - if (getRef("tx_from")) getRef("tx_from").value = sender; - if (getRef("tx_to")) getRef("tx_to").value = receiver; }) .catch((error) => { console.error("Error:", error); @@ -2766,7 +2820,6 @@ ); } } - async function fetchSolanaTransactionDetails(signature) { try { // Check for valid signature format From 28c819ec856ac3279963ddbf28b54c35b590c377 Mon Sep 17 00:00:00 2001 From: void-57 Date: Wed, 14 Jan 2026 17:20:14 +0530 Subject: [PATCH 4/5] Enhance transaction processing to accurately extract sender, receiver, and transaction types (transfer, swap, nft_mint) --- index.html | 229 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 195 insertions(+), 34 deletions(-) diff --git a/index.html b/index.html index 354dffe..a6ee76a 100644 --- a/index.html +++ b/index.html @@ -1539,15 +1539,34 @@ const amount = calculateTransactionAmount(tx, address); - // Extract from and to addresses with proper null checks + // Extract from and to addresses - prioritize parsed instructions let fromAddress = "N/A"; let toAddress = "N/A"; - // Add null checks before accessing transaction properties - if (tx?.transaction?.message?.accountKeys) { + // First, try to get from parsed instructions (most reliable) + if (tx?.transaction?.message?.instructions) { + for (const ix of tx.transaction.message.instructions) { + if (ix.parsed && ix.parsed.info) { + // System transfer + if (ix.parsed.type === "transfer" && ix.parsed.info.source && ix.parsed.info.destination) { + fromAddress = ix.parsed.info.source; + toAddress = ix.parsed.info.destination; + break; + } + // Token transfer + if (ix.parsed.info.authority && ix.parsed.info.destination) { + fromAddress = ix.parsed.info.authority; + toAddress = ix.parsed.info.destination; + break; + } + } + } + } + + // Fallback: use account keys if parsed instructions not available + if (fromAddress === "N/A" && tx?.transaction?.message?.accountKeys) { const keys = tx.transaction.message.accountKeys; - fromAddress = - keys[0]?.pubkey?.toString() || String(keys[0]) || "N/A"; + fromAddress = keys[0]?.pubkey?.toString() || String(keys[0]) || "N/A"; toAddress = keys[1]?.pubkey?.toString() || String(keys[1]) || "N/A"; } @@ -1576,6 +1595,49 @@ } txid = String(txid); + // Detect transaction type + let transactionType = "transfer"; + const logMessages = tx.meta?.logMessages || []; + + // Check for NFT mint + const isNFTMint = logMessages.some(log => + log.includes("MintToCollectionV1") || + log.includes("MintV1") || + log.includes("Bubblegum") || + (log.includes("Instruction: Mint") && !log.includes("MintTo") && !log.includes("JUP")) + ); + + if (isNFTMint) { + transactionType = "nft_mint"; + } + // Check for token swap + else if (tx.meta?.preTokenBalances && tx.meta?.postTokenBalances) { + const preTokens = tx.meta.preTokenBalances; + const postTokens = tx.meta.postTokenBalances; + + let hasTokenSent = false; + let hasTokenReceived = false; + + for (const preTok of preTokens) { + const postTok = postTokens.find(p => p.accountIndex === preTok.accountIndex); + if (postTok) { + const preAmount = parseFloat(preTok.uiTokenAmount.amount); + const postAmount = parseFloat(postTok.uiTokenAmount.amount); + + if (preAmount > postAmount && (preAmount - postAmount) > 0.000001) { + hasTokenSent = true; + } + if (postAmount > preAmount && (postAmount - preAmount) > 0.000001) { + hasTokenReceived = true; + } + } + } + + if (hasTokenSent && hasTokenReceived) { + transactionType = "swap"; + } + } + return { txid: txid, time: tx.blockTime ? getFormattedTime(tx.blockTime) : "Unknown", @@ -1586,6 +1648,9 @@ sender: fromAddress, receiver: toAddress, address: address, + transactionType: transactionType, + typeLabel: transactionType === "swap" ? "Swap" : transactionType === "nft_mint" ? "NFT Mint" : "Transfer", + }; } catch (err) { console.error("Error in formatTransaction:", err); @@ -1837,13 +1902,14 @@
-
+ ${transactionDetails.transactionType === "transfer" ? `
${transactionDetails.type === - "out" - ? "Sent" - : "Received" - } -
+ "out" + ? "Sent" + : "Received" + } +
` : ""} + ${transactionDetails.typeLabel ? ` ${transactionDetails.typeLabel}` : ""}
- ${transactionDetails.sender + ${transactionDetails.transactionType === "transfer" && transactionDetails.sender ? `
From: ${fromAddress}
` : "" } - ${transactionDetails.receiver + ${transactionDetails.transactionType === "transfer" && transactionDetails.receiver ? `
To: ${toAddress} @@ -2481,15 +2547,25 @@ html`
-
-
-
-

-

+ @@ -2515,6 +2591,13 @@
+ +
+ +
+
+ +
@@ -2578,7 +2661,7 @@ status === "confirmed" ? `Included in Slot #${tx.slot}` : ""; } - + // Extract sender and receiver from transaction let sender = "Unknown"; let receiver = "Unknown"; @@ -2639,25 +2722,78 @@ // If we found a token swap, use that instead of FROM/TO if (tokenSent && tokenReceived) { transactionType = "swap"; - sender = `${tokenSent.symbol}`; - receiver = `${tokenReceived.symbol}`; + + // Map of common token mint addresses to symbols + const tokenSymbolMap = { + "So11111111111111111111111111111111111111112": "SOL", + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": "USDC", + "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB": "USDT", + "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263": "Bonk", + "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs": "Ether (Wormhole)", + "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So": "mSOL", + "7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj": "stSOL", + "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt": "SRM", + "kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6": "KIN", + "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE": "ORCA", + "RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a": "RLB", + "MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac": "MNGO", + "SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y": "SHDW", + "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3": "PYTH", + "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN": "JUP", + }; + + // Get token symbols or use truncated addresses + const getTokenSymbol = (mint) => { + if (tokenSymbolMap[mint]) { + return tokenSymbolMap[mint]; + } + // Fallback to truncated address + if (mint.length > 20) { + return mint.substring(0, 8) + "..." + mint.substring(mint.length - 4); + } + return mint; + }; + + sender = getTokenSymbol(tokenSent.mint); + receiver = getTokenSymbol(tokenReceived.mint); } } // If not a swap or NFT mint, try to find the transfer instruction if (transactionType === "transfer") { + // First priority: Check for parsed instructions (most reliable) for (const ix of instructions) { - if (ix.accounts && ix.accounts.length >= 2) { - if (ix.data) { - try { - const dataBytes = bs58.decode(ix.data); - if (dataBytes[0] === 2) { - sender = accountKeys[ix.accounts[0]]?.toString() || "Unknown"; - receiver = accountKeys[ix.accounts[1]]?.toString() || "Unknown"; - break; + if (ix.parsed && ix.parsed.info) { + // System transfer + if (ix.parsed.type === "transfer" && ix.parsed.info.source && ix.parsed.info.destination) { + sender = ix.parsed.info.source; + receiver = ix.parsed.info.destination; + break; + } + // Token transfer + if (ix.parsed.info.authority && ix.parsed.info.destination) { + sender = ix.parsed.info.authority; + receiver = ix.parsed.info.destination; + break; + } + } + } + + // Second priority: Try raw instruction decoding + if (sender === "Unknown") { + for (const ix of instructions) { + if (ix.accounts && ix.accounts.length >= 2) { + if (ix.data) { + try { + const dataBytes = bs58.decode(ix.data); + if (dataBytes[0] === 2) { + sender = accountKeys[ix.accounts[0]]?.toString() || "Unknown"; + receiver = accountKeys[ix.accounts[1]]?.toString() || "Unknown"; + break; + } + } catch (e) { + // If decode fails, continue } - } catch (e) { - // If decode fails, continue } } } @@ -2718,8 +2854,33 @@ : "Address unknown"; } + // Hide FROM/TO section for swaps and NFT mints + const addressSection = document.querySelector(".tx-address-section"); + if (addressSection) { + if (transactionType === "swap" || transactionType === "nft_mint") { + addressSection.style.display = "none"; + } else { + addressSection.style.display = "flex"; + } + } + if (getRef("tx_hash")) getRef("tx_hash").value = txId; + // Set transaction type label + if (getRef("tx_type")) { + const typeLabels = { + "swap": "Swap", + "nft_mint": "NFT Mint", + "transfer": "Transfer" + }; + getRef("tx_type").textContent = typeLabels[transactionType] || "Transfer"; + } + + // Set explorer link + if (getRef("tx_explorer_link_header")) { + getRef("tx_explorer_link_header").href = `https://solscan.io/tx/${txId}`; + } + if (getRef("tx_value")) { getRef("tx_value").dataset.sol = value; getRef("tx_value").dataset.timestamp = tx.blockTime || ""; From 655e59c85033c5bf13a26fdce2bf230519556d31 Mon Sep 17 00:00:00 2001 From: void-57 Date: Wed, 14 Jan 2026 18:01:27 +0530 Subject: [PATCH 5/5] Enhance transaction pagination and filtering functionality with improved caching and UI updates --- index.html | 291 ++++++++++++++++++++++++++--------------------------- 1 file changed, 143 insertions(+), 148 deletions(-) diff --git a/index.html b/index.html index a6ee76a..45495dc 100644 --- a/index.html +++ b/index.html @@ -1182,6 +1182,7 @@ async update(newElements) { this.elementsToRender = newElements; this.currentPage = 1; + currentPage = 1; // Sync global state this.totalPages = Math.ceil(newElements.length / this.pageSize); await this.renderCurrentPage(); this.renderPagination(); @@ -1299,38 +1300,83 @@ } async fetchNextPage() { - if (!hasMoreTransactions) return; - - // Show loading state - this.container.innerHTML = - ''; - try { - const addressInput = currentWalletAddress; + const targetPage = this.currentPage + 1; + console.log(`Going to next page: ${targetPage}`); + // Check if we have cached data for this page + if (transactionPageCache.has(targetPage)) { + console.log(`Using cached data for page ${targetPage}`); + const cachedPage = transactionPageCache.get(targetPage); + + // Apply current filter + const filter = + document.getElementById("filter_selector")?.value || "all"; + let filteredTransactions = cachedPage.formatted; + + if (filter === "sent") { + filteredTransactions = cachedPage.formatted.filter( + (tx) => tx.type === "out" + ); + } else if (filter === "received") { + filteredTransactions = cachedPage.formatted.filter( + (tx) => tx.type === "in" + ); + } + + // Clear container and render cached transactions + this.container.innerHTML = ""; + + if (filteredTransactions.length > 0) { + for (const tx of filteredTransactions) { + try { + const card = await this.renderFn(tx); + if (card && card instanceof Element) { + this.container.appendChild(card); + } + } catch (error) { + console.error("Error rendering transaction card:", error); + } + } + } else { + this.container.innerHTML = + '

No transactions found for this filter

'; + } + + // Update page state + this.currentPage = targetPage; + currentPage = targetPage; // Sync global state + + // Update page indicator + const pageInfo = document.getElementById("page_info"); + if (pageInfo) pageInfo.textContent = `Page ${this.currentPage}`; + + // Update button states + const prevButton = document.querySelector( + ".pagination-controls button:first-child" + ); + if (prevButton) prevButton.disabled = this.currentPage <= 1; + + const nextButton = document.getElementById("next_page_button"); + if (nextButton) nextButton.disabled = !hasMoreTransactions && this.currentPage >= transactionPageCache.size; + + return; + } + + // If not in cache, fetch new transactions from blockchain + if (!hasMoreTransactions) return; + + // Show loading state + this.container.innerHTML = + ''; + + const addressInput = currentWalletAddress; if (!addressInput) { this.container.innerHTML = '

No address input found

'; return; } - const address = addressInput; - if (!address) { - this.container.innerHTML = - '

No address specified

'; - return; - } - - console.log("Fetching next page for address:", address); - - try { - new solanaWeb3.PublicKey(address); - } catch (error) { - this.container.innerHTML = - '

Invalid address

'; - return; - } - if (!lastFetchedSignature) { this.container.innerHTML = '

No more transactions to load

'; @@ -1338,64 +1384,54 @@ return; } - console.log( - "Fetching next page with signature:", - lastFetchedSignature - ); - // Get next batch of signatures using the last signature from previous batch const newSignatures = await getSolanaTransactionHistory( - address, + addressInput, lastFetchedSignature, false ); - console.log("Fetched new signatures:", newSignatures?.length); - if (!newSignatures || newSignatures.length === 0) { hasMoreTransactions = false; this.container.innerHTML = '

No more transactions to load

'; - - // Update button states - getRef("next_page_button").disabled = true; + const nextBtn = document.getElementById("next_page_button"); + if (nextBtn) nextBtn.disabled = true; return; } // Process the new transactions const newTransactions = await processTransactions(newSignatures); - // Format and filter the transactions using the specific formatTransaction function - const formattedTxs = newTransactions.map((tx) => { - return formatTransaction(address, tx); + // Format + const formattedTxsBatch = newTransactions.map((tx) => { + return formatTransaction(addressInput, tx); }); - // Save this page for instant backward navigation - transactionPageCache.set(currentPage + 1, { + // Save this page for instant navigation + transactionPageCache.set(targetPage, { signatures: newSignatures, transactions: newTransactions, - formatted: formattedTxs + formatted: formattedTxsBatch }); // Apply current filter const filterSelector = document.getElementById("filter_selector"); const filter = filterSelector ? filterSelector.value : "all"; - let filteredTransactions = formattedTxs; + let filteredTransactions = formattedTxsBatch; if (filter === "sent") { - filteredTransactions = formattedTxs.filter( + filteredTransactions = formattedTxsBatch.filter( (tx) => tx.type === "out" ); } else if (filter === "received") { - filteredTransactions = formattedTxs.filter( + filteredTransactions = formattedTxsBatch.filter( (tx) => tx.type === "in" ); } - // Clear previous content and show new transactions + // Render this.container.innerHTML = ""; - - // Add new transactions to the display if (filteredTransactions.length > 0) { for (const tx of filteredTransactions) { try { @@ -1410,20 +1446,21 @@ '

No transactions found for this filter

'; } - currentPage++; + this.currentPage = targetPage; + currentPage = targetPage; // Sync global state - // Update page indicator + // Update UI const pageInfo = document.getElementById("page_info"); - if (pageInfo) pageInfo.textContent = `Page ${currentPage}`; + if (pageInfo) pageInfo.textContent = `Page ${this.currentPage}`; - // Update button states - const prevButton = document.querySelector( + const prevBtn = document.querySelector( ".pagination-controls button:first-child" ); - if (prevButton) prevButton.disabled = false; + if (prevBtn) prevBtn.disabled = false; + + const nextBtn = document.getElementById("next_page_button"); + if (nextBtn) nextBtn.disabled = !hasMoreTransactions; - const nextButton = document.getElementById("next_page_button"); - if (nextButton) nextButton.disabled = !hasMoreTransactions; } catch (error) { console.error("Error fetching next page:", error, error.stack); this.container.innerHTML = @@ -1448,7 +1485,7 @@ return; } - const targetPage = currentPage - 1; + const targetPage = this.currentPage - 1; console.log(`Going to previous page: ${targetPage}`); // Check if we have cached data for this page @@ -1491,17 +1528,18 @@ } // Update page state - currentPage = targetPage; + this.currentPage = targetPage; + currentPage = targetPage; // Sync global state // Update page indicator const pageInfo = document.getElementById("page_info"); - if (pageInfo) pageInfo.textContent = `Page ${currentPage}`; + if (pageInfo) pageInfo.textContent = `Page ${this.currentPage}`; // Update button states const prevButton = document.querySelector( ".pagination-controls button:first-child" ); - if (prevButton) prevButton.disabled = currentPage <= 1; + if (prevButton) prevButton.disabled = this.currentPage <= 1; const nextButton = document.getElementById("next_page_button"); if (nextButton) nextButton.disabled = false; // Can always go forward from cached page @@ -1698,7 +1736,7 @@ const preBalance = tx.meta.preBalances[addressIndex] || 0; const postBalance = tx.meta.postBalances[addressIndex] || 0; - return (postBalance - preBalance) / solanaWeb3.LAMPORTS_PER_SOL; + return Number(((postBalance - preBalance) / solanaWeb3.LAMPORTS_PER_SOL).toFixed(9)); } catch (error) { console.error("Error calculating transaction amount:", error); return 0; @@ -1775,70 +1813,7 @@ ); transactionsLazyLoader.init(); } - // Handle filter changes - getRef("filter_selector").addEventListener("change", async (e) => { - const address = currentWalletAddress; - if (!address) return; - // Show loading state - getRef("transactions_list").innerHTML = - ''; - - try { - // Don't refetch transactions - use the already processed ones stored in memory - // Just filter what we already have - const filter = e.target.value || "all"; - let filteredTransactions; - - if (filter === "sent") { - filteredTransactions = formattedTxs.filter( - (tx) => tx.type === "out" - ); - } else if (filter === "received") { - filteredTransactions = formattedTxs.filter( - (tx) => tx.type === "in" - ); - } else { - // "all" filter - use all transactions - filteredTransactions = formattedTxs; - } - - // Clear container - getRef("transactions_list").innerHTML = ""; - - // Render filtered transactions - if (filteredTransactions.length > 0) { - for (const tx of filteredTransactions) { - try { - const card = await render.transactionCard(tx); - if (card) getRef("transactions_list").appendChild(card); - } catch (error) { - console.error("Error rendering transaction card:", error); - } - } - } else { - getRef("transactions_list").innerHTML = - '

No transactions found for this filter

'; - } - - // Update pagination info - const pageInfo = document.getElementById("page_info"); - if (pageInfo) pageInfo.textContent = `Page ${currentPage}`; - - // Update button states - const prevButton = document.querySelector( - ".pagination-controls button:first-child" - ); - if (prevButton) prevButton.disabled = currentPage <= 1; - - const nextButton = document.getElementById("next_page_button"); - if (nextButton) nextButton.disabled = !hasMoreTransactions; - } catch (error) { - console.error("Error applying filter:", error); - getRef("transactions_list").innerHTML = - '

Error filtering transactions

'; - } - }); // Enable transaction card click to view details const list = getRef("transactions_list"); @@ -2130,6 +2105,47 @@ } } + + async function applyFilter() { + const filterSelector = document.getElementById("filter_selector"); + if (!filterSelector) return; + + const filter = filterSelector.value || "all"; + const container = document.getElementById("transactions_list"); + if (!container) return; + + // Get current page data from cache + const cachedPage = transactionPageCache.get(currentPage); + if (!cachedPage) return; + + let filtered = cachedPage.formatted; + if (filter === "sent") { + filtered = cachedPage.formatted.filter((tx) => tx.type === "out"); + } else if (filter === "received") { + filtered = cachedPage.formatted.filter((tx) => tx.type === "in"); + } + + // Show loading spinner for visual feedback + container.innerHTML = '
'; + + // Use a timeout to let the spinner show and keep UI snappy + setTimeout(async () => { + const filteredList = filtered; // Capture current filtered state + + // Render all cards first in memory to avoid jumping + const cards = await Promise.all(filteredList.map(tx => render.transactionCard(tx))); + + container.innerHTML = ""; + if (cards.length > 0) { + cards.forEach(card => { + if (card) container.appendChild(card); + }); + } else { + container.innerHTML = '

No transactions found for this filter in this page

'; + } + }, 300); + } + function handleInvalidSearch() { if (document.startViewTransition) document.startViewTransition(() => { @@ -2249,10 +2265,7 @@

Transactions

- render.transactions( - getRef("wallet_address_input").value - )} + onchange=${() => applyFilter()} > All Sent @@ -2972,9 +2985,9 @@
⚠️

Error Loading Transaction

-

-
` @@ -3738,25 +3751,7 @@ return uint8Array; } - function bnObjectToUint8(bnObject) { - const bn = bnObject._bn; // Extract the BN instance from the object - const words = bn.words; // Get the words array from the BN instance - // Convert each word to its corresponding bytes - const byteArray = []; - for (let i = 0; i < words.length; i++) { - const word = words[i]; - byteArray.push((word >> 24) & 0xff); // Extract first byte - byteArray.push((word >> 16) & 0xff); // Extract second byte - byteArray.push((word >> 8) & 0xff); // Extract third byte - byteArray.push(word & 0xff); // Extract fourth byte - } - - // Create Uint8Array from the byte array - const uint8Array = new Uint8Array(byteArray); - - return uint8Array; - } function retrieveSolanaAddr() { function retrieve() { let seed = getRef("retrieve_btc_addr_field").value.trim();