@polkadot/util-crypto#createKeyMulti TypeScript Examples

The following examples show how to use @polkadot/util-crypto#createKeyMulti. 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: multisig.ts    From subscan-multisig-react with Apache License 2.0 6 votes vote down vote up
export function findMultiAccount({
  threshold,
  members,
}: Pick<WalletFormValue, 'members' | 'threshold'>): KeyringAddress | null {
  const existsAccounts = keyring.getAccounts().filter((account) => account.meta.isMultisig);
  const key = createKeyMulti(
    members.map((item) => item.address),
    threshold
  );

  return existsAccounts.find((acc) => acc.publicKey.toString() === key.toString()) ?? null;
}
Example #2
Source File: multisig.ts    From subscan-multisig-react with Apache License 2.0 6 votes vote down vote up
export function updateMultiAccountScope(
  { share, scope = [], members, threshold }: WalletFormValue,
  network: Network
): void {
  const saveScope =
    ShareScope.custom === share ? (scope as Network[]) : share === ShareScope.all ? ShareScope.all : [network];
  const key = createKeyMulti(
    members.map((item) => item.address),
    threshold
  );

  store.set(scopeKey(key), saveScope);
}
Example #3
Source File: Wallets.tsx    From subscan-multisig-react with Apache License 2.0 4 votes vote down vote up
export function Wallets() {
  const { t } = useTranslation();
  const history = useHistory();
  const { api, chain, network } = useApi();
  const [multisigAccounts, setMultisigAccounts] = useState<KeyringAddress[]>([]);
  const isExtensionAccount = useIsInjected();
  const [isCalculating, setIsCalculating] = useState<boolean>(true);
  const [searchKeyword, setSearchKeyword] = useState('');

  const displayMultisigAccounts = useMemo(() => {
    if (!searchKeyword.trim()) {
      return multisigAccounts;
    }
    return multisigAccounts.filter(
      (account) =>
        (account.meta.name && account.meta.name.indexOf(searchKeyword.trim()) >= 0) ||
        account.address.indexOf(searchKeyword.trim()) >= 0
    );
  }, [multisigAccounts, searchKeyword]);

  const { linkColor, mainColor } = useMemo(() => {
    return { linkColor: getLinkColor(network), mainColor: getThemeColor(network) };
  }, [network]);

  const exportAllWallets = () => {
    if (!multisigAccounts || multisigAccounts.length < 1) {
      return;
    }
    const configs: MultisigAccountConfig[] = [];
    multisigAccounts.forEach((multisigAccount) => {
      const config = {
        name: multisigAccount.meta.name || '',
        members: multisigAccount.meta.addressPair as { name: string; address: string }[],
        threshold: multisigAccount.meta.threshold as number,
        scope: getMultiAccountScope(multisigAccount.publicKey),
      };
      configs.push(config);
    });

    const blob = new Blob([JSON.stringify(configs)], { type: 'text/plain;charset=utf-8' });
    saveAs(blob, `multisig_accounts.json`);
  };

  const uploadProps = {
    name: 'file',
    headers: {
      authorization: 'authorization-text',
    },
    onChange(info: any) {
      if (info.file.status !== 'uploading') {
        // console.log(info.file, info.fileList);
      }
      // if (info.file.status === 'done') {
      //   message.success(`${info.file.name} file uploaded successfully`);
      // } else if (info.file.status === 'error') {
      //   message.error(`${info.file.name} file upload failed.`);
      // }
    },
    customRequest(info: any) {
      try {
        const reader = new FileReader();

        reader.onload = (e: any) => {
          // eslint-disable-next-line no-console
          // console.log(e.target.result);

          try {
            const configs = JSON.parse(e.target.result) as MultisigAccountConfig[];
            configs
              .filter((config) => {
                const encodeMembers = config.members.map((member) => {
                  return {
                    name: member.name,
                    address: encodeAddress(member.address, Number(chain.ss58Format)),
                  };
                });
                const publicKey = createKeyMulti(
                  encodeMembers.map((item) => item.address),
                  config.threshold
                );
                const acc = findMultiAccountFromKey(publicKey);
                return !acc;
              })
              .forEach((config) => {
                const encodeMembers = config.members.map((member) => {
                  return {
                    name: member.name,
                    address: encodeAddress(member.address, Number(chain.ss58Format)),
                  };
                });
                const signatories = encodeMembers.map(({ address }) => address);

                const addressPair = config.members.map(({ address, ...other }) => ({
                  ...other,
                  address: encodeAddress(address, Number(chain.ss58Format)),
                }));

                keyring.addMultisig(signatories, config.threshold, {
                  name: config.name,
                  addressPair,
                  genesisHash: api?.genesisHash.toHex(),
                });

                const publicKey = createKeyMulti(
                  encodeMembers.map((item) => item.address),
                  config.threshold
                );

                updateMultiAccountScopeFromKey(publicKey, ShareScope.all, [], network);
              });

            message.success(t('success'));
            refreshMultisigAccounts();
          } catch {
            message.error(t('account config error'));
          }
        };
        reader.readAsText(info.file);
      } catch (err: unknown) {
        if (err instanceof Error) {
          // eslint-disable-next-line no-console
          console.log('err:', err);
        }
      }
    },
  };

  const renderAddress = (address: string) => (
    <Link to={Path.extrinsic + '/' + address + history.location.hash} style={{ color: linkColor }}>
      {address}
    </Link>
  );

  const renderAction = useCallback(
    (row: KeyringAddress) => {
      // const { address } = row;

      return (
        <Space size="middle">
          <div className="flex items-center">
            <Button
              type="primary"
              className="flex items-center justify-center h-7"
              onClick={() => {
                history.push(Path.extrinsic + '/' + row.address + history.location.hash);
              }}
              style={{
                borderRadius: '4px',
              }}
            >
              {t('actions')}
            </Button>

            {(row as unknown as any).entries && (row as unknown as any).entries.length > 0 && (
              <div className="ml-2 bg-red-500 rounded-full w-3 h-3"></div>
            )}
          </div>
        </Space>
      );
    },
    [history, t]
  );

  const columns: ColumnsType<KeyringAddress> = [
    {
      title: t('name'),
      dataIndex: ['meta', 'name'],
    },
    {
      title: t('address'),
      dataIndex: 'address',
      render: renderAddress,
    },
    {
      title: t('balance'),
      key: 'balance',
      render: (account) => {
        return <Space direction="vertical">{renderBalances(account, chain)}</Space>;
      },
    },
    {
      title: t('status.index'),
      key: 'status',
      render: (_, record) => {
        const {
          meta: { addressPair },
        } = record;

        return (addressPair as AddressPair[])?.some((item) => isExtensionAccount(item.address))
          ? t('available')
          : t('watch only');
      },
    },
    {
      // title: t('actions'),
      title: '',
      key: 'action',
      render: (_1: unknown, row) => renderAction(row),
    },
  ];

  const expandedRowRender = (record: KeyringAddress) => {
    const columnsNested: ColumnsType<KeyringJson> = [
      { dataIndex: 'name' },
      {
        dataIndex: 'address',
        render: (address) => (
          <Space size="middle">
            <BaseIdentityIcon theme="polkadot" size={32} value={address} />
            <SubscanLink address={address} />
          </Space>
        ),
      },
      {
        key: 'isInject',
        render: (_, pair) => t(isExtensionAccount(pair.address) ? 'injected' : 'external'),
      },
    ];

    return (
      <div className="multisig-list-expand bg-gray-100 p-5 members">
        <Table
          columns={columnsNested}
          dataSource={record.meta.addressPair as KeyringJson[]}
          pagination={false}
          bordered
          rowKey="address"
          className=" table-without-head"
        />
      </div>
    );
  };

  const refreshMultisigAccounts = useCallback(async () => {
    setIsCalculating(true);

    const accounts = keyring
      .getAccounts()
      .filter((account) => account.meta.isMultisig && isInCurrentScope(account.publicKey, network));
    const balances = await api?.query.system.account.multi(accounts.map(({ address }) => address));
    const entries = await Promise.all(
      accounts.map(async ({ address }) => await api?.query.multisig.multisigs.entries(address))
    );

    setMultisigAccounts(
      accounts.map((item, index) => {
        (item.meta.addressPair as KeyringJson[]).forEach((key) => {
          key.address = convertToSS58(key.address, Number(chain.ss58Format));
        });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const source = (balances as any)[index] as unknown as any;
        return {
          ...item,
          value: source.data.free.toString(),
          kton: source.data.freeKton?.toString() ?? '0',
          entries: entries[index] || [],
        };
      })
    );
    setIsCalculating(false);
  }, [api, network, chain]);

  useEffect(() => {
    refreshMultisigAccounts();
  }, [refreshMultisigAccounts]);

  const menu = (
    <Menu>
      <Menu.Item key="1" onClick={exportAllWallets}>
        {t('export all')}
      </Menu.Item>

      <Upload {...uploadProps} showUploadList={false}>
        <Menu.Item key="2">{t('import all')}</Menu.Item>
      </Upload>
    </Menu>
  );

  if (!isCalculating && multisigAccounts.length === 0) {
    return (
      <Space
        direction="vertical"
        className="w-full h-full flex flex-col items-center justify-center absolute"
        id="wallets"
      >
        <div className="flex flex-col items-center">
          <AddIcon className="w-24 h-24" />

          <div className="text-black-800 font-semibold text-xl lg:mt-16 lg:mb-10 mt-6 mb-4">
            Please create Multisig wallet first
          </div>

          <Link to={Path.wallet + history.location.hash}>
            <Button type="primary" className="w-48">
              {t('wallet.add')}
            </Button>
          </Link>

          <div className="my-1">{t('or')}</div>

          <Upload {...uploadProps} showUploadList={false}>
            <Button type="primary" className="w-48">
              {t('import all')}
            </Button>
          </Upload>
        </div>
      </Space>
    );
  }

  return (
    <Space direction="vertical" className="absolute top-4 bottom-4 left-4 right-4 overflow-auto" id="wallets">
      <div className="flex flex-col md:justify-between md:flex-row">
        <div className="flex items-center">
          <Link to={Path.wallet + history.location.hash}>
            <Button type="primary" className="w-44">
              {t('wallet.add')}
            </Button>
          </Link>

          <Dropdown overlay={menu} trigger={['click']} placement="bottomCenter">
            <MoreOutlined
              className="ml-4 rounded-full opacity-60 cursor-pointer p-1"
              style={{
                color: mainColor,
                backgroundColor: mainColor + '40',
              }}
              onClick={(e) => e.preventDefault()}
            />
          </Dropdown>

          {/* {multisigAccounts && multisigAccounts.length >= 1 && (
          <Tooltip title={t('export all')}>
            <ExportIcon className="ml-5 mt-1 w-8 h-8 cursor-pointer" onClick={exportAllWallets} />
          </Tooltip>
        )}

        <Tooltip title={t('import all')}>
          <Upload {...uploadProps} showUploadList={false}>
            <ImportIcon className="ml-5 mt-1 w-8 h-8 cursor-pointer" />
          </Upload>
        </Tooltip> */}
        </div>

        <div className="w-56 mt-4 md:mt-0 md:w-72">
          <Input
            placeholder={t('wallet search placeholder')}
            value={searchKeyword}
            onChange={(e) => {
              setSearchKeyword(e.target.value);
            }}
          />
        </div>
      </div>

      <Table
        columns={columns}
        dataSource={displayMultisigAccounts}
        rowKey="address"
        expandable={{ expandedRowRender, expandIcon: genExpandMembersIcon(t('members')), expandIconColumnIndex: 4 }}
        pagination={false}
        loading={isCalculating}
        className="lg:block hidden multisig-list-table"
      />

      <Space direction="vertical" className="lg:hidden block">
        {displayMultisigAccounts.map((account) => {
          const { address, meta } = account;

          return (
            <Collapse
              key={address}
              expandIcon={() => <></>}
              collapsible={
                (meta.addressPair as AddressPair[])?.some((item) => isExtensionAccount(item.address))
                  ? 'header'
                  : 'disabled'
              }
              className="wallet-collapse"
            >
              <Panel
                header={
                  <Space direction="vertical" className="w-full">
                    <Typography.Text className="mr-4">{meta.name}</Typography.Text>

                    <Typography.Text copyable>{address}</Typography.Text>
                  </Space>
                }
                key={address}
                extra={
                  <Space direction="vertical" className="text-right">
                    <Space>{renderBalances(account, chain)}</Space>
                    {renderAction(account)}
                  </Space>
                }
                className="overflow-hidden mb-4"
              >
                <MemberList data={account} />
              </Panel>
            </Collapse>
          );
        })}
      </Space>
    </Space>
  );
}