-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(explorer): global transactions listener #3285
Merged
Merged
Changes from 9 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
624cf9a
move txs to global store
karooolis 6832813
manage toasts
karooolis 4576741
undo toasts
karooolis c96f461
remove empty line
karooolis dfe2fce
Create tall-penguins-promise.md
karooolis 6f5cca0
simplify store
karooolis 24e20ba
Merge branch 'kumpis/listen-txs-global' of github.com:latticexyz/mud …
karooolis 6ef212a
adjust ReactNode prop
karooolis 99f4945
init observer store, process events
karooolis eb1c954
update to useObservedTransactions
karooolis d44343d
narrow down to state transactions
karooolis da7566f
Update packages/explorer/src/app/(explorer)/[chainName]/worlds/Worlds…
karooolis 6a39d1d
remove useStore dep
karooolis 1ce297e
Update packages/explorer/src/app/(explorer)/[chainName]/worlds/Worlds…
karooolis 800f5d5
move store ref outside component
karooolis 36b4206
move store into layout
karooolis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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; | ||
} |
196 changes: 0 additions & 196 deletions
196
...rer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/observe/useTransactionWatcher.ts
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this cause a rerender of this component every time the store changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if the zustand suggested approach of using a provider in next.js is for this reason 🙈 I assumed it was an SSR thing but maybe its a broad solution to this particular issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, unfortunately causes re-renders, it's definitely a code smell to say the least. Using the provider does feel cleaner despite the all the boilerplate. But I think the Provider solution is specifically for SSR because I'm not sure how common of a scenario it is to initialize a store in such way that we're doing it now. Usually, you'd use one of store's getters/setters, and initialize the store implicitly when it's needed.
Perhaps a scenario where a store is initialized with props is more fitting for us https://zustand.docs.pmnd.rs/guides/initialize-state-with-props#store-creator-with-createstore but if we do that, then we're back to using providers.
Given that the current problem essentially is that the broadcast channel listener does not get started, I'm thinking what about moving the broadcast channel event listener into a hook, and initialize it somewhere higher-up the tree that way? Or alternatively, go back to the Providers solution from earlier, and include observer transactions into a common store too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you know if the provider would solve for this?
if so I feel like maybe the provider approach is ~fine and solves for multiple problems (SSR, global mounting/initializing the store and observer)
but curious if we can generalize the store provider boilerplate to avoid repeating the pattern for every store
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd need to set it up to double-check but I'd think so. Given though that the
store
solution works, even if somewhat hacky, I'm included to stick with that