Ex 5b. IOTA Rebased Dapp - FreeBeer

unofficial site

- return to IOTA Rebased Useful Links

FreeBeer React Dapp

[1] Context and Template

A contract has already been deployed on the IOTA Rebased Testnet (Freebeer Contract docs here) Now a Dapp is developed to allow others to access the contract through a webpage where they can use their IOTA Rebased Wallet which is a Chrome browser extension.

Important. IOTA DAPP Template. To get started building a Dapp this very useful tool was used. This is a link to the official IOTA docs page https://docs.iota.org/references/ts-sdk/dapp-kit/create-dapp.

You may also find this Github useful:
https://github.com/iotaledger/iota/tree/develop/sdk/dapp-kit.

Here is another useful link: www.npmjs.com/package/@iota/create-dapp

If you use npm then on the command line enter:

% npm create @iota/dapp

This message results:

Which starter template would you like to use? â€Ļ
react-client-dapp React Client dApp that reads data from wallet and the blockchain
react-e2e-counter React dApp with a move smart contract that implements a distributed counter


The second question follows:
? What is the name of your dApp? (this will be used as the directory name)

This project started with the first template which gives inbuilt functionality to have the IOTA Chrome Extension wallet connect and sign transactions. The template has a ReadMe file with useful information about its use of React, Typescript, Vite and the IOTA Dapp Kit.

To run the default project use:

% npm install

Next step is % npm run dev
This starts the development server at somewhere like http://localhost:5173/ with:

[2] Functionality of the Dapp

The Freebeer Dapp is intended to give web access to a system that will gift users a small amount (say 0.007 IOTA) which is larger than their 'gas' cost. This is restricted (three 'free beers' max) and there is always a delay (about 1 hour) after anybody has secured a free beer. The three main functions therefore are:

[a] Add beer to top up the Treasury
[b] Check 'beer availability'
[c] Get free beer (when available)

[3] Project File Structure

Assuming that the template described above is used then the starting point is:

- a 'node_modules' folder
- a 'src' folder containing these files:
(App.tsx, main.tsx, networkConfig.ts, WalletStatus.tsx, vite-env.d.ts and OwnedObjects.tsx)
- various files including index.html, package.json, tsconfig.json etc

[4] FreeBeer Key Files

These files are the key to the functionality

[4.1] main.tsx

This is the starting point and provides all of the core functionality for using the Wallet and the Iota Client


           <!-- React imports and setup -->
        <script>
        import React from "react";
        import ReactDOM from "react-dom/client";
        import "@iota/dapp-kit/dist/index.css";
        import "@radix-ui/themes/styles.css";
        import { IotaClientProvider, WalletProvider } from "@iota/dapp-kit";
        import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
        import { Theme } from "@radix-ui/themes";
        import App from "./App.tsx";
        import { networkConfig } from "./networkConfig.ts";

        const queryClient = new QueryClient();

        ReactDOM.createRoot(document.getElementById("root")).render(
          <React.StrictMode>
            <Theme appearance="dark">
              <QueryClientProvider client={queryClient}>
                <IotaClientProvider networks={networkConfig} defaultNetwork="testnet">
                  <WalletProvider autoConnect>
                    <App />
                  </WalletProvider>
                </IotaClientProvider>
              <QueryClientProvider>
            </Theme>
          </React.StrictMode>
        );
        </script>
    

[4.2] App.tsx

This primarily handles the screen display of the React Dapp.


            <!-- React App Component -->
    <script>
    import { ConnectButton, useCurrentAccount, useSignAndExecuteTransaction, useIotaClient } from "@iota/dapp-kit";
    import { Box, Container, Flex, Heading, Text, Card, Button } from "@radix-ui/themes";
    import { WalletStatus } from "./WalletStatus";
    import { TransactionFeedback } from "./TransactionFeedback";
    import { GetBeerCall } from "./GetBeerCall";
    import { Transaction } from "@iota/iota-sdk/transactions";
    import { useState } from 'react';

    const PACKAGE_ID = "0x80da07146a4060d7195f81bd25655816f55a465218f4ba7c5d355e10e8759556";

    export default function App() {
      const [status, setStatus] = useState(null);
      const [isLoading, setIsLoading] = useState(false);
      const [error, setError] = useState("");
      
      const iotaClient = useIotaClient();
      const currentAccount = useCurrentAccount();
      const { mutate: signAndExecute } = useSignAndExecuteTransaction({
        execute: async ({ bytes, signature }) =>
          await iotaClient.executeTransactionBlock({
            transactionBlock: bytes,
            signature,
            options: {
              showEvents: true,      // Get event data
              showEffects: true,     // Get effects data
              showRawEffects: true,  // Get raw effects data
              showObjectChanges: true // Get object changes (like Treasury balance)
            },
          }),
      });

      const checkStatus = async () => {
        setIsLoading(true);
        setError("");
        try {
          const tx = new Transaction();
          tx.moveCall({
            target: `${PACKAGE_ID}::beer::get_beer_status`,
            arguments: [
              tx.object("0xba1cb7687c673df83d6a0364ecd76e632b3a3fffedba6b5596d72cfdddb203da"), // Treasury
              tx.object("0x9d8cc06a57a89e63afc48805dde64c8bdd7aee9211f002566e25325b3206b95c")  // GlobalConfig
            ]
          });

          signAndExecute(
            { transaction: tx },
            {
              onSuccess: (result) => {
                console.log("Check status result:", result);
                setStatus(result);
                setIsLoading(false);
              },
              onError: (error) => {
                console.error("Check status error:", error);
                setError(error.message);
                setIsLoading(false);
              }
            }
          );
        } catch (error) {
          console.error("Check status setup error:", error);
          setError(error.message);
          setIsLoading(false);
        }
      };

      const addBeer = async (amount) => {
        setIsLoading(true);
        setError("");
        try {
          const tx = new Transaction();
          const amountInNanos = amount * 1_000_000_000;
          const [paymentCoin] = tx.splitCoins(tx.gas, [tx.pure.u64(amountInNanos)]);
          
          tx.moveCall({
            target: `${PACKAGE_ID}::beer::addbeer`,
            arguments: [
              paymentCoin,
              tx.object("0xba1cb7687c673df83d6a0364ecd76e632b3a3fffedba6b5596d72cfdddb203da") // Treasury
            ]
          });

          signAndExecute(
            { transaction: tx },
            {
              onSuccess: (result) => {
                console.log("Full transaction result:", result);
                console.log("Transaction structure:", {
                  hasEvents: !!result.events,
                  hasEffects: !!result.effects,
                  hasRawEffects: !!result.rawEffects,
                  rawEffectsLength: result.rawEffects?.length,
                  keys: Object.keys(result)
                });
                console.log("Raw effects:", result.rawEffects);
                setStatus(result);
                setIsLoading(false);
              },
              onError: (error) => {
                console.error("Add beer error:", error);
                setError(error.message);
                setIsLoading(false);
              }
            }
          );
        } catch (error) {
          console.error("Add beer setup error:", error);
          setError(error.message);
          setIsLoading(false);
        }
      };

      return (
        <>
          <Flex
            position="sticky"
            px="4"
            py="2"
            justify="between"
            style={{
              borderBottom: "1px solid var(--gray-a2)",
            }}
          >
            <Box>
              <Heading>FreeBeer DApp</Heading>
            </Box>
            <Box>
              <ConnectButton />
            </Box>
          </Flex>

          <Container>
            <Container
              mt="5"
              pt="2"
              px="4"
              style={{ background: "var(--gray-a2)", minHeight: 500 }}
            >
              <WalletStatus />
              
              <Card mt="4">
                <Heading size="3" mb="4">Add Beer to Treasury</Heading>
                <Flex gap="4">
                  <Button 
                    onClick={() => addBeer(0.1)}
                    disabled={isLoading || !currentAccount}
                  >
                    Add 0.1 IOTA
                  </Button>
                  <Button
                    onClick={() => addBeer(1)}
                    disabled={isLoading || !currentAccount}
                  >
                    Add 1 IOTA
                  </Button>
                </Flex>
              </Card>

              <Card mt="4">
                <Heading size="3" mb="4">Free Beer Actions</Heading>
                <Flex gap="4" direction="column">
                  <GetBeerCall 
                    onSuccess={(result) => {
                      console.log("Get beer result:", result);
                      setStatus(result);
                      setIsLoading(false);
                    }}
                    onError={(error) => {
                      console.error("Get beer error:", error);
                      setError(error.message);
                      setIsLoading(false);
                    }}
                    isLoading={isLoading}
                  />
                  <Button
                    onClick={checkStatus}
                    disabled={isLoading || !currentAccount}
                  >
                    Check Free Beer Status
                  </Button>
                </Flex>
                
                <TransactionFeedback result={status} />
              </Card>

              {error && (
                <Card mt="4" style={{ 
                  background: 'var(--orange-a2)',
                  border: '1px solid var(--orange-6)'
                }}>
                  <Flex align="center" gap="2">
                    <Text style={{ 
                      color: 'var(--orange-11)',
                      fontSize: '14px'
                    }}>
                      ℹī¸ {error}
                    </Text>
                  </Flex>
                </Card>
              )}

              {isLoading && (
                <Card mt="4">
                  <Text align="center">Transaction in progress...</Text>
                </Card>
              )}
            </Container>
          </Container>
        </>
      );
    }
    </script>
    

[4.3] iota-sdk-decoder.ts

This handles the first step of decoding responses from the Iota Rebased contract.


                <!-- IOTA Transaction Decoder Implementation -->
        <script>
        import { Transaction } from '@iota/iota-sdk/transactions';
        import { bcs } from '@iota/iota-sdk/bcs';

        // Generic types for IOTA transaction data
        interface IOTATransactionData {
          sender?: string;
          function?: string;
          module?: string;
          package?: string;
          gasData?: any;
          commands?: any[];
        }

        interface IOTAEffectsData {
          status?: {
            success: boolean;
            error?: string;
          };
          gasUsed?: {
            computationCost: string;
            storageCost: string;
            storageRebate: string;
          };
          eventsDigest?: string;
          transactionDigest?: string;
          executedEpoch?: string;
          objectChanges?: any[];
        }

        interface DecodedIOTATransaction {
          transaction: IOTATransactionData;
          effects?: IOTAEffectsData;
          events?: any[];
          rawData?: {
            bytes?: Uint8Array;
            effects?: Uint8Array;
          };
        }

        class IOTATransactionDecoder {
          private static base64ToBytes(base64: string | undefined): Uint8Array | undefined {
            if (!base64) return undefined;
            
            try {
              const binaryStr = atob(base64);
              const bytes = new Uint8Array(binaryStr.length);
              for (let i = 0; i < binaryStr.length; i++) {
                bytes[i] = binaryStr.charCodeAt(i);
              }
              return bytes;
            } catch (error) {
              console.warn('Failed to decode base64:', error);
              return undefined;
            }
          }

          private static isBase64(str: string): boolean {
            try {
              return btoa(atob(str)) === str;
            } catch (err) {
              return false;
            }
          }

          static async decodeTransaction(result: {
            bytes?: string;
            effects?: string;
            events?: any[];
          }): Promise<DecodedIOTATransaction> {
            try {
              let transactionData: IOTATransactionData = {};
              let effectsData: IOTAEffectsData | undefined;
              let rawData: { bytes?: Uint8Array; effects?: Uint8Array } = {};

              // Decode transaction bytes if present and valid base64
              if (result.bytes && this.isBase64(result.bytes)) {
                const bytes = this.base64ToBytes(result.bytes);
                if (bytes) {
                  rawData.bytes = bytes;

                  try {
                    const tx = Transaction.from(bytes);
                    const jsonData = await tx.toJSON();
                    const parsedJson = JSON.parse(jsonData);
                    console.log('Parsed transaction JSON:', parsedJson);

                    const command = parsedJson.commands?.[0]?.MoveCall;
                    transactionData = {
                      sender: parsedJson.sender,
                      function: command?.function,
                      module: command?.module,
                      package: command?.package,
                      gasData: parsedJson.gasData,
                      commands: parsedJson.commands
                    };
                  } catch (e) {
                    console.warn('Error decoding transaction bytes:', e);
                  }
                }
              }

              // Decode effects if present and valid base64
              if (result.effects && this.isBase64(result.effects)) {
                const effectsBytes = this.base64ToBytes(result.effects);
                if (effectsBytes) {
                  rawData.effects = effectsBytes;

                  try {
                    const deserializedEffects = bcs.TransactionEffects.parse(effectsBytes);
                    console.log('Deserialized effects:', deserializedEffects);
                    
                    effectsData = {
                      status: {
                        success: deserializedEffects.V1.status.$kind === 'Success'
                      },
                      gasUsed: deserializedEffects.V1.gasUsed,
                      eventsDigest: deserializedEffects.V1.eventsDigest,
                      transactionDigest: deserializedEffects.V1.transactionDigest,
                      executedEpoch: deserializedEffects.V1.executedEpoch
                    };
                  } catch (e) {
                    console.warn('Error decoding effects:', e);
                  }
                }
              }

              // If we have events, they're already parsed
              return {
                transaction: transactionData,
                effects: effectsData,
                events: result.events,
                rawData
              };
            } catch (error) {
              console.error('Error in transaction decoder:', error);
              // Return partial data instead of throwing
              return {
                transaction: {},
                events: result.events
              };
            }
          }
        }

        export { 
          IOTATransactionDecoder, 
          DecodedIOTATransaction,
          IOTATransactionData,
          IOTAEffectsData 
        };
        </script>
    

[4.4] TransactionFeedback.ts

This handles the second step of decoding responses from the Iota Rebased contract.


                <!-- Transaction Feedback Component -->
        <script>
        import { Card, Text, Box, Flex } from "@radix-ui/themes";
        import { useEffect, useState } from 'react';
        import { FreeBeerDecoder, DecodedBeerTransaction } from './FreeBeerDecoder';

        interface TransactionFeedbackProps {
          result: any;
        }

        export const TransactionFeedback = ({ result }: TransactionFeedbackProps) => {
          const [decodedInfo, setDecodedInfo] = useState<DecodedBeerTransaction | null>(null);

          useEffect(() => {
            async function decode() {
              if (result) {
                try {
                  const decoded = await FreeBeerDecoder.decode(result);
                  console.log('Decoded transaction:', decoded);
                  setDecodedInfo(decoded);
                } catch (error) {
                  console.error('Failed to decode transaction:', error);
                }
              }
            }
            decode();
          }, [result]);

          if (!result) return null;

          // Check for BeerReceived event
          const beerReceivedEvent = result.events?.find(event => 
            event.type.includes('BeerReceived')
          );

          if (beerReceivedEvent) {
            const { 
              amount, 
              beers_remaining, 
              next_available, 
              remaining_treasury 
            } = beerReceivedEvent.parsedJson;

            return (
              <Card mt="4">
                <Box p="3">
                  <Text weight="bold" size="4" mb="2" style={{ color: 'var(--green-9)' }}>
                    ✓ Beer Received! đŸē
                  </Text>
                  
                  <Flex direction="column" gap="2">
                    <Text size="2">
                      Amount Received: {(Number(amount) / 1_000_000_000).toFixed(2)} IOTA
                    </Text>
                    
                    <Text size="2">
                      Beers Remaining: {beers_remaining}
                    </Text>

                    <Text size="2">
                      Treasury Balance: {(Number(remaining_treasury) / 1_000_000_000).toFixed(2)} IOTA
                    </Text>

                    <Text size="2">
                      Next Available: {new Date(Number(next_available)).toLocaleString()}
                    </Text>

                    <Box mt="2">
                      <a 
                        href={`https://explorer.rebased.iota.org/txblock/${result.digest}`}
                        target="_blank"
                        rel="noopener noreferrer"
                        style={{ 
                          color: 'var(--blue-9)',
                          fontSize: '14px',
                          textDecoration: 'none',
                          display: 'inline-flex',
                          alignItems: 'center',
                          gap: '4px'
                        }}
                      >
                        View in Explorer →
                      </a>
                    </Box>
                  </Flex>
                </Box>
              </Card>
            );
          }

          // Handle AddBeer event
          const beerAddedEvent = result.events?.find(event => 
            event.type.includes('BeerAdded')
          );

          if (beerAddedEvent) {
            return (
              <Card mt="4">
                <Box p="3">
                  <Text weight="bold" size="4" mb="2" style={{ color: 'var(--green-9)' }}>
                    ✓ Beer Added to Treasury
                  </Text>
                  
                  <Flex direction="column" gap="2">
                    <Text size="2">
                      Amount Added: {(Number(beerAddedEvent.parsedJson.amount) / 1_000_000_000).toFixed(2)} IOTA
                    </Text>
                    
                    <Text size="2">
                      New Treasury Total: {(Number(beerAddedEvent.parsedJson.new_total) / 1_000_000_000).toFixed(2)} IOTA
                    </Text>

                    <Box mt="2">
                      <a 
                        href={`https://explorer.rebased.iota.org/txblock/${result.digest}`}
                        target="_blank"
                        rel="noopener noreferrer"
                        style={{ 
                          color: 'var(--blue-9)',
                          fontSize: '14px',
                          textDecoration: 'none',
                          display: 'inline-flex',
                          alignItems: 'center',
                          gap: '4px'
                        }}
                      >
                        View in Explorer →
                      </a>
                    </Box>
                  </Flex>
                </Box>
              </Card>
            );
          }

          // Handle Status Check event
          const beerStatusEvent = result.events?.find(event => 
            event.type.includes('BeerStatus')
          );

          if (beerStatusEvent) {
            const { status, treasury_amount, next_available } = beerStatusEvent.parsedJson;
            const statusText = status === 0 ? 'đŸē Beer Available!' :
                              status === 1 ? 'âŗ Cooling Down' : 
                              '❌ Out of Beer';
            const statusColor = status === 0 ? 'var(--green-9)' :
                               status === 1 ? 'var(--yellow-9)' : 
                               'var(--red-9)';

            return (
              <Card mt="4">
                <Box p="3">
                  <Text weight="bold" size="4" mb="2">
                    Current Beer Status
                  </Text>
                  
                  <Flex direction="column" gap="2">
                    <Text size="2" weight="bold" style={{ color: statusColor }}>
                      {statusText}
                    </Text>
                    
                    <Text size="2">
                      Treasury Balance: {(Number(treasury_amount) / 1_000_000_000).toFixed(2)} IOTA
                    </Text>

                    {status === 1 && (
                      <Text size="2">
                        Next Available: {new Date(Number(next_available)).toLocaleString()}
                      </Text>
                    )}

                    <Box mt="2">
                      <a 
                        href={`https://explorer.rebased.iota.org/txblock/${result.digest}`}
                        target="_blank"
                        rel="noopener noreferrer"
                        style={{ 
                          color: 'var(--blue-9)',
                          fontSize: '14px',
                          textDecoration: 'none',
                          display: 'inline-flex',
                          alignItems: 'center',
                          gap: '4px'
                        }}
                      >
                        View in Explorer →
                      </a>
                    </Box>
                  </Flex>
                </Box>
              </Card>
            );
          }

          // Default feedback for other transactions
          return (
            <Card mt="4">
              <Box p="3">
                <Text weight="bold" size="4" mb="2" style={{ color: 'var(--green-9)' }}>
                  ✓ Transaction Complete
                </Text>
                
                <Flex direction="column" gap="2">
                  <Box mt="2">
                    <a 
                      href={`https://explorer.rebased.iota.org/txblock/${result.digest}`}
                      target="_blank"
                      rel="noopener noreferrer"
                      style={{ 
                        color: 'var(--blue-9)',
                        fontSize: '14px',
                        textDecoration: 'none',
                        display: 'inline-flex',
                        alignItems: 'center',
                        gap: '4px'
                      }}
                    >
                      View in Explorer →
                    </a>
                  </Box>
                </Flex>
              </Box>
            </Card>
          );
        };
        </script>
    

[4.5] FreeBeerDecoder.ts

This handles the decoding of feedback from one particular call.


                <!-- FreeBeer Transaction Decoder Implementation -->
        <script>
        import { 
          IOTATransactionDecoder, 
          DecodedIOTATransaction 
        } from './iota-sdk-decoder';

        interface BeerStatusEvent {
          next_available: string;
          status: number;
          treasury_amount: string;
        }

        interface BeerAddedEvent {
          amount: string;
          from: string;
          new_total: string;
        }

        interface DecodedBeerTransaction extends DecodedIOTATransaction {
          beerSpecific?: {
            isStatusCheck: boolean;
            isAddBeer: boolean;
            beerStatus?: BeerStatusEvent;
            beerAdded?: BeerAddedEvent;
          };
        }

        class FreeBeerDecoder {
          static readonly CONTRACT_ID = "0x80da07146a4060d7195f81bd25655816f55a465218f4ba7c5d355e10e8759556";
          static readonly MODULE_NAME = "beer";

          static async decode(result: {
            bytes?: string;
            effects?: string;
            events?: any[];
          }): Promise<DecodedBeerTransaction> {
            // First get the generic decoded data
            const decoded = await IOTATransactionDecoder.decodeTransaction(result);

            // Add FreeBeer-specific interpretation
            const isStatusCheck = decoded.transaction.function === 'get_beer_status' &&
                                 decoded.transaction.module === this.MODULE_NAME;

            const isAddBeer = decoded.transaction.function === 'addbeer' &&
                              decoded.transaction.module === this.MODULE_NAME;

            // Extract relevant event data
            const beerStatus = this.extractBeerStatus(decoded.events);
            const beerAdded = this.extractBeerAdded(decoded.events);

            return {
              ...decoded,
              beerSpecific: {
                isStatusCheck,
                isAddBeer,
                beerStatus,
                beerAdded
              }
            };
          }

          private static extractBeerStatus(events?: any[]): BeerStatusEvent | undefined {
            if (!events) return undefined;
            
            const statusEvent = events.find(event => 
              event.type.includes('BeerStatus') &&
              event.packageId === this.CONTRACT_ID
            );
            
            return statusEvent?.parsedJson;
          }

          private static extractBeerAdded(events?: any[]): BeerAddedEvent | undefined {
            if (!events) return undefined;
            
            const addedEvent = events.find(event => 
              event.type.includes('BeerAdded') &&
              event.packageId === this.CONTRACT_ID
            );
            
            return addedEvent?.parsedJson;
          }

          static getStatusText(status: number): { text: string; color: string } {
            switch (status) {
              case 0:
                return { text: 'đŸē Beer Available!', color: 'var(--green-9)' };
              case 1:
                return { text: 'âŗ Cooling Down', color: 'var(--yellow-9)' };
              case 2:
                return { text: '❌ Out of Beer', color: 'var(--red-9)' };
              default:
                return { text: 'Unknown Status', color: 'var(--gray-9)' };
            }
          }

          static formatIOTA(nanoAmount: string | number): string {
            const amount = Number(nanoAmount) / 1_000_000_000;
            return `${amount.toFixed(2)} IOTA`;
          }

          static formatAddress(address: string): string {
            if (!address) return '';
            return address.length > 12 ? 
              `${address.slice(0, 6)}...${address.slice(-6)}` : 
              address;
          }
        }

        export { FreeBeerDecoder, DecodedBeerTransaction, BeerStatusEvent, BeerAddedEvent };
        </script>
    

[4.6] GetBeerCall.ts

This handles one particular call.


        <!-- GetBeer Component Implementation -->
        <script>
        import { useCurrentAccount, useSignAndExecuteTransaction, useIotaClient } from "@iota/dapp-kit";
        import { Transaction } from "@iota/iota-sdk/transactions";
        import { Button } from "@radix-ui/themes";

        interface GetBeerCallProps {
          onSuccess: (result: any) => void;
          onError: (error: any) => void;
          isLoading: boolean;
        }

        // Error codes from FreeBeer contract
        const ERROR_MESSAGES = {
          1: "Contract is already initialized",
          2: "You've reached the maximum number of beers allowed",
          3: "Insufficient funds in treasury",
          4: "Beer is cooling down. Please check status for next available time.",
          default: "An error occurred while getting beer"
        };

        function parseErrorMessage(error: any): string {
          // Check if it's a MoveAbort error and extract the error code
          const moveAbortMatch = error.message?.match(/MoveAbort\([^)]+, (\d+)\)/);
          if (moveAbortMatch) {
            const errorCode = parseInt(moveAbortMatch[1]);
            return ERROR_MESSAGES[errorCode] || ERROR_MESSAGES.default;
          }
          return error.message || ERROR_MESSAGES.default;
        }

        export function GetBeerCall({ onSuccess, onError, isLoading }: GetBeerCallProps) {
          const iotaClient = useIotaClient();
          const currentAccount = useCurrentAccount();
          
          const packageId = "0x80da07146a4060d7195f81bd25655816f55a465218f4ba7c5d355e10e8759556";
          const { mutate: signAndExecute } = useSignAndExecuteTransaction({
            execute: async ({ bytes, signature }) =>
              await iotaClient.executeTransactionBlock({
                transactionBlock: bytes,
                signature,
                options: {
                  showEvents: true,
                  showEffects: true,
                  showRawEffects: true,
                  showObjectChanges: true,
                },
              }),
          });

          const getBeer = () => {
            try {
              const tx = new Transaction();
              tx.moveCall({
                target: `${packageId}::beer::getbeer`,
                arguments: [
                  tx.object("0xba1cb7687c673df83d6a0364ecd76e632b3a3fffedba6b5596d72cfdddb203da"), // Treasury
                  tx.object("0xfe8bae921d2abbb42773c91110bbacb2b353c2d41a1e3b8f9cafa7fcd3956ea9"), // Registry
                  tx.object("0x9d8cc06a57a89e63afc48805dde64c8bdd7aee9211f002566e25325b3206b95c")  // GlobalConfig
                ]
              });

              signAndExecute(
                {
                  transaction: tx,
                },
                {
                  onSuccess: (result) => {
                    console.log("Get beer result:", result);
                    onSuccess(result);
                  },
                  onError: (error) => {
                    console.error("Get beer error:", error);
                    // Parse the error into a user-friendly message
                    onError(new Error(parseErrorMessage(error)));
                  }
                }
              );
            } catch (error) {
              console.error("Error setting up Get Beer transaction:", error);
              onError(new Error(parseErrorMessage(error)));
            }
          };

          return (
            <Button 
              onClick={getBeer}
              disabled={isLoading || !currentAccount}
            >
              Get Free Beer! đŸē
            </Button>
          );
        }
        </script>