diff --git a/web/cashtab/package-lock.json b/web/cashtab/package-lock.json
--- a/web/cashtab/package-lock.json
+++ b/web/cashtab/package-lock.json
@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
+ "name": "cashtab",
"version": "1.0.0",
"dependencies": {
"@ant-design/icons": "^4.3.0",
@@ -27,6 +28,7 @@
"react-dev-utils": "^11.0.4",
"react-device-detect": "^1.15.0",
"react-dom": "^17.0.1",
+ "react-easy-crop": "^3.5.3",
"react-ga": "^3.3.0",
"react-image": "^4.0.3",
"react-router": "^5.2.0",
@@ -16391,6 +16393,11 @@
"node": ">=6"
}
},
+ "node_modules/normalize-wheel": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+ "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
+ },
"node_modules/npm-run-path": {
"version": "4.0.1",
"dev": true,
@@ -22024,6 +22031,24 @@
"react": "17.0.1"
}
},
+ "node_modules/react-easy-crop": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-3.5.3.tgz",
+ "integrity": "sha512-ApTbh+lzKAvKqYW81ihd5J6ZTNN3vPDwi6ncFuUrHPI4bko2DlYOESkRm+0NYoW0H8YLaD7bxox+Z3EvIzAbUA==",
+ "dependencies": {
+ "normalize-wheel": "^1.0.1",
+ "tslib": "2.0.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.4.0",
+ "react-dom": ">=16.4.0"
+ }
+ },
+ "node_modules/react-easy-crop/node_modules/tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+ },
"node_modules/react-error-overlay": {
"version": "6.0.9",
"license": "MIT"
@@ -39784,6 +39809,11 @@
"version": "3.3.0",
"dev": true
},
+ "normalize-wheel": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+ "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
+ },
"npm-run-path": {
"version": "4.0.1",
"dev": true,
@@ -43390,6 +43420,22 @@
"scheduler": "^0.20.1"
}
},
+ "react-easy-crop": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-3.5.3.tgz",
+ "integrity": "sha512-ApTbh+lzKAvKqYW81ihd5J6ZTNN3vPDwi6ncFuUrHPI4bko2DlYOESkRm+0NYoW0H8YLaD7bxox+Z3EvIzAbUA==",
+ "requires": {
+ "normalize-wheel": "^1.0.1",
+ "tslib": "2.0.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+ }
+ }
+ },
"react-error-overlay": {
"version": "6.0.9"
},
diff --git a/web/cashtab/package.json b/web/cashtab/package.json
--- a/web/cashtab/package.json
+++ b/web/cashtab/package.json
@@ -24,6 +24,7 @@
"react-dev-utils": "^11.0.4",
"react-device-detect": "^1.15.0",
"react-dom": "^17.0.1",
+ "react-easy-crop": "^3.5.3",
"react-ga": "^3.3.0",
"react-image": "^4.0.3",
"react-router": "^5.2.0",
diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js
--- a/web/cashtab/src/components/App.js
+++ b/web/cashtab/src/components/App.js
@@ -36,14 +36,14 @@
import { currency } from './Common/Ticker';
const GlobalStyle = createGlobalStyle`
- .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button, .ant-modal > button, .ant-modal-confirm-btns > button, .ant-modal-footer > button {
+ .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button, .ant-modal > button, .ant-modal-confirm-btns > button, .ant-modal-footer > button, #cropControlsConfirm {
border-radius: 8px;
background-color: ${props => props.theme.modals.buttons.background};
color: ${props => props.theme.wallet.text.secondary};
font-weight: bold;
}
- .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button:hover,.ant-modal-confirm-btns > button:hover, .ant-modal-footer > button:hover {
+ .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button:hover,.ant-modal-confirm-btns > button:hover, .ant-modal-footer > button:hover, #cropControlsConfirm:hover {
color: ${props => props.theme.primary};
transition: color 0.3s;
background-color: ${props => props.theme.modals.buttons.background};
@@ -61,15 +61,22 @@
color: ${props => props.theme.contrast} !important;
background-color: ${props => props.theme.primary} !important;
}
- #addrSwitch {
+ #addrSwitch, #cropSwitch {
.ant-switch-checked {
background-color: white !important;
}
}
- #addrSwitch.ant-switch-checked {
+ #addrSwitch.ant-switch-checked, #cropSwitch.ant-switch-checked {
background-image: ${props =>
props.theme.buttons.primary.backgroundImage} !important;
}
+
+ .ant-slider-rail {
+ background-color: ${props => props.theme.forms.border} !important;
+ }
+ .ant-slider-track {
+ background-color: ${props => props.theme.primary} !important;
+ }
`;
const CustomApp = styled.div`
diff --git a/web/cashtab/src/components/Common/CropControlModal.js b/web/cashtab/src/components/Common/CropControlModal.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Common/CropControlModal.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { Card, Modal } from 'antd';
+
+export const CropperContainer = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 175px;
+`;
+export const ControlsContainer = styled.div`
+ position: absolute;
+ padding: 12px;
+ bottom: 0;
+ left: 50%;
+ width: 50%;
+ transform: translateX(-50%);
+ height: 175px;
+ display: block;
+ align-items: center;
+`;
+
+export const CropControlModal = ({
+ expand,
+ renderExpanded = () => null,
+ onClose,
+ style,
+ ...otherProps
+}) => {
+ return (
+
+
+ {renderExpanded()}
+
+
+ );
+};
+CropControlModal.propTypes = {
+ expand: PropTypes.bool,
+ renderExpanded: PropTypes.func,
+ onClose: PropTypes.func,
+ style: PropTypes.object,
+};
diff --git a/web/cashtab/src/components/Common/Notifications.js b/web/cashtab/src/components/Common/Notifications.js
--- a/web/cashtab/src/components/Common/Notifications.js
+++ b/web/cashtab/src/components/Common/Notifications.js
@@ -40,6 +40,17 @@
});
};
+const tokenIconSubmitSuccess = () => {
+ notification.success({
+ message: 'Success',
+ description: (
+ Your eToken icon was successfully submitted.
+ ),
+ icon: ,
+ style: { width: '100%' },
+ });
+};
+
const sendTokenNotification = link => {
notification.success({
message: 'Success',
@@ -136,6 +147,7 @@
export {
sendXecNotification,
createTokenNotification,
+ tokenIconSubmitSuccess,
sendTokenNotification,
xecReceivedNotification,
eTokenReceivedNotification,
diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js
--- a/web/cashtab/src/components/Common/Ticker.js
+++ b/web/cashtab/src/components/Common/Ticker.js
@@ -20,9 +20,10 @@
blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org',
tokenName: 'eToken',
tokenTicker: 'eToken',
+ tokenIconSubmitApi: 'https://icons.etokens.cash/new',
tokenLogo: tokenLogo,
tokenPrefixes: ['etoken'],
- tokenIconsUrl: '', //https://tokens.bitcoin.com/32 for BCH SLP
+ tokenIconsUrl: 'https://etoken-icons.s3.us-west-2.amazonaws.com/32',
txHistoryCount: 5,
hydrateUtxoBatchSize: 20,
defaultSettings: { fiatCurrency: 'usd' },
diff --git a/web/cashtab/src/components/Tokens/CreateTokenForm.js b/web/cashtab/src/components/Tokens/CreateTokenForm.js
--- a/web/cashtab/src/components/Tokens/CreateTokenForm.js
+++ b/web/cashtab/src/components/Tokens/CreateTokenForm.js
@@ -1,8 +1,13 @@
-import React, { useState } from 'react';
+import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { AntdFormWrapper } from '@components/Common/EnhancedInputs';
import { TokenCollapse } from '@components/Common/StyledCollapse';
import { currency } from '@components/Common/Ticker.js';
+import {
+ CropControlModal,
+ CropperContainer,
+ ControlsContainer,
+} from '../Common/CropControlModal';
import { WalletContext } from '@utils/context';
import {
isValidTokenName,
@@ -11,15 +16,37 @@
isValidTokenInitialQty,
isValidTokenDocumentUrl,
} from '@utils/validation';
-import { PlusSquareOutlined } from '@ant-design/icons';
+import {
+ PlusSquareOutlined,
+ UploadOutlined,
+ PaperClipOutlined,
+} from '@ant-design/icons';
import { SmartButton } from '@components/Common/PrimaryButton';
-import { Collapse, Form, Input, Modal } from 'antd';
+import {
+ notification,
+ Collapse,
+ Form,
+ Input,
+ Modal,
+ Button,
+ Slider,
+ Tooltip,
+ Upload,
+ Typography,
+ Switch,
+} from 'antd';
const { Panel } = Collapse;
import { TokenParamLabel } from '@components/Common/Atoms';
import {
createTokenNotification,
+ tokenIconSubmitSuccess,
errorNotification,
} from '@components/Common/Notifications';
+import Cropper from 'react-easy-crop';
+import getCroppedImg from '@utils/icons/cropImage';
+import getRoundImg from '@utils/icons/roundImage';
+import getResizedImage from '@utils/icons/resizeImage';
+const { Dragger } = Upload;
const CreateTokenForm = ({
BCH,
@@ -30,6 +57,196 @@
}) => {
const { wallet } = React.useContext(WalletContext);
+ // eToken icon adds
+ const [tokenIcon, setTokenIcon] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [fileName, setFileName] = useState('');
+ const [tokenIconFileList, setTokenIconFileList] = useState();
+ const [rawImageUrl, setRawImageUrl] = useState('');
+ const [imageUrl, setImageUrl] = useState('');
+ const [showCropModal, setShowCropModal] = useState(false);
+ const [roundSelection, setRoundSelection] = useState(true);
+
+ const [crop, setCrop] = useState({ x: 0, y: 0 });
+ const [rotation, setRotation] = useState(0);
+ const [zoom, setZoom] = useState(1);
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
+
+ const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => {
+ setCroppedAreaPixels(croppedAreaPixels);
+ }, []);
+
+ const showCroppedImage = useCallback(async () => {
+ setLoading(true);
+
+ try {
+ const croppedResult = await getCroppedImg(
+ rawImageUrl,
+ croppedAreaPixels,
+ rotation,
+ fileName,
+ );
+
+ if (roundSelection) {
+ const roundResult = await getRoundImg(
+ croppedResult.url,
+ fileName,
+ );
+
+ await getResizedImage(
+ roundResult.url,
+ resizedResult => {
+ setTokenIcon(resizedResult.file);
+ setImageUrl(resizedResult.url);
+ },
+ fileName,
+ );
+ } else {
+ setTokenIcon(croppedResult.file);
+ setImageUrl(croppedResult.url);
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setLoading(false);
+ }
+ }, [croppedAreaPixels, fileName, rawImageUrl, rotation, roundSelection]);
+
+ const onClose = useCallback(() => {
+ setShowCropModal(false);
+ }, []);
+
+ const handleTokenIconImage = (imgFile, callback) =>
+ new Promise((resolve, reject) => {
+ setLoading(true);
+ try {
+ const reader = new FileReader();
+
+ const width = 128;
+ const height = 128;
+ reader.readAsDataURL(imgFile);
+
+ reader.addEventListener('load', () =>
+ setRawImageUrl(reader.result),
+ );
+
+ reader.onload = event => {
+ const img = new Image();
+ img.src = event.target.result;
+ img.onload = () => {
+ const elem = document.createElement('canvas');
+ //console.log(`Canvas created`);
+ elem.width = width;
+ elem.height = height;
+ const ctx = elem.getContext('2d');
+ // img.width and img.height will contain the original dimensions
+ ctx.drawImage(img, 0, 0, width, height);
+ if (!HTMLCanvasElement.prototype.toBlob) {
+ Object.defineProperty(
+ HTMLCanvasElement.prototype,
+ 'toBlob',
+ {
+ value: function (callback, type, quality) {
+ var dataURL = this.toDataURL(
+ type,
+ quality,
+ ).split(',')[1];
+ setTimeout(function () {
+ var binStr = atob(dataURL),
+ len = binStr.length,
+ arr = new Uint8Array(len);
+ for (var i = 0; i < len; i++) {
+ arr[i] = binStr.charCodeAt(i);
+ }
+ callback(
+ new Blob([arr], {
+ type: type || 'image/png',
+ }),
+ );
+ });
+ },
+ },
+ );
+ }
+
+ ctx.canvas.toBlob(
+ blob => {
+ console.log(imgFile.name);
+
+ let fileNameParts = imgFile.name.split('.');
+ fileNameParts.pop();
+ let fileNamePng =
+ fileNameParts.join('.') + '.png';
+
+ const file = new File([blob], fileNamePng, {
+ type: 'image/png',
+ });
+ setFileName(fileNamePng);
+ const resultReader = new FileReader();
+
+ resultReader.readAsDataURL(file);
+ setTokenIcon(file);
+ resultReader.addEventListener('load', () =>
+ callback(resultReader.result),
+ );
+ setLoading(false);
+ setShowCropModal(true);
+ resolve();
+ },
+ 'image/png',
+ 1,
+ );
+ };
+ };
+ } catch (err) {
+ console.log(`Error in handleTokenIconImage()`);
+ console.log(err);
+ reject(err);
+ }
+ });
+
+ const transformTokenIconFile = file => {
+ return new Promise((resolve, reject) => {
+ reject();
+ // setLoading(false);
+ });
+ };
+
+ const beforeTokenIconUpload = file => {
+ try {
+ if (file.type.split('/')[0] !== 'image') {
+ throw new Error('You can only upload image files!');
+ } else {
+ setLoading(true);
+ handleTokenIconImage(file, imageUrl => setImageUrl(imageUrl));
+ }
+ } catch (e) {
+ console.error('error', e);
+ notification.error({
+ message: 'Error',
+ description: e.message || e.error || JSON.stringify(e),
+ duration: 0,
+ });
+ setTokenIconFileList(undefined);
+ setTokenIcon(undefined);
+ setImageUrl('');
+ return false;
+ }
+ };
+
+ const handleChangeTokenIconUpload = info => {
+ let list = [...info.fileList];
+
+ if (info.file.type.split('/')[0] !== 'image') {
+ setTokenIconFileList(undefined);
+ setImageUrl('');
+ } else {
+ setTokenIconFileList(list.slice(-1));
+ }
+ };
+
+ //end eToken icon adds
+
// New Token Name
const [newTokenName, setNewTokenName] = useState('');
const [newTokenNameIsValid, setNewTokenNameIsValid] = useState(null);
@@ -110,6 +327,66 @@
// Modal settings
const [showConfirmCreateToken, setShowConfirmCreateToken] = useState(false);
+ const submitTokenIcon = async link => {
+ // Get the tokenId from link
+ const newlyMintedTokenId = link.substr(link.length - 64);
+
+ let formData = new FormData();
+
+ const data = {
+ newTokenName,
+ newTokenTicker,
+ newTokenDecimals,
+ newTokenDocumentUrl,
+ newTokenInitialQty,
+ tokenIcon,
+ };
+ for (let key in data) {
+ formData.append(key, data[key]);
+ }
+ // Would get tokenId here
+ //formData.append('tokenId', link.substr(link.length - 64));
+ // for now, hard code it
+ formData.append('tokenId', newlyMintedTokenId);
+
+ console.log(formData);
+
+ try {
+ const tokenIconApprovalResponse = await fetch(
+ currency.tokenIconSubmitApi,
+ {
+ method: 'POST',
+ //Note: fetch automatically assigns correct header for multipart form based on formData obj
+ headers: {
+ Accept: 'application/json',
+ },
+ body: formData,
+ },
+ );
+
+ const tokenIconApprovalResponseJson =
+ await tokenIconApprovalResponse.json();
+
+ if (!tokenIconApprovalResponseJson.approvalRequested) {
+ // If the backend returns a specific error msg along with "approvalRequested = false", throw that error
+ // You may want to customize how the app reacts to different cases
+ if (tokenIconApprovalResponseJson.msg) {
+ throw new Error(tokenIconApprovalResponseJson.msg);
+ } else {
+ throw new Error('Error in uploading token icon');
+ }
+ }
+
+ tokenIconSubmitSuccess();
+ } catch (err) {
+ console.error(err.message);
+ errorNotification(
+ err,
+ err.message,
+ 'Submitting icon for approval while creating a new eToken',
+ );
+ }
+ };
const createPreviewedToken = async () => {
passLoadingStatus(true);
// If data is for some reason not valid here, bail out
@@ -139,6 +416,11 @@
configObj,
);
createTokenNotification(link);
+
+ // If this eToken has an icon, upload to server
+ if (tokenIcon !== '') {
+ submitTokenIcon(link);
+ }
} catch (e) {
// Set loading to false here as well, as balance may not change depending on where error occured in try loop
passLoadingStatus(false);
@@ -329,6 +611,160 @@
}
/>
+
+ false}
+ fileList={tokenIconFileList}
+ name="tokenIcon"
+ style={{
+ backgroundColor: '#f4f4f4',
+ }}
+ >
+ {imageUrl ? (
+
+ ) : (
+ <>
+ {' '}
+
+
+ Click, or drag file to this
+ area to upload
+
+
+ Must be an image
+
+ >
+ )}
+
+
+ {!loading && tokenIcon && (
+ <>
+
+
+ setShowCropModal(true)
+ }
+ >
+
+ {tokenIcon.name}
+
+
+ setShowCropModal(true)
+ }
+ >
+ Click here to crop or zoom
+ your icon
+
+ {' '}
+ >
+ )}
+
+ null}
+ renderExpanded={() => (
+ <>
+ {' '}
+
+
+
+
+
+ setRoundSelection(
+ !checked,
+ )
+ }
+ />{' '}
+
+ {'Zoom:'}
+
+ setZoom(zoom)
+ }
+ min={1}
+ max={10}
+ step={0.1}
+ />
+ {'Rotation:'}
+
+ setRotation(
+ rotation,
+ )
+ }
+ min={0}
+ max={360}
+ step={1}
+ />
+
+
+ >
+ )}
+ onClose={onClose}
+ />
+
+ new Promise((resolve, reject) => {
+ const image = new Image();
+ image.addEventListener('load', () => resolve(image));
+ image.addEventListener('error', error => reject(error));
+ image.setAttribute('crossOrigin', 'anonymous');
+ image.src = url;
+ });
+
+function getRadianAngle(degreeValue) {
+ return (degreeValue * Math.PI) / 180;
+}
+
+export default async function getCroppedImg(
+ imageSrc,
+ pixelCrop,
+ rotation = 0,
+ fileName,
+) {
+ const image = await createImage(imageSrc);
+ console.log('image :', image);
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ const maxSize = Math.max(image.width, image.height);
+ const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2));
+
+ canvas.width = safeArea;
+ canvas.height = safeArea;
+
+ ctx.translate(safeArea / 2, safeArea / 2);
+ ctx.rotate(getRadianAngle(rotation));
+ ctx.translate(-safeArea / 2, -safeArea / 2);
+
+ ctx.drawImage(
+ image,
+ safeArea / 2 - image.width * 0.5,
+ safeArea / 2 - image.height * 0.5,
+ );
+ const data = ctx.getImageData(0, 0, safeArea, safeArea);
+
+ canvas.width = pixelCrop.width;
+ canvas.height = pixelCrop.height;
+
+ ctx.putImageData(
+ data,
+ 0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x,
+ 0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y,
+ );
+
+ if (!HTMLCanvasElement.prototype.toBlob) {
+ Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
+ value: function (callback, type, quality) {
+ var dataURL = this.toDataURL(type, quality).split(',')[1];
+ setTimeout(function () {
+ var binStr = atob(dataURL),
+ len = binStr.length,
+ arr = new Uint8Array(len);
+ for (var i = 0; i < len; i++) {
+ arr[i] = binStr.charCodeAt(i);
+ }
+ callback(new Blob([arr], { type: type || 'image/png' }));
+ });
+ },
+ });
+ }
+ return new Promise(resolve => {
+ ctx.canvas.toBlob(
+ blob => {
+ const file = new File([blob], fileName, {
+ type: 'image/png',
+ });
+ const resultReader = new FileReader();
+
+ resultReader.readAsDataURL(file);
+
+ resultReader.addEventListener('load', () =>
+ resolve({ file, url: resultReader.result }),
+ );
+ },
+ 'image/png',
+ 1,
+ );
+ });
+}
diff --git a/web/cashtab/src/utils/icons/resizeImage.js b/web/cashtab/src/utils/icons/resizeImage.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/utils/icons/resizeImage.js
@@ -0,0 +1,57 @@
+const createImage = url =>
+ new Promise((resolve, reject) => {
+ const image = new Image();
+ image.addEventListener('load', () => resolve(image));
+ image.addEventListener('error', error => reject(error));
+ image.setAttribute('crossOrigin', 'anonymous');
+ image.src = url;
+ });
+
+export default async function getResizedImg(imageSrc, callback, fileName) {
+ const image = await createImage(imageSrc);
+
+ const width = 128;
+ const height = 128;
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext('2d');
+
+ ctx.drawImage(image, 0, 0, width, height);
+ if (!HTMLCanvasElement.prototype.toBlob) {
+ Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
+ value: function (callback, type, quality) {
+ var dataURL = this.toDataURL(type, quality).split(',')[1];
+ setTimeout(function () {
+ var binStr = atob(dataURL),
+ len = binStr.length,
+ arr = new Uint8Array(len);
+ for (var i = 0; i < len; i++) {
+ arr[i] = binStr.charCodeAt(i);
+ }
+ callback(new Blob([arr], { type: type || 'image/png' }));
+ });
+ },
+ });
+ }
+
+ return new Promise(resolve => {
+ ctx.canvas.toBlob(
+ blob => {
+ const file = new File([blob], fileName, {
+ type: 'image/png',
+ });
+ const resultReader = new FileReader();
+
+ resultReader.readAsDataURL(file);
+
+ resultReader.addEventListener('load', () =>
+ callback({ file, url: resultReader.result }),
+ );
+ resolve();
+ },
+ 'image/png',
+ 1,
+ );
+ });
+}
diff --git a/web/cashtab/src/utils/icons/roundImage.js b/web/cashtab/src/utils/icons/roundImage.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/utils/icons/roundImage.js
@@ -0,0 +1,64 @@
+const createImage = url =>
+ new Promise((resolve, reject) => {
+ const image = new Image();
+ image.addEventListener('load', () => resolve(image));
+ image.addEventListener('error', error => reject(error));
+ image.setAttribute('crossOrigin', 'anonymous');
+ image.src = url;
+ });
+
+export default async function getRoundImg(imageSrc, fileName) {
+ const image = await createImage(imageSrc);
+ console.log('image :', image);
+ const canvas = document.createElement('canvas');
+ canvas.width = image.width;
+ canvas.height = image.height;
+ const ctx = canvas.getContext('2d');
+
+ ctx.drawImage(image, 0, 0);
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.beginPath();
+ ctx.arc(
+ image.width / 2,
+ image.height / 2,
+ image.height / 2,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.closePath();
+ ctx.fill();
+ if (!HTMLCanvasElement.prototype.toBlob) {
+ Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
+ value: function (callback, type, quality) {
+ var dataURL = this.toDataURL(type, quality).split(',')[1];
+ setTimeout(function () {
+ var binStr = atob(dataURL),
+ len = binStr.length,
+ arr = new Uint8Array(len);
+ for (var i = 0; i < len; i++) {
+ arr[i] = binStr.charCodeAt(i);
+ }
+ callback(new Blob([arr], { type: type || 'image/png' }));
+ });
+ },
+ });
+ }
+ return new Promise(resolve => {
+ ctx.canvas.toBlob(
+ blob => {
+ const file = new File([blob], fileName, {
+ type: 'image/png',
+ });
+ const resultReader = new FileReader();
+
+ resultReader.readAsDataURL(file);
+
+ resultReader.addEventListener('load', () =>
+ resolve({ file, url: resultReader.result }),
+ );
+ },
+ 'image/png',
+ 1,
+ );
+ });
+}