- 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>