Onion Marketplace
Onion Marketplace
este un magazin online care ruleaza folosind un blockchain.
Acest magazin permite utilizatorilor sa vanda sau sa cumpere lucruri prin
intermediul criptomonedei Ether.
Ce este un blockchain?
Un blockchain este o retea de noduri peer-to-peer ce comunica intre ele. Aceste noduri sunt de fapt calculatoare care impart responsabilitati asemenea unor web servere, precum rularea unor programe si stocarea unor date ce pot fi accesate oricand suntem conectati la blockchain. Toate nodurile lucreaza impreuna pentru a crea o retea publica la care oricine se poate conecta. Totusi, blockchain-urile lucreaza diferit fata de web servele traditionale. Tot codul si datele dintr-un blockchain sunt descentralizate, sunt distribuite pe toate nodurile din retea.
Toate datele sunt continute in pachete de inregistrari numite blocuri care sunt inlantuite pentru a alcatui ledger-ul public. Toate nodurile din retea participa la asigurarea ca datele raman securizate si neschimbate.
Ce este un Smart Contract?
Smart contractele sunt sunt programe pe care le putem implementa intr-un blockchain. Sunt scrise in limbajul de programare Solidity. Smart contractele sunt imutabile ceea ce inseamna ca o data ce au fost create, nu mai pot fi modificate. De aceea este esential sa se realizeze o testare a acestora locala inainte de a fi implementate in blockchain.
In cazul nostru Smart Contract-ul Marketplace.sol
va functiona precum o
masina de distribuire prin distribuirea articolelor catre cumparator si prin
transferul platii instantaneu catre vanzator.
Dependinte
- Ganache Personal Blockchain -> Este un blockchain care ruleaza local si poate fi folosit pentru development.
- Node.js;
- npm;
- Truffle Framework:
- Managementul unui Smart Contract -> scrierea unor smart contracte folosind Solidity si compilarea acestuia intr-un bytecode care poate fi rulat pe Ethereum Virtal Machine (EVM);
- Testare automata -> scrierea unor teste pentru a valida comportamentul dorit pentru un Smart Contract;
- Implementarea & Migrarea -> Scrierea unor script-uri pentru implementarea si migrarea unor smart contracte catre orice retea de Ethereum blockchain;
- Metamask -> O extensie de browser care ne permite sa ne conectam catre un blockchain;
Explicatia Smart Contract-ului Marketplace.sol
pragma solidity ^0.5.0;
contract Marketplace {
// Identificatorul name reprezinta o variabila de tip state.
// Ce inseamna o variabila de tip state? Ei bine variabilele de tip state
// sunt stocate si sincronizate pe intreaga retea de noduri din blockchain.
// Aceasta variabila va stoca numele Smart Contractului.
string public name;
// Construim o structura in Smart Contract care va stoca toate atributele
// de care un produs are nevoie pentru a fi reprezentat in Marketplace
struct Product {
uint id;
string name;
uint price;
address payable owner; // Deoarece vrem sa platim detinatorul bunului
// respectiv acest camp devine payable
bool purchased;
}
// Pentru a stoca produsele in blockchain vom crea un mapping care
// functioneaza asemenea dictionarelor din Python. Mappings au
// chei unice prin care sunt returnate valori unice. In cazul
// nostru vom folosi un id ca cheia, iar rezultatul va fi un produs
mapping(uint => Product) public products;
// Pentru a retine numarul de produse care exista in Smart Contract
// adaugam o variabila productCount
uint public productCount = 0;
event ProductCreated(
uint id,
string name,
uint price,
address payable owner,
bool purchased
);
event ProductPurchased(
uint id,
string name,
uint price,
address payable owner,
bool purchased
);
// Constructorul este o functie speciala din Solidity ce este apelata
// cand Smart Contractul este creat ( cu alte cuvinte cand acesta este
// implementat in Blockchain ).
constructor() public {
name = "Onion Marketplace";
}
// Realizam o functie prin care sunt creeate produse noi
// Aceasta functie primeste un nume de produs prin intermediul parametrului
// name si un pret exprimat in Wei (cea mai mica subdiviziuneaa Ether-ului)
// prin intermediul variabilei price
function createProduct(string memory _name, uint _price) public {
// Adaugam niste conditii ca numele si pretul sa fie niste variabile
// valide
require(bytes(_name).length > 0);
require(_price > 0);
// Incrementam numarul total de produse
productCount++;
// Momentul in care obiectul este creat efectiv si adaugat in Mapping.
// msg.sender este adresa utilizatorului care creeaza produsul
products[productCount] = Product(productCount, _name, _price, msg.sender, false);
// In cele din urma, facem trigger unui eveniment pentru a anunta
// intreaga retea de noduri ca produsul a fost creat cu succes
emit ProductCreated(productCount, _name, _price, msg.sender, false);
}
// Realizam o functie prin care sunt cumparate produsele. De fiecare data
// cand cineva apeleaza aceasta functie, va subscrie id-ul produsului pe
// care doreste sa il cumpere ( acest lucru este realizat de componenta
// de React din frontend). De ce am facut functia payable? Ei bine payable
// inseamna ca va accepta Ethereum ca metoda de plata.
function purchaseProduct(uint _id) public payable {
// Obtinem produsul din mapping si ii realizam o copie locala
Product memory _product = products[_id];
// Stocam propietarul curent intr-o variabila. In aceasta functie
// vom transfera detinatorul produsului respectiv, deci trebuie
// sa stim cine a fost detinatorul initial
address payable _seller = _product.owner;
// ne asiguram ca produsul este valid
require(_product.id > 0 && _product.id <- productCount);
// ne asiguram ca exista destui ether in tranzactie
require(msg.value >= _product.price);
// ne asiguram ca produsul nu a fost cumparat deja
require(!_product.purchased);
// ne asiguram ca produsul nu este cumparat tot de catre vanzator
require(_seller != msg.sender);
// Transferam detinatorul
_product.owner = msg.sender;
// marcam produsul ca fiind cumparat
_product.purchased = true;
// actualizam produsul in mapping
products[_id] = _product;
// platim vanzatorul
address(_seller).transfer(msg.value);
// emitem un eveniment pentru a marca faptul ca produsul a fost
// cumparat cu success
emit ProductPurchased(productCount, _product.name, _product.price, msg.sender, true);
}
}
Compilarea se realizeaza folosind comanda:
truffle compile
Realizarea implementarii in blockchain se face prin createa unui script 2_deploy_contracts.js
.
Acest fisier ii spune lui Truffle ce smart contract sa implementeze in blockchain.
Comanda:
truffle migrate
Crearea testelor este foarte importanta in Blockchain deoarece o data ce un blockchain a fost implementat in retea acesta nu mai poate fi schimbat:
const Marketplace = artifacts.require("./Marketplace.sol");
require('chai')
.use(require('chai-as-promised'))
.should()
contract('Marketplace', ([deployer, seller, buyer]) => {
let marketplace
before(async () => {
marketplace = await Marketplace.deployed()
})
describe('deployment', async () => {
it('deploys successfully', async () => {
const address = await marketplace.address
assert.notEqual(address, 0x0)
assert.notEqual(address, '')
assert.notEqual(address, null)
assert.notEqual(address, undefined)
})
it('has a name', async () => {
const name = await marketplace.name()
assert.equal(name, 'Onion Marketplace')
})
})
describe('products', async () => {
let result, productCount
before(async () => {
result = await marketplace.createProduct('iPhone X', web3.utils.toWei('1', 'Ether'), { from: seller })
productCount = await marketplace.productCount()
})
it('creates products', async () => {
// succes
assert.equal(productCount, 1)
const event = result.logs[0].args
assert.equal(event.id.toNumber(), productCount.toNumber(), 'id is correct')
assert.equal(event.name, 'iPhone X', 'name is correct')
assert.equal(event.price, '1000000000000000000', 'price is correct')
assert.equal(event.owner, seller, 'owner is correct')
assert.equal(event.purchased, false, 'purchased is correct')
// fail: Produsul trebuie sa aiba un nume
await await marketplace.createProduct('', web3.utils.toWei('1', 'Ether'), { from: seller }).should.be.rejected;
// fail: Produsul trebuie sa aiba un pret
await await marketplace.createProduct('iPhone X', 0, { from: seller }).should.be.rejected;
})
it('sells products', async () => {
let oldSellerBalance
oldSellerBalance = await web3.eth.getBalance(seller)
oldSellerBalance = new web3.utils.BN(oldSellerBalance)
result = await marketplace.purchaseProduct(productCount, { from: buyer, value: web3.utils.toWei('1', 'Ether')})
const event = result.logs[0].args
assert.equal(event.id.toNumber(), productCount.toNumber(), 'id is correct')
assert.equal(event.name, 'iPhone X', 'name is correct')
assert.equal(event.price, '1000000000000000000', 'price is correct')
assert.equal(event.owner, buyer, 'owner is correct')
assert.equal(event.purchased, true, 'purchased is correct')
let newSellerBalance
newSellerBalance = await web3.eth.getBalance(seller)
newSellerBalance = new web3.utils.BN(newSellerBalance)
let price
price = web3.utils.toWei('1', 'Ether')
price = new web3.utils.BN(price)
const exepectedBalance = oldSellerBalance.add(price)
assert.equal(newSellerBalance.toString(), exepectedBalance.toString())
// fail: Incearca sa se cumpere un produs care nu exista, nu are un id valid;
await marketplace.purchaseProduct(99, { from: buyer, value: web3.utils.toWei('1', 'Ether')}).should.be.rejected;
// fail: Cumparatorul incearca sa cumpere neavand fonduri suficiente
await marketplace.purchaseProduct(productCount, { from: buyer, value: web3.utils.toWei('0.5', 'Ether') }).should.be.rejected;
// fail: Cumparatorul nu poate fi vanzatorul
await marketplace.purchaseProduct(productCount, { from: buyer, value: web3.utils.toWei('1', 'Ether') }).should.be.rejected;
})
})
})
Partea de frontend
In partea de Frontend am folosit:
- React.js pentru a construi interfata;
- Bootstrap tot pentru interfata;
- Web3.js pentru a conecta aplicatia la blockchain.
Pentru a conecta browserul la blockchain am folosit Metamask. Apoi pentru a mima interactiunea dintre un vanzator si un cumparator adaugam doua conturi.
import React, { Component } from 'react';
import './App.css';
import Web3 from 'web3'
import Navbar from './Navbar'
import Marketplace from '../abis/Marketplace.json'
import Main from './Main'
class App extends Component {
constructor(props) {
super(props)
this.state = {
account: '',
productCount: 0,
products: [],
loading: true
}
this.createProduct = this.createProduct.bind(this)
this.purchaseProduct = this.purchaseProduct.bind(this)
}
async componentWillMount() {
// Incarcam web3
await this.loadWeb3()
await this.loadBlockchainData()
}
// Functia detecteaza prezenta unui furnizor de Ethereum in webbrowser
// care ne permite conectarea la un blockchain.
async loadWeb3() {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
await window.ethereum.enable()
}
else if (window.web3) {
window.web3 = new Web3(window.web3.currentProvider)
}
else {
window.alert('Non-Ethereum browser detected. You should consider trying MetaMask!')
}
}
async loadBlockchainData() {
const web3 = window.web3
// incarca conturile din blockchain in memorie
const accounts = await web3.eth.getAccounts()
this.setState({ account: accounts[0] })
const networkId = await web3.eth.net.getId()
const networkData = Marketplace.networks[networkId]
if (networkData) {
const marketplace = web3.eth.Contract(Marketplace.abi, networkData.address)
this.setState({ marketplace })
const productCount = await marketplace.methods.productCount().call()
this.setState({ loading: false })
this.setState({ productCount })
// incarca produsele din blockchain
for (var i = 1; i <= productCount; i++) {
const product = await marketplace.methods.products(i).call()
this.setState({
products: [...this.state.products, product]
})
}
} else {
window.alert('Marketplace contract not deployed to detected network.')
}
}
// Functiile care apeleaza Smart Contractul pentru a crea si a vinde produsele
createProduct(name, price) {
this.setState({ loading: true })
this.state.marketplace.methods.createProduct(name, price).send({ from: this.state.account })
.once('receipt', (receipt) => {
this.setState({ loading: false })
})
}
purchaseProduct(id, price) {
this.setState({ loading: true })
this.state.marketplace.methods.purchaseProduct(id).send({ from: this.state.account, value: price })
.once('receipt', (receipt) => {
this.setState({ loading: false })
})
}
render() {
return (
<div>
<Navbar account={this.state.account} />
<div className="container-fluid mt-5">
<div className="row">
<main role="main" className="col-lg-12 d-flex">
{this.state.loading
? <div id="loader" className="text-center"><p className="text-center">Loading...</p></div>
: <Main
products={this.state.products}
createProduct={this.createProduct}
purchaseProduct={this.purchaseProduct} />
}
</main>
</div>
</div>
</div>
);
}
}
export default App;