Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
"@repo/nillion": "workspace:*",
"@tanstack/react-table": "^8.21.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"alchemy-sdk": "^3.5.2",
Expand All @@ -52,8 +54,7 @@
"viem": "^2.22.22",
"wagmi": "^2.14.11",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.1",
"@repo/nillion": "workspace:*"
"zod-to-json-schema": "^3.24.1"
},
"devDependencies": {
"@types/react-grid-layout": "^1.3.5",
Expand Down
252 changes: 111 additions & 141 deletions apps/web/src/components/Transactions.tsx
Original file line number Diff line number Diff line change
@@ -1,164 +1,134 @@
import { useStore } from "@nanostores/react";
import { Alchemy, Network } from "alchemy-sdk";
import { Alchemy, AssetTransfersCategory, Network } from "alchemy-sdk";
import React from "react";
import { $messages } from "../store/messages";
import { YieldHistoricalChart } from "./YieldHistoricalChart";
import { TransactionTable } from "./geist/transaction-table";
import type { TransactionMeta } from "@/lib/domain/transaction/transaction";
import { parseEther } from "viem";
import { baseSepolia } from "viem/chains";
import { Explorer } from "@/lib/explorer/url";

const alchemy = new Alchemy({
apiKey: import.meta.env.PUBLIC_ALCHEMY_API_KEY,
network: Network.BASE_SEPOLIA,
apiKey: import.meta.env.PUBLIC_ALCHEMY_API_KEY,
network: Network.BASE_SEPOLIA,
});

export const invokeApi = async (endpoint: string, body?: any) => {
return fetch(endpoint, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
}).then((res) => {
return res.json();
});
return fetch(endpoint, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
}).then((res) => {
return res.json();
});
};

export const getTxnsByFilter = async ({
filter,
type,
method,
address,
filter,
type,
method,
address,
}: {
filter?: string;
type?: string[];
method?: string;
chainId?: number;
address?: string;
filter?: string;
type?: string[];
method?: string;
chainId?: number;
address?: string;
}) => {
if (address) {
try {
const txns = await alchemy.core.getAssetTransfers({
fromBlock: "0x0",
fromAddress: address,
category: ["external", "erc20", "erc721", "erc1155"],
});
if (address) {
try {
const txns = await alchemy.core.getAssetTransfers({
fromBlock: "0x0",
fromAddress: address,
category: [
AssetTransfersCategory.EXTERNAL,
AssetTransfersCategory.ERC20,
AssetTransfersCategory.ERC721,
AssetTransfersCategory.ERC1155,
],
});

// Transform Alchemy response to match existing format
return {
items: txns.transfers.map((tx: any) => ({
hash: tx.hash,
method: tx.category,
status: "ok", // Alchemy doesn't provide status directly
from: { hash: tx.from },
to: { hash: tx.to },
value: tx.value,
})),
};
} catch (error) {
console.error("Alchemy API error:", error);
throw error;
}
}
console.log("txns", txns);

// Fallback to original Blockscout API if no address provided
const queryString = new URLSearchParams({
...(filter && { filter }),
...(method && { method }),
...(type && { type: type.join(",") }),
});
const endpoint = `https://base-sepolia.blockscout.com/api/v2/transactions?${queryString.toString()}`;
return await invokeApi(endpoint);
const formatted = txns.transfers.map(
(tx) =>
({
hash: tx.hash as `0x${string}`,
from: tx.from as `0x${string}`,
to: tx.to as `0x${string}`,
value: tx.value ? BigInt(tx.value * 10 ** 18) : 0n,
gas: 0n,
blockNumber: tx.blockNum ? BigInt(tx.blockNum) : 0n,
isSuccess: true,
tokenTransfers: [],
} satisfies Partial<TransactionMeta>),
);

console.log({ formatted });

// Transform Alchemy response to match existing format
return {
items: formatted,
};
} catch (error) {
console.error("Alchemy API error:", error);
throw error;
}
}

// Fallback to original Blockscout API if no address provided
const queryString = new URLSearchParams({
...(filter && { filter }),
...(method && { method }),
...(type && { type: type.join(",") }),
});
const endpoint = `https://base-sepolia.blockscout.com/api/v2/transactions?${queryString.toString()}`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for changes in this dmeo repo is relatives fine, but if we are able to get wagmi working, we should use tanstack query to avoid setIsLoading ?

for contributing back to upstream (geist dapp kit),

  1. I think it is hard to share same abstraction of params across data sources. e.g. here filter is not use by alchemy.

sharing txn object parsed from their responses might work to certain extent

fundamentally we might want totally separate hooks, e.g.
in wagmi we have watchContractEvent and watchAccount

Lets add type to indiciate too

  1. lets create / use existing util for blockscout endpoint on L66

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setIsLoading was a quick fix haha for the hack.

fundamentally we might want totally separate hooks, e.g.
in wagmi we have watchContractEvent and watchAccount

You mean hooks by data provider?

return await invokeApi(endpoint);
};

const TransactionsWidget = ({ address }: { address: string }) => {
const [transactions, setTransactions] = React.useState<any[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
const [transactions, setTransactions] = React.useState<TransactionMeta[]>([]);
const [isLoading, setIsLoading] = React.useState(true);

React.useEffect(() => {
const fetchTransactions = async () => {
try {
setIsLoading(true);
const response = await getTxnsByFilter({
filter: "contract_call",
method: "supply,withdraw",
type: ["validated"],
address: address,
});
console.log(response);
setTransactions(response.items || []);
} catch (error) {
console.error("Failed to fetch transactions:", error);
setTransactions([]);
} finally {
setIsLoading(false);
}
};
React.useEffect(() => {
const fetchTransactions = async () => {
try {
setIsLoading(true);
const response = await getTxnsByFilter({
filter: "contract_call",
method: "supply,withdraw",
type: ["validated"],
address: address,
});
console.log(response);
setTransactions(response.items || []);
} catch (error) {
console.error("Failed to fetch transactions:", error);
setTransactions([]);
} finally {
setIsLoading(false);
}
};

fetchTransactions();
}, [address]);
fetchTransactions();
}, [address]);

if (isLoading) {
return (
<div className="w-full min-h-[200px] bg-white rounded-lg shadow flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
);
}
if (isLoading) {
return (
<div className="w-full min-h-[200px] bg-white rounded-lg shadow flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
);
}

return (
<div className="w-full min-h-[200px] bg-white rounded-lg shadow">
<table className="w-full border-collapse">
<thead>
<tr className="text-left border-b">
<th className="p-4">Transaction Hash</th>
<th className="p-4">Method</th>
<th className="p-4">From</th>
<th className="p-4">Status</th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => (
<tr key={tx.hash} className="border-b hover:bg-gray-50">
<td className="p-4">
<a
href={`https://base-sepolia.blockscout.com/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 font-mono"
>
{tx.hash.slice(0, 10)}...{tx.hash.slice(-8)}
</a>
</td>
<td className="p-4">
<a
href={`https://sepolia.basescan.org/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 font-mono"
>
supply
</a>
</td>
<td className="p-4 font-mono">
{/* {tx.from.hash.slice(0, 6)}...{tx.from.hash.slice(-4)} */}
{address.slice(0, 6)}...{address.slice(-4)}
</td>
<td className="p-4">
<span
className={`px-2 py-1 rounded-full text-sm ${
tx.status === "ok"
? "bg-green-100 text-green-800"
: tx.status === "pending"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}`}
>
{tx.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
return (
<div className="w-full min-h-[200px] bg-white rounded-lg shadow">
<TransactionTable
transactions={transactions}
chainId={baseSepolia.id}
explorer={Explorer.Blockscout}
/>
</div>
);
};

export default TransactionsWidget;
Loading