diff --git a/pages/test2.tsx b/pages/test2.tsx index c9256c9843..2ada14f76b 100644 --- a/pages/test2.tsx +++ b/pages/test2.tsx @@ -1,137 +1,502 @@ -import { useState } from 'react'; -import { useQueries } from '@tanstack/react-query'; -import { useAccount } from 'wagmi'; -import { - PublicKey, - LAMPORTS_PER_SOL, - Transaction, - SystemProgram, -} from '@solana/web3.js'; -import BigNumber from 'bignumber.js'; -import { useConnection, useWallet } from '@solana/wallet-adapter-react'; -import FailedDonation, { - EDonationFailedType, -} from '@/components/modals/FailedDonation'; -import { getTotalGIVpower } from '@/helpers/givpower'; -import { formatWeiHelper } from '@/helpers/number'; -import config from '@/configuration'; -import { fetchSubgraphData } from '@/services/subgraph.service'; +import React, { useEffect, useState } from 'react'; +import { ethers } from 'ethers'; +import { Framework, Operation } from '@superfluid-finance/sdk-core'; + +const GIVETH_HOUSE_ADDRESS = '0x567c4B141ED61923967cA25Ef4906C8781069a10'; + +const TOKEN_ADDRESSES = [ + { + name: 'USDCex', + address: '0x8430f084b939208e2eded1584889c9a66b90562f', + decimals: 18, + }, + { + name: 'USDC bridged', + address: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + decimals: 6, + }, + { + name: 'USDCx', + address: '0x35adeb0638eb192755b6e52544650603fe65a006', + decimals: 18, + }, + { + name: 'USDC native', + address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + decimals: 6, + }, +]; + +const OPTIMISM_CHAIN_ID = 10; const YourApp = () => { - const [failedModalType, setFailedModalType] = - useState(); - const { address } = useAccount(); - const subgraphValues = useQueries({ - queries: config.CHAINS_WITH_SUBGRAPH.map(chain => ({ - queryKey: ['subgraph', chain.id, address], - queryFn: async () => { - return await fetchSubgraphData(chain.id, address); - }, - staleTime: config.SUBGRAPH_POLLING_INTERVAL, - })), - }); - - // Solana wallet hooks - const { - publicKey, - disconnect: solanaWalletDisconnect, - signMessage: solanaSignMessage, - sendTransaction: solanaSendTransaction, - connecting: solanaIsConnecting, - connected: solanaIsConnected, - } = useWallet(); - - const { connection: solanaConnection } = useConnection(); - - const donateToSolana = async () => { - if (!publicKey) { - console.error('Wallet is not connected'); - return; - } + const [tokens, setTokens] = useState< + { name: string; address: string; balance: string }[] + >([]); + const [selectedToken, setSelectedToken] = useState(''); + const [destinationAddress, setDestinationAddress] = + useState(GIVETH_HOUSE_ADDRESS); + const [loading, setLoading] = useState(true); + const [amount, setAmount] = useState('0.001'); // Initial amount + const [notification, setNotification] = useState(''); - console.log('Connection endpoint:', solanaConnection.rpcEndpoint); + const determineTokenType = async ( + sf: { + loadNativeAssetSuperToken: (arg0: any) => any; + loadWrapperSuperToken: (arg0: any) => any; + }, + tokenAddress: any, + ) => { + let superToken; - const to = 'B6bfJUMPnpL2ddngPPe3M7QNpvrv7hiYYiGtg9iCJDMS'; - const donationValue = 0.001; + // Check if it's a native super token + try { + superToken = await sf.loadNativeAssetSuperToken(tokenAddress); + console.log('Native Super Token detected.'); + return { type: 'native', superToken }; + } catch (error) { + console.log('Not a Native Super Token.'); + } + + // Check if it's a wrapper super token + try { + superToken = await sf.loadWrapperSuperToken(tokenAddress); + console.log('Wrapper Super Token detected.'); + return { type: 'wrapper', superToken }; + } catch (error) { + console.log('Not a Wrapper Super Token.'); + } - console.log('publicKey', publicKey); - console.log('Public Key string:', publicKey.toString()); + // If both checks fail, it's a regular ERC-20 token + console.log('Regular ERC-20 token detected.'); + return { type: 'erc20', superToken: null }; + }; - // Ensure the wallet has enough funds by requesting an airdrop if necessary - let balance = await solanaConnection.getBalance(publicKey); - console.log('Initial balance:', balance); - if (balance < LAMPORTS_PER_SOL) { - console.log('Airdropping 1 SOL for testing...'); - const airdropSignature = await solanaConnection.requestAirdrop( - publicKey, - LAMPORTS_PER_SOL, + // Function to check if a flow exists + const checkIfFlowExists = async ( + sf: { + cfaV1: { + getFlow: (arg0: { + superToken: any; + sender: any; + receiver: any; + providerOrSigner: any; + }) => any; + }; + }, + superTokenAddress: any, + senderAddress: any, + receiverAddress: any, + signer: any, + ) => { + try { + const flowInfo = await sf.cfaV1.getFlow({ + superToken: superTokenAddress, + sender: senderAddress, + receiver: receiverAddress, + providerOrSigner: signer, + }); + console.log( + `Existing flow found. Current flow rate: ${flowInfo.flowRate}`, ); - await solanaConnection.confirmTransaction(airdropSignature); - balance = await solanaConnection.getBalance(publicKey); - console.log('New balance:', balance); + return { exists: true, flowRate: flowInfo.flowRate }; + } catch (error) { + console.log('No existing flow found.'); + return { exists: false, flowRate: '0' }; } + }; - const lamports = new BigNumber(donationValue) - .times(LAMPORTS_PER_SOL) - .toFixed(); + const handleApproveAndExecute = async () => { + try { + // Connect to MetaMask + if (!window.ethereum) { + alert('MetaMask not detected'); + return; + } - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: publicKey!, - toPubkey: new PublicKey(to), - lamports: BigInt(lamports), - }), - ); + const provider = new ethers.providers.Web3Provider(window.ethereum); + await provider.send('eth_requestAccounts', []); + const signer = provider.getSigner(); + const sf = await Framework.create({ + chainId: OPTIMISM_CHAIN_ID, + provider, + }); - console.log('Transaction', transaction); + console.log({ sf }); - console.log( - 'Fee Payer:', - transaction.feePayer ? transaction.feePayer.toBase58() : 'None', - ); + const address = await signer.getAddress(); - transaction.feePayer = publicKey; + // Get token details (decimals, etc.) + const token = TOKEN_ADDRESSES.find( + t => t.address === selectedToken, + ); + if (!token) { + alert('Invalid token selected.'); + return; + } + const tokenDecimals = token.decimals; - const simulationResult = - await solanaConnection.simulateTransaction(transaction); - console.log('Simulation Result:', simulationResult); + // Define the amount to approve (X.XX USDCx) + const amountToApprove = ethers.utils.parseUnits( + amount, + tokenDecimals, + ); - if (simulationResult.value.err) { - console.error('Simulation error:', simulationResult.value.err); - return; - } + const flowRatePerSecond = amountToApprove.div(30 * 24 * 60 * 60); // Convert monthly to per-second rate + + // Determine the token type + const { type, superToken } = await determineTokenType( + sf, + selectedToken, + ); + + console.log('Selected token:', selectedToken); + console.log('Super type:', type); + console.log('Super Token:', superToken); + + if (type === 'native' || type === 'wrapper') { + console.log( + `${type.charAt(0).toUpperCase() + type.slice(1)} Super Token detected`, + ); + + // Attempt to check allowance (skip if it fails) + let allowance; + try { + allowance = await superToken.allowance({ + owner: await signer.getAddress(), + spender: destinationAddress, + providerOrSigner: signer, + }); + console.log(`Current allowance: ${allowance.toString()}`); + } catch (error) { + console.log( + 'Allowance does not exist or cannot be fetched. Proceeding to approve...', + ); + } + + // Approve if needed + if (ethers.BigNumber.from(allowance).lt(amountToApprove)) { + const approveOperation = superToken.approve({ + receiver: destinationAddress, + amount: amountToApprove.toString(), + }); + + const approveTxResponse = await signer.sendTransaction( + await approveOperation.getPopulatedTransactionRequest( + signer, + ), + ); + await approveTxResponse.wait(); + console.log(`Approved ${amount} ${type} super tokens.`); + } + + // Check for existing flow + const flowStatus = await checkIfFlowExists( + sf, + superToken.address, + address, + destinationAddress, + signer, + ); + + if (flowStatus.exists && flowStatus.flowRate !== '0') { + // Add new flow rate to existing flow rate + const newFlowRate = ethers.BigNumber.from( + flowStatus.flowRate, + ).add(flowRatePerSecond); + + // Update the flow + const updateFlowOperation = superToken.updateFlow({ + sender: address, + receiver: destinationAddress, + flowRate: newFlowRate.toString(), // New total flow rate + }); + + console.log('Updating existing flow...'); + const updateFlowTxResponse = await updateFlowOperation.exec( + signer, + 70, + ); + await updateFlowTxResponse.wait(); + console.log('Flow updated successfully.'); + setNotification('Flow updated successfully!'); + } else { + // Create a new flow if none exists + const createFlowOperation = superToken.createFlow({ + sender: address, + receiver: destinationAddress, + flowRate: flowRatePerSecond.toString(), // New flow rate + }); + + console.log('Creating new flow...'); + const createFlowTxResponse = await createFlowOperation.exec( + signer, + 2, + ); + await createFlowTxResponse.wait(); + console.log('Flow created successfully.'); + setNotification('Flow created successfully!'); + } + } else if (type === 'erc20') { + console.log('Approving underlying ERC-20 token for upgrade...'); + + const operations: Operation[] = []; + + const wrapperSuperToken = + await sf.loadSuperToken(selectedToken); + + const erc20Contract = new ethers.Contract( + selectedToken, // Underlying ERC-20 token address + [ + 'function approve(address spender, uint256 amount) public returns (bool)', + ], + signer, + ); + + // Approve the Super Token contract to spend the ERC-20 token + const approveTx = await erc20Contract.approve( + wrapperSuperToken.address, // Address of the Super Token + amountToApprove.toString(), + ); + await approveTx.wait(); + console.log('Underlying ERC-20 token approved for upgrade.'); + + // Interact with the Super Token contract to perform the upgrade + // const superTokenAbi = [ + // 'function upgrade(uint256 amount) external', + // ]; + // const superTokenContract = new ethers.Contract( + // wrapperSuperToken.address, + // superTokenAbi, + // signer, + // ); + + console.log('Upgrading ERC-20 token to Super Token...'); + const newSuperToken = await sf.loadWrapperSuperToken( + '0x35adeb0638eb192755b6e52544650603fe65a006', + ); + + console.log({ newSuperToken }); - const hash = await solanaSendTransaction(transaction, solanaConnection); + const upgradeTx = await newSuperToken.upgrade({ + amount: amountToApprove.toString(), + }); + // await upgradeTx.wait(); + // await upgradeTx.exec(signer); + operations.push(upgradeTx); + console.log('Upgrade to Super Token complete.', upgradeTx); - console.log('hash', hash); + // Attempt to check allowance (skip if it fails) + let allowance; + try { + allowance = await newSuperToken.allowance({ + owner: await signer.getAddress(), + spender: destinationAddress, + providerOrSigner: signer, + }); + console.log(`Current allowance: ${allowance.toString()}`); + } catch (error) { + console.log( + 'Allowance does not exist or cannot be fetched. Proceeding to approve...', + ); + } + + // Approve if needed + if (ethers.BigNumber.from(allowance).lt(amountToApprove)) { + const approveOperation = newSuperToken.approve({ + receiver: destinationAddress, + amount: amountToApprove.toString(), + }); + + const approveTxResponse = await signer.sendTransaction( + await approveOperation.getPopulatedTransactionRequest( + signer, + ), + ); + await approveTxResponse.wait(); + console.log(`Approved ${amount} ${type} super tokens.`); + } + + // Create or update the stream + const flowStatus = await checkIfFlowExists( + sf, + newSuperToken.address, + address, + destinationAddress, + signer, + ); + + if (flowStatus.exists && flowStatus.flowRate !== '0') { + console.log('Updating existing flow...'); + const updateFlowOperation = newSuperToken.updateFlow({ + sender: address, + receiver: destinationAddress, + flowRate: flowRatePerSecond.toString(), // New flow rate + }); + // const flowTxResponse = + // await updateFlowOperation.exec(signer); + // await flowTxResponse.wait(); + + operations.push(updateFlowOperation); + + const batchOp = sf.batchCall(operations); + const txUpdate = await batchOp.exec(signer, 70); + + console.log('Flow updated successfully.', txUpdate); + setNotification('Flow updated successfully!'); + } else { + console.log('Creating new flow...'); + const createFlowOperation = newSuperToken.createFlow({ + sender: address, + receiver: destinationAddress, + flowRate: flowRatePerSecond.toString(), // New flow rate + }); + // const flowTxResponse = await createFlowOperation.exec( + // signer, + // 2, + // ); + // await flowTxResponse.wait(); + operations.push(createFlowOperation); + + const batchOp = sf.batchCall(operations); + const txCreate = await batchOp.exec(signer, 70); + + console.log('Flow created successfully.', txCreate); + setNotification('Flow created successfully!'); + } + } + } catch (error) { + console.error('Error during approval or execution:', error); + setNotification( + 'Transaction failed! Please check the console for details.', + ); + } }; + useEffect(() => { + const fetchTokens = async () => { + try { + if (!window.ethereum) { + alert('MetaMask not detected'); + return; + } + + const provider = new ethers.providers.Web3Provider( + window.ethereum, + ); + await provider.send('eth_requestAccounts', []); + const signer = provider.getSigner(); + const address = await signer.getAddress(); + + const balances = await Promise.all( + TOKEN_ADDRESSES.map(async token => { + const contract = new ethers.Contract( + token.address, + [ + // ERC20 ABI for balanceOf and decimals + 'function balanceOf(address owner) view returns (uint256)', + 'function decimals() view returns (uint8)', + ], + provider, + ); + + const balance = await contract.balanceOf(address); + const decimals = await contract.decimals(); + + return { + name: token.name, + address: token.address, + balance: ethers.utils.formatUnits( + balance, + decimals, + ), + }; + }), + ); + + setTokens(balances); + setSelectedToken(balances[0]?.address || ''); + setLoading(false); + } catch (error) { + console.error('Error fetching tokens:', error); + alert('An error occurred while fetching tokens.'); + } + }; + + fetchTokens(); + }, []); + return ( -
- - +
+

Approve and Execute {amount} USDC

+

+ Network ID: {OPTIMISM_CHAIN_ID} Optimism Network +

+

+ Transaction to wallet: {destinationAddress} + Giveth House +

- +

Select a Token

+ {loading ? ( +

Loading tokens...

+ ) : ( + + )}
- {failedModalType && ( - setFailedModalType(undefined)} - type={failedModalType} +
+
+

Enter Amount

+ setAmount(e.target.value)} + style={{ padding: '10px', width: '200px' }} + /> +
+
+
+

Destination Address

+ setDestinationAddress(e.target.value)} + style={{ padding: '10px', width: '500px' }} /> +
+
+ + {notification && ( +
+ {notification} +
)}
); diff --git a/src/components/views/donate/Recurring/RecurringDonationCard.tsx b/src/components/views/donate/Recurring/RecurringDonationCard.tsx index d40b596581..b669daa804 100644 --- a/src/components/views/donate/Recurring/RecurringDonationCard.tsx +++ b/src/components/views/donate/Recurring/RecurringDonationCard.tsx @@ -146,8 +146,12 @@ export const RecurringDonationCard = () => { const underlyingToken = selectedRecurringToken?.token.underlyingToken; + // Introduce a scaling factor to handle tokens with different decimals + const scaleFactor = + selectedRecurringToken?.token.decimals === 6 ? 10000n : 1n; + // total means project + giveth - const totalPerSec = perMonthAmount / ONE_MONTH_SECONDS; + const totalPerSec = perMonthAmount / (ONE_MONTH_SECONDS / scaleFactor); const projectPerMonth = (perMonthAmount * BigInt(100 - donationToGiveth)) / 100n; const givethPerMonth = perMonthAmount - projectPerMonth; @@ -168,7 +172,8 @@ export const RecurringDonationCard = () => { 0n, ) || 0n; const totalStreamPerSec = totalPerSec + otherStreamsPerSec; - const totalStreamPerMonth = totalStreamPerSec * ONE_MONTH_SECONDS; + const totalStreamPerMonth = + totalStreamPerSec * (ONE_MONTH_SECONDS / scaleFactor); const streamRunOutInMonth = totalStreamPerSec > 0 ? amount / totalStreamPerMonth : 0n; const isTotalStreamExceed = @@ -238,6 +243,14 @@ export const RecurringDonationCard = () => { ? Number((perMonthAmount * 1000n) / amount) / 10 : 0; + // Reset the input value when the token is changed + useEffect(() => { + if (selectedRecurringToken || anchorContractAddress) { + setAmount(0n); + setPerMonthAmount(0n); + } + }, [anchorContractAddress, selectedRecurringToken]); + return ( <> diff --git a/src/components/views/donate/Recurring/RecurringDonationModal/Item.tsx b/src/components/views/donate/Recurring/RecurringDonationModal/Item.tsx index 2366a6de13..44d2ccf395 100644 --- a/src/components/views/donate/Recurring/RecurringDonationModal/Item.tsx +++ b/src/components/views/donate/Recurring/RecurringDonationModal/Item.tsx @@ -29,10 +29,7 @@ export const Item: FC<IItemProps> = ({ <Flex gap='4px'> <B> {limitFraction( - formatUnits( - amount, - token.underlyingToken?.decimals || 18, - ), + formatUnits(amount, token?.decimals || 18), )}  {token.symbol} </B> @@ -47,7 +44,7 @@ export const Item: FC<IItemProps> = ({ .multipliedBy(amount.toString()) .toFixed(0), ), - token.underlyingToken?.decimals || 18, + token?.decimals || 18, ), 2, )} diff --git a/src/components/views/donate/Recurring/RecurringDonationModal/RecurringDonationModal.tsx b/src/components/views/donate/Recurring/RecurringDonationModal/RecurringDonationModal.tsx index 76021e47e1..2bd6353364 100644 --- a/src/components/views/donate/Recurring/RecurringDonationModal/RecurringDonationModal.tsx +++ b/src/components/views/donate/Recurring/RecurringDonationModal/RecurringDonationModal.tsx @@ -217,6 +217,17 @@ const RecurringDonationInnerModal: FC<IRecurringDonationInnerModalProps> = ({ }; const sf = await Framework.create(_options); + const usdce = await sf.loadWrapperSuperToken( + '0x8430f084b939208e2eded1584889c9a66b90562f', + ); + console.log('USDC.ex SuperToken loaded:', usdce.address); + + const balance = await usdce.balanceOf({ + account: address, + providerOrSigner: signer, // Use the signer to ensure correct network + }); + console.log('USDC.ex SuperToken Balance:', balance.toString()); + // EThx is not a Wrapper Super Token and should load separately let superToken; if (_superToken.symbol === 'ETHx') { diff --git a/src/config/development.tsx b/src/config/development.tsx index 11f737c951..228b678061 100644 --- a/src/config/development.tsx +++ b/src/config/development.tsx @@ -4,6 +4,7 @@ import { gnosis, sepolia, optimismSepolia, + optimism, polygon, arbitrumSepolia, baseSepolia, @@ -121,6 +122,7 @@ const EVM_CHAINS = [ sepolia, gnosis, optimismSepolia, + optimism, celoAlfajores, arbitrumSepolia, baseSepolia, diff --git a/src/config/production.tsx b/src/config/production.tsx index 27c51ceb91..a2f5b040a6 100644 --- a/src/config/production.tsx +++ b/src/config/production.tsx @@ -526,13 +526,28 @@ const config: EnvConfig = { underlyingToken: { decimals: 6, id: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', - name: 'USD Coin', + name: 'USD Coin E', symbol: 'USDC', coingeckoId: 'usd-coin', }, decimals: 18, id: '0x8430f084b939208e2eded1584889c9a66b90562f', name: 'Super USD Coin', + symbol: 'USDC.ex', + isSuperToken: true, + coingeckoId: 'usd-coin', + }, + { + underlyingToken: { + decimals: 6, + id: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', + name: 'USD Coin', + symbol: 'USDC', + coingeckoId: 'usd-coin', + }, + decimals: 18, + id: '0x35adeb0638eb192755b6e52544650603fe65a006', + name: 'Super USD Coin', symbol: 'USDCx', isSuperToken: true, coingeckoId: 'usd-coin',