types#ChainType TypeScript Examples

The following examples show how to use types#ChainType. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Account.ts    From commonwealth with GNU General Public License v3.0 6 votes vote down vote up
public async validate(signature: string) {
    if (!this._validationToken) {
      throw new Error('no validation token found');
    }
    if (!signature) {
      throw new Error('signature required for validation');
    }

    const params : any = {
      address: this.address,
      chain: this.chain.id,
      isToken: this.chain.type === ChainType.Token,
      jwt: this.app.user.jwt,
      signature,
      wallet_id: this.walletId,
    };
    const result = await $.post(`${this.app.serverUrl()}/verifyAddress`, params);
    if (result.status === 'Success') {
      // update ghost address for discourse users
      const hasGhostAddress = app.user.addresses.some(({ address, ghostAddress, chain }) => (
          ghostAddress && this.chain.id === chain &&
          app.user.activeAccounts.some((account) => account.address === address)
      ))
      if (hasGhostAddress) {
        const { success, ghostAddressId } = await $.post(`${this.app.serverUrl()}/updateAddress`, params);
        if (success && ghostAddressId) {
          // remove ghost address from addresses
          app.user.setAddresses(app.user.addresses.filter(({ ghostAddress }) => {
            return !ghostAddress
          }));
          app.user.setActiveAccounts([]);
        }
      }
    }
  }
Example #2
Source File: index.ts    From contracts-ui with GNU General Public License v3.0 5 votes vote down vote up
export function isResultReady(result: SubmittableResult, systemChainType: ChainType): boolean {
  return systemChainType.isDevelopment ? result.isInBlock : result.isFinalized;
}
Example #3
Source File: index.ts    From contracts-ui with GNU General Public License v3.0 5 votes vote down vote up
export function getBlockHash(
  status: SubmittableResult['status'],
  systemChainType: ChainType
): Hash {
  return systemChainType.isDevelopment ? status.asInBlock : status.asFinalized;
}
Example #4
Source File: app.ts    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
// called by the user, when clicking on the chain/node switcher menu
// returns a boolean reflecting whether initialization of chain via the
// initChain fn ought to proceed or abort
export async function selectChain(
  chain?: ChainInfo,
  deferred = false
): Promise<boolean> {
  // Select the default node, if one wasn't provided
  if (!chain) {
    if (app.user.selectedChain) {
      chain = app.user.selectedChain;
    } else {
      chain = app.config.chains.getById(app.config.defaultChain);
    }
    if (!chain) {
      throw new Error('no chain available');
    }
  }

  // Check for valid chain selection, and that we need to switch
  if (app.chain && chain === app.chain.meta) {
    return;
  }

  // Shut down old chain if applicable
  await deinitChainOrCommunity();
  app.chainPreloading = true;
  document.title = `Commonwealth – ${chain.name}`;
  setTimeout(() => m.redraw()); // redraw to show API status indicator

  // Import top-level chain adapter lazily, to facilitate code split.
  let newChain;
  let initApi; // required for NEAR
  if (chain.base === ChainBase.Substrate) {
    const Substrate = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "substrate-main" */
        './controllers/chain/substrate/main'
      )
    ).default;
    newChain = new Substrate(chain, app);
  } else if (chain.base === ChainBase.CosmosSDK) {
    const Cosmos = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "cosmos-main" */
        './controllers/chain/cosmos/main'
      )
    ).default;
    newChain = new Cosmos(chain, app);
  } else if (chain.network === ChainNetwork.Ethereum) {
    const Ethereum = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "ethereum-main" */
        './controllers/chain/ethereum/main'
      )
    ).default;
    newChain = new Ethereum(chain, app);
  } else if (
    chain.network === ChainNetwork.NEAR ||
    chain.network === ChainNetwork.NEARTestnet
  ) {
    const Near = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "near-main" */
        './controllers/chain/near/main'
      )
    ).default;
    newChain = new Near(chain, app);
    initApi = true;
  } else if (chain.network === ChainNetwork.Sputnik) {
    const Sputnik = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "sputnik-main" */
        './controllers/chain/near/sputnik/adapter'
      )
    ).default;
    newChain = new Sputnik(chain, app);
    initApi = true;
  } else if (chain.network === ChainNetwork.Moloch) {
    const Moloch = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "moloch-main" */
        './controllers/chain/ethereum/moloch/adapter'
      )
    ).default;
    newChain = new Moloch(chain, app);
  } else if (chain.network === ChainNetwork.Compound) {
    const Compound = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "compound-main" */
        './controllers/chain/ethereum/compound/adapter'
      )
    ).default;
    newChain = new Compound(chain, app);
  } else if (chain.network === ChainNetwork.Aave) {
    const Aave = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "aave-main" */
        './controllers/chain/ethereum/aave/adapter'
      )
    ).default;
    newChain = new Aave(chain, app);
  } else if (
    chain.network === ChainNetwork.ERC20 ||
    chain.network === ChainNetwork.AxieInfinity
  ) {
    const ERC20 = (
      await import(
        //   /* webpackMode: "lazy" */
        //   /* webpackChunkName: "erc20-main" */
        './controllers/chain/ethereum/tokenAdapter'
      )
    ).default;
    newChain = new ERC20(chain, app);
  } else if (chain.network === ChainNetwork.ERC721) {
    const ERC721 = (
      await import(
        //   /* webpackMode: "lazy" */
        //   /* webpackChunkName: "erc721-main" */
        './controllers/chain/ethereum/NftAdapter'
      )
    ).default;
    newChain = new ERC721(chain, app);
  } else if (chain.network === ChainNetwork.SPL) {
    const SPL = (
      await import(
        //   /* webpackMode: "lazy" */
        //   /* webpackChunkName: "spl-main" */
        './controllers/chain/solana/tokenAdapter'
      )
    ).default;
    newChain = new SPL(chain, app);
  } else if (chain.base === ChainBase.Solana) {
    const Solana = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "solana-main" */
        './controllers/chain/solana/main'
      )
    ).default;
    newChain = new Solana(chain, app);
  } else if (chain.network === ChainNetwork.Commonwealth) {
    const Commonwealth = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "commonwealth-main" */
        './controllers/chain/ethereum/commonwealth/adapter'
      )
    ).default;
    newChain = new Commonwealth(chain, app);
  } else if (
    chain.base === ChainBase.Ethereum &&
    chain.type === ChainType.Offchain
  ) {
    const Ethereum = (
      await import(
        /* webpackMode: "lazy" */
        /* webpackChunkName: "ethereum-main" */
        './controllers/chain/ethereum/main'
      )
    ).default;
    newChain = new Ethereum(chain, app);
  } else {
    throw new Error('Invalid chain');
  }

  // Load server data without initializing modules/chain connection.
  const finalizeInitialization = await newChain.initServer();

  // If the user is still on the initializing node, finalize the
  // initialization; otherwise, abort, deinit, and return false.
  //
  // Also make sure the state is sufficiently reset so that the
  // next redraw cycle will reinitialize any needed chain.
  if (!finalizeInitialization) {
    console.log('Chain loading aborted');
    app.chainPreloading = false;
    app.chain = null;
    return false;
  } else {
    app.chain = newChain;
  }
  if (initApi) {
    await app.chain.initApi(); // required for loading NearAccounts
  }
  app.chainPreloading = false;
  app.chain.deferred = deferred;

  // Instantiate active addresses before chain fully loads
  await updateActiveAddresses(chain);

  // Update default on server if logged in
  if (app.isLoggedIn()) {
    await app.user.selectChain({
      chain: chain.id,
    });
  }

  // If the user was invited to a chain/community, we can now pop up a dialog for them to accept the invite
  handleInviteLinkRedirect();

  // Redraw with not-yet-loaded chain and return true to indicate
  // initialization has finalized.
  m.redraw();
  return true;
}
Example #5
Source File: app.ts    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
Promise.all([$.ready, $.get('/api/domain')]).then(
  async ([ready, { customDomain }]) => {
    // set window error handler
    // ignore ResizeObserver error: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
    const resizeObserverLoopErrRe = /^ResizeObserver loop limit exceeded/;
    // replace chunk loading errors with a notification that the app has been updated
    const chunkLoadingErrRe = /^Uncaught SyntaxError: Unexpected token/;
    window.onerror = (errorMsg, url, lineNumber, colNumber, error) => {
      if (
        typeof errorMsg === 'string' &&
        resizeObserverLoopErrRe.test(errorMsg)
      )
        return false;
      if (typeof errorMsg === 'string' && chunkLoadingErrRe.test(errorMsg)) {
        alertModalWithText(
          APPLICATION_UPDATE_MESSAGE,
          APPLICATION_UPDATE_ACTION
        )();
        return false;
      }
      notifyError(`${errorMsg}`);
      return false;
    };

    const redirectRoute = (path: string | Function) => ({
      render: (vnode) => {
        m.route.set(
          typeof path === 'string' ? path : path(vnode.attrs),
          {},
          { replace: true }
        );
        return m(Layout);
      },
    });

    interface RouteAttrs {
      scoped: string | boolean;
      hideSidebar?: boolean;
      deferChain?: boolean;
    }

    let hasCompletedSuccessfulPageLoad = false;
    const importRoute = (path: string, attrs: RouteAttrs) => ({
      onmatch: async () => {
        return import(
          /* webpackMode: "lazy" */
          /* webpackChunkName: "route-[request]" */
          `./${path}`
        )
          .then((p) => {
            hasCompletedSuccessfulPageLoad = true;
            return p.default;
          })
          .catch((err) => {
            // handle import() error
            console.error(err);
            if (err.name === 'ChunkLoadError') {
              alertModalWithText(
                APPLICATION_UPDATE_MESSAGE,
                APPLICATION_UPDATE_ACTION
              )();
            }
            // return to the last page, if it was on commonwealth
            // eslint-disable-next-line no-restricted-globals
            if (hasCompletedSuccessfulPageLoad) history.back();
          });
      },
      render: (vnode) => {
        const { scoped, hideSidebar } = attrs;
        const scope =
          typeof scoped === 'string'
            ? // string => scope is defined by route
              scoped
            : scoped
            ? // true => scope is derived from path
              vnode.attrs.scope?.toString() || customDomain
            : // false => scope is null
              null;

        if (scope) {
          const scopeIsEthereumAddress =
            scope.startsWith('0x') && scope.length === 42;
          if (scopeIsEthereumAddress) {
            const chains = app.config.chains.getAll();
            const chain = chains.find((o) => o.address === scope);
            if (chain) {
              const pagePath = window.location.href.substr(
                window.location.href.indexOf(scope) + scope.length
              );
              m.route.set(`/${chain.id}${pagePath}`);
            }
          }
        }

        // Special case to defer chain loading specifically for viewing an offchain thread. We need
        // a special case because OffchainThreads and on-chain proposals are all viewed through the
        // same "/:scope/proposal/:type/:id" route.
        let deferChain = attrs.deferChain;
        const isDiscussion =
          vnode.attrs.type === 'discussion' ||
          pathIsDiscussion(scope, window.location.pathname);
        if (path === 'views/pages/view_proposal/index' && isDiscussion) {
          deferChain = true;
        }
        if (app.chain?.meta.type === ChainType.Token) {
          deferChain = false;
        }
        return m(Layout, { scope, deferChain, hideSidebar }, [vnode]);
      },
    });

    const isCustomDomain = !!customDomain;
    const { activeAccount } = app.user;
    m.route(document.body, '/', {
      // Sitewide pages
      '/about': importRoute('views/pages/commonwealth', {
        scoped: false,
      }),
      '/terms': importRoute('views/pages/landing/terms', { scoped: false }),
      '/privacy': importRoute('views/pages/landing/privacy', { scoped: false }),
      '/components': importRoute('views/pages/components', {
        scoped: false,
        hideSidebar: true,
      }),
      '/createCommunity': importRoute('views/pages/create_community', {
        scoped: false,
      }),
      ...(isCustomDomain
        ? {
            //
            // Custom domain routes
            //
            '/': importRoute('views/pages/discussions', {
              scoped: true,
              deferChain: true,
            }),
            '/search': importRoute('views/pages/search', {
              scoped: false,
              deferChain: true,
            }),
            // Notifications
            '/notification-settings': importRoute(
              'views/pages/notification_settings',
              { scoped: true, deferChain: true }
            ),
            '/notifications': importRoute('views/pages/notifications_page', {
              scoped: true,
              deferChain: true,
            }),
            // CMN
            '/projects': importRoute('views/pages/commonwealth/projects', {
              scoped: true,
            }),
            '/backers': importRoute('views/pages/commonwealth/backers', {
              scoped: true,
            }),
            '/collectives': importRoute(
              'views/pages/commonwealth/collectives',
              { scoped: true }
            ),
            // NEAR
            '/finishNearLogin': importRoute('views/pages/finish_near_login', {
              scoped: true,
            }),
            '/finishaxielogin': importRoute('views/pages/finish_axie_login', {
              scoped: true,
            }),
            // Discussions
            '/home': redirectRoute((attrs) => `/${attrs.scope}/`),
            '/discussions': redirectRoute((attrs) => `/${attrs.scope}/`),
            '/discussions/:topic': importRoute('views/pages/discussions', {
              scoped: true,
              deferChain: true,
            }),
            '/members': importRoute('views/pages/members', {
              scoped: true,
              deferChain: true,
            }),
            '/sputnik-daos': importRoute('views/pages/sputnikdaos', {
              scoped: true,
              deferChain: true,
            }),
            '/chat/:channel': importRoute('views/pages/chat', {
              scoped: true,
              deferChain: true,
            }),
            '/new/discussion': importRoute('views/pages/new_thread', {
              scoped: true,
              deferChain: true,
            }),
            // Profiles
            '/account/:address': importRoute('views/pages/profile', {
              scoped: true,
              deferChain: true,
            }),
            '/account': redirectRoute((a) =>
              activeAccount ? `/account/${activeAccount.address}` : '/'
            ),
            // Governance
            '/referenda': importRoute('views/pages/referenda', {
              scoped: true,
            }),
            '/proposals': importRoute('views/pages/proposals', {
              scoped: true,
            }),
            '/council': importRoute('views/pages/council', { scoped: true }),
            '/delegate': importRoute('views/pages/delegate', { scoped: true }),
            '/proposal/:type/:identifier': importRoute(
              'views/pages/view_proposal/index',
              { scoped: true }
            ),
            '/proposal/:identifier': importRoute(
              'views/pages/view_proposal/index',
              { scoped: true }
            ),
            '/discussion/:identifier': importRoute(
              'views/pages/view_proposal/index',
              { scoped: true }
            ),
            '/new/proposal/:type': importRoute(
              'views/pages/new_proposal/index',
              { scoped: true }
            ),
            '/new/proposal': importRoute('views/pages/new_proposal/index', {
              scoped: true,
            }),
            // Treasury
            '/treasury': importRoute('views/pages/treasury', { scoped: true }),
            '/bounties': importRoute('views/pages/bounties', { scoped: true }),
            '/tips': importRoute('views/pages/tips', { scoped: true }),
            '/validators': importRoute('views/pages/validators', {
              scoped: true,
            }),
            // Settings
            '/login': importRoute('views/pages/login', {
              scoped: true,
              deferChain: true,
            }),
            '/web3login': importRoute('views/pages/web3login', {
              scoped: true,
              deferChain: true,
            }),
            // Admin
            '/admin': importRoute('views/pages/admin', { scoped: true }),
            '/manage': importRoute('views/pages/manage_community/index', {
              scoped: true,
            }),
            '/spec_settings': importRoute('views/pages/spec_settings', {
              scoped: true,
              deferChain: true,
            }),
            '/settings': importRoute('views/pages/settings', { scoped: true }),
            '/analytics': importRoute('views/pages/stats', {
              scoped: true,
              deferChain: true,
            }),

            '/snapshot/:snapshotId': importRoute(
              'views/pages/snapshot_proposals',
              { scoped: true, deferChain: true }
            ),
            '/multiple-snapshots': importRoute(
              'views/pages/view_multiple_snapshot_spaces',
              { scoped: true, deferChain: true }
            ),
            '/snapshot/:snapshotId/:identifier': importRoute(
              'views/pages/view_snapshot_proposal',
              { scoped: true, deferChain: true }
            ),
            '/new/snapshot/:snapshotId': importRoute(
              'views/pages/new_snapshot_proposal',
              { scoped: true, deferChain: true }
            ),

            // Redirects
            '/:scope/dashboard': redirectRoute(() => '/'),
            '/:scope/notifications': redirectRoute(() => '/notifications'),
            '/:scope/notification-settings': redirectRoute(
              () => '/notification-settings'
            ),
            '/:scope/projects': redirectRoute(() => '/projects'),
            '/:scope/backers': redirectRoute(() => '/backers'),
            '/:scope/collectives': redirectRoute(() => '/collectives'),
            '/:scope/finishNearLogin': redirectRoute(() => '/finishNearLogin'),
            '/:scope/finishaxielogin': redirectRoute(() => '/finishaxielogin'),
            '/:scope/home': redirectRoute(() => '/'),
            '/:scope/discussions': redirectRoute(() => '/'),
            '/:scope': redirectRoute(() => '/'),
            '/:scope/discussions/:topic': redirectRoute(
              (attrs) => `/discussions/${attrs.topic}/`
            ),
            '/:scope/search': redirectRoute(() => '/search'),
            '/:scope/members': redirectRoute(() => '/members'),
            '/:scope/sputnik-daos': redirectRoute(() => '/sputnik-daos'),
            '/:scope/chat/:channel': redirectRoute(
              (attrs) => `/chat/${attrs.channel}`
            ),
            '/:scope/new/discussion': redirectRoute(() => '/new/discussion'),
            '/:scope/account/:address': redirectRoute(
              (attrs) => `/account/${attrs.address}/`
            ),
            '/:scope/account': redirectRoute(() =>
              activeAccount ? `/account/${activeAccount.address}` : '/'
            ),
            '/:scope/referenda': redirectRoute(() => '/referenda'),
            '/:scope/proposals': redirectRoute(() => '/proposals'),
            '/:scope/council': redirectRoute(() => '/council'),
            '/:scope/delegate': redirectRoute(() => '/delegate'),
            '/:scope/proposal/:type/:identifier': redirectRoute(
              (attrs) => `/proposal/${attrs.type}/${attrs.identifier}/`
            ),
            '/:scope/proposal/:identifier': redirectRoute(
              (attrs) => `/proposal/${attrs.identifier}/`
            ),
            '/:scope/discussion/:identifier': redirectRoute(
              (attrs) => `/discussion/${attrs.identifier}/`
            ),
            '/:scope/new/proposal/:type': redirectRoute(
              (attrs) => `/new/proposal/${attrs.type}/`
            ),
            '/:scope/new/proposal': redirectRoute(() => '/new/proposal'),
            '/:scope/treasury': redirectRoute(() => '/treasury'),
            '/:scope/bounties': redirectRoute(() => '/bounties'),
            '/:scope/tips': redirectRoute(() => '/tips'),
            '/:scope/validators': redirectRoute(() => '/validators'),
            '/:scope/login': redirectRoute(() => '/login'),
            '/:scope/web3login': redirectRoute(() => '/web3login'),
            '/:scope/settings': redirectRoute(() => '/settings'),
            '/:scope/admin': redirectRoute(() => '/admin'),
            '/:scope/manage': redirectRoute(() => '/manage'),
            '/:scope/spec_settings': redirectRoute(() => '/spec_settings'),
            '/:scope/analytics': redirectRoute(() => '/analytics'),
            '/:scope/snapshot-proposals/:snapshotId': redirectRoute(
              (attrs) => `/snapshot/${attrs.snapshotId}`
            ),
            '/:scope/snapshot-proposal/:snapshotId/:identifier': redirectRoute(
              (attrs) => `/snapshot/${attrs.snapshotId}/${attrs.identifier}`
            ),
            '/:scope/new/snapshot-proposal/:snapshotId': redirectRoute(
              (attrs) => `/new/snapshot/${attrs.snapshotId}`
            ),
            '/:scope/snapshot-proposals/:snapshotId/:identifier': redirectRoute(
              (attrs) => `/snapshot/${attrs.snapshotId}/${attrs.identifier}`
            ),
            '/:scope/new/snapshot-proposals/:snapshotId': redirectRoute(
              (attrs) => `/new/snapshot/${attrs.snapshotId}`
            ),
          }
        : {
            //
            // Global routes
            //
            '/': importRoute('views/pages/landing', {
              scoped: false,
              hideSidebar: false,
            }),
            '/communities': importRoute('views/pages/community_cards', {
              scoped: false,
              hideSidebar: false,
            }),
            '/search': importRoute('views/pages/search', {
              scoped: false,
              deferChain: true,
            }),
            '/whyCommonwealth': importRoute('views/pages/commonwealth', {
              scoped: false,
              hideSidebar: true,
            }),
            '/dashboard': importRoute('views/pages/user_dashboard', {
              scoped: false,
              deferChain: true,
            }),
            '/dashboard/:type': importRoute('views/pages/user_dashboard', {
              scoped: false,
              deferChain: true,
            }),
            // Scoped routes
            //

            // Notifications
            '/:scope/notifications': importRoute(
              'views/pages/notifications_page',
              { scoped: true, deferChain: true }
            ),
            '/notifications': redirectRoute(() => '/edgeware/notifications'),
            '/:scope/notification-settings': importRoute(
              'views/pages/notification_settings',
              { scoped: true, deferChain: true }
            ),
            '/notification-settings': redirectRoute(
              () => '/edgeware/notification-settings'
            ),
            // CMN
            '/:scope/projects': importRoute(
              'views/pages/commonwealth/projects',
              { scoped: true }
            ),
            '/:scope/backers': importRoute('views/pages/commonwealth/backers', {
              scoped: true,
            }),
            '/:scope/collectives': importRoute(
              'views/pages/commonwealth/collectives',
              { scoped: true }
            ),
            // NEAR
            '/:scope/finishNearLogin': importRoute(
              'views/pages/finish_near_login',
              { scoped: true }
            ),
            '/finishaxielogin': importRoute('views/pages/finish_axie_login', {
              scoped: false,
            }),
            // Settings
            '/settings': redirectRoute(() => '/edgeware/settings'),
            '/:scope/settings': importRoute('views/pages/settings', {
              scoped: true,
            }),

            // Discussions
            '/home': redirectRoute('/'), // legacy redirect, here for compatibility only
            '/discussions': redirectRoute('/'), // legacy redirect, here for compatibility only
            '/:scope/home': redirectRoute((attrs) => `/${attrs.scope}/`),
            '/:scope/discussions': redirectRoute((attrs) => `/${attrs.scope}/`),
            '/:scope': importRoute('views/pages/discussions', {
              scoped: true,
              deferChain: true,
            }),
            '/:scope/discussions/:topic': importRoute(
              'views/pages/discussions',
              { scoped: true, deferChain: true }
            ),
            '/:scope/search': importRoute('views/pages/search', {
              scoped: true,
              deferChain: true,
            }),
            '/:scope/members': importRoute('views/pages/members', {
              scoped: true,
              deferChain: true,
            }),
            '/:scope/sputnik-daos': importRoute('views/pages/sputnikdaos', {
              scoped: true,
              deferChain: true,
            }),
            '/:scope/chat/:channel': importRoute('views/pages/chat', {
              scoped: true,
              deferChain: true,
            }),
            '/:scope/new/discussion': importRoute('views/pages/new_thread', {
              scoped: true,
              deferChain: true,
            }),
            // Profiles
            '/:scope/account/:address': importRoute('views/pages/profile', {
              scoped: true,
              deferChain: true,
            }),
            '/:scope/account': redirectRoute((a) =>
              activeAccount
                ? `/${a.scope}/account/${activeAccount.address}`
                : `/${a.scope}/`
            ),
            // Governance
            '/:scope/referenda': importRoute('views/pages/referenda', {
              scoped: true,
            }),
            '/:scope/proposals': importRoute('views/pages/proposals', {
              scoped: true,
            }),
            '/:scope/council': importRoute('views/pages/council', {
              scoped: true,
            }),
            '/:scope/delegate': importRoute('views/pages/delegate', {
              scoped: true,
            }),
            '/:scope/proposal/:type/:identifier': importRoute(
              'views/pages/view_proposal/index',
              { scoped: true }
            ),
            '/:scope/proposal/:identifier': importRoute(
              'views/pages/view_proposal/index',
              { scoped: true }
            ),
            '/:scope/discussion/:identifier': importRoute(
              'views/pages/view_proposal/index',
              { scoped: true }
            ),
            '/:scope/new/proposal/:type': importRoute(
              'views/pages/new_proposal/index',
              { scoped: true }
            ),
            '/:scope/new/proposal': importRoute(
              'views/pages/new_proposal/index',
              { scoped: true }
            ),

            // Treasury
            '/:scope/treasury': importRoute('views/pages/treasury', {
              scoped: true,
            }),
            '/:scope/bounties': importRoute('views/pages/bounties', {
              scoped: true,
            }),
            '/:scope/tips': importRoute('views/pages/tips', { scoped: true }),
            '/:scope/validators': importRoute('views/pages/validators', {
              scoped: true,
            }),
            // Settings
            '/login': importRoute('views/pages/login', { scoped: false }),
            '/:scope/login': importRoute('views/pages/login', {
              scoped: true,
              deferChain: true,
            }),
            '/:scope/web3login': importRoute('views/pages/web3login', {
              scoped: true,
              deferChain: true,
            }),
            // Admin
            '/:scope/admin': importRoute('views/pages/admin', { scoped: true }),
            '/manage': importRoute('views/pages/manage_community/index', {
              scoped: false,
            }),
            '/:scope/manage': importRoute(
              'views/pages/manage_community/index',
              { scoped: true }
            ),
            '/:scope/spec_settings': importRoute('views/pages/spec_settings', {
              scoped: true,
              deferChain: true,
            }),
            '/:scope/analytics': importRoute('views/pages/stats', {
              scoped: true,
              deferChain: true,
            }),

            '/:scope/snapshot/:snapshotId': importRoute(
              'views/pages/snapshot_proposals',
              { scoped: true, deferChain: true }
            ),
            '/:scope/multiple-snapshots': importRoute(
              'views/pages/view_multiple_snapshot_spaces',
              { scoped: true, deferChain: true }
            ),
            '/:scope/snapshot/:snapshotId/:identifier': importRoute(
              'views/pages/view_snapshot_proposal',
              { scoped: true, deferChain: true }
            ),
            '/:scope/new/snapshot/:snapshotId': importRoute(
              'views/pages/new_snapshot_proposal',
              { scoped: true, deferChain: true }
            ),
            '/:scope/snapshot-proposals/:snapshotId': redirectRoute(
              (attrs) => `/${attrs.scope}/snapshot/${attrs.snapshotId}`
            ),
            '/:scope/snapshot-proposal/:snapshotId/:identifier': redirectRoute(
              (attrs) =>
                `/${attrs.scope}/snapshot/${attrs.snapshotId}/${attrs.identifier}`
            ),
            '/:scope/new/snapshot-proposal/:snapshotId': redirectRoute(
              (attrs) => `/${attrs.scope}/new/snapshot/${attrs.snapshotId}`
            ),
            '/:scope/snapshot-proposals/:snapshotId/:identifier': redirectRoute(
              (attrs) =>
                `/${attrs.scope}/snapshot/${attrs.snapshotId}/${attrs.identifier}`
            ),
            '/:scope/new/snapshot-proposals/:snapshotId': redirectRoute(
              (attrs) => `/${attrs.scope}/new/snapshot/${attrs.snapshotId}`
            ),
          }),
    });

    const script = document.createElement('noscript');
    // eslint-disable-next-line max-len
    m.render(
      script,
      m.trust(
        '<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-KRWH69V" height="0" width="0" style="display:none;visibility:hidden"></iframe>'
      )
    );
    document.body.insertBefore(script, document.body.firstChild);

    // initialize construct-ui focus manager
    FocusManager.showFocusOnlyOnTab();

    // initialize mixpanel, before adding an alias or tracking identity
    try {
      if (
        document.location.host.startsWith('localhost') ||
        document.location.host.startsWith('127.0.0.1')
      ) {
        mixpanel.init(MIXPANEL_DEV_TOKEN, { debug: true });
      } else {
        // Production Mixpanel Project
        mixpanel.init(MIXPANEL_PROD_TOKEN, { debug: true });
      }
    } catch (e) {
      console.error('Mixpanel initialization error');
    }

    // handle login redirects
    if (
      m.route.param('loggedin') &&
      m.route.param('loggedin').toString() === 'true' &&
      m.route.param('path') &&
      !m.route.param('path').startsWith('/login')
    ) {
      // (we call toString() because m.route.param() returns booleans, even though the types don't reflect this)
      // handle param-based redirect after email login

      /* If we are creating a new account, then we alias to create a new mixpanel user
       else we identify to associate mixpanel events
    */
      if (m.route.param('new') && m.route.param('new').toString() === 'true') {
        console.log('creating account');

        try {
        } catch (err) {
          // Don't do anything... Just identify if there is an error
          // mixpanel.identify(m.route.param('email').toString());
        }
      } else {
      }
      m.route.set(m.route.param('path'), {}, { replace: true });
    } else if (
      localStorage &&
      localStorage.getItem &&
      localStorage.getItem('githubPostAuthRedirect')
    ) {
      // handle localStorage-based redirect after Github login (callback must occur within 30 seconds)
      try {
        const postAuth = JSON.parse(
          localStorage.getItem('githubPostAuthRedirect')
        );
        if (postAuth.path && +new Date() - postAuth.timestamp < 30 * 1000) {
          m.route.set(postAuth.path, {}, { replace: true });
        }
        localStorage.removeItem('githubPostAuthRedirect');
      } catch (e) {
        console.log('Error restoring path from localStorage');
      }
    } else if (
      localStorage &&
      localStorage.getItem &&
      localStorage.getItem('discordPostAuthRedirect')
    ) {
      try {
        const postAuth = JSON.parse(
          localStorage.getItem('discordPostAuthRedirect')
        );
        if (postAuth.path && +new Date() - postAuth.timestamp < 30 * 1000) {
          m.route.set(postAuth.path, {}, { replace: true });
        }
        localStorage.removeItem('discordPostAuthRedirect');
      } catch (e) {
        console.log('Error restoring path from localStorage');
      }
    }
    if (m.route.param('loggedin')) {
      notifySuccess('Logged in!');
    } else if (m.route.param('loginerror')) {
      notifyError('Could not log in');
      console.error(m.route.param('loginerror'));
    }

    // initialize the app
    initAppState(true, customDomain)
      .then(async () => {
        if (app.loginState === LoginState.LoggedIn) {
          // refresh notifications once
          app.user.notifications.refresh().then(() => m.redraw());
          // grab all discussion drafts
          app.user.discussionDrafts.refreshAll().then(() => m.redraw());
        }

        handleInviteLinkRedirect();
        // If the user updates their email
        handleUpdateEmailConfirmation();

        m.redraw();
      })
      .catch(() => {
        m.redraw();
      });
  }
);
Example #6
Source File: governance_section.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view(vnode) {
    // Conditional Render Details
    const hasProposals =
      app.chain &&
      (app.chain.base === ChainBase.CosmosSDK ||
        app.chain.network === ChainNetwork.Sputnik ||
        (app.chain.base === ChainBase.Substrate &&
          app.chain.network !== ChainNetwork.Plasm) ||
        app.chain.network === ChainNetwork.Moloch ||
        app.chain.network === ChainNetwork.Compound ||
        app.chain.network === ChainNetwork.Aave ||
        app.chain.network === ChainNetwork.Commonwealth ||
        app.chain.meta.snapshot);
    if (!hasProposals) return;

    const isNotOffchain = app.chain?.meta.type !== ChainType.Offchain;

    const showMolochMenuOptions =
      isNotOffchain &&
      app.user.activeAccount &&
      app.chain?.network === ChainNetwork.Moloch;
    const showMolochMemberOptions =
      isNotOffchain &&
      showMolochMenuOptions &&
      (app.user.activeAccount as any)?.shares?.gtn(0);
    const showCommonwealthMenuOptions =
      isNotOffchain && app.chain?.network === ChainNetwork.Commonwealth;
    const showCompoundOptions =
      isNotOffchain &&
      app.user.activeAccount &&
      app.chain?.network === ChainNetwork.Compound;
    const showAaveOptions =
      isNotOffchain &&
      app.user.activeAccount &&
      app.chain?.network === ChainNetwork.Aave;
    const showSnapshotOptions =
      isNotOffchain && app.chain?.meta.snapshot?.length > 0;
    const showReferenda =
      isNotOffchain &&
      app.chain?.base === ChainBase.Substrate &&
      app.chain.network !== ChainNetwork.Darwinia &&
      app.chain.network !== ChainNetwork.HydraDX;
    const showProposals =
      isNotOffchain &&
      ((app.chain?.base === ChainBase.Substrate &&
        app.chain.network !== ChainNetwork.Darwinia) ||
        (app.chain?.base === ChainBase.CosmosSDK && 
        app.chain.network !== ChainNetwork.Terra) ||
        app.chain?.network === ChainNetwork.Sputnik ||
        app.chain?.network === ChainNetwork.Moloch ||
        app.chain?.network === ChainNetwork.Compound ||
        app.chain?.network === ChainNetwork.Aave);
    const showCouncillors =
      isNotOffchain && app.chain?.base === ChainBase.Substrate;
    const showTreasury =
      isNotOffchain &&
      app.chain?.base === ChainBase.Substrate &&
      app.chain.network !== ChainNetwork.Centrifuge;
    const showBounties =
      isNotOffchain &&
      app.chain?.base === ChainBase.Substrate &&
      app.chain.network !== ChainNetwork.Centrifuge &&
      app.chain.network !== ChainNetwork.HydraDX;
    const showTips =
      isNotOffchain &&
      app.chain?.base === ChainBase.Substrate &&
      app.chain.network !== ChainNetwork.Centrifuge;
    const showValidators =
      isNotOffchain &&
      app.chain?.base === ChainBase.Substrate &&
      app.chain?.network !== ChainNetwork.Kulupu &&
      app.chain?.network !== ChainNetwork.Darwinia;

    // ---------- Build Toggle Tree ---------- //
    const governanceDefaultToggleTree: ToggleTree = {
      toggledState: true,
      children: {
        Members: {
          toggledState: false,
          children: {},
        },
        ...(showSnapshotOptions && {
          Snapshots: {
            toggledState: false,
            children: {},
          },
        }),
        ...(showCompoundOptions && {
          Delegate: {
            toggledState: true,
            children: {},
          },
        }),
        ...(showTreasury && {
          Treasury: {
            toggledState: false,
            children: {},
          },
        }),
        ...(showBounties && {
          Bounties: {
            toggledState: false,
            children: {},
          },
        }),
        ...(showReferenda && {
          Referenda: {
            toggledState: false,
            children: {},
          },
        }),
        ...(showProposals && {
          Proposals: {
            toggledState: false,
            children: {},
          },
        }),
        ...(showTips && {
          Tips: {
            toggledState: false,
            children: {},
          },
        }),
        ...(showCouncillors && {
          Councillors: {
            toggledState: false,
            children: {},
          },
        }),
        ...(showValidators && {
          Validators: {
            toggledState: false,
            children: {},
          },
        }),
      },
    };

    // Check if an existing toggle tree is stored
    if (!localStorage[`${app.activeChainId()}-governance-toggle-tree`]) {
      console.log('setting toggle tree from scratch');
      localStorage[`${app.activeChainId()}-governance-toggle-tree`] =
        JSON.stringify(governanceDefaultToggleTree);
    } else if (
      !verifyCachedToggleTree('governance', governanceDefaultToggleTree)
    ) {
      console.log(
        'setting discussions toggle tree since the cached version differs from the updated version'
      );
      localStorage[`${app.activeChainId()}-governance-toggle-tree`] =
        JSON.stringify(governanceDefaultToggleTree);
    }
    let toggleTreeState = JSON.parse(
      localStorage[`${app.activeChainId()}-governance-toggle-tree`]
    );
    if (vnode.attrs.mobile) {
      toggleTreeState = governanceDefaultToggleTree;
    }

    const onSnapshotProposal = (p) =>
      p.startsWith(`/${app.activeChainId()}/snapshot`);
    const onSnapshotProposalCreation = (p) =>
      p.startsWith(`/${app.activeChainId()}/new/snapshot/`);
    const onProposalPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/proposals`) ||
      p.startsWith(
        `/${app.activeChainId()}/proposal/${
          ProposalType.SubstrateDemocracyProposal
        }`
      );
    const onReferendaPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/referenda`) ||
      p.startsWith(
        `/${app.activeChainId()}/proposal/${
          ProposalType.SubstrateDemocracyReferendum
        }`
      );
    const onTreasuryPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/treasury`) ||
      p.startsWith(
        `/${app.activeChainId()}/proposal/${
          ProposalType.SubstrateTreasuryProposal
        }`
      );
    const onBountiesPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/bounties`);
    const onTipsPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/tips`) ||
      p.startsWith(
        `/${app.activeChainId()}/proposal/${ProposalType.SubstrateTreasuryTip}`
      );
    const onCouncilPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/council`);
    const onMotionPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/motions`) ||
      p.startsWith(
        `/${app.activeChainId()}/proposal/${
          ProposalType.SubstrateCollectiveProposal
        }`
      );
    const onValidatorsPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/validators`);
    const onNotificationsPage = (p) => p.startsWith('/notifications');
    const onMembersPage = (p) =>
      p.startsWith(`/${app.activeChainId()}/members`) ||
      p.startsWith(`/${app.activeChainId()}/account/`);

    if (onNotificationsPage(m.route.get())) return;

    // ---------- Build Section Props ---------- //

    // Members
    const membersData: SectionGroupAttrs = {
      title: 'Members',
      containsChildren: false,
      hasDefaultToggle: toggleTreeState['children']['Members']['toggledState'],
      isVisible: true,
      isUpdated: true,
      isActive:
        onMembersPage(m.route.get()) &&
        (app.chain ? app.chain.serverLoaded : true),
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        setGovernanceToggleTree('children.Members.toggledState', toggle);
        navigateToSubpage('/members');
      },
      displayData: null,
    };

    // Snapshots
    const snapshotData: SectionGroupAttrs = {
      title: 'Snapshots',
      containsChildren: false,
      hasDefaultToggle: showSnapshotOptions
        ? toggleTreeState['children']['Snapshots']['toggledState']
        : false,
      isVisible: showSnapshotOptions,
      isActive: onSnapshotProposal(m.route.get()),
      isUpdated: true,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        setGovernanceToggleTree('children.Snapshots.toggledState', toggle);
        // Check if we have multiple snapshots for conditional redirect
        const snapshotSpaces = app.chain.meta.snapshot;
        if (snapshotSpaces.length > 1) {
          navigateToSubpage('/multiple-snapshots', { action: 'select-space' });
        } else {
          navigateToSubpage(`/snapshot/${snapshotSpaces}`);
        }
      },
      displayData: null,
    };

    // Proposals
    const proposalsData: SectionGroupAttrs = {
      title: 'Proposals',
      containsChildren: false,
      hasDefaultToggle: showProposals
        ? toggleTreeState['children']['Proposals']['toggledState']
        : false,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        navigateToSubpage('/proposals');
        setGovernanceToggleTree('children.Proposals.toggledState', toggle);
      },
      isVisible: showProposals,
      isUpdated: true,
      isActive: onProposalPage(m.route.get()),
      displayData: null,
    };

    // Treasury
    const treasuryData: SectionGroupAttrs = {
      title: 'Treasury',
      containsChildren: false,
      hasDefaultToggle: showTreasury
        ? toggleTreeState['children']['Treasury']['toggledState']
        : false,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        navigateToSubpage('/treasury');
        setGovernanceToggleTree('children.Treasury.toggledState', toggle);
      },
      isVisible: showTreasury,
      isUpdated: true,
      isActive: onTreasuryPage(m.route.get()),
      displayData: null,
    };

    const bountyData: SectionGroupAttrs = {
      title: 'Bounties',
      containsChildren: false,
      hasDefaultToggle: showBounties
        ? toggleTreeState['children']['Bounties']['toggledState']
        : false,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        navigateToSubpage('/bounties');
        setGovernanceToggleTree('children.Bounties.toggledState', toggle);
      },
      isVisible: showBounties,
      isUpdated: true,
      isActive: onBountiesPage(m.route.get()),
      displayData: null,
    };

    const referendaData: SectionGroupAttrs = {
      title: 'Referenda',
      containsChildren: false,
      hasDefaultToggle: showReferenda
        ? toggleTreeState['children']['Referenda']['toggledState']
        : false,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        navigateToSubpage('/referenda');
        setGovernanceToggleTree('children.Referenda.toggledState', toggle);
      },
      isVisible: showReferenda,
      isUpdated: true,
      isActive: onReferendaPage(m.route.get()),
      displayData: null,
    };

    const tipsData: SectionGroupAttrs = {
      title: 'Tips',
      containsChildren: false,
      hasDefaultToggle: showTips
        ? toggleTreeState['children']['Tips']['toggledState']
        : false,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        navigateToSubpage('/tips');
        setGovernanceToggleTree('children.Tips.toggledState', toggle);
      },
      isVisible: showTips,
      isUpdated: true,
      isActive: onTipsPage(m.route.get()),
      displayData: null,
    };

    const councillorsData: SectionGroupAttrs = {
      title: 'Councillors',
      containsChildren: false,
      hasDefaultToggle: showCouncillors
        ? toggleTreeState['children']['Councillors']['toggledState']
        : false,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        navigateToSubpage('/council');
        setGovernanceToggleTree('children.Councillors.toggledState', toggle);
      },
      isVisible: showCouncillors,
      isUpdated: true,
      isActive: onCouncilPage(m.route.get()),
      displayData: null,
    };

    const validatorsData: SectionGroupAttrs = {
      title: 'Validators',
      containsChildren: false,
      hasDefaultToggle: showValidators
        ? toggleTreeState['children']['Validators']['toggledState']
        : false,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        navigateToSubpage('/validators');
        setGovernanceToggleTree('children.Validators.toggledState', toggle);
      },
      isVisible: showValidators,
      isUpdated: true,
      isActive: onValidatorsPage(m.route.get()),
      displayData: null,
    };

    // Delegate
    const delegateData: SectionGroupAttrs = {
      title: 'Delegate',
      containsChildren: false,
      hasDefaultToggle: showCompoundOptions
        ? toggleTreeState['children']['Delegate']['toggledState']
        : false,
      isVisible: showCompoundOptions,
      isUpdated: true,
      isActive: m.route.get() === `/${app.activeChainId()}/delegate`,
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        setGovernanceToggleTree('children.Delegate.toggledState', toggle);
        navigateToSubpage('/delegate');
      },
      displayData: null,
    };

    const governanceGroupData: SectionGroupAttrs[] = [
      membersData,
      snapshotData,
      delegateData,
      treasuryData,
      bountyData,
      referendaData,
      proposalsData,
      tipsData,
      councillorsData,
      validatorsData,
    ];

    const sidebarSectionData: SidebarSectionAttrs = {
      title: 'GOVERNANCE',
      hasDefaultToggle: toggleTreeState['toggledState'],
      onclick: (e, toggle: boolean) => {
        e.preventDefault();
        setGovernanceToggleTree('toggledState', toggle);
      },
      displayData: governanceGroupData,
      isActive: false,
      toggleDisabled: vnode.attrs.mobile,
    };

    return <SidebarSectionGroup {...sidebarSectionData} />;
  }
Example #7
Source File: cosmos_form.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view() {
    return (
      <div class="CreateCommunityForm">
        <InputRow
          title="RPC URL"
          defaultValue={this.state.form.nodeUrl}
          placeholder="http://my-rpc.cosmos-chain.com:26657/"
          onChangeHandler={async (v) => {
            this.state.form.nodeUrl = v;
          }}
        />
        <InputRow
          title="Name"
          defaultValue={this.state.form.name}
          onChangeHandler={(v) => {
            this.state.form.name = v;
            this.state.form.id = slugifyPreserveDashes(v);
          }}
        />
        <IdRow id={this.state.form.id} />
        <InputRow
          title="Symbol"
          defaultValue={this.state.form.symbol}
          placeholder="XYZ"
          onChangeHandler={(v) => {
            this.state.form.symbol = v;
          }}
        />
        <InputRow
          title="Bech32 Prefix"
          defaultValue={this.state.form.bech32Prefix}
          placeholder="cosmos"
          onChangeHandler={async (v) => {
            this.state.form.bech32Prefix = v;
          }}
        />
        <InputRow
          title="Decimals"
          defaultValue={`${this.state.form.decimals}`}
          disabled={true}
          onChangeHandler={(v) => {
            this.state.form.decimals = +v;
          }}
        />
        {/* TODO: add alt wallet URL field */}
        {...defaultChainRows(this.state.form)}
        <CWButton
          label="Save changes"
          disabled={this.state.saving}
          onclick={async () => {
            const {
              altWalletUrl,
              bech32Prefix,
              chainString,
              ethChainId,
              nodeUrl,
            } = this.state.form;
            this.state.saving = true;
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
              chainBase: null,
              isCustomDomain: app.isCustomDomain(),
              communityType: null,
            });
            try {
              const res = await $.post(`${app.serverUrl()}/createChain`, {
                alt_wallet_url: altWalletUrl,
                base: ChainBase.CosmosSDK,
                bech32_prefix: bech32Prefix,
                chain_string: chainString,
                eth_chain_id: ethChainId,
                jwt: app.user.jwt,
                network: this.state.form.id,
                node_url: nodeUrl,
                type: ChainType.Chain,
                ...this.state.form,
              });
              await initAppState(false);
              m.route.set(`/${res.result.chain?.id}`);
            } catch (err) {
              this.state.error =
                err.responseJSON?.error ||
                'Creating new Cosmos community failed';
            } finally {
              this.state.saving = false;
              m.redraw();
            }
          }}
        />
        <ValidationRow error={this.state.error} />
      </div>
    );
  }
Example #8
Source File: erc20_form.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view(vnode) {
    const validAddress = isAddress(this.state.form.address);
    const disableField = !validAddress || !this.state.loaded;

    const updateTokenForum = async () => {
      if (!this.state.form.address || !this.state.form.ethChainId) return;
      this.state.status = '';
      this.state.error = '';
      this.state.loading = true;
      const args = {
        address: this.state.form.address,
        chain_id: this.state.form.ethChainId,
        chain_network: ChainNetwork.ERC20,
        url: this.state.form.nodeUrl,
        allowUncached: true,
      };
      try {
        console.log('Querying backend for token data');
        const res = await $.get(`${app.serverUrl()}/getTokenForum`, args);
        if (res.status === 'Success') {
          if (res?.token?.name) {
            this.state.form.name = res.token.name || '';
            this.state.form.id = res.token.id && slugify(res.token.id);
            this.state.form.symbol = res.token.symbol || '';
            this.state.form.decimals = +res.token.decimals || 18;
            this.state.form.iconUrl = res.token.icon_url || '';
            if (this.state.form.iconUrl.startsWith('/')) {
              this.state.form.iconUrl = `https://commonwealth.im${this.state.form.iconUrl}`;
            }
            this.state.form.description = res.token.description || '';
            this.state.form.website = res.token.website || '';
            this.state.form.discord = res.token.discord || '';
            this.state.form.element = res.token.element || '';
            this.state.form.telegram = res.token.telegram || '';
            this.state.form.github = res.token.github || '';
            this.state.status = 'Success!';
          } else {
            // attempt to query ERC20Detailed token info from chain
            console.log('Querying chain for ERC info');
            const provider = new Web3.providers.WebsocketProvider(args.url);
            try {
              const ethersProvider = new providers.Web3Provider(provider);
              const contract = IERC20Metadata__factory.connect(
                args.address,
                ethersProvider
              );
              const name = await contract.name();
              const symbol = await contract.symbol();
              const decimals = await contract.decimals();
              this.state.form.name = name || '';
              this.state.form.id = name && slugify(name);
              this.state.form.symbol = symbol || '';
              this.state.form.decimals = decimals || 18;
              this.state.status = 'Success!';
            } catch (e) {
              this.state.form.name = '';
              this.state.form.id = '';
              this.state.form.symbol = '';
              this.state.form.decimals = 18;
              this.state.status = 'Verified token but could not load metadata.';
            }
            this.state.form.iconUrl = '';
            this.state.form.description = '';
            this.state.form.website = '';
            this.state.form.discord = '';
            this.state.form.element = '';
            this.state.form.telegram = '';
            this.state.form.github = '';
            provider.disconnect(1000, 'finished');
          }
          this.state.loaded = true;
        } else {
          this.state.error = res.message || 'Failed to load Token Information';
        }
      } catch (err) {
        this.state.error =
          err.responseJSON?.error || 'Failed to load Token Information';
      }
      this.state.loading = false;
      m.redraw();
    };

    return (
      <div class="CreateCommunityForm">
        {...ethChainRows(vnode.attrs, this.state.form)}
        <CWButton
          label="Populate fields"
          disabled={
            this.state.saving ||
            !validAddress ||
            !this.state.form.ethChainId ||
            this.state.loading
          }
          onclick={async () => {
            await updateTokenForum();
          }}
        />
        <ValidationRow error={this.state.error} status={this.state.status} />
        <InputRow
          title="Name"
          defaultValue={this.state.form.name}
          disabled={disableField}
          onChangeHandler={(v) => {
            this.state.form.name = v;
            this.state.form.id = slugifyPreserveDashes(v);
          }}
        />
        <IdRow id={this.state.form.id} />
        <InputRow
          title="Symbol"
          disabled={disableField}
          defaultValue={this.state.form.symbol}
          placeholder="XYZ"
          onChangeHandler={(v) => {
            this.state.form.symbol = v;
          }}
        />
        {...defaultChainRows(this.state.form, disableField)}
        <CWButton
          label="Save changes"
          disabled={this.state.saving || !validAddress || !this.state.loaded}
          onclick={async () => {
            const { altWalletUrl, chainString, ethChainId, nodeUrl } =
              this.state.form;
            this.state.saving = true;
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
              chainBase: null,
              isCustomDomain: app.isCustomDomain(),
              communityType: null,
            });
            try {
              const res = await $.post(`${app.serverUrl()}/createChain`, {
                alt_wallet_url: altWalletUrl,
                base: ChainBase.Ethereum,
                chain_string: chainString,
                eth_chain_id: ethChainId,
                jwt: app.user.jwt,
                network: ChainNetwork.ERC20,
                node_url: nodeUrl,
                type: ChainType.Token,
                ...this.state.form,
              });
              await initAppState(false);
              m.route.set(`/${res.result.chain?.id}`);
            } catch (err) {
              notifyError(
                err.responseJSON?.error || 'Creating new ERC20 community failed'
              );
            } finally {
              this.state.saving = false;
            }
          }}
        />
      </div>
    );
  }
Example #9
Source File: erc721_form.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view(vnode) {
    const validAddress = isAddress(this.state.form.address);
    const disableField = !validAddress || !this.state.loaded;

    const updateTokenForum = async () => {
      if (!this.state.form.address || !this.state.form.ethChainId) return;
      this.state.status = '';
      this.state.error = '';
      this.state.loading = true;
      const args = {
        address: this.state.form.address,
        chain_id: this.state.form.ethChainId,
        chain_network: ChainNetwork.ERC721,
        url: this.state.form.nodeUrl,
        allowUncached: true,
      };
      try {
        console.log('Querying backend for token data');
        const res = await $.get(`${app.serverUrl()}/getTokenForum`, args);
        if (res.status === 'Success') {
          if (res?.token?.name) {
            this.state.form.name = res.token.name || '';
            this.state.form.id = res.token.id && slugify(res.token.id);
            this.state.form.symbol = res.token.symbol || '';
            this.state.form.iconUrl = res.token.icon_url || '';
            if (this.state.form.iconUrl.startsWith('/')) {
              this.state.form.iconUrl = `https://commonwealth.im${this.state.form.iconUrl}`;
            }
            this.state.form.description = res.token.description || '';
            this.state.form.website = res.token.website || '';
            this.state.form.discord = res.token.discord || '';
            this.state.form.element = res.token.element || '';
            this.state.form.telegram = res.token.telegram || '';
            this.state.form.github = res.token.github || '';
            this.state.status = 'Success!';
          } else {
            // attempt to query ERC721Detailed token info from chain
            console.log('Querying chain for ERC info');
            const provider = new Web3.providers.WebsocketProvider(args.url);
            try {
              const ethersProvider = new providers.Web3Provider(provider);
              const contract = IERC721Metadata__factory.connect(
                args.address,
                ethersProvider
              );
              const name = await contract.name();
              const symbol = await contract.symbol();
              const decimals = await contract.decimals();
              this.state.form.name = name || '';
              this.state.form.id = name && slugify(name);
              this.state.form.symbol = symbol || '';
              this.state.status = 'Success!';
            } catch (e) {
              this.state.form.name = '';
              this.state.form.id = '';
              this.state.form.symbol = '';
              this.state.status = 'Verified token but could not load metadata.';
            }
            this.state.form.iconUrl = '';
            this.state.form.description = '';
            this.state.form.website = '';
            this.state.form.discord = '';
            this.state.form.element = '';
            this.state.form.telegram = '';
            this.state.form.github = '';
            provider.disconnect(1000, 'finished');
          }
          this.state.loaded = true;
        } else {
          this.state.error = res.message || 'Failed to load Token Information';
        }
      } catch (err) {
        this.state.error =
          err.responseJSON?.error || 'Failed to load Token Information';
      }
      this.state.loading = false;
      m.redraw();
    };

    return (
      <div class="CreateCommunityForm">
        {...ethChainRows(vnode.attrs, this.state.form)}
        <CWButton
          label="Populate fields"
          disabled={
            this.state.saving ||
            !validAddress ||
            !this.state.form.ethChainId ||
            this.state.loading
          }
          onclick={async () => {
            await updateTokenForum();
          }}
        />
        <ValidationRow error={this.state.error} status={this.state.status} />
        <InputRow
          title="Name"
          defaultValue={this.state.form.name}
          disabled={disableField}
          onChangeHandler={(v) => {
            this.state.form.name = v;
            this.state.form.id = slugifyPreserveDashes(v);
          }}
        />
        <IdRow id={this.state.form.id} />
        <InputRow
          title="Symbol"
          disabled={disableField}
          defaultValue={this.state.form.symbol}
          placeholder="XYZ"
          onChangeHandler={(v) => {
            this.state.form.symbol = v;
          }}
        />
        {...defaultChainRows(this.state.form, disableField)}
        <CWButton
          label="Save changes"
          disabled={this.state.saving || !validAddress || !this.state.loaded}
          onclick={async () => {
            const { altWalletUrl, chainString, ethChainId, nodeUrl } =
              this.state.form;
            this.state.saving = true;
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
              chainBase: null,
              isCustomDomain: app.isCustomDomain(),
              communityType: null,
            });

            try {
              const res = await $.post(`${app.serverUrl()}/createChain`, {
                alt_wallet_url: altWalletUrl,
                base: ChainBase.Ethereum,
                chain_string: chainString,
                eth_chain_id: ethChainId,
                jwt: app.user.jwt,
                network: ChainNetwork.ERC721,
                node_url: nodeUrl,
                type: ChainType.Token,
                ...this.state.form,
              });
              await initAppState(false);
              m.route.set(`/${res.result.chain?.id}`);
            } catch (err) {
              notifyError(
                err.responseJSON?.error ||
                  'Creating new ERC721 community failed'
              );
            } finally {
              this.state.saving = false;
            }
          }}
        />
      </div>
    );
  }
Example #10
Source File: eth_dao_form.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view(vnode) {
    const validAddress = isAddress(this.state.form.address);
    const disableField = !validAddress || !this.state.loaded;

    const updateDAO = async () => {
      if (
        !this.state.form.address ||
        !this.state.form.ethChainId ||
        !this.state.form.nodeUrl
      )
        return;
      this.state.loading = true;
      this.state.status = '';
      this.state.error = '';
      try {
        if (this.state.form.network === ChainNetwork.Compound) {
          const provider = new Web3.providers.WebsocketProvider(
            this.state.form.nodeUrl
          );
          const compoundApi = new CompoundAPI(
            null,
            this.state.form.address,
            provider
          );
          await compoundApi.init(this.state.form.tokenName);
          if (!compoundApi.Token) {
            throw new Error(
              'Could not find governance token. Is "Token Name" field valid?'
            );
          }
          const govType = GovernorType[compoundApi.govType];
          const tokenType = GovernorTokenType[compoundApi.tokenType];
          this.state.status = `Found ${govType} with token type ${tokenType}`;
        } else if (this.state.form.network === ChainNetwork.Aave) {
          const provider = new Web3.providers.WebsocketProvider(
            this.state.form.nodeUrl
          );
          const aaveApi = new AaveApi(
            IAaveGovernanceV2__factory.connect,
            this.state.form.address,
            provider
          );
          await aaveApi.init();
          this.state.status = `Found Aave type DAO`;
        } else {
          throw new Error('invalid chain network');
        }
      } catch (e) {
        this.state.error = e.message;
        this.state.loading = false;
        m.redraw();
        return;
      }
      this.state.loaded = true;
      this.state.loading = false;
      m.redraw();
    };

    return (
      <div class="CreateCommunityForm">
        {...ethChainRows(vnode.attrs, this.state.form)}
        <SelectRow
          title="DAO Type"
          options={[ChainNetwork.Aave, ChainNetwork.Compound]}
          value={this.state.form.network}
          onchange={(value) => {
            this.state.form.network = value;
            this.state.loaded = false;
          }}
        />
        {this.state.form.network === ChainNetwork.Compound && (
          <InputRow
            title="Token Name (Case Sensitive)"
            defaultValue={this.state.form.tokenName}
            onChangeHandler={(v) => {
              this.state.form.tokenName = v;
              this.state.loaded = false;
            }}
          />
        )}
        <CWButton
          label="Test contract"
          disabled={
            this.state.saving ||
            !validAddress ||
            !this.state.form.ethChainId ||
            this.state.loading
          }
          onclick={async () => {
            await updateDAO();
          }}
        />
        <ValidationRow error={this.state.error} status={this.state.status} />
        <InputRow
          title="Name"
          defaultValue={this.state.form.name}
          disabled={disableField}
          onChangeHandler={(v) => {
            this.state.form.name = v;
            this.state.form.id = slugifyPreserveDashes(v);
          }}
        />
        <IdRow id={this.state.form.id} />
        <InputRow
          title="Symbol"
          disabled={disableField}
          defaultValue={this.state.form.symbol}
          placeholder="XYZ"
          onChangeHandler={(v) => {
            this.state.form.symbol = v;
          }}
        />
        {...defaultChainRows(this.state.form, disableField)}
        <CWButton
          label="Save changes"
          disabled={this.state.saving || !validAddress || !this.state.loaded}
          onclick={async () => {
            const { chainString, ethChainId, nodeUrl, tokenName } =
              this.state.form;
            this.state.saving = true;
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
              chainBase: null,
              isCustomDomain: app.isCustomDomain(),
              communityType: null,
            });
            try {
              const res = await $.post(`${app.serverUrl()}/createChain`, {
                base: ChainBase.Ethereum,
                chain_string: chainString,
                eth_chain_id: ethChainId,
                jwt: app.user.jwt,
                node_url: nodeUrl,
                token_name: tokenName,
                type: ChainType.DAO,
                ...this.state.form,
              });
              await initAppState(false);
              // TODO: notify about needing to run event migration
              m.route.set(`/${res.result.chain?.id}`);
            } catch (err) {
              notifyError(
                err.responseJSON?.error ||
                  'Creating new ETH DAO community failed'
              );
            } finally {
              this.state.saving = false;
            }
          }}
        />
      </div>
    );
  }
Example #11
Source File: spl_token_form.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view() {
    const disableField = !this.state.loaded;

    const updateTokenForum = async () => {
      this.state.status = '';
      this.state.error = '';
      let mintPubKey: solw3.PublicKey;
      try {
        mintPubKey = new solw3.PublicKey(this.state.form.mint);
      } catch (e) {
        this.state.error = 'Invalid mint address';
        return false;
      }
      if (!mintPubKey) return;
      this.state.loading = true;
      try {
        const url = solw3.clusterApiUrl(this.state.form.cluster);
        const connection = new solw3.Connection(url);
        const supply = await connection.getTokenSupply(mintPubKey);
        const { decimals, amount } = supply.value;
        this.state.form.decimals = decimals;
        this.state.loaded = true;
        this.state.status = `Found ${amount} supply!`;
      } catch (err) {
        this.state.error = `Error: ${err.message}` || 'Failed to load token';
      }
      this.state.loading = false;
      m.redraw();
    };

    return (
      <div class="CreateCommunityForm">
        <SelectRow
          title="Cluster"
          options={['mainnet-beta', 'testnet', 'devnet']}
          value={this.state.form.cluster}
          onchange={(value) => {
            this.state.form.cluster = value;
            this.state.loaded = false;
          }}
        />
        <InputRow
          title="Mint Address"
          defaultValue={this.state.form.mint}
          placeholder="2sgDUTgTP6e9CrJtexGdba7qZZajVVHf9TiaCtS9Hp3P"
          onChangeHandler={(v) => {
            this.state.form.mint = v.trim();
            this.state.loaded = false;
          }}
        />
        <CWButton
          label="Check address"
          disabled={this.state.saving || this.state.loading}
          onclick={async () => {
            await updateTokenForum();
          }}
        />
        <ValidationRow error={this.state.error} status={this.state.status} />
        <InputRow
          title="Name"
          defaultValue={this.state.form.name}
          disabled={disableField}
          onChangeHandler={(v) => {
            this.state.form.name = v;
            this.state.form.id = slugifyPreserveDashes(v);
          }}
        />
        <IdRow id={this.state.form.id} />
        <InputRow
          title="Symbol"
          disabled={disableField}
          defaultValue={this.state.form.symbol}
          placeholder="XYZ"
          onChangeHandler={(v) => {
            this.state.form.symbol = v;
          }}
        />
        <InputRow
          title="Decimals"
          defaultValue={`${this.state.form.decimals}`}
          disabled={true}
          onChangeHandler={(v) => {
            this.state.form.decimals = +v;
          }}
        />
        {...defaultChainRows(this.state.form, disableField)}
        <CWButton
          label="Save changes"
          disabled={this.state.saving || !this.state.loaded}
          onclick={async () => {
            const { cluster, iconUrl, mint } = this.state.form;
            this.state.saving = true;
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
              chainBase: null,
              isCustomDomain: app.isCustomDomain(),
              communityType: null,
            });
            try {
              const res = await $.post(`${app.serverUrl()}/createChain`, {
                address: mint,
                base: ChainBase.Solana,
                icon_url: iconUrl,
                jwt: app.user.jwt,
                network: ChainNetwork.SPL,
                node_url: cluster,
                type: ChainType.Token,
                ...this.state.form,
              });
              await initAppState(false);
              m.route.set(`/${res.result.chain?.id}`);
            } catch (err) {
              notifyError(
                err.responseJSON?.error || 'Creating new ERC20 community failed'
              );
            } finally {
              this.state.saving = false;
            }
          }}
        />
      </div>
    );
  }
Example #12
Source File: sputnik_form.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view(vnode) {
    return (
      <div class="CreateCommunityForm">
        <InputRow
          title="DAO Name"
          defaultValue={this.state.form.name}
          onChangeHandler={(v) => {
            this.state.form.name = v.toLowerCase();
          }}
          placeholder="genesis"
        />
        <ToggleRow
          title="Network"
          defaultValue={this.state.form.isMainnet}
          onToggle={(checked) => {
            vnode.state.isMainnet = checked;
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CHAIN_SELECTED,
              chainBase: ChainBase.CosmosSDK,
              isCustomDomain: app.isCustomDomain(),
              communityType: CommunityType.SputnikDao,
            });
          }}
          label={(checked) => {
            if (checked !== this.state.form.isMainnet) {
              return 'Unknown network!';
            }
            return checked ? 'Mainnet' : 'Testnet';
          }}
        />
        {/* TODO: add divider to distinguish on-chain data */}
        {...defaultChainRows(this.state.form)}
        <CWButton
          label="Save changes"
          disabled={this.state.saving}
          onclick={async () => {
            const { iconUrl, name } = this.state.form;

            this.state.saving = true;

            const isMainnet = this.state.form.isMainnet;

            const id = isMainnet
              ? `${name}.sputnik-dao.near`
              : `${name}.sputnikv2.testnet`;

            const url = isMainnet
              ? 'https://rpc.mainnet.near.org'
              : 'https://rpc.testnet.near.org';

            const createChainArgs = {
              base: ChainBase.NEAR,
              icon_url: iconUrl,
              id,
              jwt: app.user.jwt,
              name: id,
              network: ChainNetwork.Sputnik,
              node_url: url,
              symbol: isMainnet ? 'NEAR' : 'tNEAR',
              type: ChainType.DAO,
              ...this.state.form,
            };
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
              chainBase: null,
              isCustomDomain: app.isCustomDomain(),
              communityType: null,
            });

            try {
              // Gabe 2/14/22 Commenting this bit out because it isn't actually used, but maybe it will be someday?
              //
              // verify the DAO exists
              //   const config: ConnectConfig = {
              //     networkId: isMainnet ? 'mainnet' : 'testnet',
              //     nodeUrl: url,
              //     keyStore: new keyStores.BrowserLocalStorageKeyStore(
              //       localStorage
              //     ),
              //   };
              //   const api = await nearConnect(config);

              //   const rawResult = await api.connection.provider.query<CodeResult>(
              //     {
              //       request_type: 'call_function',
              //       account_id: id,
              //       method_name: 'get_last_proposal_id',
              //       args_base64: Buffer.from(JSON.stringify({})).toString(
              //         'base64'
              //       ),
              //       finality: 'optimistic',
              //     }
              //   );
              //   const _validResponse = JSON.parse(
              //     Buffer.from(rawResult.result).toString()
              //   );

              // POST object
              const res = await $.post(
                `${app.serverUrl()}/createChain`,
                createChainArgs
              );
              await initAppState(false);
              m.route.set(`${window.location.origin}/${res.result.chain.id}`);
            } catch (err) {
              notifyError(err.responseJSON?.error || 'Adding DAO failed.');
              console.error(err.responseJSON?.error || err.message);
              this.state.saving = false;
            }
          }}
        />
      </div>
    );
  }
Example #13
Source File: starter_community_form.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view() {
    return (
      <div class="CreateCommunityForm">
        <InputRow
          title="Name"
          placeholder="Enter the name of your community"
          defaultValue={this.state.form.name}
          onChangeHandler={(v) => {
            this.state.form.name = v;
            this.state.form.id = slugifyPreserveDashes(v);
          }}
        />
        <IdRow id={this.state.form.id} />
        <InputRow
          title="Symbol"
          defaultValue={this.state.form.symbol}
          onChangeHandler={(v) => {
            this.state.form.symbol = v;
          }}
        />
        <SelectRow
          title="Base Chain"
          options={['cosmos', 'ethereum', 'near']}
          value={this.state.form.base}
          onchange={(value) => {
            this.state.form.base = value;
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CHAIN_SELECTED,
              chainBase: value,
              isCustomDomain: app.isCustomDomain(),
              communityType: CommunityType.StarterCommunity,
            });
          }}
        />
        {...defaultChainRows(this.state.form)}
        <CWButton
          label="Save changes"
          disabled={this.state.saving || this.state.form.id.length < 1}
          onclick={async () => {
            this.state.saving = true;
            const additionalArgs: {
              eth_chain_id?: number;
              node_url?: string;
              bech32_prefix?: string;
              alt_wallet_url?: string;
            } = {};
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
              chainBase: this.state.form.base,
              isCustomDomain: app.isCustomDomain(),
              communityType: CommunityType.StarterCommunity,
            });

            // defaults to be overridden when chain is no longer "starter" type
            switch (this.state.form.base) {
              case ChainBase.CosmosSDK: {
                additionalArgs.node_url = 'https://rpc-osmosis.blockapsis.com';
                additionalArgs.bech32_prefix = 'osmo';
                additionalArgs.alt_wallet_url =
                  'https://lcd-osmosis.blockapsis.com';
                break;
              }
              case ChainBase.NEAR: {
                additionalArgs.node_url = 'https://rpc.mainnet.near.org';
                break;
              }
              case ChainBase.Solana: {
                additionalArgs.node_url = 'https://api.mainnet-beta.solana.com';
                break;
              }
              case ChainBase.Substrate: {
                additionalArgs.node_url = 'wss://mainnet.edgewa.re';
                break;
              }
              case ChainBase.Ethereum:
              default: {
                additionalArgs.eth_chain_id = 1;
                additionalArgs.node_url =
                  'wss://eth-mainnet.alchemyapi.io/v2/BCNLWCaGqaXwCDHlZymPy3HpjXSxK7j_';
                additionalArgs.alt_wallet_url =
                  'https://eth-mainnet.alchemyapi.io/v2/BCNLWCaGqaXwCDHlZymPy3HpjXSxK7j_';
                break;
              }
            }
            const {
              id,
              name,
              symbol,
              iconUrl,
              description,
              website,
              discord,
              telegram,
              github,
              element,
              base,
            } = this.state.form;
            try {
              const res = await $.post(`${app.serverUrl()}/createChain`, {
                jwt: app.user.jwt,
                address: '',
                type: ChainType.Offchain,
                network: baseToNetwork(this.state.form.base),
                icon_url: iconUrl,
                id,
                name,
                symbol,
                base,
                description,
                discord,
                element,
                github,
                telegram,
                website,
                ...additionalArgs,
              });
              await initAppState(false);
              m.route.set(`/${res.result.chain?.id}`);
            } catch (err) {
              notifyError(
                err.responseJSON?.error ||
                  'Creating new starter community failed'
              );
            } finally {
              this.state.saving = false;
            }
          }}
        />
      </div>
    );
  }
Example #14
Source File: substrate_form.tsx    From commonwealth with GNU General Public License v3.0 4 votes vote down vote up
view() {
    return (
      <div class="CreateCommunityForm">
        <InputRow
          title="Name"
          defaultValue={this.state.form.name}
          onChangeHandler={(v) => {
            this.state.form.name = v;
          }}
        />
        <InputRow
          title="Node URL"
          defaultValue={this.state.form.nodeUrl}
          placeholder="wss://"
          onChangeHandler={(v) => {
            this.state.form.nodeUrl = v;
          }}
        />
        <InputRow
          title="Symbol"
          defaultValue={this.state.form.symbol}
          placeholder="XYZ"
          onChangeHandler={(v) => {
            this.state.form.symbol = v;
          }}
        />
        <InputRow
          title="Spec (JSON)"
          defaultValue={this.state.form.substrateSpec}
          // TODO: how to make this resizable vertically?
          //   looks like CUI specifies an !important height tag, which prevents this
          textarea={true}
          placeholder='{"types": {"Address": "MultiAddress", "ChainId": "u8", "Reveals": "Vec<(AccountId, Vec<VoteOutcome>)>", "Balance2": "u128", "VoteData": {"stage": "VoteStage", "initiator": "AccountId", "vote_type": "VoteType", "tally_type": "TallyType", "is_commit_reveal": "bool"}, "VoteType": {"_enum": ["Binary", "MultiOption", "RankedChoice"]}, "TallyType": {"_enum": ["OnePerson", "OneCoin"]}, "VoteStage": {"_enum": ["PreVoting", "Commit", "Voting", "Completed"]}, "ResourceId": "[u8; 32]", "VoteRecord": {"id": "u64", "data": "VoteData", "reveals": "Reveals", "outcomes": "Vec<VoteOutcome>", "commitments": "Commitments"}, "AccountInfo": "AccountInfoWithRefCount", "Commitments": "Vec<(AccountId, VoteOutcome)>", "VoteOutcome": "[u8; 32]", "VotingTally": "Option<Vec<(VoteOutcome, u128)>>", "DepositNonce": "u64", "LookupSource": "MultiAddress", "ProposalTitle": "Bytes", "ProposalVotes": {"staus": "ProposalStatus", "expiry": "BlockNumber", "votes_for": "Vec<AccountId>", "votes_against": "Vec<AccountId>"}, "ProposalRecord": {"index": "u32", "stage": "VoteStage", "title": "Text", "author": "AccountId", "vote_id": "u64", "contents": "Text", "transition_time": "u32"}, "ProposalStatus": {"_enum": ["Initiated", "Approved", "Rejected"]}, "ProposalContents": "Bytes"}}'
          onChangeHandler={(v) => {
            this.state.form.substrateSpec = v;
          }}
        />
        <CWButton
          label="Test Connection"
          className="button-margin-bottom"
          onclick={async () => {
            // deinit substrate API if one exists
            if (app.chain?.apiInitialized) {
              await app.chain.deinit();
            }

            // create new API
            const provider = new WsProvider(
              constructSubstrateUrl(this.state.form.nodeUrl),
              false
            );
            try {
              await provider.connect();
              const api = await ApiPromise.create({
                throwOnConnect: true,
                provider,
                ...JSON.parse(this.state.form.substrateSpec),
              });
              await api.disconnect();
              notifySuccess('Test has passed');
            } catch (err) {
              console.error(err.message);
              notifyError('Test API initialization failed');
            }
          }}
        />
        {...defaultChainRows(this.state.form)}
        <CWButton
          label="Save changes"
          disabled={this.state.saving}
          onclick={async () => {
            const { name, nodeUrl, iconUrl, substrateSpec } = this.state.form;
            mixpanelBrowserTrack({
              event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
              chainBase: null,
              isCustomDomain: app.isCustomDomain(),
              communityType: null,
            });
            try {
              JSON.parse(substrateSpec);
            } catch (err) {
              notifyError('Spec provided has invalid JSON');
              return;
            }
            this.state.saving = true;
            $.post(`${app.serverUrl()}/createChain`, {
              base: ChainBase.Substrate,
              icon_url: iconUrl,
              id: slugify(name),
              jwt: app.user.jwt,
              network: slugify(name),
              node_url: nodeUrl,
              substrate_spec: substrateSpec,
              type: ChainType.Chain,
              ...this.state.form,
            })
              .then(async (res) => {
                await initAppState(false);
                m.route.set(`/${res.result.chain.id}`);
              })
              .catch((err: any) => {
                notifyError(
                  err.responseJSON?.error || 'Creating new community failed'
                );
              })
              .always(() => {
                this.state.saving = false;
              });
          }}
        />
      </div>
    );
  }