Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13114986
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
50 KB
Subscribers
None
View Options
diff --git a/cashtab/src/components/Etokens/CreateTokenForm/index.tsx b/cashtab/src/components/Etokens/CreateTokenForm/index.tsx
index 7396f42c5c..8c7158957b 100644
--- a/cashtab/src/components/Etokens/CreateTokenForm/index.tsx
+++ b/cashtab/src/components/Etokens/CreateTokenForm/index.tsx
@@ -1,1155 +1,1155 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
import React, { useState, useCallback, useEffect } from 'react';
import Modal from 'components/Common/Modal';
import { WalletContext } from 'wallet/context';
import {
isValidTokenName,
isValidTokenTicker,
isValidTokenDecimals,
isValidTokenMintAmount,
getTokenDocumentUrlError,
isProbablyNotAScam,
} from 'validation';
import PrimaryButton, { IconButton } from 'components/Common/Buttons';
import { QuestionIcon } from 'components/Common/CustomIcons';
import {
Input,
SendTokenInput,
Slider,
CashtabDragger,
} from 'components/Common/Inputs';
import CashtabSwitch from 'components/Common/Switch';
import { TokenParamLabel } from 'components/Common/Atoms';
import Cropper, { Area, Point } from 'react-easy-crop';
import getCroppedImg, {
ReaderResult,
} from 'components/Etokens/icons/cropImage';
import getRoundImg from 'components/Etokens/icons/roundImage';
import getResizedImage from 'components/Etokens/icons/resizeImage';
import { token as tokenConfig } from 'config/token';
import appConfig from 'config/app';
import {
getSlpGenesisTargetOutput,
getMaxDecimalizedSlpQty,
getNftParentGenesisTargetOutputs,
getNftChildGenesisTargetOutputs,
} from 'token-protocols/slpv1';
import { getAlpGenesisTargetOutputs } from 'token-protocols/alp';
import { sendXec } from 'transactions';
import { TokenNotificationIcon } from 'components/Common/CustomIcons';
import { explorer } from 'config/explorer';
import { getWalletState } from 'utils/cashMethods';
import {
hasEnoughToken,
undecimalizeTokenAmount,
TokenUtxo,
SlpDecimals,
} from 'wallet';
import { toast } from 'react-toastify';
import Switch from 'components/Common/Switch';
import { useNavigate, useLocation } from 'react-router-dom';
import {
Form,
SwitchRow,
SwitchLabel,
EditIcon,
TokenCreatedLink,
IconModalForm,
IconModalRow,
SliderLabel,
SliderBox,
CropperContainer,
CreateTokenTitle,
TokenCreationSummaryTable,
SummaryRow,
TokenParam,
ButtonDisabledMsg,
TokenInfoParagraph,
TokenTypeDescription,
OuterCtn,
} from 'components/Etokens/CreateTokenForm/styles';
import { sha256, Message } from 'js-sha256';
import { getUserLocale } from 'helpers';
import { decimalizedTokenQtyToLocaleFormat } from 'utils/formatting';
import { toHex } from 'ecash-lib';
interface CreateTokenFormProps {
nftChildGenesisInput?: TokenUtxo[];
}
const CreateTokenForm: React.FC<CreateTokenFormProps> = ({
nftChildGenesisInput,
}) => {
// Constant to handle rendering of CreateTokenForm for NFT Minting
const isNftMint = Array.isArray(nftChildGenesisInput);
const NFT_DECIMALS = 0;
const NFT_GENESIS_QTY = '1';
const ICON_MAX_UPLOAD_BYTES = 1000000;
const navigate = useNavigate();
const location = useLocation();
const userLocale = getUserLocale(navigator);
const { chronik, ecc, chaintipBlockheight, cashtabState } =
React.useContext(WalletContext);
const { settings, wallets } = cashtabState;
const wallet = wallets.length > 0 ? wallets[0] : false;
const walletState = getWalletState(wallet);
const { tokens } = walletState;
// eToken icon adds
const [tokenIcon, setTokenIcon] = useState<null | File>(null);
const [createdTokenId, setCreatedTokenId] = useState<null | string>(null);
const [loading, setLoading] = useState<boolean>(false);
const [fileName, setFileName] = useState<string>('');
const [rawImageUrl, setRawImageUrl] = useState<string>('');
const [imageUrl, setImageUrl] = useState<string>('');
const [showCropModal, setShowCropModal] = useState<boolean>(false);
const [roundSelection, setRoundSelection] = useState<boolean>(true);
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
const [rotation, setRotation] = useState<string>('0');
const [zoom, setZoom] = useState<string>('1');
const [croppedAreaPixels, setCroppedAreaPixels] = useState<null | Area>(
null,
);
// NFT handling
const [createNftCollection, setCreateNftCollection] =
useState<boolean>(false);
// Modal settings
const [showConfirmCreateToken, setShowConfirmCreateToken] =
useState<boolean>(false);
// Token form items
interface CreateTokenFormFormData {
name: string;
ticker: string;
decimals: string;
genesisQty: string;
url: string;
hash: string;
}
const emptyFormData: CreateTokenFormFormData = {
name: '',
ticker: '',
decimals: '',
genesisQty: '',
url: '',
hash: '',
};
interface CreateTokenFormFormDataErrors {
name: false | string;
ticker: false | string;
decimals: false | string;
genesisQty: false | string;
url: false | string;
}
const initialFormDataErrors: CreateTokenFormFormDataErrors = {
name: false,
ticker: false,
decimals: false,
genesisQty: false,
url: false,
// No error for hash as this is only generated
};
const [formData, setFormData] =
useState<CreateTokenFormFormData>(emptyFormData);
const [formDataErrors, setFormDataErrors] =
useState<CreateTokenFormFormDataErrors>(initialFormDataErrors);
// This switch is form data, but since it is a bool and not a string, keep it with its own state field
const [createWithMintBaton, setCreateWithMintBaton] =
useState<boolean>(true);
interface TokenTypeSwitches {
slp: boolean;
alp: boolean;
}
const switchesOff: TokenTypeSwitches = {
slp: false,
alp: false,
};
// Default SLP
const [tokenTypeSwitches, setTokenTypeSwitches] =
useState<TokenTypeSwitches>({
...switchesOff,
slp: true,
});
const [showTypeInfoSlp, setShowTypeInfoSlp] = useState<boolean>(false);
const [showTypeInfoAlp, setShowTypeInfoAlp] = useState<boolean>(false);
// Note: We do not include a UI input for token document hash
// Questionable value to casual users and requires significant complication
useEffect(() => {
// If we routed here from the Create NFT Collection link, toggle NFT switch to true
if (location?.pathname?.includes('create-nft-collection')) {
setCreateNftCollection(true);
}
}, []);
useEffect(() => {
// After the user has created a token, we wait until the wallet has updated its balance
// and the page is available, then we navigate to the page
if (createdTokenId === null || isNftMint) {
// If we do not have a created tokenId or if this was an NFT Mint,
// do not navigate anywhere
return;
}
if (typeof tokens.get(createdTokenId) !== 'undefined') {
navigate(`/token/${createdTokenId}`);
}
}, [createdTokenId, tokens]);
useEffect(() => {
if (createNftCollection === true) {
// Cashtab only creates NFT1 Parent tokens (aka NFT Collections) with 0 decimals
setFormData(previous => ({
...previous,
decimals: '0',
}));
}
}, [createNftCollection]);
const onCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => {
setCroppedAreaPixels(croppedAreaPixels);
}, []);
const showCroppedImage = useCallback(async () => {
setLoading(true);
try {
let imageToResize;
const croppedResult = await getCroppedImg(
rawImageUrl as string,
croppedAreaPixels as Area,
parseFloat(rotation),
fileName,
);
if (roundSelection) {
imageToResize = await getRoundImg(croppedResult.url, fileName);
} else {
imageToResize = croppedResult;
}
await getResizedImage(
imageToResize.url,
(resizedResult: ReaderResult) => {
setTokenIcon(resizedResult.file);
setImageUrl(resizedResult.url);
},
fileName,
);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}, [croppedAreaPixels, fileName, rawImageUrl, rotation, roundSelection]);
const onClose = useCallback(() => {
setShowCropModal(false);
}, []);
const handleTokenIconImage = (
imgFile: File,
callback: (file: string) => void,
) =>
new Promise((resolve, reject) => {
setLoading(true);
try {
// Get the sha256 hash of the user's original uploaded file
// For an NFT, this will be set as the document hash
// Note that this will not match the hash of the image on the token server due to resizing
// and renaming
// The hash should be of the creator's original file
const hashreader = new FileReader();
hashreader.readAsArrayBuffer(imgFile);
hashreader.addEventListener('load', () => {
// Handle Input expects an event with key target
// target to have keys name and hash
handleInput({
target: {
name: 'hash',
value: sha256(hashreader.result as Message),
},
} as React.ChangeEvent<HTMLInputElement>);
});
const reader = new FileReader();
const width = 512;
const height = 512;
reader.readAsDataURL(imgFile);
reader.addEventListener('load', () =>
setRawImageUrl(reader.result as string),
);
reader.onload = event => {
const img = new Image();
if (typeof event.target?.result !== 'string') {
// Should never happen
return toast.error('Error loading icon');
}
img.src = event.target.result;
img.onload = () => {
const elem = document.createElement('canvas');
elem.width = width;
elem.height = height;
const ctx = elem.getContext('2d');
if (ctx === null) {
// Should never happen
return toast.error('Error drawing image');
}
// 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: (blob: Blob) => void,
type: string,
quality: number,
) {
const dataURL = this.toDataURL(
type,
quality,
).split(',')[1];
setTimeout(function () {
const binStr = atob(dataURL),
len = binStr.length,
arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(
new Blob([arr], {
type: type || 'image/png',
}),
);
});
},
},
);
}
return new Promise<void>((resolve, reject) => {
ctx.canvas.toBlob(
blob => {
if (blob === null) {
return toast.error(
'Error rendering blob',
);
reject();
}
const fileNameParts =
imgFile.name.split('.');
fileNameParts.pop();
const 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 as string),
);
setLoading(false);
setShowCropModal(true);
resolve();
},
'image/png',
1,
);
});
};
};
} catch (err) {
console.error(`Error in handleTokenIconImage()`, err);
reject(err);
}
});
const validateTokenIconUpload = (file: File) => {
const approvedFileTypes = ['image/png', 'image/jpg', 'image/jpeg'];
try {
if (!approvedFileTypes.includes(file.type)) {
throw new Error('Only jpg or png image files are accepted');
}
setLoading(true);
handleTokenIconImage(file, imageUrl => setImageUrl(imageUrl));
} catch (e) {
toast.error(
`Cashtab can only process jpg or png files for token icon uploads.`,
);
setTokenIcon(null);
setImageUrl('');
return false;
}
};
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
switch (name) {
case 'name': {
// Handle validation and state updates for new token name
// validation
const validTokenName = isValidTokenName(value);
const probablyNotScam = isProbablyNotAScam(value);
setFormDataErrors(previous => ({
...previous,
[name]: !validTokenName
? `Token name must be a valid string between 1 and 68 characters long.`
: !probablyNotScam
- ? 'Token name must not conflict with existing crypto or fiat'
- : false,
+ ? 'Token name must not conflict with existing crypto or fiat'
+ : false,
}));
break;
}
case 'ticker': {
// validation
const validTokenTicker = isValidTokenTicker(value);
const probablyNotScamTicker = isProbablyNotAScam(value);
setFormDataErrors(previous => ({
...previous,
[name]: !validTokenTicker
? `Token ticker must be a valid string between 1 and 12 characters long`
: !probablyNotScamTicker
- ? 'Token ticker must not conflict with existing crypto or fiat'
- : false,
+ ? 'Token ticker must not conflict with existing crypto or fiat'
+ : false,
}));
break;
}
case 'decimals': {
setFormDataErrors(previous => ({
...previous,
[name]: isValidTokenDecimals(value)
? false
: 'Token decimals must be an integer between 0 and 9',
}));
// Also validate the supply here if the form has been touched
// Supply validation may change when decimals changes
if (formData.genesisQty !== '') {
const isValidOrStringErrorMsg = isValidTokenMintAmount(
formData.genesisQty,
// Note that, in this code block, value is formData.decimals
parseInt(value) as SlpDecimals,
);
setFormDataErrors(previous => ({
...previous,
genesisQty:
typeof isValidOrStringErrorMsg === 'string'
? isValidOrStringErrorMsg
: false,
}));
}
break;
}
case 'genesisQty': {
const isValidOrStringErrorMsg = isValidTokenMintAmount(
value,
// If user has not yet input decimals, assume 0 decimals
formData.decimals === ''
? 0
: (parseInt(formData.decimals) as SlpDecimals),
);
setFormDataErrors(previous => ({
...previous,
[name]:
typeof isValidOrStringErrorMsg === 'string'
? isValidOrStringErrorMsg
: false,
}));
break;
}
case 'url': {
setFormDataErrors(previous => ({
...previous,
[name]: getTokenDocumentUrlError(value),
}));
break;
}
case 'hash': {
// Do nothing, we disable user input for this field
// Input can only come from a user uploaded image
break;
}
default:
break;
}
setFormData(previous => ({
...previous,
[name]: value,
}));
};
const onMaxGenesis = () => {
// Use 0 for decimals if user has not input decimals yet
const usedDecimals =
formData.decimals === ''
? 0
: (parseInt(formData.decimals) as SlpDecimals);
const maxGenesisAmount = getMaxDecimalizedSlpQty(usedDecimals);
handleInput({
target: {
name: 'genesisQty',
value: maxGenesisAmount,
},
} as React.ChangeEvent<HTMLInputElement>);
};
// Only enable CreateToken button if all form entries are valid
const tokenGenesisDataIsValid =
// No formdata errors
formDataErrors.name === false &&
formDataErrors.ticker === false &&
formDataErrors.decimals === false &&
formDataErrors.genesisQty === false &&
formDataErrors.url === false &&
// Name must not be empty
formData.name !== '' &&
// Ticker must not be empty
formData.ticker !== '' &&
// If this is an nft mint, we need an NFT Mint Input
((isNftMint && nftChildGenesisInput.length === 1) || !isNftMint) &&
(tokenIcon === null || tokenIcon.size <= ICON_MAX_UPLOAD_BYTES);
interface TokenIconData {
name: string;
ticker: string;
decimals: string;
url: string;
genesisQty: string;
tokenIcon: File;
}
const submitTokenIcon = async (tokenId: string) => {
const submittedFormData = new FormData();
const data: TokenIconData = {
name: formData.name,
ticker: formData.ticker,
decimals: isNftMint ? NFT_DECIMALS.toString() : formData.decimals,
url: formData.url,
genesisQty: isNftMint ? NFT_GENESIS_QTY : formData.genesisQty,
tokenIcon: tokenIcon as File,
};
for (const key in data) {
submittedFormData.append(key, data[key as keyof TokenIconData]);
}
// This function is called after the genesis tx is broadcast, using tokenId as a calling param
submittedFormData.append('tokenId', tokenId);
try {
const tokenIconApprovalResponse = await fetch(
tokenConfig.tokenIconSubmitApi,
{
method: 'POST',
//Note: fetch automatically assigns correct header for multipart form based on formData obj
headers: {
Accept: 'application/json',
},
body: submittedFormData,
},
);
const tokenIconApprovalResponseJson =
await tokenIconApprovalResponse.json();
if (
typeof tokenIconApprovalResponseJson.status === 'undefined' ||
tokenIconApprovalResponseJson.status !== 'ok'
) {
// Let the user know there was an issue with submitting the token icon
if (tokenIconApprovalResponseJson.msg) {
throw new Error(tokenIconApprovalResponseJson.msg);
} else {
throw new Error(
'Error uploading token icon. Please email icons@e.cash for support.',
);
}
}
toast.success(`Successfully uploaded token icon`);
} catch (err) {
console.error(err);
toast.error(
`Error submitting token icon for approval, please contact icons@e.cash for support`,
);
}
};
const createPreviewedToken = async () => {
// If data is for some reason not valid here, bail out
if (!tokenGenesisDataIsValid) {
return;
}
// data must be valid and user reviewed to get here
const genesisInfo = {
tokenName: formData.name,
tokenTicker: formData.ticker,
url:
formData.url === ''
? tokenConfig.newTokenDefaultUrl
: formData.url,
// Support documentHash for NFT Collection, but only for uploaded image file
hash: createNftCollection || isNftMint ? formData.hash : '',
decimals: isNftMint ? NFT_DECIMALS : parseInt(formData.decimals),
};
// Create token per specified user data
try {
// Get target outputs for an SLP v1 genesis tx
const targetOutputs = createNftCollection
? getNftParentGenesisTargetOutputs(
genesisInfo,
BigInt(
undecimalizeTokenAmount(
formData.genesisQty,
parseInt(formData.decimals) as SlpDecimals,
),
),
createWithMintBaton ? 2 : undefined,
)
: isNftMint
- ? getNftChildGenesisTargetOutputs(genesisInfo)
- : tokenTypeSwitches.slp
- ? getSlpGenesisTargetOutput(
- genesisInfo,
- BigInt(
- undecimalizeTokenAmount(
- formData.genesisQty,
- parseInt(formData.decimals) as SlpDecimals,
- ),
+ ? getNftChildGenesisTargetOutputs(genesisInfo)
+ : tokenTypeSwitches.slp
+ ? getSlpGenesisTargetOutput(
+ genesisInfo,
+ BigInt(
+ undecimalizeTokenAmount(
+ formData.genesisQty,
+ parseInt(formData.decimals) as SlpDecimals,
),
- createWithMintBaton ? 2 : undefined,
- )
- : getAlpGenesisTargetOutputs(
- {
- ...genesisInfo,
- // Set as Cashtab active wallet public key
- authPubkey: toHex(
- wallet.paths.get(appConfig.derivationPath).pk,
- ),
- // Note we are omitting the "data" key for now
- },
- BigInt(
- undecimalizeTokenAmount(
- formData.genesisQty,
- parseInt(formData.decimals) as SlpDecimals,
- ),
+ ),
+ createWithMintBaton ? 2 : undefined,
+ )
+ : getAlpGenesisTargetOutputs(
+ {
+ ...genesisInfo,
+ // Set as Cashtab active wallet public key
+ authPubkey: toHex(
+ wallet.paths.get(appConfig.derivationPath).pk,
),
- createWithMintBaton,
- );
+ // Note we are omitting the "data" key for now
+ },
+ BigInt(
+ undecimalizeTokenAmount(
+ formData.genesisQty,
+ parseInt(formData.decimals) as SlpDecimals,
+ ),
+ ),
+ createWithMintBaton,
+ );
const { response } = isNftMint
? await sendXec(
chronik,
ecc,
wallet,
targetOutputs,
settings.minFeeSends &&
(hasEnoughToken(
tokens,
appConfig.vipTokens.grumpy.tokenId,
appConfig.vipTokens.grumpy.vipBalance,
) ||
hasEnoughToken(
tokens,
appConfig.vipTokens.cachet.tokenId,
appConfig.vipTokens.cachet.vipBalance,
))
? appConfig.minFee
: appConfig.defaultFee,
chaintipBlockheight,
// per spec, this must be at index 0
// https://github.com/simpleledger/slp-specifications/blob/master/slp-nft-1.md
nftChildGenesisInput,
)
: await sendXec(
chronik,
ecc,
wallet,
targetOutputs,
settings.minFeeSends &&
(hasEnoughToken(
tokens,
appConfig.vipTokens.grumpy.tokenId,
appConfig.vipTokens.grumpy.vipBalance,
) ||
hasEnoughToken(
tokens,
appConfig.vipTokens.cachet.tokenId,
appConfig.vipTokens.cachet.vipBalance,
))
? appConfig.minFee
: appConfig.defaultFee,
chaintipBlockheight,
);
const { txid } = response;
setCreatedTokenId(txid);
toast(
<TokenCreatedLink
href={`${explorer.blockExplorerUrl}/tx/${txid}`}
target="_blank"
rel="noopener noreferrer"
>
{createNftCollection
? 'NFT Collection created!'
: isNftMint
- ? 'NFT Minted!'
- : 'Token created!'}
+ ? 'NFT Minted!'
+ : 'Token created!'}
</TokenCreatedLink>,
{
icon: TokenNotificationIcon,
},
);
// If this eToken/NFT Collection/NFT has an icon, upload to server
if (tokenIcon !== null) {
submitTokenIcon(txid);
}
} catch (e) {
toast.error(`${e}`);
}
// Hide the modal
setShowConfirmCreateToken(false);
// If this is an NFT, clear the form
if (isNftMint) {
setFormData(emptyFormData);
setFormDataErrors(initialFormDataErrors);
setTokenIcon(null);
setRawImageUrl('');
setImageUrl('');
}
};
return (
<OuterCtn>
{showConfirmCreateToken && (
<Modal
title={`Your ${
createNftCollection
? 'NFT Collection'
: isNftMint
- ? 'NFT'
- : 'Token'
+ ? 'NFT'
+ : 'Token'
}`}
handleOk={createPreviewedToken}
handleCancel={() => setShowConfirmCreateToken(false)}
showCancelButton
height={260}
>
<TokenCreationSummaryTable>
{!createNftCollection && (
<SummaryRow>
<TokenParamLabel>Token Type:</TokenParamLabel>
<TokenParam>
{tokenTypeSwitches.slp ? 'SLP' : 'ALP'}
</TokenParam>
</SummaryRow>
)}
<SummaryRow>
<TokenParamLabel>Name:</TokenParamLabel>
<TokenParam>{formData.name}</TokenParam>
</SummaryRow>
<SummaryRow>
<TokenParamLabel>Ticker:</TokenParamLabel>{' '}
<TokenParam>{formData.ticker}</TokenParam>
</SummaryRow>
{!isNftMint && (
<>
<SummaryRow>
<TokenParamLabel>Decimals:</TokenParamLabel>
<TokenParam>
{' '}
{formData.decimals}
</TokenParam>
</SummaryRow>
<SummaryRow>
<TokenParamLabel>Supply:</TokenParamLabel>
<TokenParam>
{decimalizedTokenQtyToLocaleFormat(
formData.genesisQty,
userLocale,
)}
{createWithMintBaton
? ' (variable)'
: ' (fixed)'}
</TokenParam>
</SummaryRow>
</>
)}
<SummaryRow>
<TokenParamLabel>URL:</TokenParamLabel>
<TokenParam>
{formData.url === ''
? tokenConfig.newTokenDefaultUrl
: formData.url}
</TokenParam>
</SummaryRow>
{createNftCollection ||
(isNftMint && (
<SummaryRow>
<TokenParamLabel>Hash:</TokenParamLabel>
<TokenParam>{formData.hash}</TokenParam>
</SummaryRow>
))}
</TokenCreationSummaryTable>
</Modal>
)}
{!isNftMint && (
<CreateTokenTitle>
Create {createNftCollection ? 'NFT Collection' : 'Token'}
</CreateTokenTitle>
)}
{showTypeInfoSlp && (
<Modal
title={`SLP Tokens`}
height={300}
handleOk={() => setShowTypeInfoSlp(false)}
handleCancel={() => setShowTypeInfoSlp(false)}
>
<TokenTypeDescription>
<TokenInfoParagraph>
SLP v1 fungible token. Token may be of fixed or
variable supply.
</TokenInfoParagraph>
<TokenInfoParagraph>
If you have a mint baton, you can mint more of this
token at any time.
</TokenInfoParagraph>
<TokenInfoParagraph>
May have up to 9 decimal places. SLP txs are limited
to 19 outputs.
</TokenInfoParagraph>
</TokenTypeDescription>
</Modal>
)}
{showTypeInfoAlp && (
<Modal
title={`ALP Tokens`}
height={475}
handleOk={() => setShowTypeInfoAlp(false)}
handleCancel={() => setShowTypeInfoAlp(false)}
>
<TokenTypeDescription>
<TokenInfoParagraph>
ALP v1 fungible token. Token may be of fixed or
variable supply.
</TokenInfoParagraph>
<TokenInfoParagraph>
If you have a mint baton, you can mint more of this
token at any time.
</TokenInfoParagraph>
<TokenInfoParagraph>
May have up to 9 decimal places.
</TokenInfoParagraph>
<TokenInfoParagraph>
ALP tokens use EMPP technology, which supports more
token actions compared to SLP and more complex
combinations of token and app actions.
</TokenInfoParagraph>
<TokenInfoParagraph>
ALP token txs may have up to 127 outputs, though
current OP_RETURN size de facto limits a single tx
to 29 outputs.
</TokenInfoParagraph>
</TokenTypeDescription>
</Modal>
)}
<Form>
- {!createNftCollection && (
+ {!createNftCollection && !isNftMint && (
<>
<SwitchRow>
<Switch
name="Create SLP"
on=""
off=""
checked={tokenTypeSwitches.slp}
handleToggle={() => {
// We can only select one token type at a time
setTokenTypeSwitches({
slp: !tokenTypeSwitches.slp,
alp: !tokenTypeSwitches.alp,
});
}}
/>
<SwitchLabel>SLP</SwitchLabel>
<IconButton
name={`Click for more info about SLP token type`}
icon={<QuestionIcon />}
onClick={() => setShowTypeInfoSlp(true)}
/>
</SwitchRow>
<SwitchRow>
<Switch
name="Create ALP"
on="🏔"
off="🏔"
checked={tokenTypeSwitches.alp}
handleToggle={() => {
// We can only select one token type at a time
setTokenTypeSwitches({
slp: !tokenTypeSwitches.slp,
alp: !tokenTypeSwitches.alp,
});
}}
/>
<SwitchLabel>ALP</SwitchLabel>
<IconButton
name={`Click for more info about ALP token type`}
icon={<QuestionIcon />}
onClick={() => setShowTypeInfoAlp(true)}
/>
</SwitchRow>
</>
)}
<Input
placeholder={`Enter a name for your ${
createNftCollection
? 'NFT collection'
: isNftMint
- ? 'NFT'
- : 'token'
+ ? 'NFT'
+ : 'token'
}`}
name="name"
value={formData.name}
handleInput={handleInput}
error={formDataErrors.name}
/>
<Input
placeholder={`Enter a ticker for your ${
createNftCollection
? 'NFT collection'
: isNftMint
- ? 'NFT'
- : 'token'
+ ? 'NFT'
+ : 'token'
}`}
name="ticker"
value={formData.ticker}
handleInput={handleInput}
error={formDataErrors.ticker}
/>
{!isNftMint && (
<>
<Input
placeholder="Enter number of decimal places"
name="decimals"
type="number"
disabled={createNftCollection}
value={formData.decimals}
handleInput={handleInput}
error={formDataErrors.decimals}
/>
<SendTokenInput
placeholder={`Enter ${
createNftCollection
? 'NFT collection size'
: 'initial token supply'
}`}
name="genesisQty"
value={formData.genesisQty}
decimals={
parseInt(formData.decimals) as SlpDecimals
}
handleInput={handleInput}
error={formDataErrors.genesisQty}
handleOnMax={onMaxGenesis}
/>
</>
)}
<Input
placeholder={`Enter a website for your ${
createNftCollection
? 'NFT collection'
: isNftMint
- ? 'NFT'
- : 'token'
+ ? 'NFT'
+ : 'token'
}`}
name="url"
value={formData.url}
handleInput={handleInput}
error={formDataErrors.url}
/>
{createNftCollection ||
(isNftMint && (
<Input
placeholder={`Upload a jpg or png to generate document hash`}
name="hash"
value={formData.hash}
disabled={true}
handleInput={handleInput}
/>
))}
{!isNftMint && (
<SwitchRow>
<Switch
name="Toggle Mint Baton"
on="Variable"
off="Fixed"
width={110}
right={74}
checked={createWithMintBaton}
handleToggle={() => {
setCreateWithMintBaton(!createWithMintBaton);
}}
/>
<SwitchLabel>
{createNftCollection
? 'NFT Collection Size'
: 'Token supply'}
</SwitchLabel>
</SwitchRow>
)}
<CashtabDragger
name="Cashtab Dragger"
handleFile={validateTokenIconUpload}
imageUrl={imageUrl}
nft={isNftMint}
/>
{!loading && tokenIcon && (
<EditIcon onClick={() => setShowCropModal(true)}>
{tokenIcon.name} [edit]
</EditIcon>
)}
{showCropModal && (
<Modal
handleCancel={onClose}
handleOk={() => {
showCroppedImage();
onClose();
}}
height={400}
>
<IconModalForm>
<CropperContainer>
<Cropper
showGrid={true}
zoomWithScroll={true}
image={rawImageUrl}
crop={crop}
zoom={parseFloat(zoom)}
rotation={parseFloat(rotation)}
cropShape={
roundSelection ? 'round' : 'rect'
}
aspect={1}
onCropChange={
setCrop as (location: Point) => void
}
onRotationChange={
setRotation as unknown as (
rotation: number,
) => void
}
onCropComplete={onCropComplete}
onZoomChange={
setZoom as unknown as (
zoom: number,
) => void
}
/>
</CropperContainer>
<IconModalRow>
<CashtabSwitch
off="Square"
on="Round"
name="cropShape"
width={100}
right={66}
checked={roundSelection}
handleToggle={() =>
setRoundSelection(!roundSelection)
}
/>
</IconModalRow>
<IconModalRow>
<SliderLabel>Zoom:</SliderLabel>
<SliderBox>
<Slider
name="zoom"
value={zoom}
handleSlide={e =>
setZoom(e.target.value)
}
min={1}
max={10}
step={0.01}
/>
</SliderBox>
</IconModalRow>
<IconModalRow>
<SliderLabel>Rotation:</SliderLabel>
<SliderBox>
<Slider
name="rotation"
value={rotation}
handleSlide={e =>
setRotation(e.target.value)
}
min={0}
max={360}
step={1}
/>
</SliderBox>
</IconModalRow>
</IconModalForm>
</Modal>
)}
<PrimaryButton
onClick={() => setShowConfirmCreateToken(true)}
disabled={!tokenGenesisDataIsValid}
style={{ marginTop: '30px', marginBottom: '0px' }}
>
{createNftCollection
? 'Create NFT Collection'
: isNftMint
- ? 'Mint NFT'
- : 'Create eToken'}
+ ? 'Mint NFT'
+ : 'Create eToken'}
</PrimaryButton>
{formData.name === '' ||
(formData.ticker === '' && (
<ButtonDisabledMsg>
{isNftMint
? 'NFT'
: createNftCollection
- ? 'NFT Collection'
- : 'Token'}{' '}
+ ? 'NFT Collection'
+ : 'Token'}{' '}
must have a name and a ticker
</ButtonDisabledMsg>
))}
{tokenIcon !== null &&
tokenIcon.size > ICON_MAX_UPLOAD_BYTES && (
<ButtonDisabledMsg>
Icon exceeds max upload size of{' '}
{ICON_MAX_UPLOAD_BYTES.toLocaleString()} bytes
</ButtonDisabledMsg>
)}
</Form>
</OuterCtn>
);
};
export default CreateTokenForm;
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Mar 2, 09:03 (21 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5187157
Default Alt Text
(50 KB)
Attached To
rSTAGING Bitcoin ABC staging
Event Timeline
Log In to Comment