Solidity
February 12, 2023

Solidity  | Библиотеки, string methods

Привет, сегодня пойдет речь о библиотеках и их применение. Напишем свои методы для строки. А именно перевод из строки в число и перевод из числа в строку.

Начну с того, что библиотеки нужны для оптимизации смарт-контрактов, и использовании каких то популярных методов для разных смарт-контрактов (чтоб их заново не переписывать и опять же не тратить газ на их реализацию)

Рассмотрим пример библиотеки:

Так как в solidity очень плохо реализованы строки, то мы будем писать методы именно для них.

Первая функция будет str2uint - этот метод будет переводить строку в положительное число.

Как записываются библиотеки?

Создаем новый файл расширения .sol, как в контрактах( у меня будет conver.sol)

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

library convert {

}

В целом запись как в интерфейсах или контрактах, но ключевое слово library.

Напишем первую функцию.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

library convert {
    function str2uint(string memory str) public pure returns(uint){  
    
        uint len = bytes(str).length;        
        uint num = 0;        
        bytes memory str1 = bytes(str);  
                      
        for(uint i = 0; i < len;i ++){       
       
            num = num * 10 + (uint8(str1[i]) - 48); 
                        
        }                
        return num;        
    }
}

Что мы тут делаем? Так как у нас нет такой функции, которая взяла и перевела бы строку в число, например как в питоне int(str), то нам нужно самим придумать и написать эту функцию.

Вся работа происходит через байты. Основное, что нужно знать это то, байты и строки дружат, а числа и байты нет.

Поэтому строка:

uint len = bytes(str).length; 

Мы можем конвертировать строку в байты и таким образом узнать длину строки.

В солидити нет реализации нахождения длины строки, поэтому такой метод можно тоже сделать.

Дальше объявляем num, куда мы и будем перезаписывать строку в число.

Ну и временную переменную str1, которую мы делаем байтовой.

Теперь мы можем идти по циклы for до длины строки и рассматривать каждый элемент строки, но в байтовой форме.

Дальше строка

num = num * 10 + (uint8(str1[i]) - 48); 

Что тут происходит: Мы берем строку умножаем на 10 и превращаем байты в uint. Тут очень важный момент: Такая запись возможно при условии что str1[i] является bytes1 и перевод только в uint8.

Почему умнадаем на 10?

Если у нас трока "123", и мы идем по каждой цифре по очереди. То есть:

num = 0, str[i] = 1 => 0 * 10 = 0 => 0 + 1 = 1
num = 1, str[i] = 2 => 1 * 10 = 10 => 10 + 2 = 12
num 12, str[i] = 3 => 12 * 10 = 120 => 120 + 3 = 123

И вот у нас получается наше число.

Почему же -48?

Если мы выведем строчку "123" в байтовом виде, то мы получим 0x313233. Есть такая таблица ASCII. Она нужно для кодировки символов для компьютера, если вкратце. И поэтому, например, 0x31 это шестнадцатеричная запись числа 1 в таблице ASCII. Когда мы говорим uint8(str1[i]), мы переводим в десятичную СС число один. И теперь магия: 0x31 в hex = 49 в dec. И теперь 49 - 48 = 1. Так работает со всеми цифрами. Вот такая не замысловатая система перевода через таблицу ASCII. Конечно это не единственный способ, но самый короткий и эстетичный.

Надеюсь понятно объяснил этот метод, потому что щас будем писать обратный метод uint2str, и тут будет немного по сложнее.

Напишем функцию uint2str:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library convert {    
    function uint2str(uint num) public pure returns (string memory) {    
        if (num == 0){            
            return "0";        
        }        
        string memory str = "";        
        uint num1 = num;        
        uint length;            
        while (num1 != 0){             
            length++;            
            num1 /= 10;        
        }                
        bytes memory b = new bytes(length);                
        while (num != 0){            
            b[--length] = bytes1(uint8(num % 10 + 48));            
            num /= 10;        
        }        
        str = string(b);        
        return str;    
    }
}

Что мы тут делаем:

Первый if нужен при условии если у нас число равно 0, если же нет, то мы создаем сроку куда будем перезаписывать наше число.
Дальше создаем временное число, с которым мы будем работать в начале.

Через while(num1 != 0) ищем длину строки, ведь мы будем переводить в строку и нам нужно знать длину этой строки. Дальше объясню зачем. Цикл while работает пока num1 не станет равен нулю и каждый круг мы прибавляем 1 в length. Есил у нас число 123, то length = 3.

Теперь важная строчка.

bytes memory b = new bytes(length);   

Тут мы создаем новую байтовую переменную равную длине числа. То есть, если у нас есть число 123, его длина равна 3, то bytes memory b = 0х000000.

Дальше мы идем по нашей строке через while(num != 0).

Что же мы делаем внутри вайла:

b[--length] = bytes1(uint8(num % 10 + 48)) Можно сказать это обратное действие предыдущему методу str2int.

Рассмотрим на примере числа 123:

length = 3, b[--3] == b[2] => b[2] = bytes1(uint8(123 % 10 + 48))
=> b[2] = bytes1(uint8(3+48)) => b[2] = bytes1(uint8(51)) => b[2] = 0x000033

num = 123 / 10 == 12

length = 2, b[--2] == b[1] => b[1] = bytes1(uint8(12 % 10 + 48))
=> b[1] = bytes1(uint8(2+48)) => b[1] = bytes1(uint8(50)) => b[1] = 0х003233

num = 12 / 10 == 1

length = 1, b[--1] == b[0] => b[0] = bytes1(uint8(1 % 10 + 48))
=> b[0] = bytes1(uint8(1+48)) => b[0] = bytes1(uint8(49)) => b[0] = 0х313233

Надеюсь понятно написано, если словами, то мы берем и с конце начинаем перезаполнять байтовый массив, созданный ранее. Идем с конца,
поэтому --length. Ну и таким образом мы получаем байтовую строчку нашего числа.

И помните в начале я сказал, что bytes дружит c string и не дружит с uint. Именно поэтому нам пришел делать такую сложную махинацию перевода.

Мы не могли сказать bytes b = bytes(num) и bytes len = bytes(num).length()

Но так как bytes дружит с string, то можем сделать просто str = string(b). И получить строку.

Весь код библиотеки:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library convert {    
    function uint2str(uint num) public pure returns (string memory) {    
        if (num == 0){            
            return "0";        
        }        
        string memory str = "";        
        uint num1 = num;        
        uint length;            
        while (num1 != 0){             
            length++;            
            num1 /= 10;        
        }                
        bytes memory b = new bytes(length);                
        while (num != 0){            
            b[--length] = bytes1(uint8(num % 10 + 48));            
            num /= 10;        
        }        
        str = string(b);        
        return str;    
    }
    function str2uint(string memory str) public pure returns(uint){  
    
        uint len = bytes(str).length;        
        uint num = 0;        
        bytes memory str1 = bytes(str);  
                      
        for(uint i = 0; i < len;i ++){       
       
            num = num * 10 + (uint8(str1[i]) - 48); 
                        
        }                
        return num;        
    }
}

Можно так же дописать в эти методы и для отрицательных чисел, но это можете доделать сами. Просто добавив условие на знак в начале.

Ух ну вроде все. Теперь как же нам подрубить эту библиотеку и воспользоваться нашими методами.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./convert.sol";

contract myGame{    

    using convert for string;    
    using convert for uint;
    
    function uint2str(uint a) public pure returns(string memory){        
        return a.uint2str();    
    }    
    
    function str2uint(string memory str) public pure returns(uint){ 
        return str.str2uint();    
    }       
    
}

Импортируем нашу библиотеку.

строчки:

using convert for string;    
using convert for uint;

Говорят нам, что используем нашу библиотеку для строк и чисел.

название convert должно совпадать с названием библиотеки.

Дальше 2 функции проверки работы методов из библиотеки.

Давайте проверим работу.

Перове мы деплоим нашу библиотеку convert.

И деплоим наш контракт new contract. И проверяем наши функции.

Как видно у нас есть 2 функции и нам ремикс подсказывает, что в первую функцию он ждет на вход строку и во вторую число. Давайте введем.

Теперь мы видим, что возвращают нам эти функции и их тип данных.

Первая функция вернула uint256 и вторая string. Все работает ура!

На этом все. Сегодня написали целых 2 метода для строки, которые не реализованы в solidity. Получается доработали язык ахаха. Дальше больше. Можно также сделать функцию сравнивания строк и поиск какого-то элемента из строки. Это все достаточно просто можно реализовать и использовать у себя в смарт контактах кучу методов для строки, которых не существует в solidity.

Кому интересно, то я буду доделывать свою строчку и все ее методы. Проект будет на github. Можете себе забрать и использовать.

tg: мой телеграмчик)

Этот проет на github (возможно еще не полностью дописанный)