UmiUmi

Interacting with Your Deployed Move Smart Contract

In this guide, we will create a simple website that interacts with your deployed Move smart contract. The website will allow users to increment the counter and display its current value using Move transactions and Viem SDK.

1. Setting Up the Next.js Project

  1. Create a new Next.js project with TypeScript:
    npx create-next-app@latest my-move-dapp --typescript
    cd my-move-dapp
  2. Install the required dependencies:
    npm install viem @aptos-labs/ts-sdk @mysten/bcs

2. Adding Configuration for Transactions

Create a new file config.ts to define methods for creating a transaction payload and sending transactions using the Viem SDK.

import { AccountAddress, EntryFunction, FixedBytes, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk';
import { bcs } from '@mysten/bcs';
import { createPublicClient, createWalletClient, custom, defineChain } from 'viem';
import { publicActionsL2, walletActionsL2 } from 'viem/op-stack';

export const devnet = defineChain({
  id: 42069,
  sourceId: 42069,
  name: 'Umi',
  nativeCurrency: {
    decimals: 18,
    name: 'Ether',
    symbol: 'ETH',
  },
  rpcUrls: {
    default: {
      http: ['https://devnet.moved.network'],
    },
  },
});

export const getAccount = async () => {
  const [account] = await window.ethereum!.request({
    method: 'eth_requestAccounts',
  });
  return account;
};

export const getMoveAccount = async () => {
  const account = await getAccount();
  const moveAccount = account.slice(0, 2) + '000000000000000000000000' + account.slice(2);
  return moveAccount;
};

export const publicClient = () =>
  createPublicClient({
    chain: devnet,
    transport: custom(window.ethereum!),
  }).extend(publicActionsL2());

export const walletClient = () =>
  createWalletClient({
    chain: devnet,
    transport: custom(window.ethereum!),
  }).extend(walletActionsL2());

export const counterPayload = async (method: string) => {
  const moveAccount = await getMoveAccount();
  const address = method === 'get' ? AccountAddress.fromString(moveAccount) : getSigner(moveAccount);
  const entryFunction = EntryFunction.build(`${moveAccount}::counter10`, method, [], [address]);
  const transactionPayload = new TransactionPayloadEntryFunction(entryFunction);
  return transactionPayload.bcsToHex().toString() as `0x${string}`;
};

/// Converts address into serialized signer object.
export const getSigner = (address: string) => {
  /// Signer value is defined as Signer(AccountAddress) in Rust, so when it's deserialized it needs
  /// an extra 0 in the beginning to indicate that the address is the first field.
  /// Then the entire data is serialized as a vector of size 33 bytes.
  const addressBytes = [33, 0, ...AccountAddress.fromString(address).toUint8Array()];
  return new FixedBytes(new Uint8Array(addressBytes));
};

export const extractOutput = (data: `0x${string}` | undefined) => {
  if (!data) throw Error('No data found');
  if (typeof data == 'string') throw Error('Data is not an array of bytes');
  // The returned data is a vector of results with mostly a single result.
  // Each result is a tuple of output data bytes followed by the serialized Move type layout.
  // The following code extracts the output bytes from inside this complex returned data structure.
  return new Uint8Array(bcs.vector(bcs.tuple([bcs.vector(bcs.u8())])).parse(new Uint8Array(data))[0][0]);
};

3. Building the Web Interface

  1. Create a simple UI with an input field, buttons, and a display for the counter value.
  2. Add a function to fetch the current counter value from the smart contract using Viem.
  3. Implement a function to send a transaction that increments the counter.

Example Code (Counter Interaction)

Create a new file src/app/Counter.tsx:

'use client';
import { useEffect, useState } from 'react';
import { counterPayload, extractOutput, getAccount, publicClient, walletClient } from '@/config';
import { bcs } from '@mysten/bcs';

const MoveCounter = bcs.struct('Counter', {
  value: bcs.u64(),
});

export default function Counter() {
  const [counter, setCounter] = useState(0);

  const fetchCounter = async () => {
    const callResponse = await publicClient().call({
      to: await getAccount(),
      data: await counterPayload('get'),
    });

    const output = extractOutput(callResponse.data);
    const counter = MoveCounter.parse(output);
    setCounter(parseInt(counter.value));
  };

  const incrementCounter = async () => {
    const hash = await walletClient().sendTransaction({
      account: await getAccount(),
      to: await getAccount(),
      data: await counterPayload('increment'),
    });
    await publicClient().waitForTransactionReceipt({ hash });
    fetchCounter();
  };

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

  return (
    <div className="text-center m-24">
      <h1 className="py-6">Counter: {counter}</h1>
      <button className="bg-blue-700 rounded-lg px-5 py-2.5" type="button" onClick={incrementCounter}>
        Increment
      </button>
    </div>
  );
}

4. Integrating the Counter Component

Replace the contents of src/app/page.tsx with:

import Counter from "./Counter";

export default function Home() {
  return (
    <div>
      <h1>Move Counter DApp</h1>
      <Counter />
    </div>
  );
}

5. Running the Website

Start the development server:

npm run dev

Open http://localhost:3000 in your browser to interact with the contract.

To add an extra layer of protection for your account, consider using a secondary Ethereum wallet. We've successfully tested this approach with Rabby Wallet.

Conclusion

You've successfully created a simple Next.js website to interact with your Move smart contract. Users can now send transactions to increment the counter and retrieve its value. In the next section, we will explore deploying this website for public access.

On this page