-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(explorer): global transactions listener (#3285)
Co-authored-by: Kevin Ingersoll <[email protected]>
- Loading branch information
Showing
9 changed files
with
238 additions
and
204 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@latticexyz/explorer": patch | ||
--- | ||
|
||
Transactions are now monitored across all tabs while the World Explorer is open. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
...orer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/TransactionsWatcher.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { useParams } from "next/navigation"; | ||
import { BaseError, Hex, TransactionReceipt, decodeFunctionData, parseEventLogs } from "viem"; | ||
import { useConfig, useWatchBlocks } from "wagmi"; | ||
import { getTransaction, simulateContract, waitForTransactionReceipt } from "wagmi/actions"; | ||
import { useStore } from "zustand"; | ||
import { useCallback, useEffect } from "react"; | ||
import { store as observerStore } from "../../../../../../observer/store"; | ||
import { useChain } from "../../../../hooks/useChain"; | ||
import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery"; | ||
import { store as worldStore } from "../store"; | ||
|
||
export function TransactionsWatcher() { | ||
const { id: chainId } = useChain(); | ||
const { worldAddress } = useParams(); | ||
const wagmiConfig = useConfig(); | ||
const { data: worldAbiData } = useWorldAbiQuery(); | ||
const abi = worldAbiData?.abi; | ||
const { transactions, setTransaction, updateTransaction } = useStore(worldStore); | ||
const observerWrites = useStore(observerStore, (state) => state.writes); | ||
|
||
const handleTransaction = useCallback( | ||
async (hash: Hex, timestamp: bigint) => { | ||
if (!abi) return; | ||
|
||
const transaction = await getTransaction(wagmiConfig, { hash }); | ||
if (transaction.to !== worldAddress) return; | ||
|
||
let functionName: string | undefined; | ||
let args: readonly unknown[] | undefined; | ||
let transactionError: BaseError | undefined; | ||
|
||
try { | ||
const functionData = decodeFunctionData({ abi, data: transaction.input }); | ||
functionName = functionData.functionName; | ||
args = functionData.args; | ||
} catch (error) { | ||
transactionError = error as BaseError; | ||
functionName = transaction.input.length > 10 ? transaction.input.slice(0, 10) : "unknown"; | ||
} | ||
|
||
const write = Object.values(observerWrites).find((write) => write.hash === hash); | ||
setTransaction({ | ||
hash, | ||
writeId: write?.writeId ?? hash, | ||
from: transaction.from, | ||
timestamp, | ||
transaction, | ||
status: "pending", | ||
functionData: { | ||
functionName, | ||
args, | ||
}, | ||
value: transaction.value, | ||
}); | ||
|
||
let receipt: TransactionReceipt | undefined; | ||
try { | ||
receipt = await waitForTransactionReceipt(wagmiConfig, { hash }); | ||
} catch { | ||
console.error(`Failed to fetch transaction receipt. Transaction hash: ${hash}`); | ||
} | ||
|
||
if (receipt && receipt.status === "reverted" && functionName) { | ||
try { | ||
// Simulate the failed transaction to retrieve the revert reason | ||
// Note, it only works for functions that are declared in the ABI | ||
// See: https://github.com/wevm/viem/discussions/462 | ||
await simulateContract(wagmiConfig, { | ||
account: transaction.from, | ||
address: worldAddress, | ||
abi, | ||
value: transaction.value, | ||
blockNumber: receipt.blockNumber, | ||
functionName, | ||
args, | ||
}); | ||
} catch (error) { | ||
transactionError = error as BaseError; | ||
} | ||
} | ||
|
||
const status = receipt ? receipt.status : "unknown"; | ||
const logs = parseEventLogs({ | ||
abi, | ||
logs: receipt?.logs || [], | ||
}); | ||
|
||
updateTransaction(hash, { | ||
receipt, | ||
logs, | ||
status, | ||
error: transactionError as BaseError, | ||
}); | ||
}, | ||
[abi, wagmiConfig, worldAddress, observerWrites, setTransaction, updateTransaction], | ||
); | ||
|
||
useEffect(() => { | ||
for (const write of Object.values(observerWrites)) { | ||
const hash = write.hash; | ||
if (hash && write.address === worldAddress) { | ||
const transaction = transactions.find((transaction) => transaction.hash === hash); | ||
if (!transaction) { | ||
handleTransaction(hash, BigInt(write.time) / 1000n); | ||
} | ||
} | ||
} | ||
}, [handleTransaction, observerWrites, transactions, worldAddress]); | ||
|
||
useWatchBlocks({ | ||
onBlock(block) { | ||
for (const hash of block.transactions) { | ||
if (transactions.find((transaction) => transaction.hash === hash)) continue; | ||
handleTransaction(hash, block.timestamp); | ||
} | ||
}, | ||
chainId, | ||
pollingInterval: 500, | ||
}); | ||
|
||
return null; | ||
} |
77 changes: 77 additions & 0 deletions
77
...r/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useObservedTransactions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { useParams } from "next/navigation"; | ||
import { | ||
AbiFunction, | ||
Address, | ||
BaseError, | ||
DecodeFunctionDataReturnType, | ||
Hex, | ||
Log, | ||
Transaction, | ||
TransactionReceipt, | ||
parseAbiItem, | ||
} from "viem"; | ||
import { useStore } from "zustand"; | ||
import { useMemo } from "react"; | ||
import { Message } from "../../../../../../observer/messages"; | ||
import { type Write, store as observerStore } from "../../../../../../observer/store"; | ||
import { store as worldStore } from "../store"; | ||
|
||
export type ObservedTransaction = { | ||
writeId: string; | ||
hash?: Hex; | ||
from?: Address; | ||
timestamp?: bigint; | ||
transaction?: Transaction; | ||
functionData?: DecodeFunctionDataReturnType; | ||
value?: bigint; | ||
receipt?: TransactionReceipt; | ||
status: "pending" | "success" | "reverted" | "rejected" | "unknown"; | ||
write?: Write; | ||
logs?: Log[]; | ||
error?: BaseError; | ||
}; | ||
|
||
export function useObservedTransactions() { | ||
const { worldAddress } = useParams(); | ||
const transactions = useStore(worldStore, (state) => state.transactions); | ||
const observerWrites = useStore(observerStore, (state) => state.writes); | ||
|
||
const mergedTransactions = useMemo((): ObservedTransaction[] => { | ||
const mergedMap = new Map<string | undefined, ObservedTransaction>(); | ||
|
||
for (const write of Object.values(observerWrites)) { | ||
if (write.address !== worldAddress) continue; | ||
|
||
const parsedAbiItem = parseAbiItem(`function ${write.functionSignature}`) as AbiFunction; | ||
const writeResult = write.events.find((event): event is Message<"write:result"> => event.type === "write:result"); | ||
|
||
mergedMap.set(write.hash || write.writeId, { | ||
hash: write.hash, | ||
writeId: write.writeId, | ||
from: write.from, | ||
status: writeResult?.status === "rejected" ? "rejected" : "pending", | ||
timestamp: BigInt(write.time) / 1000n, | ||
functionData: { | ||
functionName: parsedAbiItem.name, | ||
args: write.args, | ||
}, | ||
value: write.value, | ||
error: writeResult && "reason" in writeResult ? (writeResult.reason as BaseError) : undefined, | ||
write, | ||
}); | ||
} | ||
|
||
for (const transaction of transactions) { | ||
const existing = mergedMap.get(transaction.hash); | ||
if (existing) { | ||
mergedMap.set(transaction.hash, { ...transaction, write: existing.write }); | ||
} else { | ||
mergedMap.set(transaction.hash, { ...transaction }); | ||
} | ||
} | ||
|
||
return Array.from(mergedMap.values()).sort((a, b) => Number(b.timestamp ?? 0n) - Number(a.timestamp ?? 0n)); | ||
}, [observerWrites, worldAddress, transactions]); | ||
|
||
return mergedTransactions; | ||
} |
Oops, something went wrong.