For Developers #4
Olá, desenvolvedor! Seja bem-vindo à sua fonte de insights e ensinamentos técnicos sobre blockchain. Hoje vamos construir um projeto de loyalty completo utilizando as rotas do Lumx Protocol.
Nesta edição, nosso DevRel e PM de Inovações, Afonso Dalvi, traz um conteúdo guiado, com menos teoria e mais "mão na massa", como ele gosta de dizer. Você irá se aventurar no desenvolvimento completo de um projeto de loyalty, utilizando como base um programa de fidelidade fictício para uma empresa aérea e as rotas de APIs da Lumx para sua construção.
Tempo de leitura: 8 minutos.
Projeto: Airline Loyalty
Já imaginou comprar passagens aéreas e participar de um programa de fidelidade, acumulando pontos na forma de tokens pelas etapas cumpridas na plataforma? Vamos criar um passo a passo para um projeto de fidelidade chamado ‘Loyalty Airlines’. Nesse projeto, integraremos diversas rotas da Lumx. Vamos lá!
Passo 1: Configuração Inicial do Projeto:
npx create-react-app airline-loyalty cd airline-loyalty
Instalação do TailwindCSS:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Estrutura dos diretorios:
airline-loyalty/
├── public/ │
├── index.html
│ └── ...
├── src/
│
├── components/
│ │ ├── ConnectWallet.js
│ │ └── ... │ ├── pages/
│ │ ├── CreateAeroNFT.js
│ │ ├── MintToken.js │
│ └── ... │ ├── App.js
│ ├── index.js
│ ├── CustomStyles.css
Configuração do TailwindCSS Edite tailwind.config.js
para incluir os caminhos corretos:
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
primary: '#1E40AF', // Azul
secondary: '#000000', // Preto
light: '#FFFFFF' // Branco
},
},
},
plugins: [],
}
Configuração do src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Passo 2: Configuração do Axios
Instalação do Axios
npm install axios
Criação do Arquivo de Configuração do Axios src/api/axios.js:
import axios from 'axios';
const instance = axios.create({
baseURL: 'https://protocol-sandbox.lumx.io/v2',
headers: {
'Content-Type': 'application/json',
},
});
export const setAuthToken = token => {
instance.defaults.headers.common['Authorization'] = `Bearer ${token}`;
};
export default instance;
Passo 3: Configuração do React Router
Instalação do React Router:
npm install react-router-dom
Passo 4: Criação dos Componentes
Componente de Conexão de Carteira
src/components/ConnectWallet.js:
import React, { useState } from 'react';
import axios, { setAuthToken } from '../api/axios';
const apiKey = 'Bearer <Sua apiKey>'
const ConnectWallet = ({ onWalletCreated }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const connectWallet = () => {
setLoading(true);
setError(null);
const options = {
method: 'POST',
headers: {
Authorization: apiKey,
},
};
axios.post('/wallets', {}, options)
.then(response => {
setAuthToken(apiKey);
const { id, address } = response.data;
onWalletCreated({ id, address });
setLoading(false);
})
.catch(err => {
setError(err.response ? err.response.data : 'Unknown error');
setLoading(false);
});
};
return (
<div className="p-4 bg-primary text-light">
{loading ? <p>Loading...</p> : <button onClick={connectWallet}>Connect Wallet</button>}
{error && <p className="text-red-500">{error}</p>}
</div>
);
};
export default ConnectWallet;
Página de Criação de AeroNFT src/pages/CreateAeroNFT.js:
import React, { useState } from 'react';
import axios from 'axios';
import { Form, Button, Container, Row, Col } from 'react-bootstrap';
import './CustomStyles.css';
const apiKey = 'Bearer SUA_API_KEY_AQUI';
const CreateAeroNFT = ({ onTokenCreated }) => {
const [name, setName] = useState('');
const [symbol, setSymbol] = useState('');
const [maxSupply, setMaxSupply] = useState('');
const [imageUrl, setImageUrl] = useState('');
const [tokenId, setTokenId] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [responseId, setResponseId] = useState(null);
const [responseAddress, setResponseAddress] = useState(null);
const [milesName, setMilesName] = useState('');
const [milesSymbol, setMilesSymbol] = useState('');
const [milesMaxSupply, setMilesMaxSupply] = useState('');
const [milesTokenId, setMilesTokenId] = useState(null);
const [milesLoading, setMilesLoading] = useState(false);
const [milesError, setMilesError] = useState(null);
const [milesResponseId, setMilesResponseId] = useState(null);
const [milesResponseAddress, setMilesResponseAddress] = useState(null);
const handleCreateAndDeployToken = async () => {
setLoading(true);
setError(null);
try {
const contractResponse = await axios.post(
'https://protocol-sandbox.lumx.io/v2/contracts',
{ name, symbol, type: 'non_fungible' },
{ headers: { Authorization: apiKey, 'Content-Type': 'application/json' } }
);
const contractId = contractResponse.data.id;
setTokenId(contractId);
await axios.post(
`https://protocol-sandbox.lumx.io/v2/contracts/${contractId}/token-types`,
{
name: 'Supply',
description: 'setSupply',
maxSupply: parseInt(maxSupply, 10),
},
{ headers: { Authorization: apiKey, 'Content-Type': 'application/json' } }
);
await axios.post(
`https://protocol-sandbox.lumx.io/v2/contracts/${contractId}/deploy`,
{},
{ headers: { Authorization: apiKey, 'Content-Type': 'application/json' } }
);
const pollInterval = setInterval(async () => {
try {
const pollResponse = await axios.get(`https://protocol-sandbox.lumx.io/v2/contracts/${contractId}`, {
headers: {
Authorization: apiKey,
'Content-Type': 'application/json',
},
});
const pollResult = pollResponse.data;
if (pollResult && pollResult.id && pollResult.address) {
setResponseId(pollResult.id);
setResponseAddress(pollResult.address);
setTokenId(pollResult.id);
clearInterval(pollInterval);
setLoading(false);
}
} catch (pollError) {
console.error(pollError);
setError(pollError.response ? JSON.stringify(pollError.response.data) : 'Unknown error');
clearInterval(pollInterval);
setLoading(false);
}
}, 5000);
} catch (error) {
console.error(error);
setError(error.response ? JSON.stringify(error.response.data) : 'Unknown error');
setLoading(false);
}
};
const handleCreateAndDeployMilesToken = async () => {
setMilesLoading(true);
setMilesError(null);
try {
const contractResponse = await axios.post(
'https://protocol-sandbox.lumx.io/v2/contracts',
{ name: milesName, symbol: milesSymbol, type: 'fungible' },
{ headers: { Authorization: apiKey, 'Content-Type': 'application/json' } }
);
const contractId = contractResponse.data.id;
setMilesTokenId(contractId);
await axios.post(
`https://protocol-sandbox.lumx.io/v2/contracts/${contractId}/token-types`,
{
name: 'Miles',
description: 'Miles Supply',
maxSupply: parseInt(milesMaxSupply, 10),
},
{ headers: { Authorization: apiKey, 'Content-Type': 'application/json' } }
);
await axios.post(
`https://protocol-sandbox.lumx.io/v2/contracts/${contractId}/deploy`,
{},
{ headers: { Authorization: apiKey, 'Content-Type': 'application/json' } }
);
const pollInterval = setInterval(async () => {
try {
const pollResponse = await axios.get(`https://protocol-sandbox.lumx.io/v2/contracts/${contractId}`, {
headers: {
Authorization: apiKey,
'Content-Type': 'application/json',
},
});
const pollResult = pollResponse.data;
if (pollResult && pollResult.id && pollResult.address) {
setMilesResponseId(pollResult.id);
setMilesResponseAddress(pollResult.address);
setMilesTokenId(pollResult.id);
clearInterval(pollInterval);
setMilesLoading(false);
// Passando IDs para o App.js
if (tokenId && pollResult.id) {
onTokenCreated(tokenId, pollResult.id);
}
}
} catch (pollError) {
console.error(pollError);
setMilesError(pollError.response ? JSON.stringify(pollError.response.data) : 'Unknown error');
clearInterval(pollInterval);
setMilesLoading(false);
}
}, 5000);
} catch (error) {
console.error(error);
setMilesError(error.response ? JSON.stringify(error.response.data) : 'Unknown error');
setMilesLoading(false);
}
};
return (
<Container>
<Row className="justify-content-md-center">
<Col md="8">
<div className="custom-form-container">
<div className="custom-form-content">
<h3 className="custom-form-title">Criar Itinerário</h3>
<Form>
<Form.Group controlId="formTokenName">
<Form.Label className="custom-form-label">Nome: </Form.Label>
<Form.Control
type="text"
placeholder="Nome do Token"
value={name}
onChange={e => setName(e.target.value)}
className="custom-form-input"
/>
</Form.Group>
<Form.Group controlId="formTokenSymbol">
<Form.Label className="custom-form-label">Símbolo: </Form.Label>
<Form.Control
type="text"
placeholder="Símbolo do Token"
value={symbol}
onChange={e => setSymbol(e.target.value)}
className="custom-form-input"
/>
</Form.Group>
<Form.Group controlId="formMaxSupply">
<Form.Label className="custom-form-label">Máximo de Passageiros: </Form.Label>
<Form.Control
type="text"
placeholder="Max Supply"
value={maxSupply}
onChange={e => setMaxSupply(e.target.value)}
className="custom-form-input"
/>
</Form.Group>
<Form.Group controlId="formImageUrl">
<Form.Label className="custom-form-label">URL da Imagem: </Form.Label>
<Form.Control
type="text"
placeholder="URL da Imagem"
value={imageUrl}
onChange={e => setImageUrl(e.target.value)}
className="custom-form-input"
/>
</Form.Group>
<Button className="custom-form-button" onClick={handleCreateAndDeployToken} disabled={loading}>
{loading ? 'Criando e Deployando...' : 'Criar e Deployar Avião NFT'}
</Button>
{tokenId && <p className="mt-3 custom-form-token-id">Avião ID: {tokenId}</p>}
{responseId && <p className="mt-3">Avião ID: {responseId}</p>}
{responseAddress && <p className="mt-3">Endereço do Avião na Blockchain: {responseAddress}</p>}
{error && <p className="mt-3 custom-form-error">Erro: {error}</p>}
</Form>
</div>
<div>
<img src={imageUrl || 'https://s1.1zoom.me/b5062/263/Passenger_Airplanes_Evening_Flight_Asphalt_Takeoff_516323_3840x2160.jpg'} alt="NFT" className="custom-form-image" />
{responseAddress && (
<div className="emitir-voo-image">
<img src={AviaoImg} alt="Avião" className="custom-form-image" />
</div>
)}
</div>
</div>
<div className="custom-form-container mt-4">
<div className="custom-form-content">
<h3 className="custom-form-title">Emitir Pontos para Milhas: </h3>
<Form>
<Form.Group controlId="formMilesName">
<Form.Label className="custom-form-label">Nome dos Pontos: </Form.Label>
<Form.Control
type="text"
placeholder="Nome dos Pontos"
value={milesName}
onChange={e => setMilesName(e.target.value)}
className="custom-form-input"
/>
</Form.Group>
<Form.Group controlId="formMilesSymbol">
<Form.Label className="custom-form-label">Símbolo dos Pontos: </Form.Label>
<Form.Control
type="text"
placeholder="Símbolo dos Pontos"
value={milesSymbol}
onChange={e => setMilesSymbol(e.target.value)}
className="custom-form-input"
/>
</Form.Group>
<Form.Group controlId="formMilesMaxSupply">
<Form.Label className="custom-form-label">Máximo de Pontos: </Form.Label>
<Form.Control
type="text"
placeholder="Max Supply"
value={milesMaxSupply}
onChange={e => setMilesMaxSupply(e.target.value)}
className="custom-form-input"
/>
</Form.Group>
<Button className="custom-form-button" onClick={handleCreateAndDeployMilesToken} disabled={milesLoading}>
{milesLoading ? 'Criando e Deployando...' : 'Criar e Deployar Pontos'}
</Button>
{milesTokenId && <p className="mt-3 custom-form-token-id">Token ID: {milesTokenId}</p>}
{milesResponseId && <p className="mt-3">Token ID: {milesResponseId}</p>}
{milesResponseAddress && <p className="mt-3">Endereço do Token na Blockchain: {milesResponseAddress}</p>}
{milesError && <p className="mt-3 custom-form-error">Erro: {milesError}</p>}
</Form>
</div>
{milesResponseAddress && (
<div className="emitir-milhas-image">
<img src={MilhasImg} alt="Milhas" className="custom-form-image" />
</div>
)}
</div>
</Col>
</Row>
</Container>
);
};
export default CreateAeroNFT;
Página de Mint de AeroNFT e Pontos src/pages/MintToken.js:
import React, { useState } from 'react';
import axios from 'axios';
import { Form, Button, Container, Row, Col } from 'react-bootstrap';
import LumxImg from './Lumx.png'; //insira sua foto
import AviaoImg from './Foto-Avião-PNG.png'; // Importando a imagem do avião
const MintToken = ({ walletId, aeroTokenId, pointsTokenId }) => {
const [quantity, setQuantity] = useState(1);
const [uriNumber, setUriNumber] = useState(0);
const [transactionHashes, setTransactionHashes] = useState({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [mintCompleted, setMintCompleted] = useState(false); // Para controle da exibição da imagem do avião
const API_KEY = 'Bearer sua apiKey'; // Substitua pelo seu API key
const mintToken = async (contractId, tokenType) => {
try {
const response = await axios.post('https://protocol-sandbox.lumx.io/v2/transactions/mints', {
walletId,
quantity,
uriNumber,
contractId,
}, {
headers: {
Authorization: API_KEY,
'Content-Type': 'application/json',
},
});
const result = response.data;
const pollInterval = setInterval(async () => {
try {
const pollResponse = await axios.get(`https://protocol-sandbox.lumx.io/v2/transactions/${result.id}`, {
headers: {
Authorization: API_KEY,
'Content-Type': 'application/json',
},
});
const pollResult = pollResponse.data;
if (pollResult && pollResult.transactionHash) {
setTransactionHashes(prev => ({ ...prev, [tokenType]: pollResult.transactionHash }));
clearInterval(pollInterval);
if (tokenType === 'Pontos') setMintCompleted(true); // Marca o fim da mintagem
}
} catch (pollError) {
console.error(pollError);
setError(pollError.response ? JSON.stringify(pollError.response.data) : 'Erro desconhecido');
clearInterval(pollInterval);
}
}, 5000);
} catch (error) {
console.error(error);
setError(error.response ? JSON.stringify(error.response.data) : 'Erro desconhecido');
} finally {
setLoading(false);
}
};
const handleMint = async () => {
setLoading(true);
setTransactionHashes({});
setError(null);
try {
await mintToken(aeroTokenId, 'Avião');
await mintToken(pointsTokenId, 'Pontos');
} catch (err) {
setError('Ocorreu um erro durante o processo de minting.');
setLoading(false);
}
};
return (
<Container>
<Row className="justify-content-md-center">
<Col md="8">
<div className="custom-form-container">
<div className="custom-form-content">
<h3 className="custom-form-title">Compre sua passagem e ganhe pontos do nosso programa de fidelidade</h3>
<Form>
<Form.Group controlId="formWalletId">
<Form.Label className="custom-form-label">Wallet ID: </Form.Label>
<Form.Control
type="text"
value={walletId}
disabled
placeholder="Wallet ID"
className="custom-form-input"
/>
</Form.Group>
<Form.Group controlId="formQuantity">
<Form.Label className="custom-form-label">Quantidade de Passagens: </Form.Label>
<Form.Control
type="number"
value={quantity}
onChange={e => setQuantity(parseInt(e.target.value, 10))}
placeholder="Insira a Quantidade"
className="custom-form-input"
/>
</Form.Group>
<Form.Group controlId="formUriNumber">
<Form.Label className="custom-form-label">Numero do Avião: </Form.Label>
<Form.Control
type="number"
value={uriNumber}
onChange={e => setUriNumber(parseInt(e.target.value, 10))}
placeholder="Insira o URI Number"
className="custom-form-input"
/>
</Form.Group>
<Button className="custom-form-button" onClick={handleMint} disabled={loading || !walletId || !aeroTokenId || !pointsTokenId}>
{loading ? 'Minting...' : 'Mint Token e Pontos'}
</Button>
{transactionHashes.Avião && <p className="mt-3">Transaction Hash Avião: {transactionHashes.Avião}</p>}
{transactionHashes.Pontos && <p className="mt-3">Transaction Hash Pontos: {transactionHashes.Pontos}</p>}
{error && <p className="mt-3 custom-form-error">Erro: {error}</p>}
</Form>
</div>
<div className="mint-image-container">
<img src={LumxImg} alt="Loyalty Program" className="mint-form-image" />
</div>
</div>
</Col>
</Row>
{mintCompleted && (
<Row className="justify-content-md-center mt-4">
<Col md="8">
<div className="mint-completed-container">
<img src={AviaoImg} alt="Avião" className="mint-completed-image" />
</div>
</Col>
</Row>
)}
</Container>
);
};
export default MintToken;
Estilos Personalizados src/CustomStyles.css:
.custom-form-container {
background-color: #f0f4f8;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.custom-form-content {
flex: 1;
}
.custom-form-label {
font-weight: bold;
color: #04070f;
}
.custom-form-input {
border: 2px solid #131313;
border-radius: 5px;
padding: 10px;
margin-bottom: 30px;
}
.custom-form-button {
background-color: #1e40af;
border: none;
color: white;
padding: 10px 20px;
border-radius: 100px;
cursor: pointer;
}
.custom-form-button:hover {
background-color: #1a368a;
}
.custom-form-error {
color: #e3342f;
font-weight: bold;
}
.custom-form-token-id {
color: #38a169;
font-weight: bold;
}
.custom-form-image {
max-width: 400px;
border-radius: 30px;
}
.custom-form-title {
color: #1e40af;
text-align: center;
font-weight: bold;
margin-bottom: 100px;
}
.emitir-voo-image {
max-width: 200px;
margin-left: 2px;
border-radius: 1px;
}
.emitir-milhas-image {
max-width: 1000px;
margin-left: 10px;
border-radius: 30px;
}
.mint-form-image {
max-width: 400px;
border-radius: 20px;
}
.custom-form-title {
color: #1e40af;
text-align: center;
font-weight: bold;
margin-bottom: 20px;
}
.mint-image-container {
max-width: 400px;
margin-left: 20px;
display: flex;
align-items: center;
}
.mint-completed-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
.mint-completed-image {
max-width: 400px;
border-radius: 20px;
}
.logo-image {
max-height: 40px; /* Ajuste o tamanho conforme necessário */
margin-right: 15px; /* Espaço entre a logo e o texto */
}
header .text-xl {
margin-left: 15px; /* Espaço entre a logo e o título */
}
Edite src/App.js
para configurar as rotas:
import React, { useState } from 'react';
import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom';
import ConnectWallet from './components/ConnectWallet';
import CreateAeroNFT from './pages/CreateAeroNFT';
import MintToken from './pages/MintToken';
import Logo from './Logo_03.png'; // insira sua logo
const App = () => {
const [wallet, setWallet] = useState(null);
const [aeroTokenId, setAeroTokenId] = useState(null);
const [pointsTokenId, setPointsTokenId] = useState(null);
return (
<Router>
<div className="min-h-screen bg-light text-secondary">
<header className="p-4 bg-primary text-light flex justify-between items-center">
<div className="flex items-center">
<img src={Logo} alt="Logo" className="logo-image" />
<h1 className="text-xl ml-4">Airline Loyalty</h1>
</div>
<div className="flex items-center">
<Link to="/create-nft" className="text-white mr-4">Create NFT</Link>
<Link to="/flyNow" className="text-white mr-4">Fly Now</Link>
{wallet ? (
<span>Wallet ID: {wallet.id} - Address: {wallet.address}</span>
) : (
<ConnectWallet onWalletCreated={setWallet} />
)}
</div>
</header>
<main className="p-4">
<Routes>
<Route
path="/create-nft"
element={<CreateAeroNFT onTokenCreated={(aeroId, pointsId) => {
setAeroTokenId(aeroId);
setPointsTokenId(pointsId);
}} />}
/>
<Route
path="/flyNow"
element={<MintToken walletId={wallet?.id} aeroTokenId={aeroTokenId} pointsTokenId={pointsTokenId} />}
/>
</Routes>
</main>
</div>
</Router>
);
};
export default App;
Feito isso, execute o comando para iniciar seu projeto localmente:
npm start
Parabéns! seu projeto foi concluído. Agora você já pode pegar todo o aprendizado e desenvolver seus próprios projetos utilizando as rotas do protocolo da Lumx.
Repositorio: https://github.com/Afonsodalvi/Lumx-loyalty
Obrigado por ler até aqui! 💜
Me despeço por hora, desejando a todos um ótima quinta-feira.
Até breve,