@polkadot/util#isNull TypeScript Examples

The following examples show how to use @polkadot/util#isNull. 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: useCall.ts    From crust-apps with Apache License 2.0 6 votes vote down vote up
// extract the serialized and mapped params, all ready for use in our call
function extractParams <T> (fn: unknown, params: unknown[], { paramMap = transformIdentity }: CallOptions<T> = {}): [string, CallParams | null] {
  return [
    JSON.stringify({ f: (fn as { name: string })?.name, p: params }),
    params.length === 0 || !params.some((param) => isNull(param) || isUndefined(param))
      ? paramMap(params)
      : null
  ];
}
Example #2
Source File: valueToText.tsx    From subscan-multisig-react with Apache License 2.0 6 votes vote down vote up
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line complexity
export default function valueToText(
  type: string,
  value: Codec | undefined | null,
  _swallowError = true,
  contentShorten = true
): React.ReactNode {
  if (isNull(value) || isUndefined(value)) {
    return div({}, '<unknown>');
  }

  return div(
    {},
    ['Bytes', 'Raw', 'Option<Keys>', 'Keys'].includes(type) && isFunction(value.toU8a)
      ? // eslint-disable-next-line no-magic-numbers
        u8aToHex(value.toU8a(true), contentShorten ? 512 : -1)
      : // HACK Handle Keys as hex-only (this should go away once the node value is
      // consistently swapped to `Bytes`)
      type === 'Vec<(ValidatorId,Keys)>'
      ? toString(formatKeys(value as unknown as [ValidatorId, Keys][]))
      : value instanceof Raw
      ? value.isEmpty
        ? '<empty>'
        : value.toString()
      : value instanceof Option && value.isNone
      ? '<none>'
      : toString(toHuman(value))
  );
}
Example #3
Source File: useCall.ts    From subscan-multisig-react with Apache License 2.0 6 votes vote down vote up
// extract the serialized and mapped params, all ready for use in our call
function extractParams<T>(
  fn: unknown,
  params: unknown[],
  { paramMap = transformIdentity }: CallOptions<T> = {}
): [string, CallParams | null] {
  return [
    JSON.stringify({ f: (fn as { name: string })?.name, p: params }),
    params.length === 0 || !params.some((param) => isNull(param) || isUndefined(param)) ? paramMap(params) : null,
  ];
}
Example #4
Source File: index.tsx    From subscan-multisig-react with Apache License 2.0 6 votes vote down vote up
private onSearch = (filteredOptions: KeyringSectionOptions, _query: string): KeyringSectionOptions => {
    const { isInput = true } = this.props;
    const query = _query.trim();
    const queryLower = query.toLowerCase();
    const matches = filteredOptions.filter(
      (item): boolean =>
        !!item.value &&
        ((item.name.toLowerCase && item.name.toLowerCase().includes(queryLower)) ||
          item.value.toLowerCase().includes(queryLower))
    );

    if (isInput && matches.length === 0) {
      const accountId = transformToAccountId(query);

      if (accountId) {
        matches.push(keyring.saveRecent(accountId.toString()).option);
      }
    }

    return matches.filter((item, index): boolean => {
      const isLast = index === matches.length - 1;
      const nextItem = matches[index + 1];
      const hasNext = nextItem && nextItem.value;

      return !(isNull(item.value) || isUndefined(item.value)) || (!isLast && !!hasNext);
    });
  };
Example #5
Source File: useQueueTx.ts    From contracts-ui with GNU General Public License v3.0 6 votes vote down vote up
export function useQueueTx(
  extrinsic: SubmittableExtrinsic<'promise'> | null,
  accountId: string | null | undefined,
  onSuccess: (_: SubmittableResult) => Promise<void>,
  onError: VoidFn,
  isValid: (_: SubmittableResult) => boolean
): [VoidFn, VoidFn, boolean, boolean] {
  const { queue, dismiss, process, txs } = useTransactions();
  const [txId, setTxId] = useState<number>(0);

  const txIdRef = useRef(txId);

  const isProcessing = useMemo(
    (): boolean => !!(txs[txId] && txs[txId]?.status === 'processing'),
    [txs, txId]
  );

  const onSubmit = useCallback((): void => {
    txId && process(txId);
  }, [process, txId]);

  const onCancel = useCallback((): void => {
    txId && dismiss(txId);
    setTxId(0);
  }, [dismiss, txId]);

  useEffect((): void => {
    if (extrinsic && accountId && txId === 0) {
      const newId = queue({ extrinsic, accountId, onSuccess, onError, isValid });

      setTxId(newId);
      txIdRef.current = newId;
    }
  }, [accountId, extrinsic, isValid, onError, onSuccess, queue, txId]);

  return [onSubmit, onCancel, !isNull(txId), isProcessing];
}
Example #6
Source File: valueToText.tsx    From crust-apps with Apache License 2.0 6 votes vote down vote up
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function valueToText (type: string, value: Codec | undefined | null, swallowError = true, contentShorten = true): React.ReactNode {
  if (isNull(value) || isUndefined(value)) {
    return div({}, '<unknown>');
  }

  return div(
    {},
    ['Bytes', 'Raw', 'Option<Keys>', 'Keys'].includes(type)
      ? u8aToHex(value.toU8a(true), contentShorten ? 512 : -1)
      // HACK Handle Keys as hex-only (this should go away once the node value is
      // consistently swapped to `Bytes`)
      : type === 'Vec<(ValidatorId,Keys)>'
        ? toString(formatKeys(value as unknown as [ValidatorId, Keys][]))
        : value instanceof Raw
          ? value.isEmpty
            ? '<empty>'
            : value.toString()
          : (value instanceof Option) && value.isNone
            ? '<none>'
            : toString(toHuman(value))
  );
}
Example #7
Source File: index.tsx    From crust-apps with Apache License 2.0 6 votes vote down vote up
private onSearch = (filteredOptions: KeyringSectionOptions, _query: string): KeyringSectionOptions => {
    const { isInput = true } = this.props;
    const query = _query.trim();
    const queryLower = query.toLowerCase();
    const matches = filteredOptions.filter((item): boolean =>
      !!item.value && (
        (item.name.toLowerCase && item.name.toLowerCase().includes(queryLower)) ||
        item.value.toLowerCase().includes(queryLower)
      )
    );

    if (isInput && matches.length === 0) {
      const accountId = transformToAccountId(query);

      if (accountId) {
        matches.push(
          keyring.saveRecent(
            accountId.toString()
          ).option
        );
      }
    }

    return matches.filter((item, index): boolean => {
      const isLast = index === matches.length - 1;
      const nextItem = matches[index + 1];
      const hasNext = nextItem && nextItem.value;

      return !(isNull(item.value) || isUndefined(item.value)) || (!isLast && !!hasNext);
    });
  }
Example #8
Source File: Output.tsx    From crust-apps with Apache License 2.0 6 votes vote down vote up
format = (value: unknown): string => {
  if (isError(value)) {
    return value.stack ? value.stack : value.toString();
  } else if (isUndefined(value)) {
    return 'undefined';
  } else if (isNull(value)) {
    return 'null';
  } else if (Array.isArray(value)) {
    return `[${value.map((value): string => format(value)).join(', ')}]`;
  } else if (value instanceof Map) {
    return `{${[...value.entries()].map(([key, value]): string => (key as string) + ': ' + format(value)).join(', ')}}`;
  }

  return (value as string).toString();
}
Example #9
Source File: base-provider.ts    From bodhi.js with Apache License 2.0 5 votes vote down vote up
queryStorage = async <T = any>(
    module: `${string}.${string}`,
    args: any[],
    _blockTag?: BlockTag | Promise<BlockTag>
  ): Promise<T> => {
    const blockTag = await this._ensureSafeModeBlockTagFinalization(_blockTag);
    const blockHash = await this._getBlockHash(blockTag);

    const registry = await this.api.getBlockRegistry(u8aToU8a(blockHash));

    if (!this.storages.get(registry)) {
      const storage = decorateStorage(registry.registry, registry.metadata.asLatest, registry.metadata.version);
      this.storages.set(registry, storage);
    }

    const storage = this.storages.get(registry)!;

    const [section, method] = module.split('.');

    const entry = storage[section][method];
    const key = entry(...args);

    const outputType = unwrapStorageType(registry.registry, entry.meta.type, entry.meta.modifier.isOptional);

    const cacheKey = `${module}-${blockHash}-${args.join(',')}`;
    const cached = this._storageCache.get(cacheKey);

    let input: Uint8Array | null = null;

    if (cached) {
      input = cached;
    } else {
      const value: any = await this.api.rpc.state.getStorage(key, blockHash);

      const isEmpty = isNull(value);

      // we convert to Uint8Array since it maps to the raw encoding, all
      // data will be correctly encoded (incl. numbers, excl. :code)
      input = isEmpty
        ? null
        : u8aToU8a(entry.meta.modifier.isOptional ? value.toU8a() : value.isSome ? value.unwrap().toU8a() : null);

      this._storageCache.set(cacheKey, input);
    }

    const result = registry.registry.createTypeUnsafe(outputType, [input], {
      blockHash,
      isPedantic: !entry.meta.modifier.isOptional
    });

    return result as any as T;
  };
Example #10
Source File: Modules.tsx    From crust-apps with Apache License 2.0 5 votes vote down vote up
function Modules ({ onAdd }: Props): React.ReactElement<Props> {
  const { t } = useTranslation();
  const { api } = useApi();
  const [{ defaultValues, isIterable, key, params }, setKey] = useState<KeyState>({ defaultValues: undefined, isIterable: false, key: api.query.timestamp.now, params: [] });
  const [{ isValid, values }, setValues] = useState<ValState>({ isValid: true, values: [] });

  const _onAdd = useCallback(
    (): void => {
      isValid && onAdd({
        isConst: false,
        key,
        params: values.filter(({ value }, index) => !isIterable || (index !== values.length - 1) || !isNull(value))
      });
    },
    [isIterable, isValid, key, onAdd, values]
  );

  const _onChangeValues = useCallback(
    (values: RawParams) => setValues({
      isValid: areParamsValid(key, values),
      values
    }),
    [key]
  );

  const _onChangeKey = useCallback(
    (key: QueryableStorageEntry<'promise'>): void => {
      setKey(expandKey(api, key));
      _onChangeValues([]);
    },
    [_onChangeValues, api]
  );

  const { creator: { meta, method, section } } = key;

  return (
    <section className='storage--actionrow'>
      <div className='storage--actionrow-value'>
        <InputStorage
          defaultValue={api.query.timestamp.now}
          help={meta?.documentation.join(' ')}
          label={t<string>('selected state query')}
          onChange={_onChangeKey}
        />
        <Params
          key={`${section}.${method}:params` /* force re-render on change */}
          onChange={_onChangeValues}
          onEnter={_onAdd}
          params={params}
          values={defaultValues}
        />
      </div>
      <div className='storage--actionrow-buttons'>
        <Button
          icon='plus'
          isDisabled={!isValid}
          onClick={_onAdd}
        />
      </div>
    </section>
  );
}
Example #11
Source File: InputStorageDepositLimit.tsx    From contracts-ui with GNU General Public License v3.0 5 votes vote down vote up
export function InputStorageDepositLimit({
  className,
  isActive = false,
  maximum,
  onChange,
  toggleIsActive,
  value,
  ...props
}: Props) {
  const percentage = useMemo((): number | null => {
    if (!maximum || maximum.eqn(0)) {
      return null;
    }
    return 100 * new Big(value.toString()).div(new Big(maximum.toString())).toNumber();
  }, [maximum, value]);

  return (
    <div className={classes(className)}>
      <div className="flex items-center">
        <InputBalance
          className="flex-1"
          value={isActive ? value : undefined}
          id="storageDepositLimit"
          isDisabled={!isActive}
          onChange={onChange}
          placeholder={isActive ? undefined : 'Do not use'}
          withUnits={isActive}
          {...getValidation(props)}
        />
        <div className="flex justify-center items-center w-18">
          <Switch value={isActive} onChange={toggleIsActive} />
        </div>
      </div>
      {isActive && !isNull(percentage) && (
        <Meter
          label={isNumber(percentage) ? `${percentage.toFixed(2)}% of free balance` : null}
          percentage={isNumber(percentage) ? percentage : 100}
        />
      )}
    </div>
  );
}
Example #12
Source File: useFormField.ts    From contracts-ui with GNU General Public License v3.0 5 votes vote down vote up
export function useFormField<T>(
  defaultValue: T,
  validate: ValidateFn<T> = value => ({ isValid: !isNull(value), message: null })
): ValidFormField<T> {
  const [value, setValue] = useState<T>(defaultValue);
  const [validation, setValidation] = useState<Omit<Validation, 'isError'>>(validate(value));
  const isTouched = useRef(false);

  const isError = useMemo(() => {
    if (!isTouched.current) {
      return false;
    }

    return !validation.isValid;
  }, [validation.isValid]);

  const onChange = useCallback(
    (value?: T | null) => {
      if (!isUndefined(value) && !isNull(value)) {
        setValue(value);
        setValidation(validate(value));
        isTouched.current = true;
      }
    },
    [validate]
  );

  return useMemo(
    () => ({
      value,
      onChange,
      isValid: validation.isValid,
      isTouched: isTouched.current,
      isWarning: validation.isWarning || false,
      message: validation.message,
      isError,
    }),
    [value, onChange, isError, validation.isValid, validation.isWarning, validation.message]
  );
}
Example #13
Source File: Selection.tsx    From crust-apps with Apache License 2.0 5 votes vote down vote up
function Selection ({ queueRpc }: Props): React.ReactElement<Props> {
  const { t } = useTranslation();
  const [{ isValid, rpc, values }, setState] = useState<State>({
    isValid: false,
    rpc: defaultMethod,
    values: []
  });

  const params = useMemo(
    () => rpc.params.map(({ isOptional, name, type }): ParamDef => ({
      name,
      type: getTypeDef(isOptional ? `Option<${type}>` : type)
    })),
    [rpc]
  );

  const _nextState = useCallback(
    (newState: Partial<State>) => setState((prevState: State): State => {
      const { rpc = prevState.rpc, values = prevState.values } = newState;
      const reqCount = rpc.params.reduce((count, { isOptional }) => count + (isOptional ? 0 : 1), 0);
      const isValid = values.reduce((isValid, value) => isValid && value.isValid === true, reqCount <= values.length);

      return {
        isValid,
        rpc,
        values
      };
    }),
    []
  );

  const _onChangeMethod = useCallback(
    (rpc: DefinitionRpcExt) => _nextState({ rpc, values: [] }),
    [_nextState]
  );

  const _onChangeValues = useCallback(
    (values: RawParam[]) => _nextState({ values }),
    [_nextState]
  );

  const _onSubmit = useCallback(
    (): void => queueRpc({
      rpc,
      values: values
        .filter(({ value }) => !isNull(value))
        .map(({ value }): any => value)
    }),
    [queueRpc, rpc, values]
  );

  return (
    <section className='rpc--Selection'>
      <InputRpc
        defaultValue={defaultMethod}
        help={t<string>('The actual JSONRPC module and function to make a call to.')}
        label={t<string>('call the selected endpoint')}
        onChange={_onChangeMethod}
      />
      <Params
        key={`${rpc.section}.${rpc.method}:params` /* force re-render on change */}
        onChange={_onChangeValues}
        params={params}
      />
      <Button.Group>
        <Button
          icon='sign-in-alt'
          isDisabled={!isValid}
          label={t<string>('Submit RPC call')}
          onClick={_onSubmit}
        />
      </Button.Group>
    </section>
  );
}
Example #14
Source File: Add.tsx    From crust-apps with Apache License 2.0 5 votes vote down vote up
function Add ({ onClose }: Props): React.ReactElement {
  const { t } = useTranslation();
  const [codeHash, setCodeHash] = useState('');
  const [isCodeHashValid, setIsCodeHashValid] = useState(false);
  const [name, setName] = useState<string | null>(null);
  const { abi, contractAbi, errorText, isAbiError, isAbiSupplied, isAbiValid, onChangeAbi, onRemoveAbi } = useAbi();

  const _onSave = useCallback(
    (): void => {
      if (!codeHash || !name) {
        return;
      }

      store
        .saveCode(codeHash, { abi, name, tags: [] })
        .then(() => onClose())
        .catch((error): void => {
          console.error('Unable to save code', error);
        });
    },
    [abi, codeHash, name, onClose]
  );

  const isNameValid = !isNull(name) && name.length > 0;
  const isValid = isCodeHashValid && isNameValid && isAbiSupplied && isAbiValid;

  return (
    <Modal header={t('Add an existing code hash')}>
      <Modal.Content>
        <Input
          autoFocus
          help={t('The code hash for the on-chain deployed code.')}
          isError={codeHash.length > 0 && !isCodeHashValid}
          label={t('code hash')}
          onChange={setCodeHash}
          value={codeHash}
        />
        <ValidateCode
          codeHash={codeHash}
          onChange={setIsCodeHashValid}
        />
        <InputName
          isError={!isNameValid}
          onChange={setName}
          value={name || undefined}
        />
        <ABI
          contractAbi={contractAbi}
          errorText={errorText}
          isError={isAbiError || !isAbiError}
          isSupplied={isAbiSupplied}
          isValid={isAbiValid}
          onChange={onChangeAbi}
          onRemove={onRemoveAbi}
        />
      </Modal.Content>
      <Modal.Actions onCancel={onClose}>
        <Button
          icon='save'
          isDisabled={!isValid}
          label={t('Save')}
          onClick={_onSave}
        />
      </Modal.Actions>
    </Modal>
  );
}
Example #15
Source File: call.tsx    From crust-apps with Apache License 2.0 4 votes vote down vote up
export default function withCall<P extends ApiProps> (endpoint: string, { at, atProp, callOnResult, fallbacks, isMulti = false, params = [], paramName, paramPick, paramValid = false, propName, skipIf = NO_SKIP, transform = echoTransform, withIndicator = false }: Options = {}): (Inner: React.ComponentType<ApiProps>) => React.ComponentType<any> {
  return (Inner: React.ComponentType<ApiProps>): React.ComponentType<SubtractProps<P, ApiProps>> => {
    class WithPromise extends React.Component<P, State> {
      public state: State = {
        callResult: undefined,
        callUpdated: false,
        callUpdatedAt: 0
      };

      private destroy?: () => void;

      private isActive = false;

      private propName: string;

      private timerId = -1;

      constructor (props: P) {
        super(props);

        const [, section, method] = endpoint.split('.');

        this.propName = `${section}_${method}`;
      }

      public componentDidUpdate (prevProps: any): void {
        const oldParams = this.getParams(prevProps);
        const newParams = this.getParams(this.props);

        if (this.isActive && !isEqual(newParams, oldParams)) {
          this
            .subscribe(newParams)
            .then(NOOP)
            .catch(NOOP);
        }
      }

      public componentDidMount (): void {
        this.isActive = true;

        if (withIndicator) {
          this.timerId = window.setInterval((): void => {
            const elapsed = Date.now() - (this.state.callUpdatedAt || 0);
            const callUpdated = elapsed <= 1500;

            if (callUpdated !== this.state.callUpdated) {
              this.nextState({ callUpdated });
            }
          }, 500);
        }

        // The attachment takes time when a lot is available, set a timeout
        // to first handle the current queue before subscribing
        setTimeout((): void => {
          this
            .subscribe(this.getParams(this.props))
            .then(NOOP)
            .catch(NOOP);
        }, 0);
      }

      public componentWillUnmount (): void {
        this.isActive = false;

        this.unsubscribe()
          .then(NOOP)
          .catch(NOOP);

        if (this.timerId !== -1) {
          clearInterval(this.timerId);
        }
      }

      private nextState (state: Partial<State>): void {
        if (this.isActive) {
          this.setState(state as State);
        }
      }

      private getParams (props: any): [boolean, any[]] {
        const paramValue = paramPick
          ? paramPick(props)
          : paramName
            ? props[paramName]
            : undefined;

        if (atProp) {
          at = props[atProp];
        }

        // When we are specifying a param and have an invalid, don't use it. For 'params',
        // we default to the original types, i.e. no validation (query app uses this)
        if (!paramValid && paramName && (isUndefined(paramValue) || isNull(paramValue))) {
          return [false, []];
        }

        const values = isUndefined(paramValue)
          ? params
          : params.concat(
            (Array.isArray(paramValue) && !(paramValue as any).toU8a)
              ? paramValue
              : [paramValue]
          );

        return [true, values];
      }

      private constructApiSection = (endpoint: string): [Record<string, Method>, string, string, string] => {
        const { api } = this.props;
        const [area, section, method, ...others] = endpoint.split('.');

        assert(area.length && section.length && method.length && others.length === 0, `Invalid API format, expected <area>.<section>.<method>, found ${endpoint}`);
        assert(['consts', 'rpc', 'query', 'derive'].includes(area), `Unknown api.${area}, expected consts, rpc, query or derive`);
        assert(!at || area === 'query', 'Only able to do an \'at\' query on the api.query interface');

        const apiSection = (api as any)[area][section];

        return [
          apiSection,
          area,
          section,
          method
        ];
      }

      private getApiMethod (newParams: any[]): ApiMethodInfo {
        if (endpoint === 'subscribe') {
          const [fn, ...params] = newParams;

          return [
            fn,
            params,
            'subscribe'
          ];
        }

        const endpoints: string[] = [endpoint].concat(fallbacks || []);
        const expanded = endpoints.map(this.constructApiSection);
        const [apiSection, area, section, method] = expanded.find(([apiSection]): boolean =>
          !!apiSection
        ) || [{}, expanded[0][1], expanded[0][2], expanded[0][3]];

        assert(apiSection && apiSection[method], `Unable to find api.${area}.${section}.${method}`);

        const meta = apiSection[method].meta;

        if (area === 'query' && meta?.type.isMap) {
          const arg = newParams[0];

          assert((!isUndefined(arg) && !isNull(arg)) || meta.type.asMap.kind.isLinkedMap, `${meta.name} expects one argument`);
        }

        return [
          apiSection[method],
          newParams,
          method.startsWith('subscribe') ? 'subscribe' : area
        ];
      }

      private async subscribe ([isValid, newParams]: [boolean, any[]]): Promise<void> {
        if (!isValid || skipIf(this.props)) {
          return;
        }

        const { api } = this.props;
        let info: ApiMethodInfo | undefined;

        await api.isReady;

        try {
          assert(at || !atProp, 'Unable to perform query on non-existent at hash');

          info = this.getApiMethod(newParams);
        } catch (error) {
          // don't flood the console with the same errors each time, just do it once, then
          // ignore it going forward
          if (!errorred[(error as Error).message]) {
            console.warn(endpoint, '::', error);

            errorred[(error as Error).message] = true;
          }
        }

        if (!info) {
          return;
        }

        const [apiMethod, params, area] = info;
        const updateCb = (value?: any): void =>
          this.triggerUpdate(this.props, value);

        await this.unsubscribe();

        try {
          if (['derive', 'subscribe'].includes(area) || (area === 'query' && (!at && !atProp))) {
            this.destroy = isMulti
              ? await apiMethod.multi(params, updateCb)
              : await apiMethod(...params, updateCb);
          } else if (area === 'consts') {
            updateCb(apiMethod);
          } else {
            updateCb(
              at
                ? await apiMethod.at(at, ...params)
                : await apiMethod(...params)
            );
          }
        } catch (error) {
          // console.warn(endpoint, '::', error);
        }
      }

      // eslint-disable-next-line @typescript-eslint/require-await
      private async unsubscribe (): Promise<void> {
        if (this.destroy) {
          this.destroy();
          this.destroy = undefined;
        }
      }

      private triggerUpdate (props: any, value?: any): void {
        try {
          const callResult = (props.transform || transform)(value);

          if (!this.isActive || isEqual(callResult, this.state.callResult)) {
            return;
          }

          triggerChange(callResult, callOnResult, props.callOnResult);

          this.nextState({
            callResult,
            callUpdated: true,
            callUpdatedAt: Date.now()
          });
        } catch (error) {
          // console.warn(endpoint, '::', (error as Error).message);
        }
      }

      public render (): React.ReactNode {
        const { callResult, callUpdated, callUpdatedAt } = this.state;
        const _props = {
          ...this.props,
          callUpdated,
          callUpdatedAt
        };

        if (!isUndefined(callResult)) {
          (_props as any)[propName || this.propName] = callResult;
        }

        return (
          <Inner {..._props} />
        );
      }
    }

    return withApi(WithPromise);
  };
}
Example #16
Source File: call.tsx    From subscan-multisig-react with Apache License 2.0 4 votes vote down vote up
export default function withCall<P extends ApiProps>(
  endpoint: string,
  {
    at,
    atProp,
    callOnResult,
    fallbacks,
    isMulti = false,
    params = [],
    paramName,
    paramPick,
    paramValid = false,
    propName,
    skipIf = NO_SKIP,
    transform = echoTransform,
    withIndicator = false,
  }: Options = {}
): (Inner: React.ComponentType<ApiProps>) => React.ComponentType<any> {
  return (Inner: React.ComponentType<ApiProps>): React.ComponentType<SubtractProps<P, ApiProps>> => {
    class WithPromise extends React.Component<P, State> {
      public state: State = {
        callResult: undefined,
        callUpdated: false,
        callUpdatedAt: 0,
      };

      private destroy?: () => void;

      private isActive = false;

      private propName: string;

      private timerId = -1;

      constructor(props: P) {
        super(props);

        const [, section, method] = endpoint.split('.');

        this.propName = `${section}_${method}`;
      }

      public componentDidUpdate(prevProps: any): void {
        const oldParams = this.getParams(prevProps);
        const newParams = this.getParams(this.props);

        if (this.isActive && !isEqual(newParams, oldParams)) {
          this.subscribe(newParams).then(NOOP).catch(NOOP);
        }
      }

      public componentDidMount(): void {
        this.isActive = true;

        if (withIndicator) {
          this.timerId = window.setInterval((): void => {
            const elapsed = Date.now() - (this.state.callUpdatedAt || 0);
            const callUpdated = elapsed <= 1500;

            if (callUpdated !== this.state.callUpdated) {
              this.nextState({ callUpdated });
            }
          }, 500);
        }

        // The attachment takes time when a lot is available, set a timeout
        // to first handle the current queue before subscribing
        setTimeout((): void => {
          this.subscribe(this.getParams(this.props)).then(NOOP).catch(NOOP);
        }, 0);
      }

      public componentWillUnmount(): void {
        this.isActive = false;

        this.unsubscribe().then(NOOP).catch(NOOP);

        if (this.timerId !== -1) {
          clearInterval(this.timerId);
        }
      }

      private nextState(state: Partial<State>): void {
        if (this.isActive) {
          this.setState(state as State);
        }
      }

      private getParams(props: any): [boolean, any[]] {
        const paramValue = paramPick ? paramPick(props) : paramName ? props[paramName] : undefined;

        if (atProp) {
          at = props[atProp];
        }

        // When we are specifying a param and have an invalid, don't use it. For 'params',
        // we default to the original types, i.e. no validation (query app uses this)
        if (!paramValid && paramName && (isUndefined(paramValue) || isNull(paramValue))) {
          return [false, []];
        }

        const values = isUndefined(paramValue)
          ? params
          : params.concat(Array.isArray(paramValue) && !(paramValue as any).toU8a ? paramValue : [paramValue]);

        return [true, values];
      }

      // eslint-disable-next-line @typescript-eslint/no-shadow
      private constructApiSection = (endpoint: string): [Record<string, Method>, string, string, string] => {
        // eslint-disable-next-line no-invalid-this
        const { api } = this.props;
        const [area, section, method, ...others] = endpoint.split('.');

        assert(
          area.length && section.length && method.length && others.length === 0,
          `Invalid API format, expected <area>.<section>.<method>, found ${endpoint}`
        );
        assert(
          ['consts', 'rpc', 'query', 'derive'].includes(area),
          `Unknown api.${area}, expected consts, rpc, query or derive`
        );
        assert(!at || area === 'query', `Only able to do an 'at' query on the api.query interface`);

        const apiSection = (api as any)[area][section];

        return [apiSection, area, section, method];
      };

      private getApiMethod(newParams: any[]): ApiMethodInfo {
        if (endpoint === 'subscribe') {
          // eslint-disable-next-line @typescript-eslint/no-shadow
          const [fn, ...params] = newParams;

          return [fn, params, 'subscribe'];
        }

        const endpoints: string[] = [endpoint].concat(fallbacks || []);
        const expanded = endpoints.map(this.constructApiSection);
        // eslint-disable-next-line @typescript-eslint/no-shadow
        const [apiSection, area, section, method] = expanded.find(([apiSection]): boolean => !!apiSection) || [
          {},
          expanded[0][1],
          expanded[0][2],
          expanded[0][3],
        ];

        assert(apiSection && apiSection[method], `Unable to find api.${area}.${section}.${method}`);

        const meta = apiSection[method].meta;

        if (area === 'query' && meta?.type.isMap) {
          const arg = newParams[0];

          assert(
            (!isUndefined(arg) && !isNull(arg)) || meta.type.asMap.kind.isLinkedMap,
            `${meta.name} expects one argument`
          );
        }

        return [apiSection[method], newParams, method.startsWith('subscribe') ? 'subscribe' : area];
      }

      private async subscribe([isValid, newParams]: [boolean, any[]]): Promise<void> {
        if (!isValid || skipIf(this.props)) {
          return;
        }

        const { api } = this.props;
        let info: ApiMethodInfo | undefined;

        await api.isReady;

        try {
          assert(at || !atProp, 'Unable to perform query on non-existent at hash');

          info = this.getApiMethod(newParams);
        } catch (error) {
          // don't flood the console with the same errors each time, just do it once, then
          // ignore it going forward
          if (!errorred[(error as Error).message]) {
            console.warn(endpoint, '::', error);

            errorred[(error as Error).message] = true;
          }
        }

        if (!info) {
          return;
        }

        // eslint-disable-next-line @typescript-eslint/no-shadow
        const [apiMethod, params, area] = info;
        const updateCb = (value?: any): void => this.triggerUpdate(this.props, value);

        await this.unsubscribe();

        try {
          if (['derive', 'subscribe'].includes(area) || (area === 'query' && !at && !atProp)) {
            this.destroy = isMulti ? await apiMethod.multi(params, updateCb) : await apiMethod(...params, updateCb);
          } else if (area === 'consts') {
            updateCb(apiMethod);
          } else {
            updateCb(at ? await apiMethod.at(at, ...params) : await apiMethod(...params));
          }
        } catch (error) {
          // console.warn(endpoint, '::', error);
        }
      }

      // eslint-disable-next-line @typescript-eslint/require-await
      private async unsubscribe(): Promise<void> {
        if (this.destroy) {
          this.destroy();
          this.destroy = undefined;
        }
      }

      private triggerUpdate(props: any, value?: any): void {
        try {
          const callResult = (props.transform || transform)(value);

          if (!this.isActive || isEqual(callResult, this.state.callResult)) {
            return;
          }

          triggerChange(callResult, callOnResult, props.callOnResult);

          this.nextState({
            callResult,
            callUpdated: true,
            callUpdatedAt: Date.now(),
          });
        } catch (error) {
          // console.warn(endpoint, '::', (error as Error).message);
        }
      }

      public render(): React.ReactNode {
        const { callResult, callUpdated, callUpdatedAt } = this.state;
        const _props = {
          ...this.props,
          callUpdated,
          callUpdatedAt,
        };

        if (!isUndefined(callResult)) {
          (_props as any)[propName || this.propName] = callResult;
        }

        return <Inner {..._props} />;
      }
    }

    return withApi(WithPromise);
  };
}
Example #17
Source File: Upload.tsx    From crust-apps with Apache License 2.0 4 votes vote down vote up
function Upload ({ onClose }: Props): React.ReactElement {
  const { t } = useTranslation();
  const { api } = useApi();
  const [accountId, setAccountId] = useAccountId();
  const [step, nextStep, prevStep] = useStepper();
  const [[uploadTx, error], setUploadTx] = useState<[SubmittableExtrinsic<'promise'> | null, string | null]>([null, null]);
  const [constructorIndex, setConstructorIndex] = useState(0);
  const [endowment, isEndowmentValid, setEndowment] = useNonZeroBn(ENDOWMENT);
  const [params, setParams] = useState<any[]>([]);
  const [[wasm, isWasmValid], setWasm] = useState<[Uint8Array | null, boolean]>([null, false]);
  const [name, isNameValid, setName] = useNonEmptyString();
  const { abiName, contractAbi, errorText, isAbiError, isAbiSupplied, isAbiValid, onChangeAbi, onRemoveAbi } = useAbi();
  const weight = useWeight();

  const code = useMemo(
    () => isAbiValid && isWasmValid && wasm && contractAbi
      ? new CodePromise(api, contractAbi, wasm)
      : null,
    [api, contractAbi, isAbiValid, isWasmValid, wasm]
  );

  const constructOptions = useMemo(
    () => contractAbi
      ? contractAbi.constructors.map((message, index) => ({
        info: message.identifier,
        key: `${index}`,
        text: (
          <MessageSignature
            asConstructor
            message={message}
          />
        ),
        value: index
      }))
      : [],
    [contractAbi]
  );

  useEffect((): void => {
    setConstructorIndex(() =>
      constructOptions.find(({ info }) => info === 'default')?.value || 0
    );
  }, [constructOptions]);

  useEffect((): void => {
    setParams([]);
  }, [constructorIndex]);

  useEffect((): void => {
    setWasm(
      contractAbi && isWasm(contractAbi.project.source.wasm)
        ? [contractAbi.project.source.wasm, true]
        : [null, false]
    );
  }, [contractAbi]);

  useEffect((): void => {
    abiName && setName(abiName);
  }, [abiName, setName]);

  useEffect((): void => {
    let contract: SubmittableExtrinsic<'promise'> | null = null;
    let error: string | null = null;

    try {
      contract = code && contractAbi && endowment
        ? code.createContract(constructorIndex, { gasLimit: weight.weight, value: endowment }, params)
        : null;
    } catch (e) {
      error = (e as Error).message;
    }

    setUploadTx(() => [contract, error]);
  }, [code, contractAbi, constructorIndex, endowment, params, weight]);

  const _onAddWasm = useCallback(
    (wasm: Uint8Array, name: string): void => {
      setWasm([wasm, isWasm(wasm)]);
      setName(name.replace('.wasm', '').replace('_', ' '));
    },
    [setName]
  );

  const _onSuccess = useCallback(
    (result: CodeSubmittableResult): void => {
      result.blueprint && store
        .saveCode(result.blueprint.codeHash, {
          abi: JSON.stringify(result.blueprint.abi.json),
          name: name || '<>',
          tags: []
        })
        .catch(console.error);
      result.contract && keyring.saveContract(result.contract.address.toString(), {
        contract: {
          abi: JSON.stringify(result.contract.abi.json),
          genesisHash: api.genesisHash.toHex()
        },
        name: name || '<>',
        tags: []
      });
    },
    [api, name]
  );

  const isSubmittable = !!accountId && (!isNull(name) && isNameValid) && isWasmValid && isAbiSupplied && isAbiValid && !!uploadTx && step === 2;
  const invalidAbi = isAbiError || !isAbiSupplied;
  const hasBatchDeploy = isFunction(api.tx.contracts.instantiateWithCode) || isFunction(api.tx.utility?.batch);

  return (
    <Modal header={t('Upload & deploy code {{info}}', { replace: { info: `${step}/2` } })}>
      <Modal.Content>
        {step === 1 && (
          <>
            <InputAddress
              help={t('Specify the user account to use for this deployment. Any fees will be deducted from this account.')}
              isInput={false}
              label={t('deployment account')}
              labelExtra={
                <Available
                  label={t<string>('transferrable')}
                  params={accountId}
                />
              }
              onChange={setAccountId}
              type='account'
              value={accountId}
            />
            <ABI
              contractAbi={contractAbi}
              errorText={errorText}
              isError={invalidAbi}
              isSupplied={isAbiSupplied}
              isValid={isAbiValid}
              label={t<string>('json for either ABI or .contract bundle')}
              onChange={onChangeAbi}
              onRemove={onRemoveAbi}
              withWasm
            />
            {!hasBatchDeploy && (
              <MarkError content={t<string>('Your environment does not support the latest instantiateWithCode contracts call, nor does it have utility.batch functionality available. This operation is not available.')} />
            )}
            {!invalidAbi && contractAbi && (
              <>
                {!contractAbi.project.source.wasm.length && (
                  <InputFile
                    help={t<string>('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
                    isError={!isWasmValid}
                    label={t<string>('compiled contract WASM')}
                    onChange={_onAddWasm}
                    placeholder={wasm && !isWasmValid && t<string>('The code is not recognized as being in valid WASM format')}
                  />
                )}
                <InputName
                  isError={!isNameValid}
                  onChange={setName}
                  value={name || undefined}
                />
              </>
            )}
          </>
        )}
        {step === 2 && contractAbi && (
          <>
            <Dropdown
              help={t<string>('The deployment constructor information for this contract, as provided by the ABI.')}
              isDisabled={contractAbi.constructors.length <= 1}
              label={t('deployment constructor')}
              onChange={setConstructorIndex}
              options={constructOptions}
              value={constructorIndex}
            />
            <Params
              onChange={setParams}
              params={contractAbi.constructors[constructorIndex].args}
              registry={contractAbi.registry}
            />
            <InputBalance
              help={t<string>('The allotted endowment for the deployed contract, i.e. the amount transferred to the contract upon instantiation.')}
              isError={!isEndowmentValid}
              label={t<string>('endowment')}
              onChange={setEndowment}
              value={endowment}
            />
            <InputMegaGas
              help={t<string>('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
              weight={weight}
            />
            {error && (
              <MarkError content={error} />
            )}
          </>
        )}
      </Modal.Content>
      <Modal.Actions onCancel={onClose}>
        {step === 1
          ? (
            <Button
              icon='step-forward'
              isDisabled={!code || !contractAbi || !hasBatchDeploy}
              label={t<string>('Next')}
              onClick={nextStep}
            />
          )
          : (
            <Button
              icon='step-backward'
              label={t<string>('Prev')}
              onClick={prevStep}
            />
          )
        }
        <TxButton
          accountId={accountId}
          extrinsic={uploadTx}
          icon='upload'
          isDisabled={!isSubmittable}
          label={t('Deploy')}
          onClick={onClose}
          onSuccess={_onSuccess}
        />
      </Modal.Actions>
    </Modal>
  );
}