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
@@ -7,8 +7,10 @@
     FolderOpenFilled,
     CaretRightOutlined,
     SettingFilled,
+    AppstoreAddOutlined,
 } from '@ant-design/icons';
 import Wallet from '@components/Wallet/Wallet';
+import Tokens from '@components/Tokens/Tokens';
 import Send from '@components/Send/Send';
 import SendToken from '@components/Send/SendToken';
 import Configure from '@components/Configure/Configure';
@@ -83,9 +85,15 @@
     cursor: pointer;
     padding: 24px 12px 12px 12px;
     margin: 0 28px;
-    @media (max-width: 360px) {
+    @media (max-width: 475px) {
+        margin: 0 20px;
+    }
+    @media (max-width: 420px) {
         margin: 0 12px;
     }
+    @media (max-width: 350px) {
+        margin: 0 8px;
+    }
     background-color: ${props => props.theme.footer.background};
     border: none;
     font-size: 12px;
@@ -219,6 +227,9 @@
                             <Route path="/wallet">
                                 <Wallet />
                             </Route>
+                            <Route path="/tokens">
+                                <Tokens />
+                            </Route>
                             <Route path="/send">
                                 <Send />
                             </Route>
@@ -247,6 +258,14 @@
                                 <fbt desc="Wallet menu button">Wallet</fbt>
                             </NavButton>
 
+                            <NavButton
+                                active={selectedKey === 'tokens'}
+                                onClick={() => history.push('/tokens')}
+                            >
+                                <AppstoreAddOutlined />
+                                Tokens
+                            </NavButton>
+
                             <NavButton
                                 active={selectedKey === 'send'}
                                 onClick={() => history.push('/send')}
diff --git a/web/cashtab/src/components/Common/Atoms.js b/web/cashtab/src/components/Common/Atoms.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Common/Atoms.js
@@ -0,0 +1,52 @@
+import styled from 'styled-components';
+
+export const LoadingCtn = styled.div`
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 400px;
+    flex-direction: column;
+
+    svg {
+        width: 50px;
+        height: 50px;
+        fill: ${props => props.theme.primary};
+    }
+`;
+
+export const BalanceHeader = styled.div`
+    color: ${props => props.theme.wallet.text.primary};
+    width: 100%;
+    font-size: 30px;
+    font-weight: bold;
+    @media (max-width: 768px) {
+        font-size: 23px;
+    }
+`;
+
+export const BalanceHeaderFiat = styled.div`
+    color: ${props => props.theme.wallet.text.secondary};
+    width: 100%;
+    font-size: 18px;
+    margin-bottom: 20px;
+    font-weight: bold;
+    @media (max-width: 768px) {
+        font-size: 16px;
+    }
+`;
+
+export const ZeroBalanceHeader = styled.div`
+    color: ${props => props.theme.wallet.text.primary};
+    width: 100%;
+    font-size: 14px;
+    margin-bottom: 5px;
+`;
+
+export const TokenParamLabel = styled.span`
+    font-weight: bold;
+`;
+
+export const AlertMsg = styled.p`
+    color: ${props => props.theme.forms.error};
+`;
diff --git a/web/cashtab/src/components/Common/PrimaryButton.js b/web/cashtab/src/components/Common/PrimaryButton.js
--- a/web/cashtab/src/components/Common/PrimaryButton.js
+++ b/web/cashtab/src/components/Common/PrimaryButton.js
@@ -78,10 +78,11 @@
                     -webkit-box-shadow: ${props.theme.buttons.primary.hoverShadow};
                     -moz-box-shadow: ${props.theme.buttons.primary.hoverShadow};
                     box-shadow: ${props.theme.buttons.primary.hoverShadow};
+                }
                 svg {
                     fill: ${props.theme.buttons.primary.color};
                 }
-            }`}
+            `}
 
     border: none;
     transition: all 0.5s ease;
diff --git a/web/cashtab/src/components/Common/StyledCollapse.js b/web/cashtab/src/components/Common/StyledCollapse.js
--- a/web/cashtab/src/components/Common/StyledCollapse.js
+++ b/web/cashtab/src/components/Common/StyledCollapse.js
@@ -14,7 +14,40 @@
         border-bottom: none !important;
     }
 
-    * {
+    *:not(button) {
         color: ${props => props.theme.collapses.color} !important;
     }
 `;
+
+export const TokenCollapse = styled(Collapse)`
+    ${({ disabled = false, ...props }) =>
+        disabled === true
+            ? `
+                background: ${props.theme.buttons.secondary.background} !important;
+           .ant-collapse-header {
+               font-size: 18px;
+               font-weight: bold;
+               color: ${props.theme.buttons.secondary.color} !important;
+               svg {
+                   color: ${props.theme.buttons.secondary.color} !important;
+               }
+           }
+           .ant-collapse-arrow {
+               font-size: 18px;
+           }
+            `
+            : `
+                background: ${props.theme.primary} !important;
+           .ant-collapse-header {
+               font-size: 18px;
+               font-weight: bold;
+               color: ${props.theme.contrast} !important;
+               svg {
+                   color: ${props.theme.contrast} !important;
+               }
+           }
+           .ant-collapse-arrow {
+               font-size: 18px;
+           }
+            `}
+`;
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
@@ -14,6 +14,7 @@
     dust: '0.00000546', // The minimum amount of BCHA that can be sent by the app
     cashDecimals: 8,
     blockExplorerUrl: 'https://explorer.bitcoinabc.org',
+    tokenExplorerUrl: 'https://explorer.be.cash',
     blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org',
     tokenName: 'Bitcoin ABC SLP',
     tokenTicker: 'SLPA',
diff --git a/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledCollapse.test.js.snap b/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledCollapse.test.js.snap
--- a/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledCollapse.test.js.snap
+++ b/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledCollapse.test.js.snap
@@ -1,8 +1,8 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
 
 exports[`Render StyledCollapse component 1`] = `
 <div
-  className="ant-collapse ant-collapse-icon-position-left sc-bdVaJa bWyEBD"
+  className="ant-collapse ant-collapse-icon-position-left sc-bdVaJa AfOYf"
   role={null}
 />
 `;
diff --git a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
--- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
+++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
@@ -1,4 +1,4 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
 
 exports[`Configure with a wallet 1`] = `
 <div
@@ -33,12 +33,12 @@
     className="ant-spin-container ant-spin-blur"
   >
     <div
-      className="sc-kgoBCf dkabgy"
+      className="sc-kGXeez bnGuui"
     >
       <h2>
         <span
           aria-label="copy"
-          className="anticon anticon-copy sc-bwzfXH eCSKEi"
+          className="anticon anticon-copy sc-htpNat bGTymY"
           role="img"
         >
           <svg
@@ -103,7 +103,7 @@
         </div>
       </div>
       <div
-        className="ant-collapse ant-collapse-icon-position-left sc-bdVaJa bWyEBD"
+        className="ant-collapse ant-collapse-icon-position-left sc-bdVaJa AfOYf"
         role={null}
       >
         <div
@@ -141,12 +141,12 @@
         </div>
       </div>
       <div
-        className="sc-kGXeez kTnHvP"
+        className="sc-kpOJdX eglhol"
       />
       <h2>
         <span
           aria-label="wallet"
-          className="anticon anticon-wallet sc-bxivhb gaDgkj"
+          className="anticon anticon-wallet sc-ifAKCX bzXUUf"
           role="img"
         >
           <svg
@@ -166,7 +166,7 @@
          Manage Wallets
       </h2>
       <button
-        className="sc-gqjmRU XGywS"
+        className="sc-VigVT hEOBKE"
         onClick={[Function]}
       >
         <span
@@ -194,7 +194,7 @@
          New Wallet
       </button>
       <button
-        className="sc-VigVT fqMxLW"
+        className="sc-jTzLTM bLkbAy"
         onClick={[Function]}
       >
         <span
@@ -219,11 +219,11 @@
          Import Wallet
       </button>
       <div
-        className="sc-kGXeez kTnHvP"
+        className="sc-kpOJdX eglhol"
       />
       [
       <a
-        className="sc-fjdhpX eJaZHQ"
+        className="sc-jzJRlG erIHmd"
         href="https://docs.cashtabapp.com/docs/"
         rel="noreferrer"
         target="_blank"
@@ -270,12 +270,12 @@
     className="ant-spin-container ant-spin-blur"
   >
     <div
-      className="sc-kgoBCf dkabgy"
+      className="sc-kGXeez bnGuui"
     >
       <h2>
         <span
           aria-label="copy"
-          className="anticon anticon-copy sc-bwzfXH eCSKEi"
+          className="anticon anticon-copy sc-htpNat bGTymY"
           role="img"
         >
           <svg
@@ -340,12 +340,12 @@
         </div>
       </div>
       <div
-        className="sc-kGXeez kTnHvP"
+        className="sc-kpOJdX eglhol"
       />
       <h2>
         <span
           aria-label="wallet"
-          className="anticon anticon-wallet sc-bxivhb gaDgkj"
+          className="anticon anticon-wallet sc-ifAKCX bzXUUf"
           role="img"
         >
           <svg
@@ -365,7 +365,7 @@
          Manage Wallets
       </h2>
       <button
-        className="sc-gqjmRU XGywS"
+        className="sc-VigVT hEOBKE"
         onClick={[Function]}
       >
         <span
@@ -393,7 +393,7 @@
          New Wallet
       </button>
       <button
-        className="sc-VigVT fqMxLW"
+        className="sc-jTzLTM bLkbAy"
         onClick={[Function]}
       >
         <span
@@ -418,11 +418,11 @@
          Import Wallet
       </button>
       <div
-        className="sc-kGXeez kTnHvP"
+        className="sc-kpOJdX eglhol"
       />
       [
       <a
-        className="sc-fjdhpX eJaZHQ"
+        className="sc-jzJRlG erIHmd"
         href="https://docs.cashtabapp.com/docs/"
         rel="noreferrer"
         target="_blank"
diff --git a/web/cashtab/src/components/Tokens/CreateTokenForm.js b/web/cashtab/src/components/Tokens/CreateTokenForm.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Tokens/CreateTokenForm.js
@@ -0,0 +1,372 @@
+import React, { useState } from 'react';
+import { AntdFormWrapper } from '@components/Common/EnhancedInputs';
+import { TokenCollapse } from '@components/Common/StyledCollapse';
+import { currency } from '@components/Common/Ticker.js';
+import { WalletContext } from '@utils/context';
+import {
+    isValidTokenName,
+    isValidTokenTicker,
+    isValidTokenDecimals,
+    isValidTokenInitialQty,
+    isValidTokenDocumentUrl,
+} from '@utils/validation';
+import { PlusSquareOutlined } from '@ant-design/icons';
+import { SmartButton } from '@components/Common/PrimaryButton';
+import { Collapse, Form, Input, Modal, notification, Spin } from 'antd';
+const { Panel } = Collapse;
+import Paragraph from 'antd/lib/typography/Paragraph';
+import { TokenParamLabel } from '@components/Common/Atoms';
+
+import { CashLoadingIcon } from '@components/Common/CustomIcons';
+
+const CreateTokenForm = ({ BCH, getRestUrl, createToken, disabled }) => {
+    const { wallet } = React.useContext(WalletContext);
+
+    //const { getBCH, getRestUrl, createToken } = useBCH();
+
+    // New Token Name
+    const [newTokenName, setNewTokenName] = useState('');
+    const [newTokenNameIsValid, setNewTokenNameIsValid] = useState(null);
+    const handleNewTokenNameInput = e => {
+        const { value } = e.target;
+        // validation
+        setNewTokenNameIsValid(isValidTokenName(value));
+        setNewTokenName(value);
+    };
+
+    // New Token Ticker
+    const [newTokenTicker, setNewTokenTicker] = useState('');
+    const [newTokenTickerIsValid, setNewTokenTickerIsValid] = useState(null);
+    const handleNewTokenTickerInput = e => {
+        const { value } = e.target;
+        // validation
+        setNewTokenTickerIsValid(isValidTokenTicker(value));
+        setNewTokenTicker(value);
+    };
+
+    // New Token Decimals
+    const [newTokenDecimals, setNewTokenDecimals] = useState(0);
+    const [newTokenDecimalsIsValid, setNewTokenDecimalsIsValid] = useState(
+        true,
+    );
+    const handleNewTokenDecimalsInput = e => {
+        const { value } = e.target;
+        // validation
+        setNewTokenDecimalsIsValid(isValidTokenDecimals(value));
+        // Also validate the supply here if it has not yet been set
+        if (newTokenInitialQtyIsValid !== null) {
+            setNewTokenInitialQtyIsValid(
+                isValidTokenInitialQty(value, newTokenDecimals),
+            );
+        }
+
+        setNewTokenDecimals(value);
+    };
+
+    // New Token Initial Quantity
+    const [newTokenInitialQty, setNewTokenInitialQty] = useState('');
+    const [newTokenInitialQtyIsValid, setNewTokenInitialQtyIsValid] = useState(
+        null,
+    );
+    const handleNewTokenInitialQtyInput = e => {
+        const { value } = e.target;
+        // validation
+        setNewTokenInitialQtyIsValid(
+            isValidTokenInitialQty(value, newTokenDecimals),
+        );
+        setNewTokenInitialQty(value);
+    };
+    // New Token document URL
+    const [newTokenDocumentUrl, setNewTokenDocumentUrl] = useState('');
+    // Start with this as true, field is not required
+    const [
+        newTokenDocumentUrlIsValid,
+        setNewTokenDocumentUrlIsValid,
+    ] = useState(true);
+
+    const handleNewTokenDocumentUrlInput = e => {
+        const { value } = e.target;
+        // validation
+        setNewTokenDocumentUrlIsValid(isValidTokenDocumentUrl(value));
+        setNewTokenDocumentUrl(value);
+    };
+
+    // New Token fixed supply
+    // Only allow creation of fixed supply tokens until Minting support is added
+
+    // New Token document hash
+    // Do not include this; questionable value to casual users and requires significant complication
+
+    // Only enable CreateToken button if all form entries are valid
+    let tokenGenesisDataIsValid =
+        newTokenNameIsValid &&
+        newTokenTickerIsValid &&
+        newTokenDecimalsIsValid &&
+        newTokenInitialQtyIsValid &&
+        newTokenDocumentUrlIsValid;
+
+    // Modal settings
+    const [showConfirmCreateToken, setShowConfirmCreateToken] = useState(false);
+
+    // Token creation loading
+    const [genesisLoading, setGenesisLoading] = useState(false);
+
+    const createPreviewedToken = async () => {
+        setGenesisLoading(true);
+        // 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 configObj = {
+            name: newTokenName,
+            ticker: newTokenTicker,
+            documentUrl:
+                newTokenDocumentUrl === ''
+                    ? 'https://cashtabapp.com/'
+                    : newTokenDocumentUrl,
+            decimals: newTokenDecimals,
+            initialQty: newTokenInitialQty,
+            documentHash: '',
+        };
+
+        // create token with data in state fields
+        try {
+            const link = await createToken(
+                BCH,
+                wallet,
+                currency.defaultFee,
+                configObj,
+            );
+
+            notification.success({
+                message: 'Success',
+                description: (
+                    <a href={link} target="_blank" rel="noopener noreferrer">
+                        <Paragraph>
+                            Token created! Click or tap here for more details
+                        </Paragraph>
+                    </a>
+                ),
+                duration: 5,
+            });
+        } catch (e) {
+            // Set loading to false here as well, as balance may not change depending on where error occured in try loop
+            setGenesisLoading(false);
+            let message;
+
+            if (!e.error && !e.message) {
+                message = `Transaction failed: no response from ${getRestUrl()}.`;
+            } else if (
+                /Could not communicate with full node or other external service/.test(
+                    e.error,
+                )
+            ) {
+                message = 'Could not communicate with API. Please try again.';
+            } else if (
+                e.error &&
+                e.error.includes(
+                    'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)',
+                )
+            ) {
+                message = `The ${currency.ticker} you are trying to send has too many unconfirmed ancestors to send (limit 50). Sending will be possible after a block confirmation. Try again in about 10 minutes.`;
+            } else {
+                message = e.message || e.error || JSON.stringify(e);
+            }
+
+            notification.error({
+                message: 'Error',
+                description: message,
+                duration: 5,
+            });
+            console.error(e);
+        }
+        // Hide the modal
+        setShowConfirmCreateToken(false);
+    };
+    return (
+        <>
+            <Modal
+                title={`Please review and confirm your token settings.`}
+                visible={showConfirmCreateToken}
+                onOk={createPreviewedToken}
+                onCancel={() => setShowConfirmCreateToken(false)}
+            >
+                <TokenParamLabel>Name:</TokenParamLabel> {newTokenName}
+                <br />
+                <TokenParamLabel>Ticker:</TokenParamLabel> {newTokenTicker}
+                <br />
+                <TokenParamLabel>Decimals:</TokenParamLabel> {newTokenDecimals}
+                <br />
+                <TokenParamLabel>Supply:</TokenParamLabel> {newTokenInitialQty}
+                <br />
+                <TokenParamLabel>Document URL:</TokenParamLabel>{' '}
+                {newTokenDocumentUrl === ''
+                    ? 'https://cashtabapp.com/'
+                    : newTokenDocumentUrl}
+                <br />
+            </Modal>
+            <>
+                <Spin spinning={genesisLoading} indicator={CashLoadingIcon}>
+                    <TokenCollapse
+                        collapsible={disabled ? 'disabled' : true}
+                        disabled={disabled}
+                        style={{
+                            marginBottom: '24px',
+                        }}
+                    >
+                        <Panel header="Create Token" key="1">
+                            <AntdFormWrapper>
+                                <Form
+                                    size="small"
+                                    style={{
+                                        width: 'auto',
+                                    }}
+                                >
+                                    <Form.Item
+                                        validateStatus={
+                                            newTokenNameIsValid === null ||
+                                            newTokenNameIsValid
+                                                ? ''
+                                                : 'error'
+                                        }
+                                        help={
+                                            newTokenNameIsValid === null ||
+                                            newTokenNameIsValid
+                                                ? ''
+                                                : 'Token name must be a string between 1 and 68 characters long'
+                                        }
+                                    >
+                                        <Input
+                                            addonBefore="Name"
+                                            placeholder="Enter a name for your token"
+                                            name="newTokenName"
+                                            value={newTokenName}
+                                            onChange={e =>
+                                                handleNewTokenNameInput(e)
+                                            }
+                                        />
+                                    </Form.Item>
+                                    <Form.Item
+                                        validateStatus={
+                                            newTokenTickerIsValid === null ||
+                                            newTokenTickerIsValid
+                                                ? ''
+                                                : 'error'
+                                        }
+                                        help={
+                                            newTokenTickerIsValid === null ||
+                                            newTokenTickerIsValid
+                                                ? ''
+                                                : 'Ticker must be a string between 1 and 12 characters long'
+                                        }
+                                    >
+                                        <Input
+                                            addonBefore="Ticker"
+                                            placeholder="Enter a ticker for your token"
+                                            name="newTokenTicker"
+                                            value={newTokenTicker}
+                                            onChange={e =>
+                                                handleNewTokenTickerInput(e)
+                                            }
+                                        />
+                                    </Form.Item>
+                                    <Form.Item
+                                        validateStatus={
+                                            newTokenDecimalsIsValid === null ||
+                                            newTokenDecimalsIsValid
+                                                ? ''
+                                                : 'error'
+                                        }
+                                        help={
+                                            newTokenDecimalsIsValid === null ||
+                                            newTokenDecimalsIsValid
+                                                ? ''
+                                                : 'Token decimals must be an integer between 0 and 9'
+                                        }
+                                    >
+                                        <Input
+                                            addonBefore="Decimals"
+                                            placeholder="Enter number of decimal places"
+                                            name="newTokenDecimals"
+                                            type="number"
+                                            value={newTokenDecimals}
+                                            onChange={e =>
+                                                handleNewTokenDecimalsInput(e)
+                                            }
+                                        />
+                                    </Form.Item>
+                                    <Form.Item
+                                        validateStatus={
+                                            newTokenInitialQtyIsValid ===
+                                                null ||
+                                            newTokenInitialQtyIsValid
+                                                ? ''
+                                                : 'error'
+                                        }
+                                        help={
+                                            newTokenInitialQtyIsValid ===
+                                                null ||
+                                            newTokenInitialQtyIsValid
+                                                ? ''
+                                                : 'Token supply must be greater than 0 and less than 100,000,000,000. Token supply decimal places cannot exceed token decimal places.'
+                                        }
+                                    >
+                                        <Input
+                                            addonBefore="Supply"
+                                            placeholder="Enter the fixed supply of your token"
+                                            name="newTokenInitialQty"
+                                            type="number"
+                                            value={newTokenInitialQty}
+                                            onChange={e =>
+                                                handleNewTokenInitialQtyInput(e)
+                                            }
+                                        />
+                                    </Form.Item>
+                                    <Form.Item
+                                        validateStatus={
+                                            newTokenDocumentUrlIsValid ===
+                                                null ||
+                                            newTokenDocumentUrlIsValid
+                                                ? ''
+                                                : 'error'
+                                        }
+                                        help={
+                                            newTokenDocumentUrlIsValid ===
+                                                null ||
+                                            newTokenDocumentUrlIsValid
+                                                ? ''
+                                                : 'Document URL cannot exceed 68 characters'
+                                        }
+                                    >
+                                        <Input
+                                            addonBefore="Document URL"
+                                            placeholder="Enter a website for your token"
+                                            name="newTokenDocumentUrl"
+                                            value={newTokenDocumentUrl}
+                                            onChange={e =>
+                                                handleNewTokenDocumentUrlInput(
+                                                    e,
+                                                )
+                                            }
+                                        />
+                                    </Form.Item>
+                                </Form>
+                            </AntdFormWrapper>
+                            <SmartButton
+                                onClick={() => setShowConfirmCreateToken(true)}
+                                disabled={!tokenGenesisDataIsValid}
+                            >
+                                <PlusSquareOutlined />
+                                &nbsp;Create Token
+                            </SmartButton>
+                        </Panel>
+                    </TokenCollapse>
+                </Spin>
+            </>
+        </>
+    );
+};
+
+export default CreateTokenForm;
diff --git a/web/cashtab/src/components/Tokens/Tokens.js b/web/cashtab/src/components/Tokens/Tokens.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Tokens/Tokens.js
@@ -0,0 +1,131 @@
+import React from 'react';
+import { LoadingOutlined } from '@ant-design/icons';
+import { CashLoader } from '@components/Common/CustomIcons';
+import { WalletContext } from '@utils/context';
+import { formatBalance, isValidStoredWallet } from '@utils/cashMethods';
+import CreateTokenForm from '@components/Tokens/CreateTokenForm';
+import { currency } from '@components/Common/Ticker.js';
+import TokenList from '@components/Wallet/TokenList';
+import useBCH from '@hooks/useBCH';
+import {
+    LoadingCtn,
+    BalanceHeader,
+    BalanceHeaderFiat,
+    ZeroBalanceHeader,
+    AlertMsg,
+} from '@components/Common/Atoms';
+
+const Tokens = ({ jestBCH }) => {
+    /*
+    Dev note
+
+    This is the first new page created after the wallet migration to include state in storage
+
+    As such, it will only load this type of wallet
+
+    If any user is still migrating at this point, this page will display a loading spinner until
+    their wallet has updated (ETA within 10 seconds)
+
+    Going forward, this approach will be the model for Wallet, Send, and SendToken, as the legacy
+    wallet state parameters not stored in the wallet object are deprecated
+    */
+
+    const { loading, wallet, apiError, fiatPrice } = React.useContext(
+        WalletContext,
+    );
+
+    // If wallet is unmigrated, do not show page until it has migrated
+    // An invalid wallet will be validated/populated after the next API call, ETA 10s
+    let validWallet = isValidStoredWallet(wallet);
+
+    // Get wallet state variables
+    let balances, tokens;
+    if (validWallet) {
+        balances = wallet.state.balances;
+        tokens = wallet.state.tokens;
+    }
+
+    const { getBCH, getRestUrl, createToken } = useBCH();
+
+    // Support using locally installed bchjs for unit tests
+    const BCH = jestBCH ? jestBCH : getBCH();
+    return (
+        <>
+            {loading || !validWallet ? (
+                <LoadingCtn>
+                    <LoadingOutlined />
+                </LoadingCtn>
+            ) : (
+                <>
+                    {!balances.totalBalance ? (
+                        <>
+                            <ZeroBalanceHeader>
+                                You need some {currency.ticker} in your wallet
+                                to create tokens.
+                            </ZeroBalanceHeader>
+                            <BalanceHeader>0 {currency.ticker}</BalanceHeader>
+                        </>
+                    ) : (
+                        <>
+                            <BalanceHeader>
+                                {formatBalance(balances.totalBalance)}{' '}
+                                {currency.ticker}
+                            </BalanceHeader>
+                            {fiatPrice !== null &&
+                                !isNaN(balances.totalBalance) && (
+                                    <BalanceHeaderFiat>
+                                        $
+                                        {(
+                                            balances.totalBalance * fiatPrice
+                                        ).toFixed(2)}{' '}
+                                        USD
+                                    </BalanceHeaderFiat>
+                                )}
+                        </>
+                    )}
+                    {apiError && (
+                        <>
+                            <p
+                                style={{
+                                    color: 'red',
+                                }}
+                            >
+                                <b>An error occurred on our end.</b>
+                                <br></br> Re-establishing connection...
+                            </p>
+                            <CashLoader />
+                        </>
+                    )}
+                    <CreateTokenForm
+                        BCH={BCH}
+                        getRestUrl={getRestUrl}
+                        createToken={createToken}
+                        disabled={balances.totalBalanceInSatoshis < 546}
+                    />
+                    {balances.totalBalanceInSatoshis < 546 && (
+                        <AlertMsg>
+                            You need at least {currency.dust} {currency.ticker}{' '}
+                            ($
+                            {(currency.dust * fiatPrice).toFixed(4)} USD) to
+                            create a token
+                        </AlertMsg>
+                    )}
+
+                    {tokens && tokens.length > 0 ? (
+                        <>
+                            <TokenList
+                                wallet={wallet}
+                                tokens={tokens}
+                                jestBCH={false}
+                            />
+                        </>
+                    ) : (
+                        <>No {currency.tokenTicker} tokens in this wallet</>
+                    )}
+                </>
+            )}
+        </>
+    );
+};
+
+export default Tokens;
diff --git a/web/cashtab/src/components/Tokens/__tests__/CreateTokenForm.test.js b/web/cashtab/src/components/Tokens/__tests__/CreateTokenForm.test.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Tokens/__tests__/CreateTokenForm.test.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { ThemeProvider } from 'styled-components';
+import { theme } from '@assets/styles/theme';
+import CreateTokenForm from '@components/Tokens/CreateTokenForm';
+import BCHJS from '@psf/bch-js';
+import useBCH from '@hooks/useBCH';
+import { walletWithBalancesAndTokensWithCorrectState } from '../../Wallet/__mocks__/walletAndBalancesMock';
+
+let useContextMock;
+let realUseContext;
+
+beforeEach(() => {
+    realUseContext = React.useContext;
+    useContextMock = React.useContext = jest.fn();
+
+    // Mock method not implemented in JSDOM
+    // See reference at https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
+    Object.defineProperty(window, 'matchMedia', {
+        writable: true,
+        value: jest.fn().mockImplementation(query => ({
+            matches: false,
+            media: query,
+            onchange: null,
+            addListener: jest.fn(), // Deprecated
+            removeListener: jest.fn(), // Deprecated
+            addEventListener: jest.fn(),
+            removeEventListener: jest.fn(),
+            dispatchEvent: jest.fn(),
+        })),
+    });
+});
+
+afterEach(() => {
+    React.useContext = realUseContext;
+});
+
+test('Wallet with BCH balances and tokens and state field', () => {
+    useContextMock.mockReturnValue(walletWithBalancesAndTokensWithCorrectState);
+
+    const testBCH = new BCHJS();
+    const { getRestUrl, createToken } = useBCH();
+    const component = renderer.create(
+        <ThemeProvider theme={theme}>
+            <CreateTokenForm
+                BCH={testBCH}
+                getRestUrl={getRestUrl}
+                createToken={createToken}
+                disabled={
+                    walletWithBalancesAndTokensWithCorrectState.wallet.state
+                        .balances.totalBalanceInSatoshis < 546
+                }
+            />
+        </ThemeProvider>,
+    );
+    let tree = component.toJSON();
+    expect(tree).toMatchSnapshot();
+});
diff --git a/web/cashtab/src/components/Tokens/__tests__/Tokens.test.js b/web/cashtab/src/components/Tokens/__tests__/Tokens.test.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Tokens/__tests__/Tokens.test.js
@@ -0,0 +1,130 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { ThemeProvider } from 'styled-components';
+import { theme } from '@assets/styles/theme';
+import Tokens from '@components/Tokens/Tokens';
+import BCHJS from '@psf/bch-js';
+import {
+    walletWithBalancesAndTokens,
+    walletWithBalancesMock,
+    walletWithoutBalancesMock,
+    walletWithBalancesAndTokensWithCorrectState,
+    walletWithBalancesAndTokensWithEmptyState,
+} from '../../Wallet/__mocks__/walletAndBalancesMock';
+import { BrowserRouter as Router } from 'react-router-dom';
+
+let realUseContext;
+let useContextMock;
+
+beforeEach(() => {
+    realUseContext = React.useContext;
+    useContextMock = React.useContext = jest.fn();
+
+    // Mock method not implemented in JSDOM
+    // See reference at https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
+    Object.defineProperty(window, 'matchMedia', {
+        writable: true,
+        value: jest.fn().mockImplementation(query => ({
+            matches: false,
+            media: query,
+            onchange: null,
+            addListener: jest.fn(), // Deprecated
+            removeListener: jest.fn(), // Deprecated
+            addEventListener: jest.fn(),
+            removeEventListener: jest.fn(),
+            dispatchEvent: jest.fn(),
+        })),
+    });
+});
+
+afterEach(() => {
+    React.useContext = realUseContext;
+});
+
+test('Wallet without BCH balance', () => {
+    useContextMock.mockReturnValue(walletWithoutBalancesMock);
+    const testBCH = new BCHJS();
+    const component = renderer.create(
+        <ThemeProvider theme={theme}>
+            <Router>
+                <Tokens jestBCH={testBCH} />
+            </Router>
+        </ThemeProvider>,
+    );
+    let tree = component.toJSON();
+    expect(tree).toMatchSnapshot();
+});
+
+test('Wallet with BCH balances', () => {
+    useContextMock.mockReturnValue(walletWithBalancesMock);
+    const testBCH = new BCHJS();
+    const component = renderer.create(
+        <ThemeProvider theme={theme}>
+            <Router>
+                <Tokens jestBCH={testBCH} />
+            </Router>
+        </ThemeProvider>,
+    );
+    let tree = component.toJSON();
+    expect(tree).toMatchSnapshot();
+});
+
+test('Wallet with BCH balances and tokens', () => {
+    useContextMock.mockReturnValue(walletWithBalancesAndTokens);
+    const testBCH = new BCHJS();
+    const component = renderer.create(
+        <ThemeProvider theme={theme}>
+            <Router>
+                <Tokens jestBCH={testBCH} />
+            </Router>
+        </ThemeProvider>,
+    );
+    let tree = component.toJSON();
+    expect(tree).toMatchSnapshot();
+});
+
+test('Wallet with BCH balances and tokens and state field', () => {
+    useContextMock.mockReturnValue(walletWithBalancesAndTokensWithCorrectState);
+    const testBCH = new BCHJS();
+    const component = renderer.create(
+        <ThemeProvider theme={theme}>
+            <Router>
+                <Tokens jestBCH={testBCH} />
+            </Router>
+        </ThemeProvider>,
+    );
+    let tree = component.toJSON();
+    expect(tree).toMatchSnapshot();
+});
+
+test('Wallet with BCH balances and tokens and state field, but no params in state', () => {
+    useContextMock.mockReturnValue(walletWithBalancesAndTokensWithEmptyState);
+    const testBCH = new BCHJS();
+    const component = renderer.create(
+        <ThemeProvider theme={theme}>
+            <Router>
+                <Tokens jestBCH={testBCH} />
+            </Router>
+        </ThemeProvider>,
+    );
+    let tree = component.toJSON();
+    expect(tree).toMatchSnapshot();
+});
+
+test('Without wallet defined', () => {
+    useContextMock.mockReturnValue({
+        wallet: {},
+        balances: { totalBalance: 0 },
+        loading: false,
+    });
+    const testBCH = new BCHJS();
+    const component = renderer.create(
+        <ThemeProvider theme={theme}>
+            <Router>
+                <Tokens jestBCH={testBCH} />
+            </Router>
+        </ThemeProvider>,
+    );
+    let tree = component.toJSON();
+    expect(tree).toMatchSnapshot();
+});
diff --git a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/CreateTokenForm.test.js.snap
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
+
+exports[`Wallet with BCH balances and tokens and state field 1`] = `
+<div
+  className="ant-spin-nested-loading"
+>
+  <div
+    className="ant-spin-container"
+  >
+    <div
+      className="ant-collapse ant-collapse-icon-position-left sc-gqjmRU hqANsM"
+      role={null}
+      style={
+        Object {
+          "marginBottom": "24px",
+        }
+      }
+    >
+      <div
+        className="ant-collapse-item"
+      >
+        <div
+          aria-expanded={false}
+          className="ant-collapse-header"
+          onClick={[Function]}
+          onKeyPress={[Function]}
+          role="button"
+          tabIndex={0}
+        >
+          <span
+            aria-label="right"
+            className="anticon anticon-right ant-collapse-arrow"
+            role="img"
+          >
+            <svg
+              aria-hidden="true"
+              data-icon="right"
+              fill="currentColor"
+              focusable="false"
+              height="1em"
+              viewBox="64 64 896 896"
+              width="1em"
+            >
+              <path
+                d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
+              />
+            </svg>
+          </span>
+          Create Token
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
@@ -0,0 +1,157 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
+
+exports[`Wallet with BCH balances 1`] = `
+<div
+  className="sc-jzJRlG iglgzj"
+>
+  <span
+    aria-label="loading"
+    className="anticon anticon-loading anticon-spin"
+    role="img"
+  >
+    <svg
+      aria-hidden="true"
+      data-icon="loading"
+      fill="currentColor"
+      focusable="false"
+      height="1em"
+      viewBox="0 0 1024 1024"
+      width="1em"
+    >
+      <path
+        d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
+      />
+    </svg>
+  </span>
+</div>
+`;
+
+exports[`Wallet with BCH balances and tokens 1`] = `
+<div
+  className="sc-jzJRlG iglgzj"
+>
+  <span
+    aria-label="loading"
+    className="anticon anticon-loading anticon-spin"
+    role="img"
+  >
+    <svg
+      aria-hidden="true"
+      data-icon="loading"
+      fill="currentColor"
+      focusable="false"
+      height="1em"
+      viewBox="0 0 1024 1024"
+      width="1em"
+    >
+      <path
+        d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
+      />
+    </svg>
+  </span>
+</div>
+`;
+
+exports[`Wallet with BCH balances and tokens and state field 1`] = `
+<div
+  className="sc-jzJRlG iglgzj"
+>
+  <span
+    aria-label="loading"
+    className="anticon anticon-loading anticon-spin"
+    role="img"
+  >
+    <svg
+      aria-hidden="true"
+      data-icon="loading"
+      fill="currentColor"
+      focusable="false"
+      height="1em"
+      viewBox="0 0 1024 1024"
+      width="1em"
+    >
+      <path
+        d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
+      />
+    </svg>
+  </span>
+</div>
+`;
+
+exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = `
+<div
+  className="sc-jzJRlG iglgzj"
+>
+  <span
+    aria-label="loading"
+    className="anticon anticon-loading anticon-spin"
+    role="img"
+  >
+    <svg
+      aria-hidden="true"
+      data-icon="loading"
+      fill="currentColor"
+      focusable="false"
+      height="1em"
+      viewBox="0 0 1024 1024"
+      width="1em"
+    >
+      <path
+        d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
+      />
+    </svg>
+  </span>
+</div>
+`;
+
+exports[`Wallet without BCH balance 1`] = `
+<div
+  className="sc-jzJRlG iglgzj"
+>
+  <span
+    aria-label="loading"
+    className="anticon anticon-loading anticon-spin"
+    role="img"
+  >
+    <svg
+      aria-hidden="true"
+      data-icon="loading"
+      fill="currentColor"
+      focusable="false"
+      height="1em"
+      viewBox="0 0 1024 1024"
+      width="1em"
+    >
+      <path
+        d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
+      />
+    </svg>
+  </span>
+</div>
+`;
+
+exports[`Without wallet defined 1`] = `
+<div
+  className="sc-jzJRlG iglgzj"
+>
+  <span
+    aria-label="loading"
+    className="anticon anticon-loading anticon-spin"
+    role="img"
+  >
+    <svg
+      aria-hidden="true"
+      data-icon="loading"
+      fill="currentColor"
+      focusable="false"
+      height="1em"
+      viewBox="0 0 1024 1024"
+      width="1em"
+    >
+      <path
+        d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
+      />
+    </svg>
+  </span>
+</div>
+`;
diff --git a/web/cashtab/src/components/Wallet/Tx.js b/web/cashtab/src/components/Wallet/Tx.js
--- a/web/cashtab/src/components/Wallet/Tx.js
+++ b/web/cashtab/src/components/Wallet/Tx.js
@@ -1,6 +1,10 @@
 import React from 'react';
 import styled from 'styled-components';
-import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
+import {
+    ArrowUpOutlined,
+    ArrowDownOutlined,
+    ExperimentOutlined,
+} from '@ant-design/icons';
 import { currency } from '@components/Common/Ticker';
 import makeBlockie from 'ethereum-blockies-base64';
 import { Img } from 'react-image';
@@ -11,6 +15,9 @@
 const ReceivedTx = styled(ArrowDownOutlined)`
     color: ${props => props.theme.primary} !important;
 `;
+const GenesisTx = styled(ExperimentOutlined)`
+    color: ${props => props.theme.primary} !important;
+`;
 const DateType = styled.div`
     text-align: left;
     padding: 12px;
@@ -141,10 +148,30 @@
 
     return (
         <TxWrapper>
-            <TxIcon>{data.outgoingTx ? <SentTx /> : <ReceivedTx />}</TxIcon>
+            <TxIcon>
+                {data.outgoingTx ? (
+                    <>
+                        {data.tokenTx &&
+                        data.tokenInfo.transactionType === 'GENESIS' ? (
+                            <GenesisTx />
+                        ) : (
+                            <SentTx />
+                        )}
+                    </>
+                ) : (
+                    <ReceivedTx />
+                )}
+            </TxIcon>
             <DateType>
                 {data.outgoingTx ? (
-                    <SentLabel>Sent</SentLabel>
+                    <>
+                        {data.tokenTx &&
+                        data.tokenInfo.transactionType === 'GENESIS' ? (
+                            <ReceivedLabel>Genesis</ReceivedLabel>
+                        ) : (
+                            <SentLabel>Sent</SentLabel>
+                        )}
+                    </>
                 ) : (
                     <ReceivedLabel>Received</ReceivedLabel>
                 )}
@@ -187,13 +214,32 @@
                             </TxTokenIcon>
                             {data.outgoingTx ? (
                                 <>
-                                    <TokenTxAmt>
-                                        - {data.tokenInfo.qtySent.toString()}
-                                        &nbsp;{data.tokenInfo.tokenTicker}
-                                    </TokenTxAmt>
-                                    <TokenName>
-                                        {data.tokenInfo.tokenName}
-                                    </TokenName>
+                                    {data.tokenInfo.transactionType ===
+                                    'GENESIS' ? (
+                                        <>
+                                            <TokenTxAmt>
+                                                +{' '}
+                                                {data.tokenInfo.qtyReceived.toString()}
+                                                &nbsp;
+                                                {data.tokenInfo.tokenTicker}
+                                            </TokenTxAmt>
+                                            <TokenName>
+                                                {data.tokenInfo.tokenName}
+                                            </TokenName>
+                                        </>
+                                    ) : (
+                                        <>
+                                            <TokenTxAmt>
+                                                -{' '}
+                                                {data.tokenInfo.qtySent.toString()}
+                                                &nbsp;
+                                                {data.tokenInfo.tokenTicker}
+                                            </TokenTxAmt>
+                                            <TokenName>
+                                                {data.tokenInfo.tokenName}
+                                            </TokenName>
+                                        </>
+                                    )}
                                 </>
                             ) : (
                                 <>
diff --git a/web/cashtab/src/components/Wallet/Wallet.js b/web/cashtab/src/components/Wallet/Wallet.js
--- a/web/cashtab/src/components/Wallet/Wallet.js
+++ b/web/cashtab/src/components/Wallet/Wallet.js
@@ -10,49 +10,12 @@
 import TxHistory from './TxHistory';
 import { CashLoader } from '@components/Common/CustomIcons';
 import { formatBalance } from '@utils/cashMethods';
-
-export const LoadingCtn = styled.div`
-    width: 100%;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    height: 400px;
-    flex-direction: column;
-
-    svg {
-        width: 50px;
-        height: 50px;
-        fill: ${props => props.theme.primary};
-    }
-`;
-
-export const BalanceHeader = styled.div`
-    color: ${props => props.theme.wallet.text.primary};
-    width: 100%;
-    font-size: 30px;
-    font-weight: bold;
-    @media (max-width: 768px) {
-        font-size: 23px;
-    }
-`;
-
-export const BalanceHeaderFiat = styled.div`
-    color: ${props => props.theme.wallet.text.secondary};
-    width: 100%;
-    font-size: 18px;
-    margin-bottom: 20px;
-    font-weight: bold;
-    @media (max-width: 768px) {
-        font-size: 16px;
-    }
-`;
-
-export const ZeroBalanceHeader = styled.div`
-    color: ${props => props.theme.wallet.text.primary};
-    width: 100%;
-    font-size: 14px;
-    margin-bottom: 5px;
-`;
+import {
+    LoadingCtn,
+    BalanceHeader,
+    BalanceHeaderFiat,
+    ZeroBalanceHeader,
+} from '@components/Common/Atoms';
 
 export const Tabs = styled.div`
     margin: auto;
@@ -357,7 +320,11 @@
                     </TabPane>
                     <TabPane active={activeTab === 'tokens'}>
                         {tokens && tokens.length > 0 ? (
-                            <TokenList tokens={tokens} />
+                            <TokenList
+                                wallet={wallet}
+                                tokens={tokens}
+                                jestBCH={false}
+                            />
                         ) : (
                             <p>
                                 Tokens sent to your {currency.tokenTicker}{' '}
diff --git a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
--- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
+++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
@@ -3,14 +3,14 @@
 exports[`Wallet with BCH balances 1`] = `
 Array [
   <div
-    className="sc-jAaTju dsaCuU"
+    className="sc-jDwBTQ giRFkr"
   >
     0.06047469
      
     BCHA
   </div>,
   <div
-    className="sc-jDwBTQ hhosDq"
+    className="sc-gPEVay bgrZDN"
   >
     $
     NaN
@@ -82,16 +82,16 @@
     </div>
   </div>,
   <div
-    className="sc-Rmtcm ldTlYI"
+    className="sc-jhAzac vWMwd"
   >
     <div
-      className="sc-bRBYWo cvfjTr"
+      className="sc-fBuWsC jGeMc"
       onClick={[Function]}
     >
       BCHA
     </div>
     <div
-      className="sc-bRBYWo cvfjTr nonactiveBtn"
+      className="sc-fBuWsC jGeMc nonactiveBtn"
       onClick={[Function]}
     >
       SLPA
@@ -103,14 +103,14 @@
 exports[`Wallet with BCH balances and tokens 1`] = `
 Array [
   <div
-    className="sc-jAaTju dsaCuU"
+    className="sc-jDwBTQ giRFkr"
   >
     0.06047469
      
     BCHA
   </div>,
   <div
-    className="sc-jDwBTQ hhosDq"
+    className="sc-gPEVay bgrZDN"
   >
     $
     NaN
@@ -182,16 +182,16 @@
     </div>
   </div>,
   <div
-    className="sc-Rmtcm ldTlYI"
+    className="sc-jhAzac vWMwd"
   >
     <div
-      className="sc-bRBYWo cvfjTr"
+      className="sc-fBuWsC jGeMc"
       onClick={[Function]}
     >
       BCHA
     </div>
     <div
-      className="sc-bRBYWo cvfjTr nonactiveBtn"
+      className="sc-fBuWsC jGeMc nonactiveBtn"
       onClick={[Function]}
     >
       SLPA
@@ -203,14 +203,14 @@
 exports[`Wallet with BCH balances and tokens and state field 1`] = `
 Array [
   <div
-    className="sc-jAaTju dsaCuU"
+    className="sc-jDwBTQ giRFkr"
   >
     0.06047469
      
     BCHA
   </div>,
   <div
-    className="sc-jDwBTQ hhosDq"
+    className="sc-gPEVay bgrZDN"
   >
     $
     NaN
@@ -282,16 +282,16 @@
     </div>
   </div>,
   <div
-    className="sc-Rmtcm ldTlYI"
+    className="sc-jhAzac vWMwd"
   >
     <div
-      className="sc-bRBYWo cvfjTr"
+      className="sc-fBuWsC jGeMc"
       onClick={[Function]}
     >
       BCHA
     </div>
     <div
-      className="sc-bRBYWo cvfjTr nonactiveBtn"
+      className="sc-fBuWsC jGeMc nonactiveBtn"
       onClick={[Function]}
     >
       SLPA
@@ -303,14 +303,14 @@
 exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = `
 Array [
   <div
-    className="sc-jAaTju dsaCuU"
+    className="sc-jDwBTQ giRFkr"
   >
     0.06047469
      
     BCHA
   </div>,
   <div
-    className="sc-jDwBTQ hhosDq"
+    className="sc-gPEVay bgrZDN"
   >
     $
     NaN
@@ -382,16 +382,16 @@
     </div>
   </div>,
   <div
-    className="sc-Rmtcm ldTlYI"
+    className="sc-jhAzac vWMwd"
   >
     <div
-      className="sc-bRBYWo cvfjTr"
+      className="sc-fBuWsC jGeMc"
       onClick={[Function]}
     >
       BCHA
     </div>
     <div
-      className="sc-bRBYWo cvfjTr nonactiveBtn"
+      className="sc-fBuWsC jGeMc nonactiveBtn"
       onClick={[Function]}
     >
       SLPA
@@ -403,7 +403,7 @@
 exports[`Wallet without BCH balance 1`] = `
 Array [
   <div
-    className="sc-gPEVay iEbfNY"
+    className="sc-iRbamj kYGqgG"
   >
     <span
       aria-label="party emoji"
@@ -429,7 +429,7 @@
      to send to others
   </div>,
   <div
-    className="sc-jAaTju dsaCuU"
+    className="sc-jDwBTQ giRFkr"
   >
     0 
     BCHA
@@ -499,16 +499,16 @@
     </div>
   </div>,
   <div
-    className="sc-Rmtcm ldTlYI"
+    className="sc-jhAzac vWMwd"
   >
     <div
-      className="sc-bRBYWo cvfjTr"
+      className="sc-fBuWsC jGeMc"
       onClick={[Function]}
     >
       BCHA
     </div>
     <div
-      className="sc-bRBYWo cvfjTr nonactiveBtn"
+      className="sc-fBuWsC jGeMc nonactiveBtn"
       onClick={[Function]}
     >
       SLPA
diff --git a/web/cashtab/src/hooks/__mocks__/createToken.js b/web/cashtab/src/hooks/__mocks__/createToken.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/hooks/__mocks__/createToken.js
@@ -0,0 +1,56 @@
+// @generated
+export default {
+    invalidWallet: {},
+    wallet: {
+        Path1899: {
+            cashAddress:
+                'bitcoincash:qpuvjl7l3crt3apc62gmtf49pfsluu7s9gsex3qnhn',
+            slpAddress:
+                'simpleledger:qpuvjl7l3crt3apc62gmtf49pfsluu7s9guzd24nfd',
+            fundingWif: 'L2gH81AegmBdnvEZuUpnd3robG8NjBaVjPddWrVD4169wS6Mqyxn',
+            fundingAddress:
+                'simpleledger:qpuvjl7l3crt3apc62gmtf49pfsluu7s9guzd24nfd',
+            legacyAddress: '1C1fUT99KT4SjbKjCE2fSCdhc6Bvj5gQjG',
+        },
+        tokens: [],
+        state: {
+            balances: [],
+            utxos: [],
+            hydratedUtxoDetails: [],
+            tokens: [],
+            slpBalancesAndUtxos: {
+                nonSlpUtxos: [
+                    {
+                        height: 0,
+                        tx_hash:
+                            'e0d6d7d46d5fc6aaa4512a7aca9223c6d7ca30b8253dee1b40b8978fe7dc501e',
+                        tx_pos: 0,
+                        value: 1000000,
+                        txid:
+                            'e0d6d7d46d5fc6aaa4512a7aca9223c6d7ca30b8253dee1b40b8978fe7dc501e',
+                        vout: 0,
+                        isValid: false,
+                        address:
+                            'bitcoincash:qpuvjl7l3crt3apc62gmtf49pfsluu7s9gsex3qnhn',
+                        wif:
+                            'L2gH81AegmBdnvEZuUpnd3robG8NjBaVjPddWrVD4169wS6Mqyxn',
+                    },
+                ],
+            },
+        },
+    },
+    configObj: {
+        name: 'Cashtab Unit Test Token',
+        ticker: 'CUTT',
+        documentUrl: 'https://cashtabapp.com/',
+        decimals: '2',
+        initialQty: '100',
+        documentHash: '',
+        mintBatonVout: null,
+    },
+    expectedTxId:
+        '9e9738e9ac3ff202736bf7775f875ebae6f812650df577a947c20c52475e43da',
+    expectedHex: [
+        '02000000011e50dce78f97b8401bee3d25b830cad7c62392ca7a2a51a4aac65f6dd4d7d6e0000000006a4730440220150c19f4274b30415174c7517ff5e3e79c224ac6aff6967537a8e1ab71880bbb0220537a8c7a91672fe3dc2f703dcb319c94a1717e220b52111f406f0d80adeb4c15412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff030000000000000000546a04534c500001010747454e455349530443555454174361736874616220556e6974205465737420546f6b656e1768747470733a2f2f636173687461626170702e636f6d2f4c0001024c0008000000000000271022020000000000001976a91478c97fdf8e06b8f438d291b5a6a50a61fe73d02a88ac283d0f00000000001976a91478c97fdf8e06b8f438d291b5a6a50a61fe73d02a88ac00000000',
+    ],
+};
diff --git a/web/cashtab/src/hooks/__mocks__/mockParseTokenInfoForTxHistory.js b/web/cashtab/src/hooks/__mocks__/mockParseTokenInfoForTxHistory.js
--- a/web/cashtab/src/hooks/__mocks__/mockParseTokenInfoForTxHistory.js
+++ b/web/cashtab/src/hooks/__mocks__/mockParseTokenInfoForTxHistory.js
@@ -1,3 +1,5 @@
+// @generated
+
 export const tokenSendWdt = {
     txid: 'b923fba5b011df438c96f7f8f715fcf2b9ac2f96ea73139885e00aee4361f98f',
     parsedTx: {
@@ -69,6 +71,7 @@
         tokenName:
             'Test Token With Exceptionally Long Name For CSS And Style Revisions',
         tokenTicker: 'WDT',
+        transactionType: 'SEND',
     },
 };
 
@@ -135,5 +138,63 @@
             'bef614aac85c0c866f4d39e4d12a96851267d38d1bca5bdd6488bbd42e28b6b1',
         tokenName: 'Cash Tab Points',
         tokenTicker: 'CTP',
+        transactionType: 'SEND',
+    },
+};
+
+export const tokenGenesisCashtabMintAlpha = {
+    txid: '45f0ff5cae7e89da6b96c26c8c48a959214c5f0e983e78d0925f8956ca8848c6',
+    parsedTx: {
+        txid:
+            '45f0ff5cae7e89da6b96c26c8c48a959214c5f0e983e78d0925f8956ca8848c6',
+        confirmations: 11,
+        height: 685170,
+        blocktime: 1620250206,
+        amountSent: 0,
+        amountReceived: 0,
+        tokenTx: true,
+        outgoingTx: true,
+        destinationAddress:
+            'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr',
+        tokenInfo: {
+            qtySent: '0',
+            qtyReceived: '55.55555',
+            tokenId:
+                '45f0ff5cae7e89da6b96c26c8c48a959214c5f0e983e78d0925f8956ca8848c6',
+            tokenName: 'CashtabMintAlpha',
+            tokenTicker: 'CMA',
+            transactionType: 'GENESIS',
+        },
+    },
+    tokenInfo: {
+        versionType: 1,
+        tokenName: 'CashtabMintAlpha',
+        tokenTicker: 'CMA',
+        transactionType: 'GENESIS',
+        tokenIdHex:
+            '45f0ff5cae7e89da6b96c26c8c48a959214c5f0e983e78d0925f8956ca8848c6',
+        sendOutputs: ['0', '5555555000'],
+        sendInputsFull: [
+            {
+                address:
+                    'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa',
+            },
+        ],
+        sendOutputsFull: [
+            {
+                address:
+                    'simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa',
+                amount: '55.55555',
+            },
+        ],
+    },
+    cashtabTokenInfo: {
+        qtyReceived: '55.55555',
+        qtySent: '0',
+        tokenId:
+            '45f0ff5cae7e89da6b96c26c8c48a959214c5f0e983e78d0925f8956ca8848c6',
+        tokenName: 'CashtabMintAlpha',
+        tokenTicker: 'CMA',
+        transactionType: 'GENESIS',
     },
 };
diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js
--- a/web/cashtab/src/hooks/__tests__/useBCH.test.js
+++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js
@@ -5,6 +5,7 @@
 import mockReturnGetHydratedUtxoDetailsWithZeroBalance from '../__mocks__/mockReturnGetHydratedUtxoDetailsWithZeroBalance';
 import mockReturnGetSlpBalancesAndUtxosNoZeroBalance from '../__mocks__/mockReturnGetSlpBalancesAndUtxosNoZeroBalance';
 import sendBCHMock from '../__mocks__/sendBCH';
+import createTokenMock from '../__mocks__/createToken';
 import mockTxHistory from '../__mocks__/mockTxHistory';
 import mockFlatTxHistory from '../__mocks__/mockFlatTxHistory';
 import mockTxDataWithPassthrough from '../__mocks__/mockTxDataWithPassthrough';
@@ -15,6 +16,7 @@
 import {
     tokenSendWdt,
     tokenReceiveTBS,
+    tokenGenesisCashtabMintAlpha,
 } from '../__mocks__/mockParseTokenInfoForTxHistory';
 import {
     mockSentCashTx,
@@ -304,6 +306,43 @@
         );
     });
 
+    it('creates a token correctly', async () => {
+        const { createToken } = useBCH();
+        const BCH = new BCHJS();
+        const {
+            expectedTxId,
+            expectedHex,
+            wallet,
+            configObj,
+        } = createTokenMock;
+
+        BCH.RawTransactions.sendRawTransaction = jest
+            .fn()
+            .mockResolvedValue(expectedTxId);
+        expect(
+            await createToken(BCH, wallet, currency.defaultFee, configObj),
+        ).toBe(`${currency.tokenExplorerUrl}/tx/${expectedTxId}`);
+        expect(BCH.RawTransactions.sendRawTransaction).toHaveBeenCalledWith(
+            expectedHex,
+        );
+    });
+
+    it('Throws correct error if user attempts to create a token with an invalid wallet', async () => {
+        const { createToken } = useBCH();
+        const BCH = new BCHJS();
+        const { invalidWallet, configObj } = createTokenMock;
+
+        const invalidWalletTokenCreation = createToken(
+            BCH,
+            invalidWallet,
+            currency.defaultFee,
+            configObj,
+        );
+        await expect(invalidWalletTokenCreation).rejects.toThrow(
+            new Error('Invalid wallet'),
+        );
+    });
+
     it('Correctly flattens transaction history', () => {
         const { flattenTransactions } = useBCH();
         expect(flattenTransactions(mockTxHistory, 10)).toStrictEqual(
@@ -358,4 +397,14 @@
             ),
         ).toStrictEqual(tokenReceiveTBS.cashtabTokenInfo);
     });
+
+    it(`Correctly parses a "GENESIS ${currency.tokenTicker}" transaction with token details`, () => {
+        const { parseTokenInfoForTxHistory } = useBCH();
+        expect(
+            parseTokenInfoForTxHistory(
+                tokenGenesisCashtabMintAlpha.parsedTx,
+                tokenGenesisCashtabMintAlpha.tokenInfo,
+            ),
+        ).toStrictEqual(tokenGenesisCashtabMintAlpha.cashtabTokenInfo);
+    });
 });
diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js
--- a/web/cashtab/src/hooks/useBCH.js
+++ b/web/cashtab/src/hooks/useBCH.js
@@ -5,6 +5,7 @@
     toSmallestDenomination,
     batchArray,
     flattenBatchedHydratedUtxos,
+    isValidStoredWallet,
 } from '@utils/cashMethods';
 
 export default function useBCH() {
@@ -202,6 +203,7 @@
             txDataPromiseResponse = await Promise.all(txDataPromises);
 
             const parsed = parseTxData(txDataPromiseResponse);
+
             return parsed;
         } catch (err) {
             console.log(`Error in Promise.all(txDataPromises):`);
@@ -212,7 +214,7 @@
 
     const parseTokenInfoForTxHistory = (parsedTx, tokenInfo) => {
         // Scan over inputs to find out originating addresses
-        const { sendInputsFull, sendOutputsFull } = tokenInfo;
+        const { transactionType, sendInputsFull, sendOutputsFull } = tokenInfo;
         const sendingTokenAddresses = [];
         for (let i = 0; i < sendInputsFull.length; i += 1) {
             const sendingAddress = sendInputsFull[i].address;
@@ -223,7 +225,13 @@
         let qtyReceived = new BigNumber(0);
         for (let i = 0; i < sendOutputsFull.length; i += 1) {
             if (sendingTokenAddresses.includes(sendOutputsFull[i].address)) {
-                // token change
+                // token change and should be ignored, unless it's a genesis transaction
+                // then this is the amount created
+                if (transactionType === 'GENESIS') {
+                    qtyReceived = qtyReceived.plus(
+                        new BigNumber(sendOutputsFull[i].amount),
+                    );
+                }
                 continue;
             }
             if (parsedTx.outgoingTx) {
@@ -242,6 +250,7 @@
         cashtabTokenInfo.tokenId = tokenInfo.tokenIdHex;
         cashtabTokenInfo.tokenName = tokenInfo.tokenName;
         cashtabTokenInfo.tokenTicker = tokenInfo.tokenTicker;
+        cashtabTokenInfo.transactionType = transactionType;
 
         return cashtabTokenInfo;
     };
@@ -489,6 +498,130 @@
         return txFee;
     };
 
+    const createToken = async (BCH, wallet, feeInSatsPerByte, configObj) => {
+        try {
+            // Throw error if wallet does not have utxo set in state
+            if (!isValidStoredWallet(wallet)) {
+                const walletError = new Error(`Invalid wallet`);
+                throw walletError;
+            }
+            const utxos = wallet.state.slpBalancesAndUtxos.nonSlpUtxos;
+
+            const CREATION_ADDR = wallet.Path1899.cashAddress;
+            const inputUtxos = [];
+            let transactionBuilder;
+
+            // instance of transaction builder
+            if (process.env.REACT_APP_NETWORK === `mainnet`)
+                transactionBuilder = new BCH.TransactionBuilder();
+            else transactionBuilder = new BCH.TransactionBuilder('testnet');
+
+            let originalAmount = new BigNumber(0);
+            const tokenOutputDust = new BigNumber(currency.dust);
+            let txFee = 0;
+            for (let i = 0; i < utxos.length; i++) {
+                const utxo = utxos[i];
+                originalAmount = originalAmount.plus(utxo.value);
+                const vout = utxo.vout;
+                const txid = utxo.txid;
+                // add input with txid and index of vout
+                transactionBuilder.addInput(txid, vout);
+
+                inputUtxos.push(utxo);
+                txFee = calcFee(BCH, inputUtxos, 3, feeInSatsPerByte);
+
+                if (originalAmount.minus(tokenOutputDust).minus(txFee).gte(0)) {
+                    break;
+                }
+            }
+
+            // amount to send back to the remainder address.
+            const remainder = originalAmount
+                .minus(tokenOutputDust)
+                .minus(txFee);
+
+            if (remainder.lt(0)) {
+                const error = new Error(`Insufficient funds`);
+                error.code = SEND_BCH_ERRORS.INSUFFICIENT_FUNDS;
+                throw error;
+            }
+
+            // Generate the OP_RETURN entry for an SLP GENESIS transaction.
+            const script = BCH.SLP.TokenType1.generateGenesisOpReturn(
+                configObj,
+            );
+            // OP_RETURN needs to be the first output in the transaction.
+            transactionBuilder.addOutput(script, 0);
+
+            // add output w/ address and amount to send
+            transactionBuilder.addOutput(
+                CREATION_ADDR,
+                parseInt(toSmallestDenomination(tokenOutputDust)),
+            );
+
+            // Send change to own address
+            if (
+                remainder.gte(
+                    toSmallestDenomination(new BigNumber(currency.dust)),
+                )
+            ) {
+                transactionBuilder.addOutput(
+                    CREATION_ADDR,
+                    parseInt(remainder),
+                );
+            }
+
+            // Sign the transactions with the HD node.
+            for (let i = 0; i < inputUtxos.length; i++) {
+                const utxo = inputUtxos[i];
+                transactionBuilder.sign(
+                    i,
+                    BCH.ECPair.fromWIF(utxo.wif),
+                    undefined,
+                    transactionBuilder.hashTypes.SIGHASH_ALL,
+                    utxo.value,
+                );
+            }
+
+            // build tx
+            const tx = transactionBuilder.build();
+            // output rawhex
+            const hex = tx.toHex();
+
+            // Broadcast transaction to the network
+            const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]);
+
+            if (txidStr && txidStr[0]) {
+                console.log(`${currency.ticker} txid`, txidStr[0]);
+            }
+            let link;
+
+            if (process.env.REACT_APP_NETWORK === `mainnet`) {
+                link = `${currency.tokenExplorerUrl}/tx/${txidStr}`;
+            } else {
+                link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`;
+            }
+            //console.log(`link`, link);
+
+            return link;
+        } catch (err) {
+            if (err.error === 'insufficient priority (code 66)') {
+                err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY;
+            } else if (err.error === 'txn-mempool-conflict (code 18)') {
+                err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING;
+            } else if (err.error === 'Network Error') {
+                err.code = SEND_BCH_ERRORS.NETWORK_ERROR;
+            } else if (
+                err.error ===
+                'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)'
+            ) {
+                err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS;
+            }
+            console.log(`error: `, err);
+            throw err;
+        }
+    };
+
     const sendToken = async (
         BCH,
         wallet,
@@ -849,5 +982,6 @@
         getRestUrl,
         sendBch,
         sendToken,
+        createToken,
     };
 }
diff --git a/web/cashtab/src/utils/__tests__/validation.test.js b/web/cashtab/src/utils/__tests__/validation.test.js
--- a/web/cashtab/src/utils/__tests__/validation.test.js
+++ b/web/cashtab/src/utils/__tests__/validation.test.js
@@ -1,4 +1,12 @@
-import { shouldRejectAmountInput, fiatToCrypto } from '../validation';
+import {
+    shouldRejectAmountInput,
+    fiatToCrypto,
+    isValidTokenName,
+    isValidTokenTicker,
+    isValidTokenDecimals,
+    isValidTokenInitialQty,
+    isValidTokenDocumentUrl,
+} from '../validation';
 import { currency } from '@components/Common/Ticker.js';
 
 describe('Validation utils', () => {
@@ -79,4 +87,90 @@
     it(`Returns expected crypto amount with ${currency.cashDecimals} decimals of precision even if inputs have lower precision`, () => {
         expect(fiatToCrypto('10.94', 10)).toBe('1.09400000');
     });
+    it(`Accepts a valid ${currency.tokenTicker} token name`, () => {
+        expect(isValidTokenName('Valid token name')).toBe(true);
+    });
+    it(`Accepts a valid ${currency.tokenTicker} token name that is a stringified number`, () => {
+        expect(isValidTokenName('123456789')).toBe(true);
+    });
+    it(`Rejects ${currency.tokenTicker} token name if longer than 68 characters`, () => {
+        expect(
+            isValidTokenName(
+                'This token name is not valid because it is longer than 68 characters which is really pretty long for a token name when you think about it and all',
+            ),
+        ).toBe(false);
+    });
+    it(`Rejects ${currency.tokenTicker} token name if empty string`, () => {
+        expect(isValidTokenName('')).toBe(false);
+    });
+    it(`Accepts a 4-char ${currency.tokenTicker} token ticker`, () => {
+        expect(isValidTokenTicker('DOGE')).toBe(true);
+    });
+    it(`Accepts a 12-char ${currency.tokenTicker} token ticker`, () => {
+        expect(isValidTokenTicker('123456789123')).toBe(true);
+    });
+    it(`Rejects ${currency.tokenTicker} token ticker if empty string`, () => {
+        expect(isValidTokenTicker('')).toBe(false);
+    });
+    it(`Rejects ${currency.tokenTicker} token ticker if > 12 chars`, () => {
+        expect(isValidTokenTicker('1234567891234')).toBe(false);
+    });
+    it(`Accepts ${currency.tokenDecimals} if zero`, () => {
+        expect(isValidTokenDecimals('0')).toBe(true);
+    });
+    it(`Accepts ${currency.tokenDecimals} if between 0 and 9 inclusive`, () => {
+        expect(isValidTokenDecimals('9')).toBe(true);
+    });
+    it(`Rejects ${currency.tokenDecimals} if empty string`, () => {
+        expect(isValidTokenDecimals('')).toBe(false);
+    });
+    it(`Rejects ${currency.tokenDecimals} if non-integer`, () => {
+        expect(isValidTokenDecimals('1.7')).toBe(false);
+    });
+    it(`Accepts ${currency.tokenDecimals} initial genesis quantity at minimum amount for 3 decimal places`, () => {
+        expect(isValidTokenInitialQty('0.001', '3')).toBe(true);
+    });
+    it(`Accepts ${currency.tokenDecimals} initial genesis quantity at minimum amount for 9 decimal places`, () => {
+        expect(isValidTokenInitialQty('0.000000001', '9')).toBe(true);
+    });
+    it(`Accepts ${currency.tokenDecimals} initial genesis quantity at amount below 100 billion`, () => {
+        expect(isValidTokenInitialQty('1000', '0')).toBe(true);
+    });
+    it(`Accepts highest possible ${currency.tokenDecimals} initial genesis quantity at amount below 100 billion`, () => {
+        expect(isValidTokenInitialQty('99999999999.999999999', '9')).toBe(true);
+    });
+    it(`Accepts ${currency.tokenDecimals} initial genesis quantity if decimal places equal tokenDecimals`, () => {
+        expect(isValidTokenInitialQty('0.123', '3')).toBe(true);
+    });
+    it(`Accepts ${currency.tokenDecimals} initial genesis quantity if decimal places are less than tokenDecimals`, () => {
+        expect(isValidTokenInitialQty('0.12345', '9')).toBe(true);
+    });
+    it(`Rejects ${currency.tokenDecimals} initial genesis quantity of zero`, () => {
+        expect(isValidTokenInitialQty('0', '9')).toBe(false);
+    });
+    it(`Rejects ${currency.tokenDecimals} initial genesis quantity if tokenDecimals is not valid`, () => {
+        expect(isValidTokenInitialQty('0', '')).toBe(false);
+    });
+    it(`Rejects ${currency.tokenDecimals} initial genesis quantity if 100 billion or higher`, () => {
+        expect(isValidTokenInitialQty('100000000000', '0')).toBe(false);
+    });
+    it(`Rejects ${currency.tokenDecimals} initial genesis quantity if it has more decimal places than tokenDecimals`, () => {
+        expect(isValidTokenInitialQty('1.5', '0')).toBe(false);
+    });
+    it(`Accepts a valid ${currency.tokenTicker} token document URL`, () => {
+        expect(isValidTokenDocumentUrl('cashtabapp.com')).toBe(true);
+    });
+    it(`Accepts a valid ${currency.tokenTicker} token document URL including special URL characters`, () => {
+        expect(isValidTokenDocumentUrl('https://cashtabapp.com/')).toBe(true);
+    });
+    it(`Accepts a blank string as a valid ${currency.tokenTicker} token document URL`, () => {
+        expect(isValidTokenDocumentUrl('')).toBe(true);
+    });
+    it(`Rejects ${currency.tokenTicker} token name if longer than 68 characters`, () => {
+        expect(
+            isValidTokenDocumentUrl(
+                'http://www.ThisTokenDocumentUrlIsActuallyMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchTooLong.com/',
+            ),
+        ).toBe(false);
+    });
 });
diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js
--- a/web/cashtab/src/utils/validation.js
+++ b/web/cashtab/src/utils/validation.js
@@ -43,3 +43,43 @@
         .toFixed(currency.cashDecimals);
     return cryptoAmount;
 };
+
+export const isValidTokenName = tokenName => {
+    return (
+        typeof tokenName === 'string' &&
+        tokenName.length > 0 &&
+        tokenName.length < 68
+    );
+};
+
+export const isValidTokenTicker = tokenTicker => {
+    return (
+        typeof tokenTicker === 'string' &&
+        tokenTicker.length > 0 &&
+        tokenTicker.length < 13
+    );
+};
+
+export const isValidTokenDecimals = tokenDecimals => {
+    return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(
+        tokenDecimals,
+    );
+};
+
+export const isValidTokenInitialQty = (tokenInitialQty, tokenDecimals) => {
+    const minimumQty = new BigNumber(1 / 10 ** tokenDecimals);
+    const tokenIntialQtyBig = new BigNumber(tokenInitialQty);
+    return (
+        tokenIntialQtyBig.gte(minimumQty) &&
+        tokenIntialQtyBig.lt(100000000000) &&
+        tokenIntialQtyBig.dp() <= tokenDecimals
+    );
+};
+
+export const isValidTokenDocumentUrl = tokenDocumentUrl => {
+    return (
+        typeof tokenDocumentUrl === 'string' &&
+        tokenDocumentUrl.length >= 0 &&
+        tokenDocumentUrl.length < 68
+    );
+};