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