Merge branch 'main' into fix/issue-127-swap-amount-display
All checks were successful
check / check (push) Successful in 21s
All checks were successful
check / check (push) Successful in 21s
This commit is contained in:
@@ -437,6 +437,10 @@ transitions.
|
|||||||
- **When**: User tapped a transaction row from AddressDetail or AddressToken.
|
- **When**: User tapped a transaction row from AddressDetail or AddressToken.
|
||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Transaction" heading, "Back" button
|
- "Transaction" heading, "Back" button
|
||||||
|
- Type: transaction classification — one of: Native ETH Transfer, ERC-20
|
||||||
|
Token Transfer, Swap, Token Approval, Contract Call, Contract Creation
|
||||||
|
- Token contract: shown for ERC-20 transfers — color dot + full contract
|
||||||
|
address (tap to copy) + etherscan token link
|
||||||
- Status: "Success" or "Failed"
|
- Status: "Success" or "Failed"
|
||||||
- Time: ISO datetime + relative age in parentheses
|
- Time: ISO datetime + relative age in parentheses
|
||||||
- Amount: value + symbol (bold)
|
- Amount: value + symbol (bold)
|
||||||
@@ -445,6 +449,11 @@ transitions.
|
|||||||
- To: blockie + color dot + full address (tap to copy) + etherscan link
|
- To: blockie + color dot + full address (tap to copy) + etherscan link
|
||||||
- ENS name if available
|
- ENS name if available
|
||||||
- Transaction hash: full hash (tap to copy) + etherscan link
|
- Transaction hash: full hash (tap to copy) + etherscan link
|
||||||
|
- Block: block number (tap to copy) + etherscan block link
|
||||||
|
- Nonce: transaction nonce (tap to copy)
|
||||||
|
- Transaction fee: ETH amount (tap to copy)
|
||||||
|
- Gas price: value in Gwei (tap to copy)
|
||||||
|
- Gas used: integer (tap to copy)
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Back" → **AddressToken** (if `selectedToken` set) or **AddressDetail**
|
- "Back" → **AddressToken** (if `selectedToken` set) or **AddressDetail**
|
||||||
|
|
||||||
|
|||||||
@@ -1092,6 +1092,13 @@
|
|||||||
<div class="text-xs text-muted mb-1">To</div>
|
<div class="text-xs text-muted mb-1">To</div>
|
||||||
<div id="tx-detail-to" class="text-xs break-all"></div>
|
<div id="tx-detail-to" class="text-xs break-all"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tx-detail-token-contract-section" class="mb-4 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Token contract</div>
|
||||||
|
<div
|
||||||
|
id="tx-detail-token-contract"
|
||||||
|
class="text-xs break-all"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
<div id="tx-detail-calldata-section" class="mb-4 hidden">
|
<div id="tx-detail-calldata-section" class="mb-4 hidden">
|
||||||
<div
|
<div
|
||||||
id="tx-detail-calldata-well"
|
id="tx-detail-calldata-well"
|
||||||
@@ -1112,6 +1119,26 @@
|
|||||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
||||||
<div id="tx-detail-hash" class="text-xs break-all"></div>
|
<div id="tx-detail-hash" class="text-xs break-all"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tx-detail-block-section" class="mb-4 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Block</div>
|
||||||
|
<div id="tx-detail-block" class="text-xs"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tx-detail-nonce-section" class="mb-4 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Nonce</div>
|
||||||
|
<div id="tx-detail-nonce" class="text-xs"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tx-detail-fee-section" class="mb-4 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Transaction fee</div>
|
||||||
|
<div id="tx-detail-fee" class="text-xs"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tx-detail-gasprice-section" class="mb-4 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Gas price</div>
|
||||||
|
<div id="tx-detail-gasprice" class="text-xs"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tx-detail-gasused-section" class="mb-4 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Gas used</div>
|
||||||
|
<div id="tx-detail-gasused" class="text-xs"></div>
|
||||||
|
</div>
|
||||||
<div id="tx-detail-rawdata-section" class="mb-4 hidden">
|
<div id="tx-detail-rawdata-section" class="mb-4 hidden">
|
||||||
<div class="text-xs text-muted mb-1">Raw data</div>
|
<div class="text-xs text-muted mb-1">Raw data</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const {
|
|||||||
timeAgo,
|
timeAgo,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state } = require("../../shared/state");
|
const { state } = require("../../shared/state");
|
||||||
|
const { formatEther, formatUnits } = require("ethers");
|
||||||
const makeBlockie = require("ethereum-blockies-base64");
|
const makeBlockie = require("ethereum-blockies-base64");
|
||||||
const { log, debugFetch } = require("../../shared/log");
|
const { log, debugFetch } = require("../../shared/log");
|
||||||
const { decodeCalldata } = require("./approval");
|
const { decodeCalldata } = require("./approval");
|
||||||
@@ -26,6 +27,25 @@ const EXT_ICON =
|
|||||||
|
|
||||||
let ctx;
|
let ctx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine a human-readable transaction type string from tx fields.
|
||||||
|
*/
|
||||||
|
function getTransactionType(tx) {
|
||||||
|
if (!tx.to) return "Contract Creation";
|
||||||
|
if (tx.direction === "contract") {
|
||||||
|
if (tx.directionLabel === "Swap") return "Swap";
|
||||||
|
if (
|
||||||
|
tx.method === "approve" ||
|
||||||
|
tx.directionLabel === "Approve" ||
|
||||||
|
tx.method === "setApprovalForAll"
|
||||||
|
)
|
||||||
|
return "Token Approval";
|
||||||
|
return "Contract Call";
|
||||||
|
}
|
||||||
|
if (tx.symbol && tx.symbol !== "ETH") return "ERC-20 Token Transfer";
|
||||||
|
return "Native ETH Transfer";
|
||||||
|
}
|
||||||
|
|
||||||
function copyableHtml(text, extraClass) {
|
function copyableHtml(text, extraClass) {
|
||||||
const cls =
|
const cls =
|
||||||
"underline decoration-dashed cursor-pointer" +
|
"underline decoration-dashed cursor-pointer" +
|
||||||
@@ -99,6 +119,7 @@ function show(tx) {
|
|||||||
direction: tx.direction || null,
|
direction: tx.direction || null,
|
||||||
isContractCall: tx.isContractCall || false,
|
isContractCall: tx.isContractCall || false,
|
||||||
method: tx.method || null,
|
method: tx.method || null,
|
||||||
|
contractAddress: tx.contractAddress || null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
render();
|
render();
|
||||||
@@ -135,30 +156,54 @@ function render() {
|
|||||||
nativeEl.parentElement.classList.add("hidden");
|
nativeEl.parentElement.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show type label for contract interactions (Swap, Execute, etc.)
|
// Always show transaction type as the first field
|
||||||
const typeSection = $("tx-detail-type-section");
|
const typeSection = $("tx-detail-type-section");
|
||||||
const typeEl = $("tx-detail-type");
|
const typeEl = $("tx-detail-type");
|
||||||
const headingEl = $("tx-detail-heading");
|
const headingEl = $("tx-detail-heading");
|
||||||
if (tx.direction === "contract" && tx.directionLabel) {
|
if (typeSection && typeEl) {
|
||||||
if (typeSection) {
|
typeEl.textContent = getTransactionType(tx);
|
||||||
typeEl.textContent = tx.directionLabel;
|
|
||||||
typeSection.classList.remove("hidden");
|
typeSection.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (typeSection) typeSection.classList.add("hidden");
|
|
||||||
}
|
|
||||||
if (headingEl) headingEl.textContent = "Transaction";
|
if (headingEl) headingEl.textContent = "Transaction";
|
||||||
|
|
||||||
// Hide calldata and raw data sections; re-fetch if this is a contract call
|
// Token contract address (for ERC-20 transfers)
|
||||||
|
const tokenContractSection = $("tx-detail-token-contract-section");
|
||||||
|
const tokenContractEl = $("tx-detail-token-contract");
|
||||||
|
if (tokenContractSection && tokenContractEl) {
|
||||||
|
if (tx.contractAddress) {
|
||||||
|
const dot = addressDotHtml(tx.contractAddress);
|
||||||
|
const link = `https://etherscan.io/token/${tx.contractAddress}`;
|
||||||
|
tokenContractEl.innerHTML =
|
||||||
|
`<div class="flex items-center">${dot}` +
|
||||||
|
copyableHtml(tx.contractAddress, "break-all") +
|
||||||
|
etherscanLinkHtml(link) +
|
||||||
|
`</div>`;
|
||||||
|
tokenContractSection.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
tokenContractSection.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide calldata and raw data sections; always fetch full tx details
|
||||||
const calldataSection = $("tx-detail-calldata-section");
|
const calldataSection = $("tx-detail-calldata-section");
|
||||||
if (calldataSection) calldataSection.classList.add("hidden");
|
if (calldataSection) calldataSection.classList.add("hidden");
|
||||||
const rawDataSection = $("tx-detail-rawdata-section");
|
const rawDataSection = $("tx-detail-rawdata-section");
|
||||||
if (rawDataSection) rawDataSection.classList.add("hidden");
|
if (rawDataSection) rawDataSection.classList.add("hidden");
|
||||||
|
|
||||||
if (tx.isContractCall || tx.direction === "contract") {
|
// Hide on-chain detail sections until populated
|
||||||
loadCalldata(tx.hash, tx.to);
|
for (const id of [
|
||||||
|
"tx-detail-block-section",
|
||||||
|
"tx-detail-nonce-section",
|
||||||
|
"tx-detail-fee-section",
|
||||||
|
"tx-detail-gasprice-section",
|
||||||
|
"tx-detail-gasused-section",
|
||||||
|
]) {
|
||||||
|
const el = $(id);
|
||||||
|
if (el) el.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadFullTxDetails(tx.hash, tx.to, tx.isContractCall);
|
||||||
|
|
||||||
const isoStr = isoDate(tx.timestamp);
|
const isoStr = isoDate(tx.timestamp);
|
||||||
$("tx-detail-time").innerHTML =
|
$("tx-detail-time").innerHTML =
|
||||||
copyableHtml(isoStr) + " (" + escapeHtml(timeAgo(tx.timestamp)) + ")";
|
copyableHtml(isoStr) + " (" + escapeHtml(timeAgo(tx.timestamp)) + ")";
|
||||||
@@ -177,7 +222,90 @@ function render() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCalldata(txHash, toAddress) {
|
function showDetailField(sectionId, contentId, value) {
|
||||||
|
const section = $(sectionId);
|
||||||
|
const el = $(contentId);
|
||||||
|
if (!section || !el) return;
|
||||||
|
el.innerHTML = copyableHtml(value, "");
|
||||||
|
section.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateOnChainDetails(txData) {
|
||||||
|
// Block number
|
||||||
|
if (txData.block_number != null) {
|
||||||
|
const blockLink = `https://etherscan.io/block/${txData.block_number}`;
|
||||||
|
const blockSection = $("tx-detail-block-section");
|
||||||
|
const blockEl = $("tx-detail-block");
|
||||||
|
if (blockSection && blockEl) {
|
||||||
|
blockEl.innerHTML =
|
||||||
|
copyableHtml(String(txData.block_number), "") +
|
||||||
|
etherscanLinkHtml(blockLink);
|
||||||
|
blockSection.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nonce
|
||||||
|
if (txData.nonce != null) {
|
||||||
|
showDetailField(
|
||||||
|
"tx-detail-nonce-section",
|
||||||
|
"tx-detail-nonce",
|
||||||
|
String(txData.nonce),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction fee
|
||||||
|
const feeWei = txData.fee?.value || txData.tx_fee;
|
||||||
|
if (feeWei) {
|
||||||
|
const feeEth = formatEther(String(feeWei));
|
||||||
|
showDetailField(
|
||||||
|
"tx-detail-fee-section",
|
||||||
|
"tx-detail-fee",
|
||||||
|
feeEth + " ETH",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gas price
|
||||||
|
const gasPrice = txData.gas_price;
|
||||||
|
if (gasPrice) {
|
||||||
|
const gwei = formatUnits(String(gasPrice), "gwei");
|
||||||
|
showDetailField(
|
||||||
|
"tx-detail-gasprice-section",
|
||||||
|
"tx-detail-gasprice",
|
||||||
|
gwei + " Gwei",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gas used
|
||||||
|
const gasUsed = txData.gas_used;
|
||||||
|
if (gasUsed) {
|
||||||
|
showDetailField(
|
||||||
|
"tx-detail-gasused-section",
|
||||||
|
"tx-detail-gasused",
|
||||||
|
String(gasUsed),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind copy handlers for newly added elements
|
||||||
|
for (const id of [
|
||||||
|
"tx-detail-block-section",
|
||||||
|
"tx-detail-nonce-section",
|
||||||
|
"tx-detail-fee-section",
|
||||||
|
"tx-detail-gasprice-section",
|
||||||
|
"tx-detail-gasused-section",
|
||||||
|
]) {
|
||||||
|
const section = $(id);
|
||||||
|
if (!section) continue;
|
||||||
|
section.querySelectorAll("[data-copy]").forEach((el) => {
|
||||||
|
el.onclick = () => {
|
||||||
|
navigator.clipboard.writeText(el.dataset.copy);
|
||||||
|
showFlash("Copied!");
|
||||||
|
flashCopyFeedback(el);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFullTxDetails(txHash, toAddress, isContractCall) {
|
||||||
const section = $("tx-detail-calldata-section");
|
const section = $("tx-detail-calldata-section");
|
||||||
const actionEl = $("tx-detail-calldata-action");
|
const actionEl = $("tx-detail-calldata-action");
|
||||||
const detailsEl = $("tx-detail-calldata-details");
|
const detailsEl = $("tx-detail-calldata-details");
|
||||||
@@ -192,6 +320,10 @@ async function loadCalldata(txHash, toAddress) {
|
|||||||
);
|
);
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const txData = await resp.json();
|
const txData = await resp.json();
|
||||||
|
|
||||||
|
// Populate on-chain detail fields (block, nonce, gas, fee)
|
||||||
|
populateOnChainDetails(txData);
|
||||||
|
|
||||||
const inputData = txData.raw_input || txData.input || null;
|
const inputData = txData.raw_input || txData.input || null;
|
||||||
if (!inputData || inputData === "0x") return;
|
if (!inputData || inputData === "0x") return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user