From 97b245cb4d62d7ee6468680344745a47a9f12525 Mon Sep 17 00:00:00 2001 From: hdser Date: Mon, 28 Jul 2025 07:25:20 +0000 Subject: [PATCH 1/3] use circles_getTokenInfo for mapping wrap token owners --- src/components/FlowMatrixParams.jsx | 224 +++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 4 deletions(-) diff --git a/src/components/FlowMatrixParams.jsx b/src/components/FlowMatrixParams.jsx index 8872e24..5ad20fd 100644 --- a/src/components/FlowMatrixParams.jsx +++ b/src/components/FlowMatrixParams.jsx @@ -2,8 +2,8 @@ import React, { useState, useEffect } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Copy, Check, Code, ChevronDown, ChevronUp } from "lucide-react"; -import { generateFlowMatrixParams } from "@/lib/utils"; import { encodeFunctionData } from "viem"; +import { packCoordinates } from "@/lib/utils"; // Just the operateFlowMatrix function ABI const OPERATE_FLOW_MATRIX_ABI = [ @@ -66,15 +66,218 @@ const OPERATE_FLOW_MATRIX_ABI = [ }, ]; +// API endpoint for RPC calls +const API_ENDPOINT = 'https://rpc.aboutcircles.com/'; + +// Fetch token info to determine if it's wrapped and get the actual owner +async function fetchTokenInfo(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; + } + + console.log(`Token info response for ${tokenAddress}:`, responseData.result); + return responseData.result; + } catch (error) { + console.error(`Error fetching token info for ${tokenAddress}:`, error); + return null; + } +} + +async function generateFlowMatrixParams(pathData, from) { + if (!pathData || !from || !pathData.transfers || pathData.transfers.length === 0) return null; + + try { + // Extract the 'to' address + const to = pathData.transfers.length > 0 + ? pathData.transfers[pathData.transfers.length - 1].to.toLowerCase() + : null; + + // Normalize from address + from = from.toLowerCase(); + + // First, collect all unique token addresses from transfers + const tokenAddresses = [...new Set(pathData.transfers.map(t => t.tokenOwner.toLowerCase()))]; + + // Fetch token info for all tokens to check if they're wrapped + const tokenInfoPromises = tokenAddresses.map(addr => fetchTokenInfo(addr)); + const tokenInfoResults = await Promise.all(tokenInfoPromises); + + // Create a mapping from token to actual owner + const tokenToOwnerMapping = {}; + + tokenAddresses.forEach((tokenAddr, index) => { + const info = tokenInfoResults[index]; + if (info) { + // Check if it's a wrapped token - use tokenType not type + if (info.tokenType === 'CrcV2_ERC20WrapperDeployed_Inflationary' || + info.tokenType === 'CrcV2_ERC20WrapperDeployed_Demurraged') { + tokenToOwnerMapping[tokenAddr] = info.tokenOwner.toLowerCase(); + console.log(`Token ${tokenAddr} is wrapped (${info.tokenType}), actual owner: ${info.tokenOwner}`); + } else { + tokenToOwnerMapping[tokenAddr] = tokenAddr; + console.log(`Token ${tokenAddr} is not wrapped, using same address`); + } + } else { + // If we couldn't fetch info, assume the token is its own owner + tokenToOwnerMapping[tokenAddr] = tokenAddr; + console.warn(`Could not fetch info for token ${tokenAddr}, assuming not wrapped`); + } + }); + + console.log('Final token to owner mapping:', tokenToOwnerMapping); + + // 1. Build the vertices list (unique addresses involved in transfers) + const addressSet = new Set(); + addressSet.add(from); + if (to) addressSet.add(to); + + // Normalize all transfers + const normalizedTransfers = pathData.transfers.map(t => ({ + from: t.from.toLowerCase(), + to: t.to.toLowerCase(), + tokenOwner: t.tokenOwner.toLowerCase(), + value: t.value + })); + + // Add all addresses from transfers (using actual token owners for wrapped tokens) + normalizedTransfers.forEach(transfer => { + addressSet.add(transfer.from); + addressSet.add(transfer.to); + // Add the actual token owner (not the wrapped token 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)); + + 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; + const bHex = b.startsWith('0x') ? b : '0x' + b; + + try { + const bigintA = BigInt(aHex); + const bigintB = BigInt(bHex); + return bigintA < bigintB ? -1 : bigintA > bigintB ? 1 : 0; + } catch (e) { + // Fallback to string comparison if BigInt conversion fails + return a.localeCompare(b); + } + }); + + // 2. Create a lookup map for addresses to indices + const lookup = {}; + flowVertices.forEach((addr, index) => { + lookup[addr] = index; + }); + + // 3. Build flow edges and coordinates + const flowEdges = []; + const coordinates = []; + + normalizedTransfers.forEach(transfer => { + // Mark edges that flow to the destination with streamSinkId=1 + const isToSink = to && transfer.to === to; + + // Add flow edge + flowEdges.push({ + streamSinkId: isToSink ? 1 : 0, + amount: transfer.value + }); + + // Add coordinates (token, from, to) - using actual token owner for wrapped tokens + const actualTokenOwner = tokenToOwnerMapping[transfer.tokenOwner] || transfer.tokenOwner; + coordinates.push( + lookup[actualTokenOwner], + lookup[transfer.from], + lookup[transfer.to] + ); + }); + + // 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); + if (lastIndex !== -1) { + flowEdges[lastIndex].streamSinkId = 1; + } else { + // If not found, set the last edge as terminal by default + flowEdges[flowEdges.length - 1].streamSinkId = 1; + } + } + + // 4. 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 + const stream = { + sourceCoordinate: lookup[from], + flowEdgeIds: flowEdgeIds, + data: "0x" // Empty bytes + }; + + // 6. Pack coordinates + const packedCoordinates = packCoordinates(coordinates); + + // Create the final params object + return { + _flowVertices: flowVertices, + _flow: flowEdges, + _streams: [stream], + _packedCoordinates: packedCoordinates + }; + } catch (error) { + console.error('Error generating operateFlowMatrix params:', error); + return null; + } +} + const FlowMatrixParams = ({ pathData, sender }) => { const [params, setParams] = useState(null); + const [isLoading, setIsLoading] = useState(false); const [copied, setCopied] = useState({ json: false, calldata: false }); const [expanded, setExpanded] = useState(false); useEffect(() => { if (!pathData || !sender) return; - const flowParams = generateFlowMatrixParams(pathData, sender); - setParams(flowParams); + + setIsLoading(true); + generateFlowMatrixParams(pathData, sender) + .then(flowParams => { + setParams(flowParams); + setIsLoading(false); + }) + .catch(err => { + console.error('Error generating params:', err); + setIsLoading(false); + }); }, [pathData, sender]); const copyToClipboard = async (type) => { @@ -118,6 +321,19 @@ const FlowMatrixParams = ({ pathData, sender }) => { }, 2000); }; + if (isLoading) { + return ( + + +
+ +

Generating operateFlowMatrix parameters...

+
+
+
+ ); + } + if (!params) return null; const shortParams = { @@ -186,4 +402,4 @@ const FlowMatrixParams = ({ pathData, sender }) => { ); }; -export default FlowMatrixParams; +export default FlowMatrixParams; \ No newline at end of file From 168dfa5af8fdedc0a095fcaac0e8324f69955c38 Mon Sep 17 00:00:00 2001 From: hdser Date: Wed, 7 Jan 2026 12:59:27 +0000 Subject: [PATCH 2/3] update wrap token calldata --- src/components/FlowMatrixParams.jsx | 257 ++++++---------------------- src/lib/utils.jsx | 89 ++++++++-- src/services/circlesApi.js | 64 ++++++- 3 files changed, 192 insertions(+), 218 deletions(-) diff --git a/src/components/FlowMatrixParams.jsx b/src/components/FlowMatrixParams.jsx index 821a72c..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"; @@ -70,202 +70,10 @@ const OPERATE_FLOW_MATRIX_ABI = [ }, ]; -// API endpoint for RPC calls -const API_ENDPOINT = 'https://rpc.aboutcircles.com/'; - -// Fetch token info to determine if it's wrapped and get the actual owner -async function fetchTokenInfo(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; - } - - console.log(`Token info response for ${tokenAddress}:`, responseData.result); - return responseData.result; - } catch (error) { - console.error(`Error fetching token info for ${tokenAddress}:`, error); - return null; - } -} - -async function generateFlowMatrixParams(pathData, from) { - if (!pathData || !from || !pathData.transfers || pathData.transfers.length === 0) return null; - - try { - // Extract the 'to' address - const to = pathData.transfers.length > 0 - ? pathData.transfers[pathData.transfers.length - 1].to.toLowerCase() - : null; - - // Normalize from address - from = from.toLowerCase(); - - // First, collect all unique token addresses from transfers - const tokenAddresses = [...new Set(pathData.transfers.map(t => t.tokenOwner.toLowerCase()))]; - - // Fetch token info for all tokens to check if they're wrapped - const tokenInfoPromises = tokenAddresses.map(addr => fetchTokenInfo(addr)); - const tokenInfoResults = await Promise.all(tokenInfoPromises); - - // Create a mapping from token to actual owner - const tokenToOwnerMapping = {}; - - tokenAddresses.forEach((tokenAddr, index) => { - const info = tokenInfoResults[index]; - if (info) { - // Check if it's a wrapped token - use tokenType not type - if (info.tokenType === 'CrcV2_ERC20WrapperDeployed_Inflationary' || - info.tokenType === 'CrcV2_ERC20WrapperDeployed_Demurraged') { - tokenToOwnerMapping[tokenAddr] = info.tokenOwner.toLowerCase(); - console.log(`Token ${tokenAddr} is wrapped (${info.tokenType}), actual owner: ${info.tokenOwner}`); - } else { - tokenToOwnerMapping[tokenAddr] = tokenAddr; - console.log(`Token ${tokenAddr} is not wrapped, using same address`); - } - } else { - // If we couldn't fetch info, assume the token is its own owner - tokenToOwnerMapping[tokenAddr] = tokenAddr; - console.warn(`Could not fetch info for token ${tokenAddr}, assuming not wrapped`); - } - }); - - console.log('Final token to owner mapping:', tokenToOwnerMapping); - - // 1. Build the vertices list (unique addresses involved in transfers) - const addressSet = new Set(); - addressSet.add(from); - if (to) addressSet.add(to); - - // Normalize all transfers - const normalizedTransfers = pathData.transfers.map(t => ({ - from: t.from.toLowerCase(), - to: t.to.toLowerCase(), - tokenOwner: t.tokenOwner.toLowerCase(), - value: t.value - })); - - // Add all addresses from transfers (using actual token owners for wrapped tokens) - normalizedTransfers.forEach(transfer => { - addressSet.add(transfer.from); - addressSet.add(transfer.to); - // Add the actual token owner (not the wrapped token 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)); - - 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; - const bHex = b.startsWith('0x') ? b : '0x' + b; - - try { - const bigintA = BigInt(aHex); - const bigintB = BigInt(bHex); - return bigintA < bigintB ? -1 : bigintA > bigintB ? 1 : 0; - } catch (e) { - // Fallback to string comparison if BigInt conversion fails - return a.localeCompare(b); - } - }); - - // 2. Create a lookup map for addresses to indices - const lookup = {}; - flowVertices.forEach((addr, index) => { - lookup[addr] = index; - }); - - // 3. Build flow edges and coordinates - const flowEdges = []; - const coordinates = []; - - normalizedTransfers.forEach(transfer => { - // Mark edges that flow to the destination with streamSinkId=1 - const isToSink = to && transfer.to === to; - - // Add flow edge - flowEdges.push({ - streamSinkId: isToSink ? 1 : 0, - amount: transfer.value - }); - - // Add coordinates (token, from, to) - using actual token owner for wrapped tokens - const actualTokenOwner = tokenToOwnerMapping[transfer.tokenOwner] || transfer.tokenOwner; - coordinates.push( - lookup[actualTokenOwner], - lookup[transfer.from], - lookup[transfer.to] - ); - }); - - // 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); - if (lastIndex !== -1) { - flowEdges[lastIndex].streamSinkId = 1; - } else { - // If not found, set the last edge as terminal by default - flowEdges[flowEdges.length - 1].streamSinkId = 1; - } - } - - // 4. 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 - const stream = { - sourceCoordinate: lookup[from], - flowEdgeIds: flowEdgeIds, - data: "0x" // Empty bytes - }; - - // 6. Pack coordinates - const packedCoordinates = packCoordinates(coordinates); - - // Create the final params object - return { - _flowVertices: flowVertices, - _flow: flowEdges, - _streams: [stream], - _packedCoordinates: packedCoordinates - }; - } catch (error) { - console.error('Error generating operateFlowMatrix params:', error); - return null; - } -} - const FlowMatrixParams = ({ pathData, sender }) => { const [params, setParams] = useState(null); - const [isLoading, setIsLoading] = useState(false); + 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); @@ -279,18 +87,29 @@ const FlowMatrixParams = ({ pathData, sender }) => { hash, }); + // Generate flow matrix params (now async to handle wrapped tokens) useEffect(() => { - if (!pathData || !sender) return; - - setIsLoading(true); + if (!pathData || !sender) { + setParams(null); + return; + } + + setIsGeneratingParams(true); + setParamsError(null); + generateFlowMatrixParams(pathData, sender) .then(flowParams => { setParams(flowParams); - setIsLoading(false); + setIsGeneratingParams(false); + if (!flowParams) { + setParamsError('Failed to generate parameters'); + } }) .catch(err => { console.error('Error generating params:', err); - setIsLoading(false); + setParamsError(err.message || 'Failed to generate parameters'); + setParams(null); + setIsGeneratingParams(false); }); }, [pathData, sender]); @@ -412,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 = { @@ -569,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..202aea8 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); + // CRITICAL: 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) + // CRITICAL: 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 From 15311584934e774f592e753b95ac4fec9d6310a9 Mon Sep 17 00:00:00 2001 From: hdser Date: Wed, 7 Jan 2026 13:00:37 +0000 Subject: [PATCH 3/3] cleanup --- src/lib/utils.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/utils.jsx b/src/lib/utils.jsx index 202aea8..360e0a8 100644 --- a/src/lib/utils.jsx +++ b/src/lib/utils.jsx @@ -119,7 +119,7 @@ export async function generateFlowMatrixParams(pathData, from) { normalizedTransfers.forEach(transfer => { addressSet.add(transfer.from); addressSet.add(transfer.to); - // CRITICAL: Use the actual token owner (not wrapper address) + // Use the actual token owner (not wrapper address) const actualOwner = tokenToOwnerMapping[transfer.tokenOwner] || transfer.tokenOwner; addressSet.add(actualOwner); }); @@ -163,7 +163,7 @@ export async function generateFlowMatrixParams(pathData, from) { }); // Add coordinates (token, from, to) - // CRITICAL: Use ACTUAL token owner for coordinates (not wrapper address) + // Use ACTUAL token owner for coordinates (not wrapper address) const actualTokenOwner = tokenToOwnerMapping[transfer.tokenOwner] || transfer.tokenOwner; coordinates.push( lookup[actualTokenOwner],