diff --git a/components/tutorial/list.tsx b/components/tutorial/list.tsx index 206c7cf..cb1c5dc 100644 --- a/components/tutorial/list.tsx +++ b/components/tutorial/list.tsx @@ -57,6 +57,46 @@ const data = [ lang: "C#", langId: "csharp", }, + { + id: "tic-tac-toe", + img: "/tic-tac-teo-contract.png", + title: "Tic-Tac-Toe Contract", + description: "Decentralized gamify Contract", + level: "Intermediate", + levelId: "intermediate", + lang: "C#", + langId: "csharp", + }, + { + id: "expense-tracker", + img: "/expense-tracker-contract.png", + title: "Expense Tracker Contract", + description: "User-friendly interface for recording expenses, categorizing them, and tracking spending habitst", + level: "Intermediate", + levelId: "intermediate", + lang: "C#", + langId: "csharp", + }, + { + id: "single-pool-staking", + img: "/single-pool-staking-contract.png", + title: "Single Pool Staking Contract", + description: "Allows users to stake their tokens in a single staking pool", + level: "Advanced", + levelId: "advanced", + lang: "C#", + langId: "csharp", + }, + { + id: "role", + img: "/allowance-contract.png", + title: "Allowance Contract", + description: "Integration of two smart contracts, RoleContract and AllowanceContract, focusing on role-based access and fund management", + level: "Advanced", + levelId: "advanced", + lang: "C#", + langId: "csharp", + }, ].filter((i) => (solidityEnabled ? true : i.langId !== "solidity")); export function TutorialList() { diff --git a/public/allowance-contract.png b/public/allowance-contract.png new file mode 100644 index 0000000..3e38e35 Binary files /dev/null and b/public/allowance-contract.png differ diff --git a/public/expense-tracker-contract.png b/public/expense-tracker-contract.png new file mode 100644 index 0000000..c7b024e Binary files /dev/null and b/public/expense-tracker-contract.png differ diff --git a/public/single-pool-staking-contract.png b/public/single-pool-staking-contract.png new file mode 100644 index 0000000..82047c3 Binary files /dev/null and b/public/single-pool-staking-contract.png differ diff --git a/public/tic-tac-teo-contract.png b/public/tic-tac-teo-contract.png new file mode 100644 index 0000000..55347bb Binary files /dev/null and b/public/tic-tac-teo-contract.png differ diff --git a/src/main.tsx b/src/main.tsx index 9f84919..e3c36a1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -13,6 +13,11 @@ import HelloWorldSolidity from './tutorials/hello-world-solidity.mdx'; import LotteryGame from './tutorials/lottery-game.mdx'; import Todo from './tutorials/todo.mdx'; import VoteContract from './tutorials/vote-contract.mdx'; +import TicTacToeContract from './tutorials/tic-tac-toe.mdx'; +import ExpenseTracker from './tutorials/expense-tracker.mdx'; +import SinglePoolStaking from './tutorials/single-pool-staking.mdx'; +import RoleContract from './tutorials/role.mdx'; +import AllowanceContract from './tutorials/allowance.mdx'; import {Component as Tutorial} from './routes/tutorial'; const tutorials = [ @@ -36,6 +41,26 @@ const tutorials = [ path: 'vote-contract', component: VoteContract, }, + { + path: 'tic-tac-toe', + component: TicTacToeContract, + }, + { + path: 'expense-tracker', + component: ExpenseTracker, + }, + { + path: 'single-pool-staking', + component: SinglePoolStaking, + }, + { + path: 'role', + component: RoleContract, + }, + { + path: 'allowance', + component: AllowanceContract, + }, ] const router = createBrowserRouter(createRoutesFromElements( import('./routes/root')}> diff --git a/src/routes/tutorial.scss b/src/routes/tutorial.scss index c3b8146..a66aa30 100644 --- a/src/routes/tutorial.scss +++ b/src/routes/tutorial.scss @@ -22,11 +22,24 @@ pre { overflow-x: auto; padding: 1rem 0; + background-color: #282A36 !important; } - + pre [data-line] { padding: 0 1rem; } + + code { + background-color: #f8f8f8; + border: 0.1rem solid #ddd; + border-radius: 0.4rem; + padding-inline: 0.2rem; + } + pre code { + background: #282A36; + border: none; + border-radius: 0; + } h1 { font-size: 2rem; @@ -55,6 +68,11 @@ margin-bottom: 1rem; } + ol{ + list-style: auto; + margin-left: 1rem; + } + [data-rehype-pretty-code-title] { border-top-left-radius: .5rem; border-top-right-radius: .5rem; @@ -138,3 +156,11 @@ } } +.dark { + .mdx-content { + code { + background-color: #ffffff1a; + border: 0.1rem solid #ffffff1a; + } + } +} \ No newline at end of file diff --git a/src/tutorials/allowance.mdx b/src/tutorials/allowance.mdx new file mode 100644 index 0000000..17e6c95 --- /dev/null +++ b/src/tutorials/allowance.mdx @@ -0,0 +1,286 @@ +import GenerateTemplate from '@/components/tutorial/generate-template'; + +# Allowance (Allowance Contract) + +## Step 1 - Develop Role Smart Contract + +### Adding Your Smart Contract Code + +First, generate the template: . + +Now that we have an AllowanceContract template, it can be customized to manage funds with role-based permissions. Below are its core functionalities: + +1. Set Allowance: Increases the current allowance, with permissions verified through parent and child roles fetched from the RoleContract. +2. Use Funds: Allows a child role to spend funds, reducing the current allowance. +3. Get Allowance: Retrieves the remaining allowance. +4. Role-based Permissions: Ensures only authorized roles, like parent of a child can modify or the child use the allowance. + +This contract showcases **inter-contract calls** and role-based fund management, demonstrating how multiple smart contracts work together for secure, controlled financial operations on the aelf blockchain. It helps in understanding how the AllowanceContract calls the RoleContract to check which sender (either of Parent, Child & Admin) is calling the methods of the AllowanceContract to put control on the access of the functions. + +#### Defining Methods and Messages + +The implementation of file `src/Protobuf/contract/allowance_contract.proto` is as follows: + +```csharp title="allowance_contract.proto" +syntax = "proto3"; + +import "aelf/core.proto"; + +import "google/protobuf/empty.proto"; +import "Protobuf/reference/acs12.proto"; +import "aelf/options.proto"; +import "google/protobuf/wrappers.proto"; + +// The namespace of this class +option csharp_namespace = "AElf.Contracts.AllowanceContract"; + +service AllowanceContract { + + // The name of the state class the smart contract is going to use to access blockchain state + option (aelf.csharp_state) = "AElf.Contracts.AllowanceContract.AllowanceContractState"; + option (aelf.base) = "Protobuf/reference/acs12.proto"; + + rpc Initialize (google.protobuf.Empty) returns (google.protobuf.Empty){ + + } + + rpc SetAllowance (google.protobuf.Int64Value) returns (google.protobuf.Empty) { + } + + rpc GetAllowance (aelf.Address) returns (google.protobuf.Int64Value) { + option (aelf.is_view) = true; + } + + rpc useFunds (google.protobuf.Int64Value) returns (google.protobuf.Empty) { + } + + rpc IfInitialized (google.protobuf.Empty) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } + + +} +``` + +- `rpc` methods define the callable functions within the contract, allowing external systems to interact with the contract's logic. +- `message` represent the structured data exchanged between the contract and external systems. + +#### Define Contract States + +The implementation of the allowance contract state inside file `src/AllowanceContractState.cs` is as follows: + +```csharp title="src/AllowanceContractState.cs" +using AElf.Sdk.CSharp.State; +using AElf.Types; + +namespace AElf.Contracts.AllowanceContract +{ + public partial class AllowanceContractState : ContractState + { + public BoolState Initialized { get; set; } + + public SingletonState
AdminAddress { get; set; } + + public SingletonState
ParentAddress { get; set; } + + public SingletonState
ChildAddress { get; set; } + + public Int32State CurrentAllowance { get; set; } + + } +} +``` + +- The `State.cs` file in an aelf blockchain smart contract holds the variables that store the contract's data, making sure this data is saved and accessible whenever the contract needs it. + +#### Implement Role Smart Contract + +The implementation of the AllowanceContract inside file `src/AllowanceContract.cs` is as follows: + +```csharp title="src/AllowanceContract.cs" +using AElf.Sdk.CSharp; +using AElf.Types; +using Google.Protobuf.WellKnownTypes; + +namespace AElf.Contracts.AllowanceContract +{ + // Contract class must inherit the base class generated from the proto file + public class AllowanceContract : AllowanceContractContainer.AllowanceContractBase + { + + private const string RoleContractAddress = "YOUR_ROLE_CONTRACT_ADDRESS"; // tDVW role contract address + + public override Empty Initialize(Empty input) + { + // Check if the contract is already initialized + Assert(State.Initialized.Value == false, "Already initialized."); + // Set the contract state + State.Initialized.Value = true; + // Set the owner address + State.AdminAddress.Value = Context.Sender; + + // Initialize the role contract + State.RoleContract.Value = Address.FromBase58(RoleContractAddress); + + return new Empty(); + } + + public override Empty SetAllowance(Int64Value input) + { + State.ParentAddress.Value = Address.FromBase58(State.RoleContract.GetParent.Call(new Empty()).Value); + + Assert(Context.Sender == State.ParentAddress.Value, "Unauthorized(Not Parent) to perform the action."); + + State.ChildAddress.Value = Address.FromBase58(State.RoleContract.GetChild.Call(new Empty()).Value); + + State.CurrentAllowance.Value = (int)(State.CurrentAllowance.Value + input.Value) ; + + return new Empty(); + } + + public override Int64Value GetAllowance(Address input) + { + + Assert(Context.Sender == State.ChildAddress.Value || Context.Sender == State.ParentAddress.Value, "Unauthorized(Not Parent or Child) to perform the action."); + + var allowance = State.CurrentAllowance.Value; + + return new Int64Value + { + Value = allowance + }; + } + + public override Empty useFunds(Int64Value input) + { + State.ChildAddress.Value = Address.FromBase58(State.RoleContract.GetChild.Call(new Empty()).Value); + + Assert(Context.Sender == State.ChildAddress.Value, "Unauthorized(Not Child) to perform the action."); + + State.CurrentAllowance.Value = (int)(State.CurrentAllowance.Value - input.Value) ; + + return new Empty(); + } + + // Function to check if the allowance contract is already initialized or not + public override BoolValue IfInitialized(Empty input) + { + return new BoolValue { Value = State.Initialized.Value }; + } + + } + +} +``` + +### Add Inter-Contract Calls + +- Now, AllowanceContract needs to create a reference with the RoleContract. First, AllowanceContract will use the `role_contract.proto` file inside the **Protobuf/reference** folder. Copy the `role_contract.proto` from the `role-contract/src/Protobuf/contract` folder. + +```csharp title="role_contract.cs" +syntax = "proto3"; + +import "aelf/core.proto"; + +import "google/protobuf/empty.proto"; +import "Protobuf/reference/acs12.proto"; +import "aelf/options.proto"; +import "google/protobuf/wrappers.proto"; + +// The namespace of this class +option csharp_namespace = "AElf.Contracts.RoleContract"; + +service RoleContract { + + // The name of the state class the smart contract is going to use to access blockchain state + option (aelf.csharp_state) = "AElf.Contracts.RoleContract.RoleContractState"; + option (aelf.base) = "Protobuf/reference/acs12.proto"; + + rpc Initialize (google.protobuf.Empty) returns (google.protobuf.Empty){ + + } + + rpc SetAdmin (aelf.Address) returns (google.protobuf.Empty) { + } + + rpc GetAdmin (google.protobuf.Empty) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + + rpc SetParent (aelf.Address) returns (google.protobuf.Empty) { + } + + rpc GetParent (google.protobuf.Empty) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + + rpc SetChild (aelf.Address) returns (google.protobuf.Empty) { + } + + rpc GetChild (google.protobuf.Empty) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + +} +``` + +- Now, create a file inside `src` folder and name it as `ContractReferences.cs` and add the following code to establish connection between AllowanceContract and the RoleContract. + +```csharp title="ContractReferences.cs" +using AElf.Contracts.RoleContract; + +namespace AElf.Contracts.AllowanceContract +{ + public partial class AllowanceContractState + { + internal RoleContractContainer.RoleContractReferenceState RoleContract { get; set; } + } +} +``` + +## Step 2 - Building Smart Contract + +Build the new code by clicking the Build button. + +## Step 3 - Deploy Smart Contract + +Deploy the new code by clicking the Deploy button. + + +## 🎯 Conclusion + +πŸŽ‰ Congratulations on completing the **Allowance dApp** tutorial! πŸŽ‰ You’ve taken significant steps in setting up your development environment, building and deploying two smart contracts on the aelf blockchain. 🌟 + +**πŸ“š What You've Learned** + +Throughout this tutorial, you've mastered: + +- **πŸ› οΈ Setting Up Your Development Environment:** You prepared your workspace by installing and configuring all the necessary tools to kickstart your smart contract project. + +- **πŸ’» Developing the Role Smart Contract:** You created a Role contract that defines roles and permissions, including Admin, Parent, and Child roles, which allow users to interact according to assigned roles. + +- **πŸ’» Developing the Allowance Smart Contract:** You built the Allowance contract to enable Parents to set spending limits for Children, creating the foundation of a decentralized allowance management system. + +- **πŸš€ Deploying Both Smart Contracts:** You deployed both contracts to the aelf blockchain, enabling your dApp to use the features in a live environment. + +**πŸ” Final Output** + +By now, you should have: + +- πŸ“œ **Two deployed smart contracts** β€” one for managing user roles (Admin, Parent, and Child) and another for setting and managing allowances within the dApp. + +- πŸ’» **A fully functional Allowance dApp** β€” allowing users to assign roles, set allowances, and spend funds within set limits, all through a secure and intuitive interface. + +**➑️ What's Next?** + +With the foundation in place, consider exploring advanced topics: + +- **πŸ“ˆ Enhancing Smart Contract Logic:** Add new features, such as notifications for spending limits, allowance resets, or reports on spending habits. + +- **πŸ”’ Improving Security:** Ensure your dApp and smart contract are secure by implementing best practices and security measures. + +- **🌍 Exploring Cross-Chain Features:** Expand your dApp’s reach by exploring aelf’s cross-chain interoperability, enabling interactions with other blockchain networks + +You’ve now acquired the tools to take your Allowance dApp to the next level! Keep building, innovating, and exploring with aelf. πŸš€ + +Happy coding and growing your **Allowance dApp! 😊** \ No newline at end of file diff --git a/src/tutorials/expense-tracker.mdx b/src/tutorials/expense-tracker.mdx new file mode 100644 index 0000000..15908b6 --- /dev/null +++ b/src/tutorials/expense-tracker.mdx @@ -0,0 +1,274 @@ +import GenerateTemplate from '@/components/tutorial/generate-template'; + +# Expense Tracker + +**Description**: The Expense Tracker dApp is a decentralized application that enables users to manage and monitor their personal finances on the aelf blockchain. It provides a user-friendly interface for recording expenses, categorizing them, and tracking spending habits, with all data securely stored and verified on the aelf blockchain. + +**Purpose**: The purpose of the Expense Tracker dApp is to showcase the integration of personal finance management with blockchain technology, offering enhanced transparency and security for users' financial data. It serves as a practical example of decentralized applications in everyday use cases, while also providing a learning platform for smart contract development and blockchain-based financial solutions. + +**Difficulty Level**: Moderate + +## Step 1 - Develop Smart Contract + +### Adding Your Smart Contract Code + +First, generate the template: . + +Now that we have a template expense tracker project, we can customise the template to incorporate our own contract logic. +Let's start by implementing methods to handle the basic functionality of AddExpense, UpdateExpense, DeleteExpense, ListExpenses, and GetExpense and GetInitialStatus as complete within the contract state. + +#### Defining Methods and Messages + +The implementation of `expense_tracker.proto` file inside folder `src/Protobuf/contract/` is as follows: + +```csharp title="expense_tracker.proto" +syntax = "proto3"; +import "aelf/options.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; +import "Protobuf/reference/acs12.proto"; +// The namespace of this class +option csharp_namespace = "AElf.Contracts.ExpenseTracker"; +service ExpenseTracker { + option (aelf.csharp_state) = "AElf.Contracts.ExpenseTracker.ExpenseTrackerState"; + option (aelf.base) = "Protobuf/reference/acs12.proto"; + rpc Initialize (google.protobuf.Empty) returns (google.protobuf.Empty) { + } + rpc AddExpense (ExpenseInput) returns (google.protobuf.StringValue) { + } + rpc UpdateExpense (ExpenseUpdateInput) returns (google.protobuf.Empty) { + } + rpc DeleteExpense (google.protobuf.StringValue) returns (google.protobuf.Empty) { + } + rpc ListExpenses (google.protobuf.StringValue) returns (ExpenseList) { + option (aelf.is_view) = true; + } + rpc GetExpense (google.protobuf.StringValue) returns (Expense) { + option (aelf.is_view) = true; + } + rpc GetInitialStatus (google.protobuf.Empty) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } +} + +message Expense { + string expense_id = 1; + string description = 2; + string category = 3; + int64 amount = 4; // Store as cents + string currency = 5; + string owner = 6; + int64 created_at = 7; + int64 updated_at = 8; +} + +message ExpenseInput { + string description = 1; + string category = 2; + int64 amount = 3; // Store as cents + string currency = 4; +} + +message ExpenseUpdateInput { + string expense_id = 1; + string description = 2; + string category = 3; + int64 amount = 4; // Store as cents + string currency = 5; +} + +message ExpenseList { + repeated Expense expenses = 1; +} +``` + +- `rpc` methods define the callable functions within the contract, allowing external systems to interact with the contract's logic. +- `message` represent the structured data exchanged between the contract and external systems. + +#### Define Contract States + +The implementation of the Expense Tracker smart contract state inside file `src/ExpenseTrackerState.cs` is as follows: + +```csharp title="src/ExpenseTrackerState.cs" +using AElf.Sdk.CSharp.State; +using AElf.Types; + +namespace AElf.Contracts.ExpenseTracker +{ + public class ExpenseTrackerState : ContractState + { + public BoolState Initialized { get; set; } + public SingletonState
Owner { get; set; } + public MappedState Expenses { get; set; } // Mapping of expense ID to Expense + public MappedState ExpenseExistence { get; set; } // Mapping to track expense existence + public StringState ExpenseIds { get; set; } // Concatenated string of expense IDs + public Int32State ExpenseCounter { get; set; } // Counter for generating unique IDs + } +} +``` + +- The `State.cs` file in an aelf blockchain smart contract holds the variables that store the contract's data, making sure this data is saved and accessible whenever the contract needs it. + +#### Implement Expense Tracker Smart Contract + +The implementation of the Expense Tracker smart contract inside file `src/ExpenseTracker.cs` is as follows: + +```csharp title="src/ExpenseTracker.cs" +using Google.Protobuf.WellKnownTypes; +using System.Collections.Generic; + +namespace AElf.Contracts.ExpenseTracker +{ + public class ExpenseTracker : ExpenseTrackerContainer.ExpenseTrackerBase + { + public override Empty Initialize(Empty input) + { + if (State.Initialized.Value) + { + return new Empty(); + } + State.Initialized.Value = true; + State.Owner.Value = Context.Sender; + State.ExpenseIds.Value = ""; + State.ExpenseCounter.Value = 0; + return new Empty(); + } + + public override StringValue AddExpense(ExpenseInput input) + { + if (!State.Initialized.Value) + { + return new StringValue { Value = "Contract not initialized." }; + } + var expenseId = (State.ExpenseCounter.Value + 1).ToString(); + State.ExpenseCounter.Value++; + var timestamp = Context.CurrentBlockTime.Seconds; + State.Expenses[expenseId] = new Expense + { + ExpenseId = expenseId, + Description = input.Description, + Category = input.Category, + Amount = input.Amount, // Now using int64 for amount + Currency = input.Currency, + CreatedAt = timestamp, + UpdatedAt = timestamp, + Owner = Context.Sender.ToString().Trim('"'), + }; + State.ExpenseExistence[expenseId] = true; + + var existingExpenseIds = State.ExpenseIds.Value; + existingExpenseIds += string.IsNullOrEmpty(existingExpenseIds) ? expenseId : $",{expenseId}"; + State.ExpenseIds.Value = existingExpenseIds; + + return new StringValue { Value = expenseId }; + } + + public override Empty UpdateExpense(ExpenseUpdateInput input) + { + var expense = State.Expenses[input.ExpenseId]; + if (expense == null) + { + return new Empty(); // Handle case if expense doesn't exist + } + expense.Description = input.Description ?? expense.Description; + expense.Category = input.Category ?? expense.Category; + expense.Amount = input.Amount != 0 ? input.Amount : expense.Amount; // Now using int64 for amount + expense.Currency = input.Currency ?? expense.Currency; + expense.UpdatedAt = Context.CurrentBlockTime.Seconds; + + State.Expenses[input.ExpenseId] = expense; + return new Empty(); + } + + public override Empty DeleteExpense(StringValue input) + { + State.Expenses.Remove(input.Value); + State.ExpenseExistence.Remove(input.Value); + + var existingExpenseIds = State.ExpenseIds.Value.Split(','); + var newExpenseIds = new List(existingExpenseIds.Length); + foreach (var expenseId in existingExpenseIds) + { + if (expenseId != input.Value) + { + newExpenseIds.Add(expenseId); + } + } + State.ExpenseIds.Value = string.Join(",", newExpenseIds); + + return new Empty(); + } + + public override ExpenseList ListExpenses(StringValue input) + { + var owner = input.Value; // Get the owner value from the input + var expenseList = new ExpenseList(); + var expenseIds = State.ExpenseIds.Value.Split(','); + foreach (var expenseId in expenseIds) + { + var expense = State.Expenses[expenseId]; + if (expense != null && expense.Owner == owner) // Filter expenses by owner + { + expenseList.Expenses.Add(expense); + } + } + return expenseList; + } + + public override Expense GetExpense(StringValue input) + { + var expense = State.Expenses[input.Value]; + if (expense == null) + { + return new Expense { ExpenseId = input.Value, Description = "Expense not found." }; + } + return expense; + } + + public override BoolValue GetInitialStatus(Empty input) + { + return new BoolValue { Value = State.Initialized.Value }; + } + } +} +``` + +## Step 2 - Building Smart Contract + +Build the new code by clicking the Build button. + +## Step 3 - Deploy Smart Contract + +Deploy the new code by clicking the Deploy button. + +## 🎯 Conclusion + +πŸŽ‰ Congratulations on completing the **Expense Tracker dApp** tutorial! πŸŽ‰ You've accomplished critical milestones, from setting up your development environment to creating, deploying, and interacting with your Expense Tracker smart contract on the aelf blockchain. 🌟 + +**πŸ“š What You've Learned** + +Throughout this tutorial, you've gained hands-on experience with: + +- **πŸ› οΈ Setting Up Your Development Environment:** You successfully installed and configured all necessary tools to start developing smart contracts on the aelf blockchain. + +- **πŸ’» Developing Your Smart Contract:** You crafted the essential logic for the Expense Tracker, building a contract to manage tasks like adding, updating, and deleting expenses while keeping a record of all transactions. + +- **πŸš€ Deploying the Smart Contract:** You deployed your Expense Tracker contract on the aelf blockchain, ensuring it was live and operational for real-world use. + +**πŸ” Final Output** + +By now, you should have: + +- πŸ“œ A deployed smart contract that governs the logic of managing expenses and storing financial data on the blockchain. + +**➑️ What's Next?** + +Now that you've built the foundation of your Expense Tracker dApp, consider extending it with advanced features: + +- **πŸ”’ Enhance Security:** Strengthen your dApp by applying smart contract security best practices to ensure that users' financial data remains private and secure. + +- **🌍 Exploring Cross-Chain Capabilities:** Explore aelf’s cross-chain capabilities to integrate your dApp with other blockchain networks and allow users to manage their finances across multiple chains. + +You've taken a significant step toward mastering blockchain development with your Expense Tracker dApp. Now, you’re ready to continue innovating and expanding your decentralized applications on the aelf platform. πŸš€ + +Happy coding and expanding your **Expense Tracker dApp!** 😊 diff --git a/src/tutorials/role.mdx b/src/tutorials/role.mdx new file mode 100644 index 0000000..9c8becf --- /dev/null +++ b/src/tutorials/role.mdx @@ -0,0 +1,210 @@ +import GenerateTemplate from '@/components/tutorial/generate-template'; +import { Button } from "@/components/ui/button"; + +# Allowance (Role Contract) + +**Description**: This project demonstrates the integration of two smart contracts, RoleContract and AllowanceContract, focusing on role-based access and fund management. It highlights **inter-contract calls**, where the allowance logic depends on roles retrieved dynamically from the role contract. + +**Purpose**: The purpose of this dApp is to teach state management, access control, and inter smart contract calls on the aelf blockchain. This example models how multi-contract systems work together and call each other to securely manage roles and funds in a blockchain environment. + +**Difficulty Level**: Moderate + +## Step 1 - Develop Role Smart Contract + +### Adding Your Smart Contract Code + +First, generate the template: . + +Now that we have a RoleContract template, we can customize it to implement role-based permissions for various use cases. Below are the core functionalities of the RoleContract: + +1. Set Admin: Allows the current admin to assign a new admin address. +2. Set Parent/Child: Grants parent or child roles with appropriate permissions. +3. Retrieve Role Addresses: Methods to fetch the current admin, parent, and child addresses. +4. Role-based Access Control: Ensures only admins or parents can assign roles using access validation logic. + +This template provides a foundation for building secure systems where role management and hierarchical permissions are essential. + +#### Defining Methods and Messages + +The implementation of file `src/Protobuf/contract/role_contract.proto` is as follows: + +```csharp title="role_contract.proto" +syntax = "proto3"; + +import "aelf/core.proto"; + +import "google/protobuf/empty.proto"; +import "Protobuf/reference/acs12.proto"; +import "aelf/options.proto"; +import "google/protobuf/wrappers.proto"; + +// The namespace of this class +option csharp_namespace = "AElf.Contracts.RoleContract"; + +service RoleContract { + + // The name of the state class the smart contract is going to use to access blockchain state + option (aelf.csharp_state) = "AElf.Contracts.RoleContract.RoleContractState"; + option (aelf.base) = "Protobuf/reference/acs12.proto"; + + rpc Initialize (google.protobuf.Empty) returns (google.protobuf.Empty){ + + } + + rpc SetAdmin (aelf.Address) returns (google.protobuf.Empty) { + } + + rpc GetAdmin (google.protobuf.Empty) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + + rpc SetParent (aelf.Address) returns (google.protobuf.Empty) { + } + + rpc GetParent (google.protobuf.Empty) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + + rpc SetChild (aelf.Address) returns (google.protobuf.Empty) { + } + + rpc GetChild (google.protobuf.Empty) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + +} +``` + +- `rpc` methods define the callable functions within the contract, allowing external systems to interact with the contract's logic. +- `message` represent the structured data exchanged between the contract and external systems. + +#### Define Contract States + +The implementation of the role contract's state inside file `src/RoleContractState.cs` is as follows: + +```csharp title="src/RoleContractState.cs" +using AElf.Sdk.CSharp.State; +using AElf.Types; + +namespace AElf.Contracts.RoleContract +{ + public class RoleContractState : ContractState + { + public BoolState Initialized { get; set; } + + public SingletonState
AdminAddress { get; set; } + + public SingletonState
ParentAddress { get; set; } + + public SingletonState
ChildAddress { get; set; } + + } +} +``` + +- The `State.cs` file in an aelf blockchain smart contract holds the variables that store the contract's data, making sure this data is saved and accessible whenever the contract needs it. + +#### Implement Role Smart Contract + +The implementation of the role contract inside file `src/RoleContract.cs` is as follows: + +```csharp title="src/RoleContract.cs" +using AElf.Sdk.CSharp; +using AElf.Types; +using Google.Protobuf.WellKnownTypes; + +namespace AElf.Contracts.RoleContract +{ + public class RoleContract : RoleContractContainer.RoleContractBase + { + + private const string DefaultAdmin = "ENTER_YOUR_PORTKEY_ADDRESS"; + + public override Empty Initialize(Empty input) + { + if (State.Initialized.Value) + { + return new Empty(); + } + State.Initialized.Value = true; + State.AdminAddress.Value = Context.Sender; //Can set Deployer as admin + State.AdminAddress.Value = Address.FromBase58(DefaultAdmin); // Can set YOUR_PORTKEY_ADDRESS as admin + return new Empty(); + } + + public override Empty SetAdmin(Address input) + { + AssertIsAdmin(); + + // Set the new admin address + State.AdminAddress.Value = input; + + return new Empty(); + } + + public override Empty SetParent(Address input) + { + AssertIsAdminOrParent(); + + // Set the parent address + State.ParentAddress.Value = input; + + return new Empty(); + } + + public override Empty SetChild(Address input) + { + AssertIsAdminOrParent(); + + // Set the chlid address + State.ChildAddress.Value = input; + + return new Empty(); + } + + + // Get the current admin address + public override StringValue GetAdmin(Empty input) + { + return State.AdminAddress.Value == null ? new StringValue() : new StringValue {Value = State.AdminAddress.Value.ToBase58()}; + } + + // Get the current parent address + public override StringValue GetParent(Empty input) + { + return State.ParentAddress.Value == null ? new StringValue() : new StringValue {Value = State.ParentAddress.Value.ToBase58()}; + } + + // Get the current child address + public override StringValue GetChild(Empty input) + { + return State.ChildAddress.Value == null ? new StringValue() : new StringValue {Value = State.ChildAddress.Value.ToBase58()}; + } + + + private void AssertIsAdmin() + { + Assert(Context.Sender == State.AdminAddress.Value, "Unauthorized(Not Admin) to perform the action."); + } + + private void AssertIsAdminOrParent() + { + Assert(Context.Sender == State.AdminAddress.Value || Context.Sender == State.ParentAddress.Value, "Unauthorized (Not Parent or Admin) to perform the action."); + } + + + } +} +``` + +## Step 2 - Building Smart Contract + +Build the new code by clicking the Build button. + +## Step 3 - Deploy Smart Contract + +Deploy the new code by clicking the Deploy button. + +Now it's time to create Allowance contract and use Role contract inside it so let's do that in next step : + + \ No newline at end of file diff --git a/src/tutorials/single-pool-staking.mdx b/src/tutorials/single-pool-staking.mdx new file mode 100644 index 0000000..e494024 --- /dev/null +++ b/src/tutorials/single-pool-staking.mdx @@ -0,0 +1,341 @@ +import GenerateTemplate from '@/components/tutorial/generate-template'; + +# Single Pool Staking + +**Description**: The Single Pool Staking dApp is a decentralized application built on the aelf blockchain that allows users to stake their tokens in a single staking pool. Users can earn rewards based on the amount and duration of their staked assets, with staking and reward distribution processes fully automated and secured by blockchain technology. The dApp offers a transparent and simple interface for users to monitor their staked assets and track reward accumulation over time. + +**Purpose**: The Single Pool Staking dApp aims to demonstrate the seamless integration of staking mechanisms with blockchain, providing users with a secure, transparent, and efficient way to grow their holdings. It serves as an educational tool for learning about staking contracts and their role in decentralized finance (DeFi), while showcasing the potential of blockchain technology for creating decentralized financial services that offer fairness and trustless reward distribution. + +**Difficulty Level**: Moderate + +## Step 1 - Develop Smart Contract + +### Adding Your Smart Contract Code + +First, generate the template: + +Now that we have a template Single Pool Staking project, we can customise the template to incorporate our own contract logic. Let's start by implementing methods to handle the basic functionality like deposit tokens to the staking pool, withdraw tokens from the staking pool, withdrawing tokens before the lock(stake) period ends (forceWithdraw), get the reward amount for an address from the pool, fetch all the deposits linked to a user and retrieve the total staked amount in the contract. Single Pool Staking dApp includes the below functionalities like: + +1. **Deposit**: Allows users to stake tokens, update the total staked amount and the deposit gets linked to the user. +2. **Withdraw**: Allows users to withdraw tokens and rewards after the lock period ends. +3. **ForceWithdraw**: Allows users to withdraw tokens before the lock period ends without rewards. +4. **GetReward**: Retrieves the reward amount earned from a specific deposit. +5. **GetDeposits**: Lists all deposits linked to a user. +6. **GetTotalStakedAmount**: Retrieves the total staked amount + +#### Defining Methods and Messages + +The implementation of `single_pool_staking.proto` file inside folder `src/Protobuf/contract/` is as follows: + +```csharp title="single_pool_staking.proto" +syntax = "proto3"; + +import "aelf/core.proto"; +import "aelf/options.proto"; +import "google/protobuf/empty.proto"; +import "Protobuf/reference/acs12.proto"; +import "google/protobuf/wrappers.proto"; + +option csharp_namespace = "AElf.Contracts.StakingContract"; + +service StakingContract { + option (aelf.csharp_state) = "AElf.Contracts.StakingContract.StakingContractState"; + option (aelf.base) = "Protobuf/reference/acs12.proto"; + + rpc Initialize (InitializeInput) returns (google.protobuf.Empty); + rpc Deposit (DepositInput) returns (google.protobuf.StringValue); + rpc Withdraw (WithdrawInput) returns (google.protobuf.Empty); + rpc ForceWithdraw (google.protobuf.StringValue) returns (google.protobuf.Empty); + + rpc GetReward (google.protobuf.StringValue) returns (google.protobuf.Int64Value) { + option (aelf.is_view) = true; + } + rpc GetDeposits (google.protobuf.StringValue) returns (DepositList) { + option (aelf.is_view) = true; + } + + // New functions + rpc IfInitialized (google.protobuf.Empty) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } + rpc GetTotalStakedAmount (google.protobuf.Empty) returns (google.protobuf.Int64Value) { + option (aelf.is_view) = true; + } +} + +message DepositInput { + string token_symbol = 1; + int64 amount = 2; + int64 lock_time = 3; +} + +message InitializeInput { + aelf.Address token_contract_address = 1; +} + +message WithdrawInput { + string deposit_id = 1; +} + +message TransferInput { + aelf.Address to = 1; + string symbol = 2; + int64 amount = 3; + string memo = 4; // Add this field +} + +message StringList { + repeated string values = 1; +} + +message Deposit { + string deposit_id = 1; + string address = 2; + string token_symbol = 3; // The specific FT token symbol + int64 amount = 4; + int64 lock_time = 5; + int64 deposit_time = 6; +} + +message DepositList { + repeated Deposit deposits = 1; +} +``` + +- `rpc` methods define the callable functions within the contract, allowing external systems to interact with the contract's logic. +- `message` represent the structured data exchanged between the contract and external systems. + +#### Define Contract States + +The implementation of the Single Pool Staking smart contract state inside file `src/SinglePoolStakingState.cs` is as follows: + +```csharp title="src/SinglePoolStakingState.cs" +using AElf.Sdk.CSharp.State; +using AElf.Types; +using AElf.Contracts.MultiToken; + +namespace AElf.Contracts.StakingContract +{ + public class StakingContractState : ContractState + { + public BoolState Initialized { get; set; } + public SingletonState
Owner { get; set; } + public MappedState Deposits { get; set; } // Mapping from deposit ID to Deposit + public MappedState UserDeposits { get; set; } // User to deposit IDs + public Int32State DepositCounter { get; set; } + public Int64State TotalStakedAmount { get; set; } // New state to track total staked amount + + internal TokenContractContainer.TokenContractReferenceState TokenContract { get; set; } + } +} +``` + +- The `State.cs` file in an aelf blockchain smart contract holds the variables that store the contract's data, making sure this data is saved and accessible whenever the contract needs it. + +#### Implement Single Pool Staking Smart Contract + +The implementation of the Single Pool Staking smart contract inside file `src/SinglePoolStaking.cs` is as follows: + +```csharp title="src/SinglePoolStaking.cs" +using Google.Protobuf.WellKnownTypes; +using AElf.Types; +using System.Collections.Generic; +using AElf.Contracts.MultiToken; + + +namespace AElf.Contracts.StakingContract +{ + public class StakingContract : StakingContractContainer.StakingContractBase + { + private const int RewardRate = 10; // 10% reward + + public override Empty Initialize(InitializeInput input) + { + if (State.Initialized.Value) + return new Empty(); + + State.Initialized.Value = true; + State.Owner.Value = Context.Sender; + State.DepositCounter.Value = 0; + State.TotalStakedAmount.Value = 0; // Initialize total staked amount + + State.TokenContract.Value = input.TokenContractAddress; + + return new Empty(); + } + + public override StringValue Deposit(DepositInput input) + { + var depositId = (State.DepositCounter.Value + 1).ToString(); + State.DepositCounter.Value++; + + var deposit = new Deposit + { + DepositId = depositId, + Address = Context.Sender.ToString(), + TokenSymbol = input.TokenSymbol, + Amount = input.Amount, + LockTime = input.LockTime, + DepositTime = Context.CurrentBlockTime.Seconds + }; + + State.Deposits[depositId] = deposit; + + var userDeposits = State.UserDeposits[Context.Sender] ?? new StringList(); + userDeposits.Values.Add(depositId); + + State.UserDeposits[Context.Sender] = userDeposits; + + State.TotalStakedAmount.Value += input.Amount; // Update total staked amount + + return new StringValue { Value = depositId }; + } + + public override Empty Withdraw(WithdrawInput input) + { + var deposit = State.Deposits[input.DepositId]; + Assert(deposit != null, "Deposit not found."); + Assert(deposit.Address == Context.Sender.ToString(), "Unauthorized."); + Assert(Context.CurrentBlockTime.Seconds >= deposit.DepositTime + deposit.LockTime, "Lock period not over."); + + var reward = CalculateReward(deposit.Amount); + + TransferFromContract(deposit.TokenSymbol, Context.Sender, deposit.Amount + reward); + + State.TotalStakedAmount.Value -= deposit.Amount; // Update total staked amount + + RemoveDeposit(deposit.DepositId); + return new Empty(); + } + + public override Empty ForceWithdraw(StringValue input) + { + var deposit = State.Deposits[input.Value]; + Assert(deposit != null, "Deposit not found."); + Assert(deposit.Address == Context.Sender.ToString(), "Unauthorized."); + + TransferFromContract(deposit.TokenSymbol, Context.Sender, deposit.Amount); + + State.TotalStakedAmount.Value -= deposit.Amount; // Update total staked amount + + RemoveDeposit(deposit.DepositId); + return new Empty(); + } + + public override Int64Value GetReward(StringValue input) + { + var deposit = State.Deposits[input.Value]; + Assert(deposit != null, "Deposit not found."); + return new Int64Value { Value = CalculateReward(deposit.Amount) }; + } + + public override DepositList GetDeposits(StringValue input) + { + var deposits = State.UserDeposits[Address.FromBase58(input.Value)]; + var depositList = new DepositList(); + + foreach (var depositId in deposits.Values) + { + var deposit = State.Deposits[depositId]; + if (deposit != null) + { + depositList.Deposits.Add(deposit); + } + } + + return depositList; + } + + // New function to check if initialized + public override BoolValue IfInitialized(Empty input) + { + return new BoolValue { Value = State.Initialized.Value }; + } + + // New function to get the total staked amount + public override Int64Value GetTotalStakedAmount(Empty input) + { + return new Int64Value { Value = State.TotalStakedAmount.Value }; + } + + private long CalculateReward(long amount) + { + return (amount * RewardRate) / 100; + } + + private void TransferFromContract(string symbol, Address to, long amount) + { + var virtualAddressHash = GetVirtualAddressHash(Context.Self, symbol); + + State.TokenContract.TransferFrom.Send( + new TransferFromInput + { + Symbol = symbol, + Amount = amount, + From = Context.Self, + Memo = "Transfer from Staking Contract", + To = to + }); + } + + private static Hash GetVirtualAddressHash(Address contractAddress, string symbol) + { + return HashHelper.ConcatAndCompute(HashHelper.ComputeFrom(contractAddress), HashHelper.ComputeFrom(symbol)); + } + + private Address GetVirtualAddress(Hash virtualAddressHash) + { + return Context.ConvertVirtualAddressToContractAddress(virtualAddressHash); + } + + private void RemoveDeposit(string depositId) + { + var deposit = State.Deposits[depositId]; + State.Deposits.Remove(depositId); + + var userDeposits = State.UserDeposits[Address.FromBase58(deposit.Address)]; + userDeposits.Values.Remove(depositId); + + State.UserDeposits[Address.FromBase58(deposit.Address)] = userDeposits; + } + } +} +``` + +## Step 2 - Building Smart Contract + +Build the new code by clicking the Build button. + +## Step 3 - Deploy Smart Contract + +Deploy the new code by clicking the Deploy button. + +## 🎯 Conclusion + +🎊 Congratulations on setting up your development environment and interacting with both the multi-token and staking smart contracts! 🎊 You've now built a solid foundation for handling advanced token operations and staking functionalities on the aelf blockchain. 🌟 + +**πŸ“š What You've Learned** + +Throughout this section, you've gained vital skills in: + + - **πŸ’» Developing Your Smart Contract:** You built the core logic of your Single Pool Staking, writing and compiling the smart contract. + +**πŸ” Final Output** + +By now, you should have: + + - πŸ“œ Successfully built smart contract of Single Pool Staking. + +**➑️ What's Next?** + +With a comprehensive understanding of token creation, staking, and contract interaction, you're prepared to explore further aspects of blockchain development. Consider diving into: + + - **πŸ“Š Advanced Smart Contract Logic**: Add more complex features and security to your contracts. + + - **Enhanced Staking Protocols**: Learn about advanced staking mechanisms and rewards structures to elevate your dApp. + + - **🌐 Cross-Chain Interoperability**: Explore how aelf’s cross-chain capabilities enable seamless communication between different blockchains. + +Keep experimenting and innovating with aelf! Your journey into decentralized finance and blockchain development is just getting started. πŸš€ + +Happy coding and building on the aelf blockchain! 😊 diff --git a/src/tutorials/tic-tac-toe.mdx b/src/tutorials/tic-tac-toe.mdx new file mode 100644 index 0000000..86e2c63 --- /dev/null +++ b/src/tutorials/tic-tac-toe.mdx @@ -0,0 +1,315 @@ +import GenerateTemplate from '@/components/tutorial/generate-template'; + +# Tic Tac Toe + +**Description**: The Tic-Tac-Toe dApp is a decentralized gamify application that allows users to play the classic game of Tic-Tac-Toe on the aelf blockchain. It offers a simple and interactive interface where two players can compete against each other, with game data securely stored and verified on the aelf blockchain. + +**Purpose**: The purpose of the Tic-Tac-Toe gamify dApp is to demonstrate how traditional games can be implemented on the aelf blockchain, ensuring transparency and immutability of game outcomes. It serves as an educational tool for learning smart contract development and the basics of decentralized application functionality. + +**Difficulty Level**: Moderate + +## Step 1 - Develop Smart Contract + +### Adding Your Smart Contract Code + +First, generate the template: + +#### Defining Methods and Messages + +The implementation of `tic_tac_toe.proto` file inside folder `src/Protobuf/contract/` is as follows: + +```csharp title="tic_tac_toe.proto" +syntax = "proto3"; + +import "aelf/options.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; +import "Protobuf/reference/acs12.proto"; +// The namespace of this class +option csharp_namespace = "AElf.Contracts.TicTacToe"; + +service TicTacToe { + // The name of the state class the smart contract is going to use to access blockchain state + option (aelf.csharp_state) = "AElf.Contracts.TicTacToe.TicTacToeState"; + option (aelf.base) = "Protobuf/reference/acs12.proto"; + + rpc Initialize (google.protobuf.Empty) returns (google.protobuf.Empty) {} + + rpc StartGame (google.protobuf.Empty) returns (google.protobuf.StringValue) {} + + rpc MakeMove (MoveInput) returns (google.protobuf.StringValue) {} + + rpc GetBoard (google.protobuf.Empty) returns (Board) { + option (aelf.is_view) = true; + } + + rpc GetGameStatus (google.protobuf.Empty) returns (GameStatus) { + option (aelf.is_view) = true; + } + rpc GetInitialStatus(google.protobuf.Empty) returns(google.protobuf.BoolValue){ + option (aelf.is_view) = true; + } +} + +// Input for making a move +message MoveInput { + int32 x = 1; + int32 y = 2; +} + +// A message to represent the game board +message Board { + repeated string rows = 1; +} + +// A message to represent the game status +message GameStatus { + string status = 1; + string winner = 2; +} +``` + +- `rpc` methods define the callable functions within the contract, allowing external systems to interact with the contract's logic. +- `message` represent the structured data exchanged between the contract and external systems. + +#### Define Contract States + +The implementation of the Tic Tac Toe app state inside file `src/TicTacToeState.cs` is as follows: + +```csharp title="src/TicTacToeState.cs" +using AElf.Sdk.CSharp.State; +using AElf.Types; + +namespace AElf.Contracts.TicTacToe +{ + // The state class is access the blockchain state + public partial class TicTacToeState : ContractState + { + // A state to check if contract is initialized + public BoolState Initialized { get; set; } + public SingletonState
Owner { get; set; } + public StringState Board { get; set; } // Board state as a concatenated string + public StringState CurrentPlayer { get; set; } // X or O + public StringState GameStatus { get; set; } // ongoing, finished, draw + public StringState Winner { get; set; } // X or O + } +} +``` + +- The `State.cs` file in an aelf blockchain smart contract holds the variables that store the contract's data, making sure this data is saved and accessible whenever the contract needs it. + +#### Implement Tic Tac Toe Smart Contract + +The implementation of the Tic Tac Toe App smart contract inside file `src/TicTacToe.cs` is as follows: + +```csharp title="src/TicTacToe.cs" +using Google.Protobuf.WellKnownTypes; +using System.Collections.Generic; + +namespace AElf.Contracts.TicTacToe +{ + // Contract class must inherit the base class generated from the proto file + public class TicTacToe : TicTacToeContainer.TicTacToeBase + { + private const int BoardSize = 3; + + public override Empty Initialize(Empty input) + { + if (State.Initialized.Value) + { + return new Empty(); + } + State.Initialized.Value = true; + State.Owner.Value = Context.Sender; + ResetBoard(); + return new Empty(); + } + + public override StringValue StartGame(Empty input) + { + if (!State.Initialized.Value) + { + return new StringValue { Value = "Contract not initialized." }; + } + + ResetBoard(); + State.CurrentPlayer.Value = "X"; + State.GameStatus.Value = "ongoing"; + State.Winner.Value = ""; + return new StringValue { Value = "Game started. Player X's turn." }; + } + + public override StringValue MakeMove(MoveInput input) + { + if (State.GameStatus.Value != "ongoing") + { + return new StringValue { Value = "Game is not ongoing." }; + } + + var board = GetBoardArray(); + if (board[input.X, input.Y] != "") + { + return new StringValue { Value = "Invalid move. Cell is already occupied." }; + } + + board[input.X, input.Y] = State.CurrentPlayer.Value; + SaveBoard(board); + + if (CheckWinner()) + { + State.GameStatus.Value = "finished"; + State.Winner.Value = State.CurrentPlayer.Value; + return new StringValue { Value = $"Player {State.CurrentPlayer.Value} wins!" }; + } + + if (IsBoardFull(board)) + { + State.GameStatus.Value = "draw"; + return new StringValue { Value = "It's a draw!" }; + } + + State.CurrentPlayer.Value = State.CurrentPlayer.Value == "X" ? "O" : "X"; + return new StringValue { Value = $"Player {State.CurrentPlayer.Value}'s turn." }; + } + + public override Board GetBoard(Empty input) + { + var board = GetBoardArray(); + var boardMessage = new Board(); + + for (var i = 0; i < 3; i++) // Adjusted to 3 for a 3x3 Tic-Tac-Toe board + { + var row = new List(); + for (var j = 0; j < 3; j++) + { + row.Add(board[i, j]); + } + boardMessage.Rows.Add(string.Join(",", row)); + } + + return boardMessage; + } + + public override GameStatus GetGameStatus(Empty input) + { + return new GameStatus + { + Status = State.GameStatus.Value, + Winner = State.Winner.Value + }; + } + + public override BoolValue GetInitialStatus(Empty input){ + return new BoolValue { Value = State.Initialized.Value }; + } + + private void ResetBoard() + { + var emptyBoard = new string[BoardSize, BoardSize]; + for (var i = 0; i < BoardSize; i++) + { + for (var j = 0; j < BoardSize; j++) + { + emptyBoard[i, j] = ""; + } + } + SaveBoard(emptyBoard); + } + + private string[,] GetBoardArray() + { + var boardString = State.Board.Value; + var rows = boardString.Split(';'); + var board = new string[BoardSize, BoardSize]; + for (var i = 0; i < BoardSize; i++) + { + var cells = rows[i].Split(','); + for (var j = 0; j < BoardSize; j++) + { + board[i, j] = cells[j]; + } + } + return board; + } + + private void SaveBoard(string[,] board) + { + var rows = new string[BoardSize]; + for (var i = 0; i < BoardSize; i++) + { + rows[i] = string.Join(",", board[i, 0], board[i, 1], board[i, 2]); + } + State.Board.Value = string.Join(";", rows); + } + + private bool CheckWinner() + { + var board = GetBoardArray(); + var player = State.CurrentPlayer.Value; + + for (var i = 0; i < BoardSize; i++) + { + if (board[i, 0] == player && board[i, 1] == player && board[i, 2] == player) return true; + if (board[0, i] == player && board[1, i] == player && board[2, i] == player) return true; + } + + if (board[0, 0] == player && board[1, 1] == player && board[2, 2] == player) return true; + if (board[0, 2] == player && board[1, 1] == player && board[2, 0] == player) return true; + + return false; + } + + private bool IsBoardFull(string[,] board) + { + for (var i = 0; i < BoardSize; i++) + { + for (var j = 0; j < BoardSize; j++) + { + if (board[i, j] == "") return false; + } + } + return true; + } + } +} +``` + +## Step 2 - Building Smart Contract + +Build the new code by clicking the Build button. + +## Step 3 - Deploy Smart Contract + +Deploy the new code by clicking the Deploy button. + +## 🎯 Conclusion + +πŸŽ‰ Congratulations on successfully completing the **Tic Tac Toe dApp** tutorial! πŸŽ‰ You've achieved significant milestones, from setting up your development environment to deploying and interacting with your Tic-Tac-Toe smart contract on the aelf blockchain. 🌟 + +**πŸ“š What You've Learned** + +Throughout this tutorial, you've mastered: + + - **πŸ› οΈ Setting Up Your Development Environment:** You equipped your workspace by installing and configuring all the necessary tools to get your smart contract project off the ground. + + - **πŸ’» Developing Your Smart Contract:** You built the core logic of your Tic Tac Toe game, writing and compiling the smart contract that manages game states, moves, and outcomes. + +**πŸ” Final Output** + +By now, you should have: + + - πŸ“œ A deployed Tic-Tac-Toe smart contract that governs the game's rules and manages players' moves on the blockchain. + +**➑️ What's Next?** + +With the foundation laid, consider advancing your Tic-Tac-Toe dApp with more sophisticated features: + + - **πŸ“ˆ Enhancing Game Logic:** Add more features like AI opponents, multiplayer functionality, or scoring systems to make the game more engaging. + + - **πŸ”’ Improving Security:** Secure your game by applying best practices in smart contract security to protect users' data and gameplay integrity. + + - **🌍 Exploring Cross-Chain Capabilities:** Expand your dApp’s reach by exploring aelf’s cross-chain interoperability, enabling interactions with other blockchains. + +Blockchain technology and decentralized applications offer limitless possibilities. With your Tic-Tac-Toe dApp, you're now poised to continue innovating and exploring new horizons with aelf. πŸš€ + +Happy coding and expanding your **Tic-Tac-Toe dApp!** 😊