Coding Tutorial: Argent Telegram Tamagotchi

How to create a Telegram Tamagotchi Bot using Argent Telegram Wallet SDK

Tagged

Antoine Mecker

Oct 3, 2024

Quick summary

In this tutorial, we'll create a Telegram bot that simulates a Tamagotchi-like virtual pet. This bot will allow users to interact with their virtual pet, feed it, play with it, and monitor its health.

Introduction

In this tutorial, we'll create a Telegram bot that simulates a Tamagotchi-like virtual pet. This bot will allow users to interact with their virtual pet, feed it, play with it, and monitor its health.

Prerequisites

  • Basic knowledge of Cairo programming, Cairo extension for vscode
  • Basic knowledge of any frontend dev framework (I’ll use Svelte x Vite here), required vscode extensions
  • A Telegram test account

PART 1: Smart-contracts

Step 1: Install devtools

  • Be sure Rust is up-to-date, otherwise run this command:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

  • Install Scarb and Starknet-foundry. The easiest way, imo, is to use asdf
    • Scarb is the main package manager and build tool for Cairo. It’s like Cargo for Rust.
asdf plugin add scarb
asdf install scarb latest
asdf global scarb latest
  • Starknet-foundry is the testing framework for Cairo and Starknet smart-contracts. It is similar to Foundry for Solidity
asdf plugin add starknet-foundry
asdf install starknet-foundry latest
asdf global starknet-foundry latest

Step 2: Setup Cairo project

Simply type  snforge init project_name , ez pz. Don’t forget to replace project_name by a nice name for your repo. You should see a src/ folder, a test/ folder and scarb config files. Let’s write some code now !

Step 3: Code

Open src/lib.cairo and erase everything. Make your file look like this:

#[starknet::interface]
pub trait ITamagotchi<TContractState> {
}

#[starknet::contract]
mod Tamagotchi {
    #[storage]
    struct Storage {
    }
    
    #[constructor]
    fn constructor(ref self: ContractState) {
    }

    #[abi(embed_v0)]
    impl TamagotchiImpl of super::ITamagotchi<ContractState> {
    }
}

I won’t talk about the structure of a Cairo code here. The Cairo Book is the best place to learn the language. Cairo Agent might help too!

So, our goal is to create a basic Tamagotchi app. We would like our pet to be described by a few variables, his hunger, his happiness and his energy. Let’s add that to our contract storage. We also would like to write getters for our variables. Don’t forget, to add the functions in the interface too!

#[starknet::interface]
pub trait ITamagotchi<TContractState> {
		fn get_hunger(self: @TContractState) -> u8;
    fn get_happiness(self: @TContractState) -> u8;
    fn get_energy(self: @TContractState) -> u8;
}

#[starknet::contract]
mod Tamagotchi {
    #[storage]
    struct Storage {
	    hunger: u8,
      happiness: u8,
      energy: u8,
    }
    
    #[constructor]
    fn constructor(ref self: ContractState) {
    }

    #[abi(embed_v0)]
    impl TamagotchiImpl of super::ITamagotchi<ContractState> {
		    fn get_hunger(self: @ContractState) -> u8 {
            self.hunger.read()
        }

        fn get_happiness(self: @ContractState) -> u8 {
            self.happiness.read()
        }

        fn get_energy(self: @ContractState) -> u8 {
            self.energy.read()
        }
    }
}

Then we need to implement user interactions. A user can play with his pet, feed him or make him rest. The characteristics of our little friend can go from 0 to 100. We can add that as constants. Let’s also configure the amount of points the pet can get or lose as constants. Of course in theory, we should bound all values to MAX or MIN (i.e not having an energy bar > 100) but we don’t really care for this tutorial. I also won’t implement the time aspect in our app. We keep it suuuuper simple. For the sake of the demo, let’s add a set_stats_to_half function so that we’ll be able to test easily our buttons on the frontend side. Let’s also fill in the constructor and set all values at 100 when deployed.

#[starknet::interface]
pub trait ITamagotchi<TContractState> {
		fn get_hunger(self: @TContractState) -> u8;
    fn get_happiness(self: @TContractState) -> u8;
    fn get_energy(self: @TContractState) -> u8;
    
    fn feed(ref self: TContractState);
    fn play(ref self: TContractState);
    fn rest(ref self: TContractState);
    fn set_stats_to_half(ref self: TContractState);
}

#[starknet::contract]
mod Tamagotchi {
		const MAX_STAT: u8 = 100_u8;
    const MIN_STAT: u8 = 0_u8;
    const FEED_AMOUNT: u8 = 30_u8;
    const PLAY_AMOUNT: u8 = 20_u8;
    const REST_AMOUNT: u8 = 25_u8;
    const ENERGY_COST: u8 = 15_u8;

    #[storage]
    struct Storage {
	    hunger: u8,
      happiness: u8,
      energy: u8,
    }
    
    #[constructor]
    fn constructor(ref self: ContractState) {
		    self.hunger.write(MAX_STAT);
        self.happiness.write(MAX_STAT);
        self.energy.write(MAX_STAT);
    }

    #[abi(embed_v0)]
    impl TamagotchiImpl of super::ITamagotchi<ContractState> {
		    fn get_hunger(self: @ContractState) -> u8 {
            self.hunger.read()
        }

        fn get_happiness(self: @ContractState) -> u8 {
            self.happiness.read()
        }

        fn get_energy(self: @ContractState) -> u8 {
            self.energy.read()
        }
        
        
        fn feed(ref self: ContractState) {
            assert(self.energy.read() >= ENERGY_COST, 'Not enough energy');
            self.energy.write(self.energy.read() - ENERGY_COST);
            self.hunger.write(self.hunger.read() + FEED_AMOUNT);
        }

        fn play(ref self: ContractState) {
            assert(self.energy.read() >= ENERGY_COST, 'Not enough energy');
            self.energy.write(self.energy.read() - ENERGY_COST);
            self.happiness.write(self.happiness.read() + PLAY_AMOUNT);
        }

        fn rest(ref self: ContractState) {
            self.energy.write(self.energy.read() + REST_AMOUNT);
        }
        
        fn set_stats_to_half(ref self: ContractState) {
            self.hunger.write(MAX_STAT / 2);
            self.happiness.write(MAX_STAT / 2);
            self.energy.write(MAX_STAT / 2);
        }
    }
}

That’s basically it for the smart-contract. It obviously lacks a lot of features to turn it into a real Tamagotchi game but it’s enough for our telegram mini app demo !

Also, since we are serious devs, we need to write a test for our code. Add this to your tests/test_contract.cairo file. Then run snforge test.

use starknet::ContractAddress;

use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};

use tamagotchi::ITamagotchiDispatcher; // Adjust with your project name
use tamagotchi::ITamagotchiDispatcherTrait;

fn deploy_contract(name: ByteArray) -> ContractAddress {
    let contract = declare(name).unwrap().contract_class();
    let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
    contract_address
}

fn setup() -> ITamagotchiDispatcher {
    let contract_address = deploy_contract("Tamagotchi");
    ITamagotchiDispatcher { contract_address }
}

#[test]
fn test_constructor() {
    let dispatcher = setup();

    let hunger = dispatcher.get_hunger();
    assert(hunger == 100, 'Invalid hunger');

    let energy = dispatcher.get_energy();
    assert(energy == 100, 'Invalid energy');

    let happiness = dispatcher.get_happiness();
    assert(happiness == 100, 'Invalid happiness');
}

#[test]
fn test_set_stats_to_half() {
    let dispatcher = setup();

    dispatcher.set_stats_to_half();

    let hunger = dispatcher.get_hunger();
    assert(hunger == 50, 'Invalid hunger');

    let energy = dispatcher.get_energy();
    assert(energy == 50, 'Invalid energy');

    let happiness = dispatcher.get_happiness();
    assert(happiness == 50, 'Invalid happiness');
}

#[test]
fn test_rest() {
    let dispatcher = setup();
    dispatcher.set_stats_to_half();

    dispatcher.rest();

    let energy = dispatcher.get_energy();
    assert(energy == 75, 'Invalid energy');
}

#[test]
fn test_feed() {
    let dispatcher = setup();
    dispatcher.set_stats_to_half();

    dispatcher.feed();

    let hunger = dispatcher.get_hunger();
    assert(hunger == 80, 'Invalid hunger');
}

#[test]
fn test_play() {
    let dispatcher = setup();
    dispatcher.set_stats_to_half();

    dispatcher.play();

    let happiness = dispatcher.get_happiness();
    assert(happiness == 70, 'Invalid happiness');

    let energy = dispatcher.get_energy();
    assert(energy == 35, 'Invalid energy');
}

Step 4: Declare contract and deploy

The next step is to deploy our contract on Sepolia. You first need to setup a wallet locally. You can either create a brand new or import an existing one. I guess you already have an Argent X testnet wallet. You can import it. In my case, this is what I did. To create an account, it is well explained here.You will need to fund it with testnet ETH or STRK, you can grab some here: https://starknet-faucet.vercel.app/.

The tool that we will use to do that is sncast . It comes with starknet-foundry  that we installed at the beginning.

We need to call this:

sncast \
    account import \
    --url rpc_url \
    --name account_argent \
    --address 0x1 \
    --private-key 0x2 \
    --type argent
  • url: your rpc url. There are several API providers on Starknet. See the list.
  • address: your argent x wallet address
  • private-key: your Argent X wallet private key. Settings → Account → Export private key
EXPORTING YOUR PRIVATE KEY IS A SENSIBLE OPERATION. PLEASE ONLY DO THAT ON TESTNET. PLEASE PUT YOUR PRIVATE KEY IN YOUR ENVIRONMENT FILE. PLEASE DON’T UPLOAD YOUR PRIVATE KEY ON GITHUB. 

That being said, we can now declare and deploy our contract. Smart-contracts on Starknet have classes (like in OOP). Multiple smart-contracts can be deployed using the same class. So we first need to declare our smart-contract class, then, in a second transaction, using the class, we will deploy the smart-contract.

For declaring the contract, please run this command:

sncast --account account_argent declare --url "$RPC_URL" --fee-token eth --contract-name Tamagochi 

It will print something like this:

command: declare
class_hash: 0x34734d307891142b8245faf5eb8bd650ff9da4a93df73aada9cce5ad9e154be
transaction_hash: 0x71facc173cbea997116c25cdf29ac2410744a08500e95c77aabd7678f5a06db

To see declaration details, visit:
class: https://sepolia.starkscan.co/class/0x34734d307891142b8245faf5eb8bd650ff9da4a93df73aada9cce5ad9e154be
transaction: https://sepolia.starkscan.co/tx/0x71facc173cbea997116c25cdf29ac2410744a08500e95c77aabd7678f5a06db

Notice that I’m using env variables for RPC_URL, PRIVATE_KEY and ADDRESS.

Now, to deploy, we need to run this:

sncast \                                                                                        
    --account account_argent \
    deploy \
    --url $RPC_URL \
    --fee-token eth \
    --class-hash "the class hash printed by the previous command"

and you will see your contract address and the deployment transaction hash.

GGWP, our contract is now deployed. Keep the address somewhere, we will need it for the next part !

PART 2: Frontend  🎨

Feel free to use your favourite framework but here I’ll use Svelte + Vite. This great guide by Manu uses React + vite if you want another example.Svelte handles how we write our components and UI logic while Vite handles how we develop and build the application.  I will also use Typescript and Tailwindcss for the style.

1. Project initialization:

We have a few commands to run:

# New Vite project with Svelte template
pnpm create vite starknet-tamagotchi -- --template svelte-ts

# Go to project directory
cd starknet-tamagotchi

# Install dependencies
pnpm install

# Add required dependencies, Argent SDK + starknet.js
pnpm add @argent/tma-wallet starknet

# Add development dependencies
pnpm add -D tailwindcss postcss autoprefixer

# Make sure typescript is installed
pnpm add -D @tsconfig/svelte typescript svelte-check

To set up tailwind we need to do a few more things:

1. Init tailwind config

npx tailwindcss init -p

2. Update tailwind.config.js to add Svelte

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{svelte,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

3. Also, add this to app.css :

@tailwind base;
@tailwind components;
@tailwind utilities;

We’re almost there, don’t worry!

Create a src/app.d.ts file and paste this:

/// <reference types="@sveltejs/kit"/>

declare module '*.svelte' {
  import type { ComponentType } from 'svelte';
  const component: ComponentType;
  export default component;
}

This is used to tell Typescript how to handle Svelte files.

Update tsconfig.ts :

{
  "extends": "@tsconfig/svelte/tsconfig.json",
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "resolveJsonModule": true,
    "allowJs": true,
    "checkJs": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "types": ["vite/client", "svelte"]
  },
  "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
}

Now everything should be all right ! You can run the app to see if there is no errors:

pnpm run dev

In order to verify that tailwind has been configured properly, feel free to change the color of the title and see the result.

Finally, in index.html , add this to the <head>

2. Game implementation

Now, we will build something like this, with a cute doggo. I asked midjourney for the chara-design.

Please be nice with my design 🥲, me no designer

2.1 Pet Component

In src/lib/, create a file called Tamagotchi.svelte. Add this:

<script lang="ts">
  import happyPet from '../assets/happy_doggo.png';
  import neutralPet from '../assets/neutral_doggo.png';
  import sadPet from '../assets/sad_doggo.png';

  export let hunger: number = 100;
  export let happiness: number = 100;
  export let energy: number = 100;

  $: averageStats = (hunger + happiness + energy) / 3;

  $: petImage = averageStats > 70 ? happyPet : averageStats > 40 ? neutralPet : sadPet;

  $: petMood = averageStats > 70 ? 'happy' : averageStats > 40 ? 'neutral' : 'sad';
</script>

<div class="flex flex-col items-center justify-center p-4">
  <img
    src={petImage}
    alt="Pet {petMood}"
    class="h-48 w-48 object-contain transition-all duration-300 hover:scale-110"
  />

  <!-- Stats -->
  <div class="mt-4 w-full max-w-sm space-y-2">
    <!-- Hunger Bar -->
    <div>
      <div class="mb-1 flex justify-between text-sm">
        <span>Hunger</span>
        <span>{hunger}%</span>
      </div>
      <div class="h-2.5 w-full rounded-full bg-gray-200">
        <div>
          class="h-2.5 rounded-full bg-blue-600 transition-all duration-300"
          style="width: {hunger}%"
        </div>
      </div>
    </div>

    <!-- Happiness Bar -->
    <div>
      <div class="mb-1 flex justify-between text-sm">
        <span>Happiness</span>
        <span>{happiness}%</span>
      </div>
      <div class="h-2.5 w-full rounded-full bg-gray-200">
        <div>
          class="h-2.5 rounded-full bg-green-600 transition-all duration-300"
          style="width: {happiness}%"
        </div>
      </div>
    </div>

    <!-- Energy Bar -->
    <div>
      <div class="mb-1 flex justify-between text-sm">
        <span>Energy</span>
        <span>{energy}%</span>
      </div>
      <div class="h-2.5 w-full rounded-full bg-gray-200">
        <div>
          class="h-2.5 rounded-full bg-yellow-600 transition-all duration-300"
          style="width: {energy}%"
        </div>
      </div>
    </div>
  </div>
</div>

For the demo, our doggo will only have 3 moods, happy, sad and neutral. You need 3 pics to illustrate that. Place them in src/assets/ . you might need to change the imports.

2.2 Buttons

In src/lib/, create another file called Buttons.svelte .

<script lang="ts">
  export let onFeed: () => void;
  export let onPlay: () => void;
  export let onRest: () => void;
  export let onResetStats: () => void;
  export let isLoading: boolean = false;
</script>

<div class="mt-4 flex flex-col gap-4">
  <div class="flex justify-center gap-4">
    <button
      on:click={onFeed}
      disabled={isLoading}
      class="rounded-lg bg-blue-600 px-6 py-3 text-white transition-colors
               hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
    >
      🍖 Feed
    </button>

    <button
      on:click={onPlay}
      disabled={isLoading}
      class="rounded-lg bg-green-600 px-6 py-3 text-white transition-colors
               hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
    >
      🎮 Play
    </button>

    <button
      on:click={onRest}
      disabled={isLoading}
      class="rounded-lg bg-yellow-600 px-6 py-3 text-white transition-colors
               hover:bg-yellow-700 disabled:cursor-not-allowed disabled:opacity-50"
    >
      💤 Rest
    </button>
  </div>
  <div class="flex justify-center">
    <button
      on:click={onResetStats}
      disabled={isLoading}
      class="rounded-lg bg-red-600 px-6 py-2 text-sm text-white transition-colors
               hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50"
    >
      🔄 Reset Stats 
    </button>
  </div>
</div>

2.3 Game logic

Create a lib/contracts.ts file. It’s a helper file for us that will contain smart-contracts and wallet interactions.

We implement the Argent telegram wallet SDK here. Don’t worry about the two environment variables VITE_TELEGRAM_APP_NAME  and VITE_TELEGRAM_APP_URL . We will configure them later.

import { ArgentTMA, type SessionAccountInterface } from '@argent/tma-wallet';
import type { Call, Contract } from 'starknet';
import toast from 'svelte-french-toast';

export const initWallet = (contractAddress: string) =>
  ArgentTMA.init({
    environment: 'sepolia',
    appName: import.meta.env.VITE_TELEGRAM_APP_NAME,
    appTelegramUrl: import.meta.env.VITE_TELEGRAM_APP_URL,
    sessionParams: {
      allowedMethods: [
        { contract: contractAddress, selector: 'feed' },
        { contract: contractAddress, selector: 'play' },
        { contract: contractAddress, selector: 'rest' },
        { contract: contractAddress, selector: 'set_stats_to_half' },
      ],
      validityDays: 90,
    },
  });

export async function executeContractAction(
  contract: Contract,
  account: SessionAccountInterface,
  argentTMA: ArgentTMA,
  action: string,
  successMessage: string,
  errorMessage: string
) {
  const call: Call = {
    contractAddress: contract.address,
    entrypoint: action,
    calldata: [],
  };

  try {
    const fees = await account?.estimateInvokeFee([call]);
    const tx = await contract[action]({
      maxFee: fees?.suggestedMaxFee ? BigInt(fees.suggestedMaxFee) * 2n : undefined,
    });
    await argentTMA.provider.waitForTransaction(tx.transaction_hash);
    toast.success(successMessage);
    return true;
  } catch (error) {
    console.error(`Error performing ${action}:`, error);
    toast.error(errorMessage);
    return false;
  }
}

If you’d like to pay the transaction fees in STRK, you’d need to implement this in a slightly more complex way, see Starknet.js doc:

const myCall = contract.populate(action, []);

    const maxQtyGasAuthorized = 1800n; // max quantity of gas authorized
    const maxPriceAuthorizeForOneGas = 20n * 10n ** 12n; // max FRI authorized to pay 1 gas (1 FRI=10**-18 STRK)
    const { transaction_hash } = await account.execute(myCall, {
      version: 3,
      maxFee: 10 ** 15,
      feeDataAvailabilityMode: RPC.EDataAvailabilityMode.L1,
      resourceBounds: {
        l1_gas: {
          max_amount: num.toHex(maxQtyGasAuthorized),
          max_price_per_unit: num.toHex(maxPriceAuthorizeForOneGas),
        },
        l2_gas: {
          max_amount: num.toHex(0),
          max_price_per_unit: num.toHex(0),
        },
      },
    });

An important thing to notice here is this:

allowedMethods: [
        { contract: contractAddress, selector: 'feed' },
        { contract: contractAddress, selector: 'play' },
        { contract: contractAddress, selector: 'rest' },
        { contract: contractAddress, selector: 'set_stats_to_half' },
      ],

The goal of the SDK it to allow devs to create mini apps with a smooth blockchain integration. We think that a good UX for a game has to abstract away the entire concept of blockchain. The user shouldn’t even notice he is interacting with one. Our SDK uses “Session keys”.

You can configure which functions the backend will be able to execute on behalf of the user.

Now comes the most important file. Create a src/lib/Game.svelte file. We will implement the game logic there. It will look like this:

<script lang="ts">
  import { onMount } from 'svelte';
  import Tamagotchi from './Tamagotchi.svelte';
  import Buttons from './Buttons.svelte';
  import { Contract, type AccountInterface, type Call } from 'starknet';
  import type { SessionAccountInterface } from '@argent/tma-wallet';
  import artifact from '../utils/abi/tamago_Tamagochi.contract_class.json';
  import { executeContractAction, initWallet } from './contracts';

  const ABI = artifact.abi;
  const TAMAGOTCHI_ADDRESS = import.meta.env.VITE_TAMAGOTCHI_CONTRACT_ADDRESS;

  const argentTMA = initWallet(TAMAGOTCHI_ADDRESS);

  let account: SessionAccountInterface | undefined;
  let isConnected = false;
  let isLoading = false;
  let contract: Contract | undefined;

  let stats = {
    hunger: 100,
    happiness: 100,
    energy: 100,
  };

  onMount(async () => {
    try {
      const res = await argentTMA.connect();

      if (!res) {
        isConnected = false;
        return;
      }

      account = res.account;
      if (account.getSessionStatus() !== 'VALID') {
        isConnected = false;
        return;
      }

      contract = new Contract(ABI, TAMAGOTCHI_ADDRESS, account as unknown as AccountInterface);

      isConnected = true;

      await updateStats();
    } catch (error) {
      console.error('Failed to connect:', error);
    }
  });

  async function handleConnect() {
    try {
      await argentTMA.requestConnection('tamagochi_connection');
    } catch (error) {
      console.error('Connection failed:', error);
    }
  }

  async function handleDisconnect() {
    try {
      await argentTMA.clearSession();
      // Reset all states
      account = undefined;
      isConnected = false;
      contract = undefined;
      stats = {
        hunger: 100,
        happiness: 100,
        energy: 100,
      };
    } catch (error) {
      console.error('Failed to disconnect:', error);
    }
  }

  async function updateStats() {
    if (!contract) return;

    const [hunger, happiness, energy] = await Promise.all([
      contract.get_hunger(),
      contract.get_happiness(),
      contract.get_energy(),
    ]);

    stats = {
      hunger: Number(hunger),
      happiness: Number(happiness),
      energy: Number(energy),
    };
  }

  async function handleAction(action: string) {
    if (!contract || !isConnected || !account) return;
    isLoading = true;

    const messages = {
      feed: { success: 'Pet has been fed! 🍖', error: 'Failed to feed pet 😕' },
      play: { success: 'You played with your Pet! 🎮', error: 'Failed to play with pet 😕' },
      rest: { success: 'Pet is sleeping! 🛌', error: 'Pet is not sleeping 😕' },
      set_stats_to_half: {
        success: 'Stats have been reset! 🔄',
        error: 'Failed to reset stats 😕',
      },
    };

    const result = await executeContractAction(
      contract,
      account,
      argentTMA,
      action,
      messages[action as keyof typeof messages].success,
      messages[action as keyof typeof messages].error
    );

    if (result) await updateStats();
    isLoading = false;
  }
</script>

<div class="min-h-screen bg-gray-100 px-4 py-8">
  <div class="mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg">
    <h1 class="mb-6 text-center text-2xl font-bold text-black">My Tamagotchi</h1>

    {#if !isConnected}
      <div class="flex justify-center">
        <button
          on:click={handleConnect}
          class="rounded-lg bg-blue-600 px-6 py-3 text-white transition-colors
                 hover:bg-blue-700"
        >
          Connect Wallet
        </button>
      </div>
    {:else}
      <button on:click={handleDisconnect} class="w-full text-left">
        Account address: <code>{account?.address.slice(0, 6)}...{account?.address.slice(-4)}</code>
      </button>
      <Tamagotchi {...stats} />

      <Buttons
        onFeed={() => handleAction('feed')}
        onPlay={() => handleAction('play')}
        onRest={() => handleAction('rest')}
        onResetStats={() => handleAction('set_stats_to_half')}
        {isLoading}
      />
    {/if}
  </div>
</div>

2.4 Last touch

Now, if you copy paste this. You will still have a few errors in the code. To solve the first one, run this command:

pnpm add svelte-french-toast

I’m using toasts to send actions notifications to the user.Add this to App.svelte

<script lang="ts">
  import Game from './lib/Game.svelte';
  import { Toaster } from 'svelte-french-toast';
</script>

<main>
  <Toaster
    position="top-right"
    gutter={8}
    toastOptions={{
      duration: 4000,
      style: 'background: #333; color: white;',
    }}
  />
  <Game />
</main>

To solve  another one, you need to go back to your smart-contract repo. Go to target/ . this folder contains the build files from scarb. Now copy a file called tamago_Tamagotchi.contract_class.json , projectName_smartcontractName.contract_class.json.

In your frontend repo, create a folder src/utils/abi  and paste the file there. you don’t really need to understand what’s in that file. We’re only interested in the abi attribute. The ABI contains every function signatures of your smart-contracts. It describes how to interact with your smart-contract. Modify your import accordingly   import artifact from '../utils/abi/tamago_Tamagotchi.contract_class.json';  in Game.svelte

Finally, create a .env file at the root of you project. Create the variable VITE_TAMAGOTCHI_CONTRACT_ADDRESS=0xyour_contract_address . The contract address was given to you when you deployed your smart-contract here.

Now, you shouldn’t see any errors in the code.

Run pnpm dev  and see what it does. Feel fry to modify the frontend as you wish !

PART 3: Telegram bot

Last chapter of the tutorial. Now that we have our app ready, let’s integrate it with Telegram.

The first step will be to create a Telegram test account if you don’t already have one. Mano explains it here.

🚨 DON’T FORGET TO SETUP A USERNAME FOR YOUR TEST ACCOUNT. 🚨

1. Talk to @BotFather to create a bot

A bot to create them all: https://telegram.me/BotFather

Interact with Botfather.

Run /newbot and follow what he says to create a new bot.

2. Talk to Botfather again to create a new app

Run /newapp  and follow the instructions.

At some point, Botfather will ask for an app URL. You need to make your webapp host available somewhere. There are several ways to do that. You could deploy it on Vercel and share the vercel link but that wouldn’t be super efficient and convenient for a development phase. With Vite you can expose your local app to the network by running pnpm run dev --host . This exposes your IP address so use only that for dev purposes.

Other tools like ngrok exist to expose a local project to the network. It’s also fine to use that and even safer.

Give Botfather the URL from the Vite server or ngrok.

He’ll then ask you for an app name.

At the end of the process, Botfather will give you an app url.

3. Configure the SDK with your Telegram app params

Go back to your frontend Game.svelte  file. Find this:

const argentTMA = ArgentTMA.init({
    environment: 'sepolia',
    appName: 'titleForApp',
    appTelegramUrl: 'http://t.me/TestBotTutoBot/titleForApp',

Replace appName and appTelegramUrl  with the values from Botfather.

4. Configure the bot

We created the app and now we need to configure the bot to launch our app.

Talk to Botfather, prompt /mybots . Select yours. Then go to Bot settings. Click on Menu Button then “Configure Menu Button”. He will ask for a URL, give him your Telegram app url http://t.me/TestBotTutoBot/titleForApp.

He will then ask for the button title. You can put something like “Open mini app”.

Now, if you open your bot chat, you’ll see the blue button “Open mini app”. If you click on it, it will open your mini app !

You could also customize your bot commands (/start, /help etc) if you wished to.

Congrats, you created a telegram mini-app using Argent Telegram wallet SDK ! 

Written by Antoine Mecker, Dev Rel @ Argent.

For any questions, ping me on Telegram @Flexouille.