diff --git a/src/components/FlowMatrixParams.jsx b/src/components/FlowMatrixParams.jsx
index 5a312b2..e098058 100644
--- a/src/components/FlowMatrixParams.jsx
+++ b/src/components/FlowMatrixParams.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
-import { Copy, Check, Code, ChevronDown, ChevronUp, Wallet, Send, X } from "lucide-react";
+import { Copy, Check, Code, ChevronDown, ChevronUp, Wallet, Send, X, Loader2 } from "lucide-react";
import { generateFlowMatrixParams } from "@/lib/utils";
import { encodeFunctionData } from "viem";
import { useAccount, useConnect, useDisconnect, useSwitchChain, useWriteContract, useWaitForTransactionReceipt } from "wagmi";
@@ -72,6 +72,8 @@ const OPERATE_FLOW_MATRIX_ABI = [
const FlowMatrixParams = ({ pathData, sender }) => {
const [params, setParams] = useState(null);
+ const [isGeneratingParams, setIsGeneratingParams] = useState(false);
+ const [paramsError, setParamsError] = useState(null);
const [copied, setCopied] = useState({ json: false, calldata: false });
const [expanded, setExpanded] = useState(false);
const [showWalletModal, setShowWalletModal] = useState(false);
@@ -85,10 +87,30 @@ const FlowMatrixParams = ({ pathData, sender }) => {
hash,
});
+ // Generate flow matrix params (now async to handle wrapped tokens)
useEffect(() => {
- if (!pathData || !sender) return;
- const flowParams = generateFlowMatrixParams(pathData, sender);
- setParams(flowParams);
+ if (!pathData || !sender) {
+ setParams(null);
+ return;
+ }
+
+ setIsGeneratingParams(true);
+ setParamsError(null);
+
+ generateFlowMatrixParams(pathData, sender)
+ .then(flowParams => {
+ setParams(flowParams);
+ setIsGeneratingParams(false);
+ if (!flowParams) {
+ setParamsError('Failed to generate parameters');
+ }
+ })
+ .catch(err => {
+ console.error('Error generating params:', err);
+ setParamsError(err.message || 'Failed to generate parameters');
+ setParams(null);
+ setIsGeneratingParams(false);
+ });
}, [pathData, sender]);
useEffect(() => {
@@ -209,6 +231,38 @@ const FlowMatrixParams = ({ pathData, sender }) => {
}
};
+ // Show loading state while generating params
+ if (isGeneratingParams) {
+ return (
+
+
+
+
+ Generating operateFlowMatrix parameters...
+
+
+ Resolving token info for wrapped tokens...
+
+
+
+ );
+ }
+
+ // Show error state
+ if (paramsError) {
+ return (
+
+
+
+
+ Error generating parameters
+
+ {paramsError}
+
+
+ );
+ }
+
if (!params) return null;
const shortParams = {
@@ -366,11 +420,11 @@ const FlowMatrixParams = ({ pathData, sender }) => {
{window?.rabby
- ? '✓ Rabby Wallet is installed and ready'
+ ? '✔ Rabby Wallet is installed and ready'
: window?.ethereum?.isMetaMask
- ? '✓ MetaMask is installed and ready'
+ ? '✔ MetaMask is installed and ready'
: window?.ethereum
- ? '✓ Wallet detected'
+ ? '✔ Wallet detected'
: 'Please install a Web3 wallet to continue'}
diff --git a/src/lib/utils.jsx b/src/lib/utils.jsx
index a67922c..360e0a8 100644
--- a/src/lib/utils.jsx
+++ b/src/lib/utils.jsx
@@ -1,5 +1,6 @@
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
+import { fetchTokenInfoByAddress } from '@/services/circlesApi';
/**
* Combines multiple class names using clsx and tailwind-merge
@@ -31,12 +32,25 @@ export function packCoordinates(coordinates) {
/**
* Helper to generate all parameters for operateFlowMatrix
* Based on the TypeScript implementation from the SDK
+ *
+ * IMPORTANT: This function handles wrapped tokens properly by resolving
+ * the actual token owner address instead of using the wrapper contract address.
+ *
+ * For wrapped tokens:
+ * - transfer.tokenOwner = wrapper contract address (e.g., 0xWrapperContract)
+ * - actual token owner = the avatar who minted the original token
+ *
+ * The operateFlowMatrix function on the Hub contract expects the actual
+ * token owner in the packed coordinates, NOT the wrapper address.
+ *
* @param {Object} pathData - The path data from API
* @param {string} from - Source address
- * @returns {Object} - Parameters object for operateFlowMatrix
+ * @returns {Promise} - Parameters object for operateFlowMatrix
*/
-export function generateFlowMatrixParams(pathData, from) {
- if (!pathData || !from || !pathData.transfers || pathData.transfers.length === 0) return null;
+export async function generateFlowMatrixParams(pathData, from) {
+ if (!pathData || !from || !pathData.transfers || pathData.transfers.length === 0) {
+ return null;
+ }
try {
// Extract the 'to' address
@@ -47,7 +61,48 @@ export function generateFlowMatrixParams(pathData, from) {
// Normalize from address
from = from.toLowerCase();
- // 1. Build the vertices list (unique addresses involved in transfers)
+ // 1. Collect all unique token addresses from transfers
+ const tokenAddresses = [...new Set(
+ pathData.transfers.map(t => t.tokenOwner.toLowerCase())
+ )];
+
+ // 2. Fetch token info for all tokens to check if they're wrapped
+ // This is the critical step that resolves wrapped token addresses to actual owners
+ console.log('Fetching token info for:', tokenAddresses);
+
+ const tokenToOwnerMapping = {};
+
+ for (const tokenAddr of tokenAddresses) {
+ const info = await fetchTokenInfoByAddress(tokenAddr);
+
+ if (info) {
+ // Check if it's a wrapped token based on tokenType
+ // Wrapped tokens have these types:
+ // - CrcV2_ERC20WrapperDeployed_Inflationary
+ // - CrcV2_ERC20WrapperDeployed_Demurraged
+ const isWrapped =
+ info.tokenType === 'CrcV2_ERC20WrapperDeployed_Inflationary' ||
+ info.tokenType === 'CrcV2_ERC20WrapperDeployed_Demurraged';
+
+ if (isWrapped && info.tokenOwner) {
+ // Use the actual token owner (the avatar who minted the original token)
+ tokenToOwnerMapping[tokenAddr] = info.tokenOwner.toLowerCase();
+ console.log(`Token ${tokenAddr} is wrapped (${info.tokenType}), actual owner: ${info.tokenOwner}`);
+ } else {
+ // Not wrapped - the token address IS the owner
+ tokenToOwnerMapping[tokenAddr] = tokenAddr;
+ console.log(`Token ${tokenAddr} is not wrapped, using same address`);
+ }
+ } else {
+ // Fallback: assume token is its own owner if we can't fetch info
+ tokenToOwnerMapping[tokenAddr] = tokenAddr;
+ console.warn(`Could not fetch info for token ${tokenAddr}, assuming not wrapped`);
+ }
+ }
+
+ console.log('Final token to owner mapping:', tokenToOwnerMapping);
+
+ // 3. Build the vertices list (unique addresses involved in transfers)
const addressSet = new Set();
addressSet.add(from);
if (to) addressSet.add(to);
@@ -60,14 +115,18 @@ export function generateFlowMatrixParams(pathData, from) {
value: t.value
}));
- // Add all addresses from transfers
+ // Add all addresses from transfers (using actual token owners for wrapped tokens)
normalizedTransfers.forEach(transfer => {
addressSet.add(transfer.from);
addressSet.add(transfer.to);
- addressSet.add(transfer.tokenOwner);
+ // Use the actual token owner (not wrapper address)
+ const actualOwner = tokenToOwnerMapping[transfer.tokenOwner] || transfer.tokenOwner;
+ addressSet.add(actualOwner);
});
- // Convert to sorted array (using BigInt sorting like in the TypeScript implementation)
+ console.log('Address set before sorting:', Array.from(addressSet));
+
+ // 4. Convert to sorted array (using BigInt sorting like in the TypeScript implementation)
const flowVertices = Array.from(addressSet).sort((a, b) => {
// Add '0x' prefix if not present to avoid conversion errors
const aHex = a.startsWith('0x') ? a : '0x' + a;
@@ -83,13 +142,13 @@ export function generateFlowMatrixParams(pathData, from) {
}
});
- // 2. Create a lookup map for addresses to indices
+ // 5. Create a lookup map for addresses to indices
const lookup = {};
flowVertices.forEach((addr, index) => {
lookup[addr] = index;
});
- // 3. Build flow edges and coordinates
+ // 6. Build flow edges and coordinates
const flowEdges = [];
const coordinates = [];
@@ -104,14 +163,16 @@ export function generateFlowMatrixParams(pathData, from) {
});
// Add coordinates (token, from, to)
+ // Use ACTUAL token owner for coordinates (not wrapper address)
+ const actualTokenOwner = tokenToOwnerMapping[transfer.tokenOwner] || transfer.tokenOwner;
coordinates.push(
- lookup[transfer.tokenOwner],
+ lookup[actualTokenOwner],
lookup[transfer.from],
lookup[transfer.to]
);
});
- // Ensure at least one terminal edge is marked, as in the TypeScript code
+ // 7. Ensure at least one terminal edge is marked, as in the TypeScript code
if (!flowEdges.some(edge => edge.streamSinkId === 1) && flowEdges.length > 0) {
// Find the last edge where transfer.to matches the 'to' address
const lastIndex = normalizedTransfers.map(t => t.to).lastIndexOf(to);
@@ -123,19 +184,19 @@ export function generateFlowMatrixParams(pathData, from) {
}
}
- // 4. Create flowEdgeIds array (indices of edges with streamSinkId = 1)
+ // 8. Create flowEdgeIds array (indices of edges with streamSinkId = 1)
const flowEdgeIds = flowEdges
.map((edge, index) => edge.streamSinkId === 1 ? index : -1)
.filter(index => index !== -1);
- // 5. Create stream object
+ // 9. Create stream object
const stream = {
sourceCoordinate: lookup[from],
flowEdgeIds: flowEdgeIds,
data: "0x" // Empty bytes
};
- // 6. Pack coordinates
+ // 10. Pack coordinates
const packedCoordinates = packCoordinates(coordinates);
// Create the final params object
diff --git a/src/services/circlesApi.js b/src/services/circlesApi.js
index 2f4a9b0..2e6c092 100644
--- a/src/services/circlesApi.js
+++ b/src/services/circlesApi.js
@@ -370,4 +370,66 @@ export const fetchProfiles = async (circlesProfiles, addresses, useCache = true)
}
};
-export const fetchTokenBalances = fetchTokenBalancesWithInfo;
\ No newline at end of file
+export const fetchTokenBalances = fetchTokenBalancesWithInfo;
+
+/**
+ * Fetch detailed token info by address using circles_getTokenInfo RPC method
+ * This is used to determine if a token is wrapped and get the actual token owner
+ * @param {string} tokenAddress - The token address to fetch info for
+ * @returns {Promise} - Token info object or null if failed
+ */
+export const fetchTokenInfoByAddress = async (tokenAddress) => {
+ try {
+ const requestBody = {
+ jsonrpc: "2.0",
+ id: Date.now(),
+ method: "circles_getTokenInfo",
+ params: [tokenAddress.toLowerCase()]
+ };
+
+ const response = await fetch(API_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(requestBody)
+ });
+
+ if (!response.ok) {
+ console.error(`Failed to fetch token info for ${tokenAddress}`);
+ return null;
+ }
+
+ const responseData = await response.json();
+
+ if (responseData.error) {
+ console.error(`RPC error for token ${tokenAddress}:`, responseData.error);
+ return null;
+ }
+
+ return responseData.result;
+ } catch (error) {
+ console.error(`Error fetching token info for ${tokenAddress}:`, error);
+ return null;
+ }
+};
+
+/**
+ * Batch fetch token info for multiple tokens
+ * More efficient than individual calls when dealing with many tokens
+ * @param {string[]} tokenAddresses - Array of token addresses
+ * @returns {Promise} - Map of token address to token info
+ */
+export const fetchTokenInfoBatch = async (tokenAddresses) => {
+ const uniqueTokens = [...new Set(tokenAddresses.map(a => a.toLowerCase()))];
+ const results = {};
+
+ // Use Promise.all for parallel fetching
+ const promises = uniqueTokens.map(async (addr) => {
+ const info = await fetchTokenInfoByAddress(addr);
+ results[addr] = info;
+ });
+
+ await Promise.all(promises);
+ return results;
+};
\ No newline at end of file