환경 : 버추얼박스 , Ubuntu 20.04.1 LTS, vscode, 클레이튼
인프런 강좌에 NFT 개발 환경 구축을 위해서 클레이튼을 사용 하던데
클레이튼 구축이라고 보면 옳다.
출처 ㅣ https://github.com/kkagill/crypto-ytt-starter.git
nodejs 설치 되어 있지 않는 default한 우분투20에서
npm만 설치 해준다.
#apt update #apt install npm # nodejs -v v10.19.0 |
인프런 강의 따라 하는디 디펜던시 하도 심해서 내가 환경 구축에 대한 가이드를 작성중이다.
무조건 nodejs 버전은 10대로 가야 된다.
바로 노드 설치하면 14 , 16이 설치 되어 버리니 npm만 설치하면 nodejs 버전이 10.19가 깔린다.
#git clone https://github.com/kkagill/crypto-ytt-starter.git #cd crypto-ytt-starter/ #npm cache clean --force #npm install |
#npm install -g npm@6.4.1 #npm install -g truffle #truffle version Truffle v5.3.2 (core: 5.3.2) Solidity v0.5.16 (solc-js) Node v10.19.0 Web3.js v1.3.5 #npm install fs |
vscode로 버추얼박스에 설치된 이미지에 remote develoment를 실행시킨다.
webpack.config.js를 아래와 같이 수정해주고
const webpack = require('webpack') const path = require('path') const fs = require('fs') const CopyWebpackPlugin = require("copy-webpack-plugin"); const host = process.env.HOST || '0.0.0.0';
module.exports = { entry: "./src/index.js", mode: 'development', node: { fs: 'empty', net: 'empty', }, output: { filename: "index.js", path: path.resolve(__dirname, 'dist') }, plugins: [ new webpack.DefinePlugin({ DEPLOYED_ADDRESS: JSON.stringify(fs.readFileSync('deployedAddress', 'utf8').replace(/\n|\r/g, "")), DEPLOYED_ABI: fs.existsSync('deployedABI') && fs.readFileSync('deployedABI', 'utf8'),
DEPLOYED_ADDRESS_TOKENSALES: JSON.stringify(fs.readFileSync('deployedAddress_TokenSales', 'utf8').replace(/\n|\r/g, "")), DEPLOYED_ABI_TOKENSALES: fs.existsSync('deployedABI_TokenSales') && fs.readFileSync('deployedABI_TokenSales', 'utf8') }), new CopyWebpackPlugin([{ from: "./src/index.html", to: "index.html"}]) ], devServer: { contentBase: path.join(__dirname, "dist"), compress: true,disableHostCheck: true } } |
index.js
import Caver from "caver-js"; import { Spinner } from 'spin.js';
const config = { rpcURL: 'https://api.baobab.klaytn.net:8651' } const cav = new Caver(config.rpcURL); //const yttContract = new cav.klay.Contract(DEPLOYED_ABI, DEPLOYED_ADDRESS);
const App = { auth: { accessType: 'keystore', keystore: '', password: '' },
//#region 계정 인증
start: async function () { const walletFromSession = sessionStorage.getItem('walletInstance'); if (walletFromSession) { try { cav.klay.accounts.wallet.add(JSON.parse(walletFromSession)); this.changeUI(JSON.parse(walletFromSession)); } catch (e) { sessionStorage.removeItem('walletInstance'); } } },
handleImport: async function () { const fileReader = new FileReader(); fileReader.readAsText(event.target.files[0]); fileReader.onload = (event) => { try { console.log("event.target.result", event.target.result); /* if (!this.checkValidKeystore(event.target.result)) { $('#message').text('유효하지 않은 keystore 파일입니다.'); return; } */ this.auth.keystore = event.target.result; $('#message').text('keystore 통과. 비밀번호를 입력하세요.'); document.querySelector('#input-password').focus(); } catch (event) { $('#message').text('유효하지 않은 keystore 파일입니다.'); return; } } },
handlePassword: async function () { this.auth.password = event.target.value; },
handleLogin: async function () { if (this.auth.accessType === 'keystore') { try { const privateKey = cav.klay.accounts.decrypt(this.auth.keystore, this.auth.password).privateKey; this.integrateWallet(privateKey); } catch (e) { $('#message').text('비밀번호가 일치하지 않습니다.'); } } },
handleLogout: async function () { this.removeWallet(); location.reload(); },
getWallet: function () { if (cav.klay.accounts.wallet.length) { return cav.klay.accounts.wallet[0]; } },
checkValidKeystore: function (keystore) { const parsedKeystore = JSON.parse(keystore); const isValidKeystore = parsedKeystore.version && parsedKeystore.id && parsedKeystore.address && parsedKeystore.crypto;
return isValidKeystore; },
integrateWallet: function (privateKey) { const walletInstance = cav.klay.accounts.privateKeyToAccount(privateKey); cav.klay.accounts.wallet.add(walletInstance) sessionStorage.setItem('walletInstance', JSON.stringify(walletInstance)); this.changeUI(walletInstance); },
reset: function () { this.auth = { keystore: '', password: '' }; },
changeUI: async function (walletInstance) { $('#loginModal').modal('hide'); $("#login").hide(); $('#logout').show(); // ... $('#address').append('<br>' + '<p>' + '내 계정 주소: ' + walletInstance.address + '</p>'); // ... // ... // ... },
removeWallet: function () { cav.klay.accounts.wallet.clear(); sessionStorage.removeItem('walletInstance'); this.reset(); },
showSpinner: function () { var target = document.getElementById('spin'); return new Spinner(opts).spin(target); }, //#endregion
checkTokenExists: async function () {
},
createToken: async function () {
},
mintYTT: async function (videoId, author, dateCreated, hash) {
},
displayMyTokensAndSale: async function (walletInstance) {
},
displayAllTokens: async function (walletInstance) {
},
renderMyTokens: function (tokenId, ytt, metadata) {
},
renderMyTokensSale: function (tokenId, ytt, metadata, price) {
},
renderAllTokens: function (tokenId, ytt, metadata) {
},
approve: function () {
},
cancelApproval: async function () {
},
checkApproval: async function(walletInstance) {
},
sellToken: async function (button) {
},
buyToken: async function (button) {
},
onCancelApprovalSuccess: async function (walletInstance) {
},
isTokenAlreadyCreated: async function (videoId) {
},
getERC721MetadataSchema: function (videoId, title, imgUrl) {
},
getBalanceOf: async function (address) {
},
getTokenOfOwnerByIndex: async function (address, index) {
},
getTokenUri: async function (tokenId) {
},
getYTT: async function (tokenId) {
},
getMetadata: function (tokenUri) {
},
getTotalSupply: async function () {
},
getTokenByIndex: async function (index) {
},
isApprovedForAll: async function (owner, operator) {
},
getTokenPrice: async function (tokenId) {
},
getOwnerOf: async function (tokenId) {
},
getBasicTemplate: function(template, tokenId, ytt, metadata) {
} };
window.App = App;
window.addEventListener("load", function () { App.start(); $("#tabs").tabs().css({'overflow': 'auto'}); });
var opts = { lines: 10, // The number of lines to draw length: 30, // The length of each line width: 17, // The line thickness radius: 45, // The radius of the inner circle scale: 1, // Scales overall size of the spinner corners: 1, // Corner roundness (0..1) color: '#5bc0de', // CSS color or array of colors fadeColor: 'transparent', // CSS color or array of colors speed: 1, // Rounds per second rotate: 0, // The rotation offset animation: 'spinner-line-fade-quick', // The CSS animation name for the lines direction: 1, // 1: clockwise, -1: counterclockwise zIndex: 2e9, // The z-index (defaults to 2000000000) className: 'spinner', // The CSS class to assign to the spinner top: '50%', // Top position relative to parent left: '50%', // Left position relative to parent shadow: '0 0 1px transparent', // Box-shadow for the lines position: 'absolute' // Element positioning }; |
index.html
<!DOCTYPE html> <html lang="en">
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="theme-color" content="#000000"> <title>크립토 유튜브 썸네일</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script src="index.js"></script> <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet'> <link href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet"> </head>
<body> <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <h3 class="text-center">크립토 <span style="color: red;">유튜브</span> 썸네일</h1> <h3 class="text-center"> <code>Collect, Buy and Sell YouTube Thumbnails!</code> <button type="button" class="btn btn-info pull-right" id="login" data-toggle="modal" data-target="#loginModal"> 로그인 </button> <button type="button" class="btn btn-info pull-right" id="logout" style="display: none;" onclick="App.handleLogout()"> 로그아웃 </button> </h3> <div class="text-center" id="address"></div> </div> </div>
<div id="spin"></div> </div>
<!-- Modals -->
<div class="modal fade" tabindex="-1" role="dialog" id="loginModal"> <div class="modal-dialog modal-sm"> <div class="modal-content"> <div class="modal-body"> <div class="form-group"> <label for="keystore">Keystore</label> <input type="file" id="keystore" onchange="App.handleImport()"> </div> <div class="form-group"> <label for="input-password">비밀번호</label> <input type="password" class="form-control" id="input-password" onchange="App.handlePassword()"> <p class="help-block" id="message"></p> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">닫기</button> <button type="button" class="btn btn-primary" onclick="App.handleLogin()">제출</button> </div> </div><!-- /.modal-content --> </div><!-- /.modal-dialog --> </div><!-- /.modal --> </body>
<!-- Templates -->
</html>
<style> @keyframes spinner-line-fade-more { 0%, 100% { opacity: 0; } 1% { opacity: 1; } }
@keyframes spinner-line-fade-quick { 0%, 39%, 100% { opacity: 0.25; } 40% { opacity: 1; } }
@keyframes spinner-line-fade-default { 0%, 100% { opacity: 0.22; } 1% { opacity: 1; } }
.panel-footer { height: 56px; overflow: hidden; } </style> |
소스를 위 소스로 변경해 준다.
npm run dev로 프로젝트를 실행하면 locahost에서는 접속이 가능하다.
root@server:~/crypto-ytt-starter# npm run dev > crypto-youtube-thumbnail-starter@0.0.1 dev /root/crypto-ytt-starter > webpack-dev-server ℹ 「wds」: Project is running at http://localhost:8080/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /root/crypto-ytt-starter/dist ℹ 「wdm」: Hash: e0e5fdf71534985470fa |
외부 접속 가능하게 ngrok로 포워딩 해준다.
#snap install ngrok #ngrok http 8080 |
포워딩 해준 주소 접속하면 접속이 가능하다.
리엑트 개발자들은 더 좋은 방법이 있을거 같다.
ERC721등 여러 함수들은 직접 구현하길 바란다.
'NFT' 카테고리의 다른 글
NFT EIP-721 Non-Fungible Token Standard (0) | 2021.03.11 |
---|