Compare commits
7 Commits
01839d9c47
...
feat/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13af842df | ||
| 6aeab54e8c | |||
| f65764d501 | |||
| 4e097c1e32 | |||
|
|
3f6f98dcaf | ||
|
|
3e900dc14c | ||
|
|
5dfc6e332b |
@@ -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**
|
||||||
|
|
||||||
|
|||||||
@@ -1064,38 +1064,82 @@
|
|||||||
<h2 id="tx-detail-heading" class="font-bold mb-2">
|
<h2 id="tx-detail-heading" class="font-bold mb-2">
|
||||||
Transaction
|
Transaction
|
||||||
</h2>
|
</h2>
|
||||||
<div id="tx-detail-type-section" class="mb-4 hidden">
|
|
||||||
<div class="text-xs text-muted mb-1">Type</div>
|
<!-- ── Identity ── -->
|
||||||
<div id="tx-detail-type" class="text-xs font-bold"></div>
|
<div class="tx-detail-group mb-1">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-xs text-muted mb-1">
|
||||||
|
Transaction hash
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div
|
||||||
|
id="tx-detail-hash"
|
||||||
|
class="text-xs break-all"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div id="tx-detail-type-section" class="mb-3 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Type</div>
|
||||||
|
<div
|
||||||
|
id="tx-detail-type"
|
||||||
|
class="text-xs font-bold"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
<div class="text-xs text-muted mb-1">Status</div>
|
<div class="text-xs text-muted mb-1">Status</div>
|
||||||
<div id="tx-detail-status" class="text-xs"></div>
|
<div id="tx-detail-status" class="text-xs"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-1">
|
||||||
<div class="text-xs text-muted mb-1">Time</div>
|
<div class="text-xs text-muted mb-1">Time</div>
|
||||||
<div id="tx-detail-time" class="text-xs"></div>
|
<div id="tx-detail-time" class="text-xs"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Value ── -->
|
||||||
|
<div class="tx-detail-group mb-1">
|
||||||
|
<div class="mb-3">
|
||||||
<div class="text-xs text-muted mb-1">Amount</div>
|
<div class="text-xs text-muted mb-1">Amount</div>
|
||||||
<div id="tx-detail-value" class="text-xs"></div>
|
<div id="tx-detail-value" class="text-xs"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 hidden">
|
<div class="mb-3 hidden">
|
||||||
<div class="text-xs text-muted mb-1">Native quantity</div>
|
<div class="text-xs text-muted mb-1">
|
||||||
|
Native quantity
|
||||||
|
</div>
|
||||||
<div id="tx-detail-native" class="text-xs"></div>
|
<div id="tx-detail-native" class="text-xs"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div
|
||||||
<div class="text-xs text-muted mb-1">From</div>
|
id="tx-detail-token-contract-section"
|
||||||
<div id="tx-detail-from" class="text-xs break-all"></div>
|
class="mb-1 hidden"
|
||||||
|
>
|
||||||
|
<div class="text-xs text-muted mb-1">
|
||||||
|
Token contract
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div
|
||||||
|
id="tx-detail-token-contract"
|
||||||
|
class="text-xs break-all"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Parties ── -->
|
||||||
|
<div class="tx-detail-group mb-1">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-xs text-muted mb-1">From</div>
|
||||||
|
<div
|
||||||
|
id="tx-detail-from"
|
||||||
|
class="text-xs break-all"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-1">
|
||||||
<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-calldata-section" class="mb-4 hidden">
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Protocol ── -->
|
||||||
|
<div id="tx-detail-calldata-section" class="mb-1 hidden">
|
||||||
|
<div class="tx-detail-group mb-1">
|
||||||
<div
|
<div
|
||||||
id="tx-detail-calldata-well"
|
id="tx-detail-calldata-well"
|
||||||
class="mb-3 border border-border border-dashed p-2"
|
class="border border-border border-dashed p-2"
|
||||||
>
|
>
|
||||||
<div class="text-xs text-muted mb-1">Action</div>
|
<div class="text-xs text-muted mb-1">Action</div>
|
||||||
<div
|
<div
|
||||||
@@ -1108,11 +1152,40 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
|
||||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
|
||||||
<div id="tx-detail-hash" class="text-xs break-all"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── On-chain details ── -->
|
||||||
|
<div
|
||||||
|
id="tx-detail-onchain-group"
|
||||||
|
class="tx-detail-group mb-1 hidden"
|
||||||
|
>
|
||||||
|
<div id="tx-detail-block-section" class="mb-3 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-3 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-3 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-3 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-1 hidden">
|
||||||
|
<div class="text-xs text-muted mb-1">Gas used</div>
|
||||||
|
<div id="tx-detail-gasused" class="text-xs"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Raw data ── -->
|
||||||
<div id="tx-detail-rawdata-section" class="mb-4 hidden">
|
<div id="tx-detail-rawdata-section" class="mb-4 hidden">
|
||||||
|
<div class="tx-detail-group">
|
||||||
<div class="text-xs text-muted mb-1">Raw data</div>
|
<div class="text-xs text-muted mb-1">Raw data</div>
|
||||||
<div
|
<div
|
||||||
id="tx-detail-rawdata"
|
id="tx-detail-rawdata"
|
||||||
@@ -1120,6 +1193,7 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ============ TRANSACTION APPROVAL ============ -->
|
<!-- ============ TRANSACTION APPROVAL ============ -->
|
||||||
<div id="view-approve-tx" class="view hidden">
|
<div id="view-approve-tx" class="view hidden">
|
||||||
|
|||||||
@@ -44,3 +44,11 @@ body {
|
|||||||
background-color 225ms ease-out,
|
background-color 225ms ease-out,
|
||||||
color 225ms ease-out;
|
color 225ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Transaction detail view — visual grouping of related fields */
|
||||||
|
.tx-detail-group {
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,56 @@ 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 (and their group wrapper) until populated
|
||||||
loadCalldata(tx.hash, tx.to);
|
const onchainGroup = $("tx-detail-onchain-group");
|
||||||
|
if (onchainGroup) onchainGroup.classList.add("hidden");
|
||||||
|
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 +224,108 @@ 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the on-chain details group if any child section is visible
|
||||||
|
const onchainGroup = $("tx-detail-onchain-group");
|
||||||
|
if (onchainGroup) {
|
||||||
|
const hasVisible = [
|
||||||
|
"tx-detail-block-section",
|
||||||
|
"tx-detail-nonce-section",
|
||||||
|
"tx-detail-fee-section",
|
||||||
|
"tx-detail-gasprice-section",
|
||||||
|
"tx-detail-gasused-section",
|
||||||
|
].some((id) => {
|
||||||
|
const el = $(id);
|
||||||
|
return el && !el.classList.contains("hidden");
|
||||||
|
});
|
||||||
|
if (hasVisible) {
|
||||||
|
onchainGroup.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 +340,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;
|
||||||
|
|
||||||
|
|||||||
@@ -153,24 +153,38 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
|
|||||||
|
|
||||||
// When a token transfer shares a hash with a normal tx, the normal tx
|
// When a token transfer shares a hash with a normal tx, the normal tx
|
||||||
// is the contract call (0 ETH) and the token transfer has the real
|
// is the contract call (0 ETH) and the token transfer has the real
|
||||||
// amount and symbol. A single transaction (e.g. a swap) can produce
|
// amount and symbol. For contract calls (swaps), a single transaction
|
||||||
// multiple token transfers (one per token involved), so we key token
|
// can produce multiple token transfers (input, intermediates, output).
|
||||||
// transfers by hash + contract address to keep all of them. We also
|
// We consolidate these into the original tx entry using the token
|
||||||
// preserve contract-call metadata (direction, label, method) from the
|
// transfer where the user *receives* tokens (the swap output), so
|
||||||
// matching normal tx so swaps display correctly.
|
// the transaction list shows the final result rather than confusing
|
||||||
|
// intermediate hops. We preserve the original tx's from/to so the
|
||||||
|
// user sees their own address, not a router or Permit2 contract.
|
||||||
for (const tt of ttJson.items || []) {
|
for (const tt of ttJson.items || []) {
|
||||||
const parsed = parseTokenTransfer(tt, addrLower);
|
const parsed = parseTokenTransfer(tt, addrLower);
|
||||||
const existing = txsByHash.get(parsed.hash);
|
const existing = txsByHash.get(parsed.hash);
|
||||||
if (existing && existing.direction === "contract") {
|
if (existing && existing.direction === "contract") {
|
||||||
parsed.direction = "contract";
|
// For contract calls (swaps), consolidate into the original
|
||||||
parsed.directionLabel = existing.directionLabel;
|
// tx entry. Prefer the "received" transfer (swap output)
|
||||||
parsed.isContractCall = true;
|
// for the display amount. If no received transfer exists,
|
||||||
parsed.method = existing.method;
|
// fall back to the first "sent" transfer (swap input).
|
||||||
// Remove the bare-hash normal tx so it doesn't appear as a
|
const isReceived = parsed.direction === "received";
|
||||||
// duplicate with empty value; token transfers replace it.
|
const needsAmount = !existing.exactValue;
|
||||||
txsByHash.delete(parsed.hash);
|
if (isReceived || needsAmount) {
|
||||||
|
existing.value = parsed.value;
|
||||||
|
existing.exactValue = parsed.exactValue;
|
||||||
|
existing.rawAmount = parsed.rawAmount;
|
||||||
|
existing.rawUnit = parsed.rawUnit;
|
||||||
|
existing.symbol = parsed.symbol;
|
||||||
|
existing.contractAddress = parsed.contractAddress;
|
||||||
|
existing.holders = parsed.holders;
|
||||||
}
|
}
|
||||||
// Use composite key so multiple token transfers per tx are kept.
|
// Keep the original tx's from/to (the user's address and the
|
||||||
|
// contract they called), not the token transfer's from/to
|
||||||
|
// which may be a router or Permit2 contract.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Non-contract token transfers get their own entries.
|
||||||
const ttKey = parsed.hash + ":" + (parsed.contractAddress || "");
|
const ttKey = parsed.hash + ":" + (parsed.contractAddress || "");
|
||||||
txsByHash.set(ttKey, parsed);
|
txsByHash.set(ttKey, parsed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,9 +359,12 @@ function decode(data, toAddress) {
|
|||||||
const s = decodeV3SwapExactIn(inputs[i]);
|
const s = decodeV3SwapExactIn(inputs[i]);
|
||||||
if (s) {
|
if (s) {
|
||||||
if (!inputToken) inputToken = s.tokenIn;
|
if (!inputToken) inputToken = s.tokenIn;
|
||||||
if (!outputToken) outputToken = s.tokenOut;
|
|
||||||
if (!inputAmount) inputAmount = s.amountIn;
|
if (!inputAmount) inputAmount = s.amountIn;
|
||||||
if (!minOutput) minOutput = s.amountOutMin;
|
// Always update output: in multi-step swaps (V3 → V4),
|
||||||
|
// the last swap step determines the final output token
|
||||||
|
// and minimum received amount.
|
||||||
|
outputToken = s.tokenOut;
|
||||||
|
minOutput = s.amountOutMin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,9 +372,9 @@ function decode(data, toAddress) {
|
|||||||
const s = decodeV2SwapExactIn(inputs[i]);
|
const s = decodeV2SwapExactIn(inputs[i]);
|
||||||
if (s) {
|
if (s) {
|
||||||
if (!inputToken) inputToken = s.tokenIn;
|
if (!inputToken) inputToken = s.tokenIn;
|
||||||
if (!outputToken) outputToken = s.tokenOut;
|
|
||||||
if (!inputAmount) inputAmount = s.amountIn;
|
if (!inputAmount) inputAmount = s.amountIn;
|
||||||
if (!minOutput) minOutput = s.amountOutMin;
|
outputToken = s.tokenOut;
|
||||||
|
minOutput = s.amountOutMin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,12 +391,11 @@ function decode(data, toAddress) {
|
|||||||
const v4 = decodeV4Swap(inputs[i]);
|
const v4 = decodeV4Swap(inputs[i]);
|
||||||
if (v4) {
|
if (v4) {
|
||||||
if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn;
|
if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn;
|
||||||
if (!outputToken && v4.tokenOut)
|
|
||||||
outputToken = v4.tokenOut;
|
|
||||||
if (!inputAmount && v4.amountIn)
|
if (!inputAmount && v4.amountIn)
|
||||||
inputAmount = v4.amountIn;
|
inputAmount = v4.amountIn;
|
||||||
if (!minOutput && v4.amountOutMin)
|
// Always update output: last swap step wins
|
||||||
minOutput = v4.amountOutMin;
|
if (v4.tokenOut) outputToken = v4.tokenOut;
|
||||||
|
if (v4.amountOutMin) minOutput = v4.amountOutMin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user