Разместить объявление
Rukassa - надёжная платёжная система для сайтов и телеграм ботов
хостинг
VIPAdmin / PREMIUM / cod / Ethereum Contract ABI Specification. Взаимодействие с контрактом

Ethereum Contract ABI Specification. Взаимодействие с контрактом

Ethereum Contract ABI Specification. Взаимодействие с контрактом


Создание контракта и взаимодействие с ним​
В данной статье я хочу познакомить вас с тем, как осуществляется кодирование данных в транзакции в соответствии с https://docs.soliditylang.org/en/v0.8.20/abi-spec.html. Мы вручную разберём весь процесс кодирования, создадим контракт и произведём вызов его методов. В конце я покажу как при помощи Contract ABI создать объект-оболочку через web3.js, и через него вызывать методы контракта.

План
  1. Настройка окружения
  2. Создание контракта
  3. Взаимодействие с контрактом
  4. Объект-оболочка над контрактом


Настройка окружения
Нам потребуются: компилятор Solidity, сам контракт, подключение к тестовой сети Sepolia и аккаунт с тестовыми Ether на балансе. Так же нам необходимо будет добавить приватный ключ этого аккаунта в Wallet библиотеки web3.js

Начнём с компилятора Solidity. Существуют разные способы установки компилятора Solidity, всё зависит от вашей ОС и каким способом вы хотите его установить: npm, Docker, Linux Packages и т.д. Как установить компилятор можно посмотреть https://docs.soliditylang.org/en/latest/installing-solidity.html
.
Создадим рабочий каталог, установим web3.js и добавим в него наш контракт. Версия web3.js на момент написания статьи 4.0.1

 $ mkdir raw-contract
$ cd raw-contract
$ npm install web3
$ nano Faucet.sol


Код контракта:
 // SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract Faucet {
// Accept any incoming amount
receive() external payable {}
<span class="hljs-comment" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(142, 144, 140); quotes: "«" "»";">// Give out ether to anyone who asks</span>
<span class="hljs-function" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; quotes: "«" "»";"><span class="hljs-keyword" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; font-weight: 700; color: rgb(137, 89, 168); quotes: "«" "»";">function</span> <span class="hljs-title" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(66, 113, 174); font-weight: 700; quotes: "«" "»";">withdraw</span>(<span class="hljs-params" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(245, 135, 31); quotes: "«" "»";">uint withdraw_amount</span>) <span class="hljs-title" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(66, 113, 174); font-weight: 700; quotes: "«" "»";">public</span> </span>{
<span class="hljs-comment" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(142, 144, 140); quotes: "«" "»";">// Limit withdrawal amount</span>
<span class="hljs-built_in" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(245, 135, 31); quotes: "«" "»";">require</span>(withdraw_amount <= <span class="hljs-number" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(245, 135, 31); quotes: "«" "»";">0.01</span> ether);
<span class="hljs-comment" style="transition: opacity 0.2s ease-in-out 0s, color 0.2s ease-in-out 0s, text-decoration 0.2s ease-in-out 0s, background-color 0.2s ease-in-out 0s, -webkit-text-decoration 0.2s ease-in-out 0s; color: rgb(142, 144, 140); quotes: "«" "»";">// Send the amount to the address that requested it</span>
payable(msg.sender).transfer(withdraw_amount);
}
}


Контракт я взял из своего примера, когда мы деплоили его в локальный блокчейн Ganache. Логика контракта позволяет зачислять Ether на его баланс, и даёт возможность каждому снять в свою пользу по 0.01 Ether за одну транзакцию.
На всякий случай проверим версию компилятора, она должна быть 0.8.x:

 $ solc --version
// out:
solc, the solidity compiler commandline interface
Version: 0.8.20+commit.a1b79de6.Darwin.appleclang

Теперь скомпилируем контракт и получим его бинарное представление:
 

$ solc --bin Faucet.sol
======= Faucet.sol:Faucet =======
Binary:
608060405234801561000f575f80fd5b506101468061001d5f395ff3fe608060405260043610610021575f3560e01c80632e1a7d4d1461002c57610028565b3661002857005b5f80fd5b348015610037575f80fd5b50610052600480360381019061004d91906100e5565b610054565b005b662386f26fc10000811115610067575f80fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156100aa573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b6100c4816100b2565b81146100ce575f80fd5b50565b5f813590506100df816100bb565b92915050565b5f602082840312156100fa576100f96100ae565b5b5f610107848285016100d1565b9150509291505056fea2646970667358221220c9fe2f78a923e108f0619a2d655b35d18297668335e649a10272c09a790a188964736f6c63430008140033


То что мы получили, это бинарное представление контракта, которое мы добавим в поле data транзакции и отправим в сеть Ethereum, после чего наш контракт будет задеплоен.
Подготовим подключение к сети Ethereum, для этого зайдем в консоль node.js:
$ node
Подключимся к тестовому блокчейну Sepolia:
 > const { Web3 } = require('web3');
> const web3 = new Web3('https://rpc2.sepolia.org');


Чтобы web3.js смог подписать нашу транзакцию, мы должны добавить приватный ключ аккаунта, с которого будем отправлять эту транзакцию. Передадим в метод add() приватный ключ:
 > web3.eth.accounts.wallet.add('0x0e...e3');


Вывод:
 Wallet(1) [
{
address: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47',
privateKey: '0x0e...e3',
signTransaction: [Function: signTransaction],
sign: [Function: sign],
encrypt: [Function: encrypt]
},
_accountProvider: {
create: [Function: createWithContext],
privateKeyToAccount: [Function: privateKeyToAccountWithContext],
decrypt: [Function: decryptWithContext]
},
_addressMap: Map(1) { '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47' => 0 },
_defaultKeyName: 'web3js_wallet'
]


Для отправки транзакции я воспользуюсь своим существующим аккаунтом и тестовыми Ether на балансе. Если у вас нет своего аккаунта, то вы можете с лёгкостью его создать одной командой, и пополнить тестовыми Ether.
У нас всё готово для отправки транзакции на создание контракта.

Создание контракта
Добавим префикс 0x к началу кода контракта и поместим его в переменную:
 > var contractCode = '0x608060405234801561000f575f80fd5b506101468061001d5f395ff3fe608060405260043610610021575f3560e01c80632e1a7d4d1461002c57610028565b3661002857005b5f80fd5b348015610037575f80fd5b50610052600480360381019061004d91906100e5565b610054565b005b662386f26fc10000811115610067575f80fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc8290811502906040515f60405180830381858888f193505050501580156100aa573d5f803e3d5ffd5b5050565b5f80fd5b5f819050919050565b6100c4816100b2565b81146100ce575f80fd5b50565b5f813590506100df816100bb565b92915050565b5f602082840312156100fa576100f96100ae565b5b5f610107848285016100d1565b9150509291505056fea2646970667358221220c9fe2f78a923e108f0619a2d655b35d18297668335e649a10272c09a790a188964736f6c63430008140033';


Пробуем отправить транзакцию:
 > await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', gasLimit: 21000, data: contractCode});


В данном случае мы получили ошибку:
Uncaught TransactionRevertInstructionerror: Transaction has been reverted by the EVM
...
innerError: undefined,
reason: 'err: intrinsic gas too low: have 21000, want 58368 (supplied gas 21000)',
signature: undefined,
receipt: undefined,
dаta: undefined,
code: 402
}
Всё дело в том что нам не хватило Gas для отправки транзакции. В коде сообщения мы видим, что было проставлено 21000, но требовалось 58368. Что самое интересное, если мы установим требуемое значение в 58368, то это не значит, что мы успешно создадим контракт. Этого хватит лишь для отправки транзакции, но не хватит для создания самого контракта в EVM, и мы увидим ситуацию как https://sepolia.etherscan.io/tx/0x4a43e6490592f2d6bb02f979acfea6b5c1811aa92e7aac705caaf247a6bb2c727:


Неудачная транзакция по созданию контракта​
Упрощённо, что здесь произошло: транзакция попала в сеть Ethereum, распространилась по нодам, и была помещена в Mempool на них. Proof of Stake алгоритм выбрал ноду, которая в данный момент будет валидировать, исполнять и помещать транзакции из Mempool в новый блок. Нашей транзакции посчастливилось, и она была выбрана нодой для добавления в блок.

Нода взяла транзакцию в обработку и запустила на своей локальной EVM код который находился в поле data. В процессе исполнения был создан аккаунт для контракта, и начался процесс деплоя контракта в storage этого аккаунта. В ходе деплоя было обнаружено, что в транзакции недостаточно Gas для завершения процесса, и возникла ошибка: Out of Gas error.

В итоге у нас появилась ситуация при которой аккаунт под контракт был создан, а его код не был сохранён в storage аккаунта. К тому же мы потеряли 21000 Gas, которые были использованы при исполнении кода, так как нода потратила свои вычислительные ресурсы на исполнение этого кода. Как видим, операции создания аккаунта контракта и его деплоя не атомарны в сети Ethereum. Изменения из storage аккаунта откатились, но сам созданный аккаунт так и остался в блокчейне с нулём вместо кода контракта:

Аккаунт контракта после неудачного деплоя​
Чтобы не воспроизводить описанный сценарий, а так же не вычислять точное значение Gas для деплоя контракта, установим gasLimit с запасом. Оставшийся после деплоя контракта Gas вернётся на баланс нашего аккаунта. Более точное количество Gas можно узнать путём деплоя контракта в локальном блокчейне, например Ganache, или же задеплоить его в RemixIDE, а потом посмотреть количество использованного Gas. При использовании библиотек можно воспользоваться вспомогательными функциями, к
оторые позволяют вычислить требуемое количество Gas до проведения транзакции.
Итак, установим gasLimit с запасом, и отправим транзакцию на создание контракта:
 > await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', gasLimit: 300000, data: contractCode});


Квитанция транзакции:
 {
blockHash: '0x57e6957ca0be6079ddd8a4af7e28a677f5fce8c19ff4a84fdc8bebf3c4957ad7',
blockNumber: 3749703n,
contractAddress: '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d',
cumulativeGasUsed: 29713841n,
effectiveGasPrice: 294172321n,
from: '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47',
gasUsed: 123683n,
logs: [],
logsBloom: '0x00000000000000000000000…0000000000000000000000000000',
status: 1n,
transactionHash: '0x3650d8427dd426fa76967a2d69dd84e67def5cc81cf9875e54221fb97ea14aaa',
transactionIndex: 35n,
type: 0n
}


Контракт успешно создан, вот его адрес:
0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d
Аккаунт контракта:

Аккаунт созданного контракта​
Код контракта:

Аккаунт контракта и его код​
Транзакция, которая создала контракт:

Транзакция создавшая контракт​
Отлично, раз контракт создан, то давайте тогда обратимся к его методу. Например пополним баланс контракта.

Взаимодействие с контрактом
Я использовал фреймворк Truffle для упрощения взаимодействия с контрактом. Мы получали объект-оболочку и через него вызывали методы контракта. На этот раз мы будем вызывать методы путём отправки транзакций с закодированным вызовом метода в поле data.

К счастью, для пополнения баланса контракта, нам не нужно ничего дополнительно кодировать, так как у нас есть fallback функция receive(), которая отработает при поступлении обычной транзакции без поля data, и зачислит Ether из поля value на баланс контракта.

Итак, отправим 0.1 Ether на контракт. GasLimit тоже установим с небольшим запасом:
 > await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', to:'0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d', value: web3.utils.toWei('0.1', 'ether'), gasLimit: 30000});

Квитанция:
 {
blockHash: '0xeb8f28d40966400fcfba7690c938534c93a295ba860b961f72520ad5cf5b3395',
blockNumber: 3749766n,
cumulativeGasUsed: 14620676n,
effectiveGasPrice: 94971021n,
from: '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47',
gasUsed: 21055n,
logs: [],
logsBloom: '0x000000000000000000000000000…00000000000000000000000000',
status: 1n,
to: '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d',
transactionHash: '0xa50902396f4ac2c15fd7c551cef084acf13be2c92c5dd2c91308793b37fe1a95',
transactionIndex: 101n,
type: 0n
}


Баланс пополнился на 0.1 Ether:

Пополнение баланса контракта на 0.1 ETH​
Транзакция на пополнение баланса контракта:

Транзакция, пополнившая баланс контракта на 0.1 ETH​
Отлично, а теперь самое интересное. Посмотрим как вызывать обычную функцию, а в нашем контракте это функция withdraw(), в понятной для протокола Ethereum форме.
Чтобы осуществить вызов метода, нам необходимо закодировать сигнатуру метода и её аргументы. Закодированная сигнатура метода в https://docs.soliditylang.org/en/v0.8.20/abi-spec.html#function-selector Solidity называется function selector.

Сигнатурой метода в Solidity являются: имя функции + типы аргументов в скобках через запятую и без пробелов. В нашем случае сигнатура выглядит следующим образом:
withdraw(uint256)
Чтобы получить function selector, вычислим Keccak-256 хэш от сигнатуры метода:
 > web3.utils.sha3('withdraw(uint256)');
// Out:
'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'

и возьмём первые 4 байта от вычисленного хэша (один байт это два hex-символа не считая 0x префикса):
0x2e1a7d4d
Это и есть function selector. Теперь осталось закодировать сам аргумент, в нашем случае это 0.01 Ether. Для этого сначала сконвертируем 0.01 Ether в Wei, так как протокол Ethereum оперирует значениями в Wei:
 > web3.utils.toWei('0.01', 'ether');
// Out:
'10000000000000000'

Затем сконвертируем полученное значение в шестнаддатеричную форму:
 > web3.utils.toHex(10000000000000000);
// Out:
'0x2386f26fc10000'


Добавим паддинг слева. Поскольку мы использовали тип uint256, а его размер равен 256 бит или 32 байта, то и отправить мы должны число длиной 256 бит. Для этого нам необходимо добавить нули слева, чтобы число в итоге имело размер 256 бит, или длину в 64 символа. Соответственно к нашим 14 символам добавим ещё 50 нулей слева:
000000000000000000000000000000000000000000000000002386f26fc10000

И теперь поместим сам аргумент после function selector:
0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000

Все. Наш вызов метода withdraw() на снятие 0.01 Ether с баланса контракта готов. Теперь поместим его в поле data транзакции и отправим её:
> await web3.eth.sendTransaction({from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47', to: '0x13C96729039F1da4Ea42Ffe1a7E9Cac1cF42801D', gasLimit: 50000, data: '0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000'});


Квитанция:
{
blockHash: '0xb4994dfc02f5ecbff87a28ee8fc157f2af34816b23401bd78e24ea24d169c6d0',
blockNumber: 3750579n,
cumulativeGasUsed: 3294721n,
effectiveGasPrice: 27416831971n,
from: '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47',
gasUsed: 28559n,
logs: [],
logsBloom: '0x0000000000000000000000…0000000000000000000000000',
status: 1n,
to: '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d',
transactionHash: '0xf8c01ab85fb32c87d2d4b98981171ee2365aa5d77f0580844909bd4104daf129',
transactionIndex: 16n,
type: 0n
}


Отлично. Транзакции прошла успешно, с контракта списалось 0.01 Ether и зачислилось на баланс аккаунта, с которого был вызван метод withdraw().

Списание 0.01 ETH с баланс контракта на EOA аккаунт​
Кстати, кодирование можно было осуществить и при помощи готовых методов в web3.js:
> web3.eth.abi.encodeFunctionSignature('withdraw(uint256)');
// out:
'0x2e1a7d4d'
> web3.eth.abi.encodeParameter('uint256', '10000000000000000');
// out:
'0x000000000000000000000000000000000000000000000000002386f26fc10000'


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

Объект-оболочка над контрактом
Выше мы разобрали процесс ручного кодирования данных для взаимодействия с контрактом. Обычно при разработке Dapp приложений взаимодействие с контрактом осуществляется при помощи таких библиотек как: Truffle, web3.js, ethers.js, http://web3.py/, web3j. Все эти библиотеки позволяют обращаться к контракту из кода приложения как к обычному объекту путём вызова его методов. Всё необходимое кодирование данных и отправку транзакции эти объекты берут на себя. Ниже мы рассмотрим как в web3.js можно получить такой объект, и при помощи него произведём вызов метода контракта.

Для создания объекта контракта в web3.js нам понадобится ABI (Application Binary Interface) контракта, который представляет собой описание методов контракта, типов данных и прочей информации, необходимой библиотекам для взаимодействия с контрактом.
Сам ABI в json формате мы можем получить следующим образом:
$ solc Faucet.sol --abi


Вывод:
======= Faucet.sol:Faucet =======
Contract JSON ABI
[{"inputs":[{"internalType":"uint256","name":"withdraw_amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]


Отформатированный ABI:
[
{
"inputs": [
{
"internalType": "uint256",
"name": "withdraw_amount",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]


Здесь мы видим метод withdraw и данные о нём, а так же fallback функцию receive, которая сообщает, что контракт может принимать Ether на свой адрес.
Этот json передаётся в конструктор объекта, через который мы будем взаимодействовать с контрактом, далее сам объект уже будет выполнять операции по кодированию вызовов методов контракта.

Сконвертируем json в jаvascript объект:
> var contractABI = JSON.parse('[{"inputs":[{"internalType":"uint256","name":"withdraw_amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]');


Передадим описание и адрес контракта в конструктор, и получим сам объект контракта:
> var myContract = new web3.eth.Contract(contractABI, '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d', {from: '0xf79ac5fa7F6e76590Ae6cE641aB42b33CAB86C47'});


Обратим внимание на объект с полем from - так мы указали контракту с какого аккаунта по-умолчанию будут происходить вызовы в его адрес.
Вызов метода будет выглядеть следующим образом:
> await myContract.methods.withdraw(web3.utils.toWei('0.01', 'ether')).send({gasLimit: 50000});


Квитанция:
{
blockHash: '0xf39c80e703689eab40d9547ffc252304996e3c6004c62e654c513f8a9d03d4a4',
blockNumber: 3763915n,
cumulativeGasUsed: 7956770n,
effectiveGasPrice: 3540322410n,
from: '0xf79ac5fa7f6e76590ae6ce641ab42b33cab86c47',
gasUsed: 28559n,
logs: [],
logsBloom: '0x0000000000000…00000000000000000000000000',
status: 1n,
to: '0x13c96729039f1da4ea42ffe1a7e9cac1cf42801d',
transactionHash: '0xca5275a466e9acd34c723203d6847e42a4cf49f2156835cd7e9418d572924e59',
transactionIndex: 55n,
type: 0n
}


Видим, что снова списалось 0.01 Ether:

Списание 0.01 ETH с баланс контракта на EOA аккаунт​
На этом всё. Мы познакомились с Contract ABI Specification и узнали как на самом деле происходит кодирование данных при вызове методов контракта. Научились при помощи ABI интерфейса получать объект-обёртку над контрактом и взаимодействовать с ним из кода приложения.
Благодарность выражаю - wakarimasen, именно с ним создавали первые дрейнеры.

Надеюсь вам это поможет и вы найдете применение слитым дрейнерам с помощью моей статьи, разжевали как могли)

Ethereum Contract ABI Specification. Взаимодействие с контрактом

16-02-2024, 22:00 .zip

Скачать

ТОП Записей

Автор: cod

Дата: 16.02.2024 22:00

Просмотров: 324

Оцените статью:

0 0

Партнерки

Обзор RollerAds: умная пуш-сеть с инновационными инструментами
Обзор RollerAds: умная пуш-сеть с инновационными инструментами
-1
Перейти
Huffson Group: премиальная CPA-сеть для iGaming
Huffson Group: премиальная CPA-сеть для iGaming
-1
Перейти

Еще немного интересного

Сервисы / Парсинг A-PARSER - парсер сайтов № 1
Сервисы / Сервера и хостинги Хостинг PrivateAlps (Игнорирует DMCA)
Сервисы / Платёжные системы Merchant001- надежный эквайринг для сайта
Статьи Дорвеи 2023-2024
Арбитраж трафика / Статьи Арбитраж трафика на пуш уведомлениях
Статьи / Софт / Антидетект Браузеры Лучшие антидетект браузеры
Сервисы / Трекеры / Клоака Keitaro PRO - трекер для арбитража трафика
Хостинг / Скрипты сайтов / Статьи Как настроить работу почты
Хостинг / Скрипты сайтов / Статьи Урок доступ mysql с любого ip
Хостинг / Скрипты сайтов / Статьи Как сделать моментальную установку сервера? HOSTINPL
Хостинг / Статьи Exim (Восстановление по e-mail)
Скрипты сайтов / Статьи JavaScript - Меняем CSS

Отзывы (0)




To connect permitted only files with the extension: .tpl or .php To connect permitted only files with the extension: .tpl or .php