Вовед во Blockchain програмирање Втор дел: Основи на Солидити

Во вториот дел од серијалот почнуваме директно со програмскиот јазик Solidity. 

Ќе претпоставам дека секој што го чита овој текст, веќе знае да програмира. Доколку не знаете да програмирате, Solidity воопшто не треба да ви е прв програмски јазик.

Smart Contracts наликуваат на класи како што сме сретнале во Java, C#, Python и слично. Од сега ќе го пишувам на скратено Smart Contract како SC. 

Секој SC се деплојува на блокчеин како посебна единка, односно секој си има своја посебна адреса, која си има свој посебен статус (state).

Доколку имате два SC ќе добиете две посебни блокчеин адреси, не може да имате апликација која ќе содржи повеќе контракти на иста адреса, такви апликации не постојат, но може да имате две инстанци од истиот SC што ќе бидат деплојувани на две различни блокчеин адреси и ќе бидат посебни единки, слично како 2 различни објекти од една иста класа во нормално програмирање.

Без да одолговлекувам повеќе, да погледнеме код:

Основна структура на еден Smart Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract HelloSolidity {
    uint private brojce;
    constructor(uint _brojce) {
        brojce = _brojce;
    }
    function setBrojce(uint novoBrojce) public {
        brojce = novoBrojce
    }
    function getBrojce() public returns(uint){
      return brojce;
    }
}

Лиценца: Првата линија е коментар, но е задолжителна, овде е дефинирана лиценцата на кодот. Обично сите се open-source лиценци со мали разлики но тоа не прави никаква разлика на извршувањето.

Solidity верзија: Веднаш потоа ја специфицираме верзијата на Solidity со која е компajлиран нашиот SC. Ова е важно бидејќи може да постојат големи разлики помеѓу верзиите на Солидити и често има промени што се breaking односно стариот код не работи на новите верзии.

Структура на SC: Дефиницијата на SC почнува со contract наместо со class како што сме навикнати во другите јазици. 

Останатото изгледа доста познато: дефиниција на променливите на почетокот, подолу конструктор за иницијализација на променливите и остатокот од функциите после конструкторот.

Во овој случај напишав смо две кратки функции за сетирање и читање на единствената променлива.

Видови променливи

Типовите на променливи може да се поделат во 3 главни категории.

Променливи со фиксна големина

bool a;
address b;
uint8 c;
bytes32 d;
  • Bool веќе знаеме, булова променлива, содржи true/false.
  • Address е променлива што се користи за чување на адреси на други Smart Contracts или надворешни корисници.
  • Uint8 е број кој е unsigned односно строго позитивен и е со големина 8 бајти што значи дека може да содржи максимална вредност од 255. Ќе објасниме подолу за броевите.
  • Bytes32 се бајти, што значи може да содржи било каков симбол или карактер, односно ова се користи чување текст или string, но со фиксна големина.

Променливи со динамична големина

string name;
bytes data;
uint[] numbers;
mapping(uint => string) users; 

String

String е за чување текст, еден од најкористените типови во другите програмски јазици, но не и овде. Бидејќи Solidity има многу ограничен сет на функции за манипулација со текст и бидејќи string може многу лесно да порасне во големина, што ќе ни го направи кодот многу скап за извршување, овој тип многу ретко се користи. За текст обично се користи низа од bytes32.

Bytes

Bytes е сличен на bytes32 но вака без број нема дефинирана должина што го прави повторно опасен за користење и затоа ретко се користи.

Array

Uint[], ова не е специфично за броеви туку само да го покажам типот на Array или листа. Листите се користат доста често, ќе погледнеме како во примери подоцна.

Mapping

Mapping е начинот на кој во Solidity претставуваме hashmaps, или dictionaries во Python. Ова се структури каде имаме пристап до елементите со индекс од различен тип. Ова е дефинитивно најкорисната структура во целиот јазик. На пример во сите токени, начинот на кој се чува балансот на корисниците е: mapping(address => uint256) balance;

За секоја адреса чуваме по еден број и тоа е тоа. Доколку ја знаеме адресата на некој корисник, можеме да му го прочитаме балансот. Едно ограничување е дека не постои начин на итерација низ клучевите на еден мапинг, и тоа е нормално бидејќи мапингот може да има неограничен број клучеви. Доколку е потребен начин за итерација се користи аrray во комбинација со мапинг, каде ги чуваме само клучевите.

Броеви во Solidity

Променливата од тип uint личи на integer, но не е баш исто.

Во блокчеин светот, секоја логика што треба да се изврши чини пари, и затоа е битно да се оптимизира кодот во поглед на логика, но и во поглед на меморија.

Со таа цел постојат два типа на броеви, int и uint. Int е за броеви кои може да бидат позитивни и негативни, а uint е за строго позитивни броеви.

Исто така постојат сите овие големини: uint8, uint16, uint32, uint64, uint128, uint256.

Исто и за int: int8 … int256.

Најчесто се користат uint бидејќи ретко кој работи со негативни броеви.

Децимални броеви

Децимални броеви не постојат во Solidity воопшто, секое делење е цело делење. Затоа криптовалутите се претставуваат со 18 децимали, но тоа овде значи 18 екстра нули.

Пример: доколку поседувате 1 ETH (етереум), тој ќе биде претставен во Solidity и на самиот блокчеин како 1,000,000,000,000,000,000. Со самото ова добивате можност за многу поситно делење на една криптовалута во споредба со денари или долари кои имаат само две децимални места, па затоа и не е толку проблем што нема децимални броеви.

Променливи дефинирани од корисникот

struct User {
    uint id;
    string name;
    uint[] friendIds;
}

Типот Struct постои во C и во некои други јазици што не се објектно ориентирани и претставува начин на креирање ентитети или структури кои содржат група на променливи. Овие структури се многу флексибилни и се користат доста често.

enum Color {
    RED,
    GREEN,
    BLUE
}

Enum исто постои во C, но и скоро во секој друг јазик. Се користи за дефинирање на променлива која може да има вредности само од одредено ограничено множество.

На брзина поминавме низ општите типови на променливи, но се ова ќе стане појасно со практичните примери подоцна.

Внатрешни променливи

Внатрешни променливи се нешто специфично за Solidity. Кодот се извршува во блокчеин околина и овие променливи ни даваат информации за состојбата на блокчеинот.

msg.sender

Секое извршување на функција во блокчеин системот е повикано од некој надворешен корисник или од Smart Contract. Адресата на овој повикувач е достапна во променливата msg.sender.

msg.value

Исто така при повикување на функции, возможно е и да се прати ether (односно криптовалута или пари). Оваа вредност, колку ether е пратено, е достапна во msg.value. Притоа се зема во предвид дека доколку е пратен 1 ether, msg.value ќе ја содржи вредноста 1 со 18 нули т.е. 1000000000000000000.

Block.timestamp или now

Променливите now и block.timestamp се синоними, и ја даваат вредноста на моменталното време, во форма на timestamp. (Доколку не знаете што е timestamp, научете програмирање, потоа продолжете со текстот).

block.number

Ова ни покажува кој е моменталниот број на тековниот блок, во самиот блокчеин. Се користи доста често услов за извршување некој функција. На пример да оневозможите повлекување некои средства додека не поминат 100 блокови. Многу често се преферира да се користи block.number наместо block.timestamp бидејќи втората променлива може да биде манипулирана од miners до некој степен.

tx.origin 

Ова е исто корисна вредност, слична на msg.sender, но не баш иста. Овде се содржи адресата на надворешниот корисник што ја извршил трансакцијата. Можно е вие како надворешен корисник да извршите трансакција со повикување на функција на некој SC, но внатре во таа ф-ја, тој SC да повика друга ф-ја од друг SC. Во вториот SC, вредноста на msg.sender ќе бида адреста на првиот SC, а tx.origin ќе биде вашата адреса како оригиналниот извршител на трансакцијата.

За крај да погледнеме малку код за да ни се исполни душата.

Ќе ја погледнеме имплементацијата на ERC20 token, ова е стандардот кој се користи за скоро сите токени како USDT, USDC, DAI, DOGE, LINK и така натаму. Да ви покажам колку малку код е потребно за да се креира вистински токен кој може да содржи милиони парична вредност.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";

contract ERC20 is Context, IERC20, IERC20Metadata {
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    uint256 private _totalSupply;
    string private _name;
    string private _symbol;

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    function mint(address account, uint256 amount) internal virtual {
       require(account != address(0), "ERC20: mint to the zero address");

       _totalSupply += amount;
       _balances[account] += amount;

       emit Transfer(address(0), account, amount);
    }

    function transfer(
        address to,
        uint256 amount
    ) internal virtual {
        address from = msg.sender;
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        _balances[from] = fromBalance - amount;
        _balances[to] += amount;

        emit Transfer(from, to, amount);
        return true;
    }

    function burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        _balances[account] = accountBalance - amount;
        _totalSupply -= amount;

        emit Transfer(account, address(0), amount);
    }
}

На почетокот ги ставаме лиценцата и верзијата на компајлерот, а потоа го дефинираме контрактот. Во овој случај имаме наследување од повеќе контракти, ова се постигнува со листање на контрактите со запирка.

Веднаш потоа ја гледаме примената на мапинг. Овде имаме еден мапинг за чување на балансот, односно сопственоста на токените. Потоа имаме уште поинтересен случај на вгнездување на еден мапинг во друг каде чуваме allowance односно дозволено трошење од балансот на друг корисник.

Останатите променливи се едноставни, едната го означува totalSupply што го чува вкупниот број на токени во циркулација, а другите две се за име и симбол на токенот.

Во конструкторот само ги дефинираме името и симболот.

Функција Mint

Функцијата mint служи за креирање на нови токени. Логиката на оваа ф-ја е многу проста, се што правиме е го зголемуваме бројот на вкупно токени и потоа го зголемуваме балансот на адресата на корисникот кој треба да ги добие новите токени. Ова е буквално се што е потребно за да се креираат нови токени, можеме буквално да ги создадеме од воздухот, (слично како доларите).

На почетокот имаме еден require повик што е всушност исто како assert во другите програмски јазици и ни служи за да се погрижиме некои услови секогаш да се исполнети. Во овој случај сакам да сме сигурни дека новите токени нема да одат на адресата 0, бидејќи никој нема пристап до оваа адреса.

Функција Burn

Многу слично на ф-јата минт за креирање нови токени, оваа burn ф-ја служи за горење, односно за уништување на токени. Се што правиме овде е го намалуваме балансот на адресата од која ги уништуваме токените и го намалуваме вкупниот број на токени во циркулација. Претходно мора да провериме дека не уништуваме од адресата 0 и дека адресата од која уништуваме токени има доволно баланс.

Да напоменам дека во токените во вистинскиот свет, не може кој сака да ви ги уништи вашите токени ниту може да креира токени колку му душа сака. Обично ф-јата за креирање се повикува во конструкторот, при креирање на контрактот и никогаш повеќе. А ф-јата за уништување може да се повика само со вашата лична адреса, односно можете да ги уништите само вашите токени, но не туѓите.

Функција Transfer

Функцијата за трансфер е навидум комбинација од ф-иите за креирање и уништување. При трансфер на токени, балансот на испраќачот се намалува, а балансот на примачот се зголемува. Секако овде мора да провериме за адресата 0 и да провериме дали испраќачот има доволно токени за трансферот.

Со овој пример можевте да видете како со овие едноставни променливи и само неколку линии код успеавме да креираме вистински финансиски инструмент.

Во функциите погоре можете да видете зборови како view, virtual, override кои не сме ги објасниле и некои од нив ги нема во другите јазици. Функциите ќе бидат објаснети во наредната лекција.

Иако ова изгледа премногу едноставно, и како ништо посебно, магијата на блокчеин кодот е тоа што кодот е закон. Односно кога еднаш е напишан кодот, никој не може да го промени и сите се сигурни дека по овие правила ќе се извршува кодот, затоа сите му веруваат и се осмелуваат да инвестираат големи суми пари.

Добивај известувања
Извести ме за
guest
0 Коментари
Inline Feedbacks
View all comments