diff --git a/packages/site/src/components/ContentManageToken/TransferTokens.tsx b/packages/site/src/components/ContentManageToken/TransferTokens.tsx index 6a4bac87..3d11b09c 100644 --- a/packages/site/src/components/ContentManageToken/TransferTokens.tsx +++ b/packages/site/src/components/ContentManageToken/TransferTokens.tsx @@ -105,6 +105,7 @@ type TransactionStatus = 'idle' | 'loading' | 'success' | 'error'; const ADDRESS_VALIDATION_DELAY = 800; + const validateAddress = (address: string): boolean => { if (address.toLowerCase().endsWith('.coti')) { return true; @@ -605,6 +606,9 @@ export const TransferTokens: React.FC = React.memo( decimals: 18, }; + const isErc20Token = Boolean(currentToken?.address && !currentToken?.tokenId); + const usePublicTransfer = isErc20Token; + const balanceNum = useMemo(() => { if (!currentBalance || currentBalance === '0') { return 0; @@ -1058,6 +1062,7 @@ export const TransferTokens: React.FC = React.memo( to: targetAddress, amount: amountInWei.toString(), aesKey: aesKey || '', + publicTransfer: usePublicTransfer, }); } @@ -1088,6 +1093,7 @@ export const TransferTokens: React.FC = React.memo( addressInput, resolvedAddress, amount, + usePublicTransfer, currentToken, fetchTokenBalance, onBack, @@ -1217,6 +1223,7 @@ export const TransferTokens: React.FC = React.memo( ))} + )} diff --git a/packages/site/src/components/Header/styles.ts b/packages/site/src/components/Header/styles.ts index 09e40410..ce200671 100644 --- a/packages/site/src/components/Header/styles.ts +++ b/packages/site/src/components/Header/styles.ts @@ -204,6 +204,20 @@ export const MobileMenuDropdown = styled.div<{ $isVisible: boolean }>` top: 60px; } + @media screen and (min-width: 771px) { + left: auto; + right: 48px; + transform: ${(props) => + props.$isVisible + ? 'translateY(0) scale(1)' + : 'translateY(-12px) scale(0.95)'}; + transform-origin: top right; + } + + @media screen and (min-width: 771px) and (max-width: 992px) { + right: 32px; + } + @media screen and (min-width: 771px) and (min-height: 821px) { display: none; } diff --git a/packages/site/src/hooks/useTokenOperations.ts b/packages/site/src/hooks/useTokenOperations.ts index 5fb4b97f..ff039e84 100644 --- a/packages/site/src/hooks/useTokenOperations.ts +++ b/packages/site/src/hooks/useTokenOperations.ts @@ -260,6 +260,7 @@ export type TransferParams = { amount?: string; tokenId?: string; aesKey?: string; + publicTransfer?: boolean; }; export type ImportTokenParams = { @@ -529,6 +530,14 @@ export const useTokenOperations = (provider: BrowserProvider) => { let detectedVersion: 64 | 256 | undefined; let isConfidential = false; + const probed256 = await probeConfidentialVersion256( + tokenAddress, + browserProvider, + ); + if (probed256) { + return { confidential: true, version: probed256 }; + } + if (typeof erc165.supportsInterface === 'function') { try { const supports256 = await erc165.supportsInterface( @@ -571,7 +580,9 @@ export const useTokenOperations = (provider: BrowserProvider) => { .id('transfer(address,(uint256,bytes))') .slice(2); const selector256 = ethers - .id('transfer(address,((uint256,uint256),bytes))') + .id( + 'transfer(address,(((uint256,uint256),(uint256,uint256)),bytes[2][2]))', + ) .slice(2); if ( @@ -621,6 +632,7 @@ export const useTokenOperations = (provider: BrowserProvider) => { to, amount, aesKey, + publicTransfer, }: TransferParams & { amount: string; aesKey?: string }) => { return withLoading(async () => { if (!amount) { @@ -632,37 +644,78 @@ export const useTokenOperations = (provider: BrowserProvider) => { const { confidential, version } = await getTokenConfidentialStatus(tokenAddress); - const signer = await getBrowserProvider().getSigner(); + const browserProvider = getBrowserProvider(); + const signer = await browserProvider.getSigner(); let tx; - if (confidential) { + if (confidential && publicTransfer) { + const contract = new ethers.Contract(tokenAddress, ERC20_ABI, signer); + tx = await (contract as any)['transfer(address,uint256)']( + to, + ethers.toBigInt(amount), + ); + } else if (confidential) { if (!aesKey) { throw new Error('AES key is required for private ERC20 transfer'); } signer.setAesKey(aesKey); - const abi = version === 256 ? PRIVATE_ERC20_256_ABI : PRIVATE_ERC20_ABI; - const selector = - version === 256 - ? 'transfer(address,((uint256,uint256),bytes))' - : 'transfer(address,(uint256,bytes))'; - const contract = new Contract(tokenAddress, abi, signer); - let encryptedAmount = (await signer.encryptValue( - ethers.toBigInt(amount), - tokenAddress, - getSelector(selector), - )) as itUint; - if (version === 256) { - encryptedAmount = normalizeItUint256ForAbi(encryptedAmount); + + let confidentialVersion = version; + if (confidentialVersion !== 256) { + const probed = await probeConfidentialVersion256( + tokenAddress, + browserProvider, + ); + if (probed) { + confidentialVersion = probed; + } + } + + if (confidentialVersion === 256) { + const SELECTOR_256 = + 'transfer(address,(((uint256,uint256),(uint256,uint256)),bytes[2][2]))'; + const selectorHex = getSelector(SELECTOR_256); + const amountBigInt = ethers.toBigInt(amount); + const mask64 = (1n << 64n) - 1n; + + const d1 = (amountBigInt >> 192n) & mask64; + const d2 = (amountBigInt >> 128n) & mask64; + const d3 = (amountBigInt >> 64n) & mask64; + const d4 = amountBigInt & mask64; + + const it1 = (await signer.encryptValue(d1, tokenAddress, selectorHex)) as itUint; + const it2 = (await signer.encryptValue(d2, tokenAddress, selectorHex)) as itUint; + const it3 = (await signer.encryptValue(d3, tokenAddress, selectorHex)) as itUint; + const it4 = (await signer.encryptValue(d4, tokenAddress, selectorHex)) as itUint; + + const encryptedAmount256 = { + ciphertext: { + high: { high: it1.ciphertext, low: it2.ciphertext }, + low: { high: it3.ciphertext, low: it4.ciphertext }, + }, + signature: [ + [it1.signature, it2.signature], + [it3.signature, it4.signature], + ], + }; + + const contract = new Contract(tokenAddress, PRIVATE_ERC20_256_ABI, signer); + tx = await (contract as any)[SELECTOR_256](to, encryptedAmount256); + } else { + const selector = 'transfer(address,(uint256,bytes))'; + const contract = new Contract(tokenAddress, PRIVATE_ERC20_ABI, signer); + const encryptedAmount = (await signer.encryptValue( + ethers.toBigInt(amount), + tokenAddress, + getSelector(selector), + )) as itUint; + tx = await (contract as any)[selector](to, encryptedAmount); } - tx = await (contract as any)[selector](to, encryptedAmount, { - gasLimit: 12000000, - }); } else { const contract = new ethers.Contract(tokenAddress, ERC20_ABI, signer); tx = await (contract as any)['transfer(address,uint256)']( to, ethers.toBigInt(amount), - { gasLimit: 12000000 }, ); } @@ -997,7 +1050,6 @@ export const useTokenOperations = (provider: BrowserProvider) => { tokenId, amount, '0x', - { gasLimit: 12000000 }, ); if (!tx) { throw new Error('Transaction could not be initiated'); @@ -1312,5 +1364,6 @@ export const useTokenOperations = (provider: BrowserProvider) => { addTokenToMetaMask, addNFTToMetaMask, addERC1155ToMetaMask, + getTokenConfidentialStatus, }; }; diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 9acafddd..c5c614f7 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/coti-io/coti-snap.git" }, "source": { - "shasum": "9rWKLu0IsCpv8L6BMQHJlJaziOG78EuEec36lIqgaDQ=", + "shasum": "WvPDLHiPruUIlHFJ5Azd6EORYbWcM+o7S+ElN9c3wcs=", "location": { "npm": { "filePath": "dist/bundle.js",