paint-brush
Как да внедрите интелигентен договор в мрежата на Ethereum с помощта на dRPC API ключ и крайна точка от@ileolami
2,950 показания
2,950 показания

Как да внедрите интелигентен договор в мрежата на Ethereum с помощта на dRPC API ключ и крайна точка

от Ileolami23m2024/09/11
Read on Terminal Reader

Твърде дълго; Чета

В тази статия ще напишете, компилирате, тествате и внедрите интелигентен договор за плащане на кафе към Ethereum Sepolia Testnet, като използвате dRPC крайна точка и API ключ. Функциите включват: плащане за кафе, преглед на цената на кафето, извличане на общия брой продадени кафета и общата сума на направените пари.
featured image - Как да внедрите интелигентен договор в мрежата на Ethereum с помощта на dRPC API ключ и крайна точка
Ileolami HackerNoon profile picture
0-item

Въведение

От разбирането на технологичния стек за разработка на Web3 DApp трябва да сте научили основния технологичен стек за разработка на web3 dApp, ролята на RPC в разработката на dApp и как да използвате dRPC за създаване на акаунт, генериране на API ключ, крайни точки, анализ на крайни точки , добавете средства към вашия dRPC акаунт и проверете баланса си.


Ролята на dRPC при внедряването на интелигентни договори е да опрости процеса на настройка на възел на Ethereum, което улеснява разработчиците да взаимодействат и внедряват само с един ред код.


В тази статия ще напишете, компилирате, тествате и внедрите интелигентен договор за плащане на кафе към Ethereum Sepolia Testnet, като използвате dRPC крайна точка и API ключ.


Характеристиките включват:
  1. Плащане за кафе
  2. Преглед на цената на кафето
  3. Извличане на общ брой продадени кафета и обща сума на спечелените пари


Хайде да си изцапаме ръцете.

Предпоставки

  1. Имате портфейл, напр. Metamask.
  2. Редактор на кодове.
  3. Вече инсталирани всички Js библиотеки или рамки по ваш избор (напр. React.js, Next.js и т.н.).
  4. Буркан с вода.

Необходими технологии и инструменти

  1. Солидност.
  2. React.js с помощта на Vite.js(Typescript)
  3. каска.
  4. Web3.js.
  5. Дотенв.
  6. dRPC API ключ и крайна точка.
  7. Частен ключ на вашия акаунт.
  8. MetaMask

Писане на интелигентния договор за плащане на кафе

  1. Създайте папка във вашата основна директория и я наименувайте contracts .


  2. Създайте файл в папката contracts и го наименувайте coffee.sol .

    Показана е файлова директория с папка с име "contracts" и файл с име "coffee.sol" вътре в папката.

ще използвате солидността, за да напишете интелигентния договор. Файловете на Solidity са именувани с разширението .sol , защото това е стандартното файлово разширение за изходния код на Solidity.


  1. Добавете следния изходен код към coffee.sol :

     // SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; contract Coffee { uint256 public constant coffeePrice = 0.0002 ether; uint256 public totalCoffeesSold; uint256 public totalEtherReceived; // Custom error definitions error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed(); // Event to log coffee purchases event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost); // Function to buy coffee function buyCoffee(uint256 quantity) external payable { if (quantity <= 0) { revert QuantityMustBeGreaterThanZero(); } uint256 totalCost = coffeePrice * quantity; if (msg.value > totalCost) { revert InsufficientEtherSent(totalCost, msg.value); } // Update the total coffees sold and total ether received totalCoffeesSold += quantity; totalEtherReceived += totalCost; console.log("Total ether received updated:", totalEtherReceived); console.log("Total coffee sold updated:", totalCoffeesSold); // Emit the purchase event emit CoffeePurchased(msg.sender, quantity, totalCost); // Refund excess Ether sent if (msg.value > totalCost) { uint256 refundAmount = msg.value - totalCost; payable(msg.sender).transfer(refundAmount); } } // Fallback function to handle Ether sent directly to the contract receive() external payable { revert DirectEtherTransferNotAllowed(); } // Public view functions to get totals function getTotalCoffeesSold() external view returns (uint256) { console.log("getTotalCoffeesSold :", totalCoffeesSold); return totalCoffeesSold; } function getTotalEtherReceived() external view returns (uint256) { console.log("getTotalEtherReceived :", totalEtherReceived); return totalEtherReceived; } }

Прагма

  • //SPDX-License-Identifier: MIT : Този идентификатор на лиценза показва, че кодът е лицензиран съгласно лиценза .


  • pragma solidity >=0.8.0 <0.9.0; : Указва, че кодът е написан за версии на Solidity между 0.8.0 (включително) и 0.9.0 (изключително).

Променлива на състоянието

 uint256 public constant coffeePrice = 0.0002 ether; uint256 public totalCoffeesSold; uint256 public totalEtherReceived;
  • coffeePrice : Задайте като постоянна стойност от 0.0002 ether .
  • totalCoffeesSold : Проследява броя на продадените кафета.
  • totalEtherReceived : Проследява общия етер, получен от договора.

Персонализирани грешки

Персонализираните грешки в Solidity са съобщения за грешка, които са пригодени за конкретен случай на употреба, а не съобщенията за грешка по подразбиране, които се предоставят от езика за програмиране . Те могат да помогнат за подобряване на потребителското изживяване и също могат да помогнат при отстраняване на грешки и поддържане на интелигентни договори.


За да дефинирате персонализирана грешка в Solidity, можете да използвате следния синтаксис:
  • error : Тази ключова дума се използва за дефиниране на персонализирана грешка
  • Уникално име: Грешката трябва да има уникално име
  • Параметри: Ако искате да включите конкретни подробности или параметри в съобщението за грешка, можете да ги добавите в скоби след името на грешката.
 error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed();
  • QuantityMustBeGreaterThanZero() : Гарантира, че количеството е по-голямо от нула.
  • InsufficientEtherSent(uint256 required, uint256 sent) : Гарантира, че изпратеният Ether е достатъчен.
  • DirectEtherTransferNotAllowed() : Предотвратява директни прехвърляния на Ether към договора.

събития

Събитието е част от договора, която съхранява аргументите, предадени в регистрационните файлове на транзакциите, когато бъдат излъчени. Събитията обикновено се използват за информиране на извикващото приложение за текущото състояние на договора, като се използва функцията за регистриране на EVM. Те уведомяват приложенията за промени, направени в договорите, които след това могат да се използват за изпълнение на свързана логика.
 event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);
  • CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost) : Регистрира покупките на кафе.

Функции

Функциите са самостоятелни модули от код, които изпълняват конкретна задача. Те елиминират излишъка от пренаписване на една и съща част от кода. Вместо това, разработчиците могат да извикат функция в програмата, когато е необходимо.

 function buyCoffee(uint256 quantity) external payable { if (quantity <= 0) { revert QuantityMustBeGreaterThanZero(); } uint256 totalCost = coffeePrice * quantity; if (msg.value > totalCost) { revert InsufficientEtherSent(totalCost, msg.value); } // Update the total coffees sold and total ether received totalCoffeesSold += quantity; totalEtherReceived += totalCost; console.log("Total ether received updated:", totalEtherReceived); console.log("Total coffee sold updated:", totalCoffeesSold); // Emit the purchase event emit CoffeePurchased(msg.sender, quantity, totalCost); // Refund excess Ether sent if (msg.value > totalCost) { uint256 refundAmount = msg.value - totalCost; payable(msg.sender).transfer(refundAmount); } } receive() external payable { revert DirectEtherTransferNotAllowed(); } function getTotalCoffeesSold() external view returns (uint256) { console.log("getTotalCoffeesSold :", totalCoffeesSold); return totalCoffeesSold; } function getTotalEtherReceived() external view returns (uint256) { console.log("getTotalEtherReceived :", totalEtherReceived); return totalEtherReceived; }
  • buyCoffee(uint256 quantity) external payable : Обработва покупките на кафе и извършва следните операции:
    • Проверете дали количеството е валидно.
    • Изчислява общата цена.
    • Гарантира изпращането на достатъчно етер.
    • Актуализира променливите на състоянието.
    • Излъчва събитието за покупка.
    • Възстановява излишния етер.
  • receive() external payable : Връща директните Ether трансфери, в случай че някой изпрати средства директно на адреса на договора.
  • getTotalCoffeesSold() external view returns (uint256) : Връща общия брой продадени кафета.
  • getTotalEtherReceived() external view returns (uint256) : Връща общия получен етер.

Съставяне на интелигентния договор за плащане на кафе

Тук ще използвате Hardhat за компилиране на интелигентния договор.


  1. Инсталирайте Hardhat, като използвате следния команден ред.

     npm install --save-dev hardhat


    Ще получите отговора по-долу след успешна инсталация.

    Терминал, показващ резултата от инсталирането на hardhat..


  2. В същата директория, където инициализирате hardhat с помощта на този команден ред:

     npx hardhat init


  3. Изберете Create a Javascript project като използвате бутона със стрелка надолу и натиснете enter.

  4. Натиснете Enter, за да инсталирате в основната папка

  5. Приемете всички подкани, като използвате y на клавиатурата, включително зависимостите @nomicfoundation/hardhat-toolbox

  6. Виждате този отговор по-долу, показващ, че сте инициализирали успешно

Ще забележите, че някои нови папки и файлове са добавени към вашия проект. напр. Lock.sol , iginition/modules , test/Lock.js и hardhat.config.cjs . Не се тревожете за тях.


Единственият полезен са iginition/modules и hardhat.config.cjs . По-късно ще разберете за какво се използват. Чувствайте се свободни да изтриете Lock.sol в папката contracts и Lock.js в папката iginition/modules .


  1. Компилирайте договора, като използвате следния команден ред:

     npx hardhat compile 

  1. Виждате допълнителни папки и файлове като тези.

  1. Вътре във файла Coffee.json е ABI кодът във формат JSON, който ще извикате, когато взаимодействате с интелигентния договор.
 { "_format": "hh-sol-artifact-1", "contractName": "Coffee", "sourceName": "contracts/coffee.sol", "abi": [ { "inputs": [], "name": "DirectEtherTransferNotAllowed", "type": "error" }, { "inputs": [ { "internalType": "uint256", "name": "required", "type": "uint256" }, { "internalType": "uint256", "name": "sent", "type": "uint256" } ], "name": "InsufficientEtherSent", "type": "error" }, { "inputs": [], "name": "QuantityMustBeGreaterThanZero", "type": "error" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "buyer", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "quantity", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "totalCost", "type": "uint256" } ], "name": "CoffeePurchased", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "quantity", "type": "uint256" } ], "name": "buyCoffee", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [], "name": "coffeePrice", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getTotalCoffeesSold", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getTotalEtherReceived", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalCoffeesSold", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalEtherReceived", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "stateMutability": "payable", "type": "receive" } ], "bytecode": "", "deployedBytecode": "", "linkReferences": {}, "deployedLinkReferences": {} }

Тестване на интелигентния договор

Писането на автоматизиран тестов скрипт, докато изграждате своя интелигентен договор, е от решаващо значение и силно се препоръчва. Той действа като двуфакторно удостоверяване (2FA), като гарантира, че вашият интелигентен договор работи според очакванията, преди да го внедрите в мрежата на живо.


Под test папка създайте нов файл и го наречете Coffee. cjs. Във файла поставете този код по-долу:

 const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers.js"); const { expect } = require("chai"); const pkg = require("hardhat"); const ABI = require('../artifacts/contracts/coffee.sol/Coffee.json'); const { web3 } = pkg; describe("Coffee Contract", function () { // Fixture to deploy the Coffee contract async function deployCoffeeFixture() { const coffeeContract = new web3.eth.Contract(ABI.abi); coffeeContract.handleRevert = true; const [deployer, buyer] = await web3.eth.getAccounts(); const rawContract = coffeeContract.deploy({ data: ABI.bytecode, }); // Estimate gas for the deployment const estimateGas = await rawContract.estimateGas({ from: deployer }); // Deploy the contract const coffee = await rawContract.send({ from: deployer, gas: estimateGas.toString(), gasPrice: "", }); console.log("Coffee contract deployed to: ", coffee.options.address); return { coffee, deployer, buyer, rawContract }; } describe("Deployment", function () { // Test to check initial values after deployment it("Should set the initial values correctly", async function () { const { coffee } = await loadFixture(deployCoffeeFixture); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal("0"); expect(totalEtherReceived).to.equal("0"); }); }); describe("Buying Coffee", function () { // Test to check coffee purchase and event emission it("Should purchase coffee and emit an event", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 3; const totalCost = web3.utils.toWei("0.0006", "ether"); // Buyer purchases coffee const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); // Check event const event = receipt.events.CoffeePurchased; expect(event).to.exist; expect(event.returnValues.buyer).to.equal(buyer); expect(event.returnValues.quantity).to.equal(String(quantity)); expect(event.returnValues.totalCost).to.equal(totalCost); }); // Test to check revert when quantity is zero it("Should revert if the quantity is zero", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") }) ).to.be.revertedWith("QuantityMustBeGreaterThanZero"); }); // Test to check if totalCoffeesSold and totalEtherReceived are updated correctly it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 5; const totalCost = web3.utils.toWei("0.001", "ether"); await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal(String(quantity)); expect(totalEtherReceived).to.equal(totalCost); }); }); describe("Fallback function", function () { // Test to check revert when ether is sent directly to the contract it("Should revert if ether is sent directly to the contract", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( web3.eth.sendTransaction({ from: buyer, to: coffee.options.address, value: web3.utils.toWei("0.001", "ether"), }) ).to.be.revertedWith("DirectEtherTransferNotAllowed"); }); }); });
Този код тества функционалността на интелигентния договор за кафе. Той включва тестове за внедряване, закупуване на кафе и обработка на директни преводи на Ether към договора.


Ето разбивка:

Функция на Fixture: deployCoffeeFixture

 async function deployCoffeeFixture() {  const coffeeContract = new web3.eth.Contract(ABI.abi);  coffeeContract.handleRevert = true;  const [deployer, buyer] = await web3.eth.getAccounts();  const rawContract = coffeeContract.deploy({    data: ABI.bytecode,  });  const estimateGas = await rawContract.estimateGas({ from: deployer });  const coffee = await rawContract.send({    from: deployer,    gas: estimateGas.toString(),  gasPrice: "",  });  console.log("Coffee contract deployed to: ", coffee.options.address);  return { coffee, deployer, buyer, rawContract }; }
  • Внедрява договора за кафе : Създава нов екземпляр на договор и го внедрява с помощта на акаунта на внедрителя.
  • Оценява газ : Оценява газа, необходим за разгръщане.
  • Връща : Внедреният екземпляр на договора, внедрителят и акаунтите на купувача.

Тестове за внедряване

 describe("Deployment", function () {  it("Should set the initial values correctly", async function () {    const { coffee } = await loadFixture(deployCoffeeFixture);    const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();    const totalEtherReceived = await coffee.methods.totalEtherReceived().call();    expect(totalCoffeesSold).to.equal("0");    expect(totalEtherReceived).to.equal("0");  }); });
  • Проверява първоначалните стойности : Гарантира, че totalCoffeesSold и totalEtherReceived са зададени на нула след внедряването.

Закупуване на тестове за кафе

 describe("Buying Coffee", function () {  it("Should purchase coffee and emit an event", async function () {    const { coffee, buyer } = await loadFixture(deployCoffeeFixture);    const quantity = 3;    const totalCost = web3.utils.toWei("0.0006", "ether");    const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });    const event = receipt.events.CoffeePurchased;    expect(event).to.exist;    expect(event.returnValues.buyer).to.equal(buyer);    expect(event.returnValues.quantity).to.equal(String(quantity));    expect(event.returnValues.totalCost).to.equal(totalCost);  });  it("Should revert if the quantity is zero", async function () {    const { coffee, buyer } = await loadFixture(deployCoffeeFixture);    expect(      coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") })    ).to.be.revertedWith("QuantityMustBeGreaterThanZero");  });  it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () {    const { coffee, buyer } = await loadFixture(deployCoffeeFixture);    const quantity = 5;    const totalCost = web3.utils.toWei("0.001", "ether");    await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });    const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();    const totalEtherReceived = await coffee.methods.totalEtherReceived().call();    expect(totalCoffeesSold).to.equal(String(quantity));    expect(totalEtherReceived).to.equal(totalCost);  }); });
  • Закупуване на кафе и излъчване на събитие : Тества дали купуването на кафе актуализира състоянието и излъчва събитието CoffeePurchased .
  • Връщане при нулево количество : Гарантира, че транзакцията се връща, ако количеството е нула.
  • Правилно актуализиране на състоянието : Проверява дали totalCoffeesSold и totalEtherReceived се актуализират правилно след покупка.

Тест на резервната функция

 describe("Fallback function", function () {  it("Should revert if ether is sent directly to the contract", async function () {    const { coffee, buyer } = await loadFixture(deployCoffeeFixture);    expect(      web3.eth.sendTransaction({        from: buyer,        to: coffee.options.address,        value: web3.utils.toWei("0.001", "ether"),      })    ).to.be.revertedWith("DirectEtherTransferNotAllowed");  }); });
  • Връщане при директен трансфер на Ether : Гарантира, че изпращането на Ether директно към договора (без извикване на функция) връща транзакцията.

Тестване на интелигентния договор

След като напишете тестовия скрипт, ще
  1. Когато изпълнявате своите договори и тестове в Hardhat Network, можете да отпечатвате съобщения за регистриране и променливи на договора, извиквайки console.log() от вашия код на Solidity. За да го използвате, трябва да импортирате hardhat/console.sol в кода на вашия договор по следния начин:
 //SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; import "hardhat/console.sol"; contract Coffee { //... }
  1. За да тествате договора, изпълнете следната команда във вашия терминал:

     npx hardhat test


  2. Трябва да имате резултат като този по-долу:

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

Ако стартирате npx hardhat test той автоматично компилира и тества интелигентния договор. Можете да го изпробвате и да ме уведомите в секцията за коментари.

Внедряване на интелигентния договор

Тук ще внедрите вашия интелигентен договор в Testnet ви позволява да тествате интелигентния си договор в среда, която имитира основната мрежа на Ethereum, без да поемате значителни разходи. Ако се справяте добре с функцията на dApp, можете да преразпределите към Ethereum Mainnet.


  1. Инсталирайте пакета dotenv и тези зависимости.

     npm install dotenv npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'
    Това ще добави Web3.Js и Dotenv към вашия проект, като го включи в папката 'node_modules'.


  2. импортирайте ги във вашия файл hardhat.config.cjs

     require('dotenv').config(); require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-web3-v4"); const HardhatUserConfig = require("hardhat/config"); module.exports = { solidity: "0.8.24", } };
  3. Създайте .env файл в основната папка.


  4. Вземете частния ключ на вашия акаунт от вашия портфейл MetaMask и dRPC API ключ.


  5. Съхранявайте ги във вашия .env файл.

     DRPC_API_KEY=your_drpc_api_key PRIVATE_KEY=your_wallet_private_key


  6. Актуализирайте файла hardhat.config.cjs за да включите конфигурацията на Sepolia Testnet:

     require('dotenv').config(); require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-web3-v4"); const HardhatUserConfig = require("hardhat/config"); const dRPC_API_KEY = process.env.VITE_dRPC_API_KEY; const PRIVATE_KEY = process.env.VITE_PRIVATE_KEY; module.exports = { solidity: "0.8.24", networks: { sepolia: { url: `//lb.drpc.org/ogrpc?network=sepolia&dkey=${dRPC_API_KEY}`, accounts: [`0x${PRIVATE_KEY}`], } } };
  7. Създайте нов скриптов файл в папката ignition/module и го наречете deploy.cjs . Добавете следния код, за да внедрите вашия интелигентен договор:

     const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); const CoffeeModule = buildModule("CoffeeModule", (m) => { const coffee = m.contract("Coffee"); return { coffee }; }); module.exports = CoffeeModule;
  8. Разположете интелигентния договор, като изпълните следната команда във вашия терминал:

     npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network sepolia


    След като изпълните командния ред, ще бъдете помолени да Confirm deploy to network sepolia (11155111)? (y/n) , въведете y . Трябва да видите адреса на вашия внедрен интелигентен договор в терминала при успешно внедряване.

    Можете също да получите достъп до адреса на договора във файла deployed_addresses.json .

    Екранна снимка на Vite-Project File Explorer и отворен файл deployed_addresses.json. Поздравления, успешно внедрихте своя интелигентен договор в Sepolia Testnet. 🎉

Заключение

Тази статия ви научи как да пишете интелигентен договор за плащане, да тествате, компилирате и разгръщате интелигентен договор с помощта на hardhat CLI.


В следващата статия ще се научите да създавате предния край за това dApp. Този потребителски интерфейс ще се състои от:

  1. Поле за въвеждане на брой закупени кафета.
  2. Бутон, който задейства платежна транзакция и я удържа от сметката ви.
  3. Показване на общо закупено кафе и получената сума в етер и USD
  4. Цената на самото кафе както в етер, така и в щатски долари.

справка


← Предишна статия Следваща статия →

L O A D I N G
. . . comments & more!

About Author

Ileolami HackerNoon profile picture
Ileolami@ileolami
I am a front-end developer, a Guidance Counsellor,and technical writer

ЗАКАЧВАЙТЕ ЕТИКЕТИ

ТАЗИ СТАТИЯ Е ПРЕДСТАВЕНА В...

바카라사이트 바카라사이트 온라인바카라