Skip to content

Owner withdraw

We have one important action left to implement for the owner: withdrawing PYUSD from the contract. This is a simple feature that will allow the owner to view the PYUSD balance on the contract and withdraw it to their wallet.

Owner actions

Monitoring the balance

In order to display a constantly up to date balance to the owner, we'll need to regularly invalidate the query of the accrued PYUSD. To do this, we'll use the useBlockNumber hook to watch for new blocks and invalidate the query when a new block is mined.

tsx
const accruedPyusd = useTokenBalance(paymentToken, helloPyusdAddress);

const queryClient = useQueryClient();
const { data: blockNumber } = useBlockNumber({ watch: true });

useEffect(() => {
  queryClient.invalidateQueries({ queryKey: accruedPyusd.queryKey });
}, [blockNumber, queryClient]);

Conditionally displaying the owner info:

We query the owner of the contract and compare it to the connected wallet address. If the connected wallet is not the owner, we return null so nothing is rendered:

tsx
const owner = useReadContract({
  address: helloPyusdAddress,
  abi: HelloPyusdAbi,
  functionName: "owner",
});

if (!account.isConnected || account.address !== owner.data) {
  return null;
}

Withdrawing PYUSD

Similar to the minting cases in the last section, we simulate the withdraw, and only let the user actually withdraw if the simulation is successful:

tsx
const simulateWithdraw = useSimulateContract({
  address: helloPyusdAddress,
  abi: HelloPyusdAbi,
  functionName: "withdrawToken",
  args: [paymentToken, account.address || zeroAddress],
  query: {
    enabled: account.isConnected,
  },
});
const writeWithdraw = useWriteContract();
useChangeObserver(writeWithdraw.status, "success", () => {
  accruedPyusd.refetch();
});

/// ...

<button
  className='w-full mx-auto block button'
  disabled={!simulateWithdraw.isSuccess || accruedPyusd.data === 0n}
  onClick={() => writeWithdraw.writeContract(simulateWithdraw.data!.request)}
>
  Withdraw
</button>;

We could move some of these actions into our domain specific hooks, but for now we'll leave them in the component.

Final OwnerActions component

tsx
import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { formatUnits, zeroAddress } from "viem";
import {
  useAccount,
  useBlockNumber,
  useReadContract,
  useSimulateContract,
  useWriteContract,
} from "wagmi";
import HelloPyusdAbi from "../abi/HelloPyusd.abi";
import { useChangeObserver } from "../hooks/changeObserver";
import { useToken, useTokenBalance } from "../hooks/erc20";
import { useHelloPyusdAddress } from "../hooks/helloPyusd";
import { usePaymentTokenAddress } from "../hooks/paymentToken";

export default function OwnerActions() {
  const account = useAccount();
  const paymentToken = usePaymentTokenAddress();
  const token = useToken(paymentToken);
  const helloPyusdAddress = useHelloPyusdAddress();
  const accruedPyusd = useTokenBalance(paymentToken, helloPyusdAddress);

  const queryClient = useQueryClient();
  const { data: blockNumber } = useBlockNumber({ watch: true });

  useEffect(() => {
    queryClient.invalidateQueries({ queryKey: accruedPyusd.queryKey });
  }, [blockNumber, queryClient]);

  const owner = useReadContract({
    address: helloPyusdAddress,
    abi: HelloPyusdAbi,
    functionName: "owner",
  });

  const simulateWithdraw = useSimulateContract({
    address: helloPyusdAddress,
    abi: HelloPyusdAbi,
    functionName: "withdrawToken",
    args: [paymentToken, account.address || zeroAddress],
    query: {
      enabled: account.isConnected,
    },
  });
  const writeWithdraw = useWriteContract();
  useChangeObserver(writeWithdraw.status, "success", () => {
    accruedPyusd.refetch();
  });

  if (!account.isConnected || account.address !== owner.data) {
    return null;
  }

  if (!token.data || !accruedPyusd.isSuccess) {
    return (
      <div className='text-sm opacity-50'>
        {token.error?.message || "Loading..."}
      </div>
    );
  }

  return (
    <section className='overflow-clip text-sm border-2 border-zinc-500 border-opacity-50 p-3 space-y-2'>
      <div className='flex justify-between'>
        <span>{token.data.symbol} Available</span>
        <span>
          {accruedPyusd.isSuccess
            ? formatUnits(accruedPyusd.data, token.data.decimals)
            : "Loading..."}
        </span>
      </div>

      <button
        className='w-full mx-auto block button'
        disabled={!simulateWithdraw.isSuccess || accruedPyusd.data === 0n}
        onClick={() =>
          writeWithdraw.writeContract(simulateWithdraw.data!.request)
        }
      >
        Withdraw
      </button>
    </section>
  );
}