Skip to content

Latest commit

 

History

History
249 lines (199 loc) · 8.69 KB

building-a-frontend.md

File metadata and controls

249 lines (199 loc) · 8.69 KB

So far we've got a local Secret Network developer testnet running, and we've exposed a rest API to interact with contracts with CosmWasm JS.

In this guide we'll build a React application, you can roll your own or clone the CosmWasm full stack example name-app to follow along.

The rest server doesn't enable Cross-Origin Resource Sharing by default, and in the current version we can't enable it, so as Cosmos Network docs suggest, we run a proxy to handle requests from our application.

NB the example proxy configuration is not suitable for production use.

The proxy also comes in handy for a faucet, to automatically fund users' accounts.

Connect with a burner-wallet

Previously we loaded the mnemonic from file, this time the "burner wallet" is kept in the browser's local storage.

Source

// generateMnemonic will give you a fresh mnemonic
// it is up to the app to store this somewhere
export function generateMnemonic(): string {
  return Bip39.encode(Random.getBytes(16)).toString();
}

export function loadOrCreateMnemonic(): string {
  const key = "burner-wallet";
  const loaded = localStorage.getItem(key);
  if (loaded) {
    return loaded;
  }
  const generated = generateMnemonic();
  localStorage.setItem(key, generated);
  return generated;
}

export async function burnerWallet(): Promise<Wallet> {
  const mnemonic = loadOrCreateMnemonic();
  const pen = await Secp256k1Pen.fromMnemonic(mnemonic);
  const pubkey = encodeSecp256k1Pubkey(pen.pubkey);
  const address = pubkeyToAddress(pubkey, "secret");
  const signer = (signBytes: Uint8Array): Promise<StdSignature> => pen.sign(signBytes);
  return { address, signer };
}

Source

export function BurnerWalletProvider(props: WalletProviderProps): JSX.Element {
  return (
    <SdkProvider config={props.config} loadWallet={burnerWallet}>
      {props.children}
    </SdkProvider>
  );
}

Connect to the server

Reading contract state is free so if our user only needs to know the current count, we can connect using the CosmWasmClient, and not need a wallet.

We want to increment the counter, so we need the SigningCosmWasmClient, note that fees are in uscrt.

export async function connect(httpUrl: string, { address, signer }: Wallet): Promise<ConnectResult> {
  const client = new SigningCosmWasmClient(httpUrl, address, signer,
    buildFeeTable("uscrt", 1));
  return { address, client };
}

A wallet service can use this connection to load or create the wallet, and tap into the faucet if the account is empty.

  // just call this once on startup
  useEffect(() => {
    loadWallet()
      .then(wallet => connect(config.httpUrl, wallet))
      .then(async ({ address, client }) => {
        // load from faucet if needed
        if (config.faucetUrl) {
          try {
            const acct = await client.getAccount();
            if (!acct?.balance?.length) {
              await ky.post(config.faucetUrl, { json: { ticker: "SCRT", address } });
            }
          } catch(error) {
            console.error(error)
          }
        }

        setValue({
          loading: false,
          address: address,
          getClient: () => client,
        });
      })
      .catch(setError);

In the browser's network tab we can see this play out, the account is queried but has no funds initially, then the faucet is hit, /credit

With this connection in hand we can now focus on the contract logic, starting with a list of all the instances of the Counter contract.

  // get the contracts
  React.useEffect(() => {
    getClient()
      .getContracts(defaultCodeId)
      .then(contracts => setContracts(contracts))
      .catch(setError);
  }, [getClient, setError]);

  return (
    <List>
      {contracts.map(props => (
        <ContractItem {...props} key={props.address} />
      ))}
    </List>
  );

Selecting an instance queries it's current count

// get_count maps to our contract's QueryMsg GetCount
    getClient()
      .queryContractSmart(contractAddress, { get_count: { } })
      .then(res => {
        const o = parseQueryJson<QueryResponse>(res);
        setState({ count: o.count, loading: false });
      }));

Incrementing the count requires executing the increment message on the contract.

// increment maps to our contract's HandleMsg Increment
    await getClient().execute(
        props.contractAddress,
        {
            increment: { } },
        );
    setState({ loading: false });

    // refresh the account balance
    refreshAccount();

    // query the counter and update the state
    await getClient().queryContractSmart(contractAddress, { get_count: { } })
    .then(res => {
        const o = parseQueryJson<QueryResponse>(res);
        setState({ count: o.count, loading: false });
    })

The Counter contract also handles reset messages, which accept a new count value, which we may implement as follows.

Reset requires an integer, so first we validate the input to avoid contract failures, improving the UX and saving gas.

export const ResetValidationSchema = Yup.object().shape({
  countField: Yup.number()
    .min(0, "Count invalid")
    .required("Count is required"),
});

We can then Create a Form using Formik for example, which keeps track of values/errors/visited fields, orchestrating validation, and handling submission.

    <Formik
      initialValues={{
        countField: "0",
      }}
      validationSchema={ResetValidationSchema}
      onSubmit={async ({ countField }, { setSubmitting }) => {
        setSubmitting(true);
        handleReset({ countField });
      }}
    >
      {({ handleSubmit }) => (
        <Form onSubmit={handleSubmit} className={classes.form}>
          <div className={classes.input}>
            <FormTextField placeholder="0" name={COUNT_FIELD} type="integer" />
          </div>
          <div>
            <Button type="submit" disabled={loading}>
              Reset
            </Button>
          </div>
        </Form>
      )}
    </Formik>

Add the ResetForm to the Counter component

    <ResetForm handleReset={reset} loading={state.loading} />

All that's left is to execute the reset when the user submits.

  const reset = async (values: FormValues): Promise<void> => {
    const newCount = values[COUNT_FIELD];
    setState({ loading: true });
    try {
      await getClient().execute(
        props.contractAddress,
        { reset: { count: parseInt(newCount) } }
      );
      setState({ count: newCount, loading: false });
    } catch (err) {
      setState({ loading: false });
      setError(err);
    }
    try {
      refreshAccount();
    } catch(err) {
      setError("Failed to reset account");
    }
  };

Because we're using the burner wallet, the user won't be authorized to reset, the contract ensures only the contract owner can execute that.

We could query the contract owner and only show the ResetForm if the current account is the contract owner.

    if env.message.signer != state.owner {
        Unauthorized {}.fail()?;
    }

Resources