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
68 changes: 61 additions & 7 deletions src/components/FlowMatrixParams.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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(() => {
Expand Down Expand Up @@ -209,6 +231,38 @@ const FlowMatrixParams = ({ pathData, sender }) => {
}
};

// Show loading state while generating params
if (isGeneratingParams) {
return (
<Card className="mt-4">
<CardContent className="pt-4">
<div className="flex items-center gap-2">
<Loader2 size={18} className="text-blue-500 animate-spin" />
<span className="text-gray-600">Generating operateFlowMatrix parameters...</span>
</div>
<p className="text-xs text-gray-500 mt-2">
Resolving token info for wrapped tokens...
</p>
</CardContent>
</Card>
);
}

// Show error state
if (paramsError) {
return (
<Card className="mt-4">
<CardContent className="pt-4">
<div className="flex items-center gap-2 text-red-600">
<Code size={18} />
<span>Error generating parameters</span>
</div>
<p className="text-sm text-red-500 mt-2">{paramsError}</p>
</CardContent>
</Card>
);
}

if (!params) return null;

const shortParams = {
Expand Down Expand Up @@ -366,11 +420,11 @@ const FlowMatrixParams = ({ pathData, sender }) => {
</div>
<p className="text-xs text-gray-500 mt-4 text-center">
{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'}
</p>
</div>
Expand Down
89 changes: 75 additions & 14 deletions src/lib/utils.jsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Object|null>} - 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
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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 = [];

Expand All @@ -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);
Expand All @@ -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
Expand Down
64 changes: 63 additions & 1 deletion src/services/circlesApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,66 @@ export const fetchProfiles = async (circlesProfiles, addresses, useCache = true)
}
};

export const fetchTokenBalances = fetchTokenBalancesWithInfo;
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<Object|null>} - 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<Object>} - 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;
};