Ver3.0 ๐ ๋ผ๋ผ๋ฒจ์์ ERC-20 ํ ํฐ ์์ฑํ๊ณ ๋ฐฐํฌ๊น์ง ์๋ฒฝ ๊ฐ์ด๋

๐ ๋ผ๋ผ๋ฒจ์์ ERC-20 ํ ํฐ ์์ฑํ๊ณ ๋ฐฐํฌ๊น์ง ์๋ฒฝ ๊ฐ์ด๋
PHP ํ๋ ์์ํฌ์ ๋ธ๋ก์ฒด์ธ์ ํ์์ ์ธ ๋ง๋จ! ๐
์๋
ํ์ธ์! ๐ ์ค๋์ ์ง์ง ํซํ ์ฃผ์ ๋ก ์ฐพ์์์ด์ ใ
ใ
ใ
๋ผ๋ผ๋ฒจ(Laravel)์ด๋ผ๋ PHP ํ๋ ์์ํฌ๋ก ERC-20 ํ ํฐ์ ๋ง๋ค๊ณ ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ์๋ ค๋๋ฆด๊ฒ์!
"์ด? ๋ผ๋ผ๋ฒจ์ ์น ๊ฐ๋ฐ ํ๋ ์์ํฌ์ธ๋ฐ ๋ธ๋ก์ฒด์ธ์ด๋ ๋ฌด์จ ์๊ด์ด์ผ?" ๋ผ๊ณ ์๊ฐํ์ค ์ ์๋๋ฐ์,
์ฌ์ค ๋ผ๋ผ๋ฒจ์ ๋ฐฑ์๋๋ก ํ์ฉํ๋ฉด์ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ๊ฑด ์๊ฐ๋ณด๋ค ๊ฐ๋จํ๋ต๋๋ค! ๐ก
์ด ๊ธ์์๋ ์ค์ ์ฝ๋์ ํจ๊ป ์ฒ์๋ถํฐ ๋๊น์ง ์ฐจ๊ทผ์ฐจ๊ทผ ์๋ ค๋๋ฆด ๊ฑฐ์์~
๐ค ERC-20 ํ ํฐ์ด ๋ญ๋ฐ?
๋จผ์ ๊ธฐ๋ณธ๋ถํฐ ์ง๊ณ ๋์ด๊ฐ๊ฒ์!
ERC-20์ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ์์ ํ ํฐ์ ๋ง๋ค ๋ ๋ฐ๋ผ์ผ ํ๋ ํ์ค ๊ท๊ฒฉ์ด์์.
์ฝ๊ฒ ๋งํ๋ฉด "ํ ํฐ ๋ง๋ค ๋ ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ๊ผญ ๋ฃ์ด์ผ ํด!"๋ผ๊ณ ์ ํด๋์ ๋ฃฐ์ด์ฃ ใ
ใ
ใ
๋๋ถ์ ๋ชจ๋ ERC-20 ํ ํฐ์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์๋ํด์ ํธํ์ฑ์ด ์์ฒญ ์ข์์! ๐ฏ
โข totalSupply() - ์ ์ฒด ํ ํฐ ๋ฐํ๋ ์กฐํ
โข balanceOf(address) - ํน์ ์ฃผ์์ ํ ํฐ ์์ก ์กฐํ
โข transfer(to, amount) - ํ ํฐ ์ ์ก
โข approve(spender, amount) - ํ ํฐ ์ฌ์ฉ ๊ถํ ๋ถ์ฌ
โข transferFrom(from, to, amount) - ๊ถํ ๋ฐ์ ํ ํฐ ์ ์ก
โข allowance(owner, spender) - ์น์ธ๋ ํ ํฐ๋ ์กฐํ
์ด 6๊ฐ์ง ํจ์๋ง ๊ตฌํํ๋ฉด ERC-20 ํ ํฐ ์์ฑ! ๐ช
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ค๋นํ๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํ๊ธฐ ์ ์ ํ์ํ ๊ฒ๋ค์ ์ค๋นํด๋ณผ๊ฒ์!
์๊ฐ๋ณด๋ค ์ค๋น๋ฌผ์ด ๋ง์ง๋ง ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ค์นํ๋ฉด ์ด๋ ต์ง ์์์ ๐
1. PHP 8.0 ์ด์ - ๋ผ๋ผ๋ฒจ ์ต์ ๋ฒ์ ๊ตฌ๋์ ์ํด ํ์ํด์
2. Composer - PHP ํจํค์ง ๊ด๋ฆฌ์ (๋ผ๋ผ๋ฒจ ์ค์น์ ํ์!)
3. Node.js & NPM - ํ๋ก ํธ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ด๋ฆฌ์ฉ
4. MySQL ๋๋ PostgreSQL - ๋ฐ์ดํฐ๋ฒ ์ด์ค (ํ ํฐ ๊ฑฐ๋ ๋ด์ญ ์ ์ฅ์ฉ)
5. MetaMask - ์ด๋๋ฆฌ์ ์ง๊ฐ (ํ ์คํธ์ฉ ํ์ ์์ดํ ! ๐ฆ)
composer create-project laravel/laravel erc20-token-project
cd erc20-token-project
php artisan serve
์ด๋ ๊ฒ ํ๋ฉด http://localhost:8000 ์์ ๋ผ๋ผ๋ฒจ์ด ์คํ๋ผ์!
"Laravel" ํ์ ํ์ด์ง๊ฐ ๋ณด์ด๋ฉด ์ฑ๊ณต ใ
ใ
ใ
๐
๋ผ๋ผ๋ฒจ์์ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ค๋ฉด Web3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํด์!
PHP์ฉ Web3 ํจํค์ง๋ฅผ ์ค์นํด๋ณผ๊ฒ์:
composer require sc0vu/web3.php
composer require kornrunner/keccak
web3.php๋ PHP์์ ์ด๋๋ฆฌ์ ๋
ธ๋์ ํต์ ํ ์ ์๊ฒ ํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์!
keccak์ ์ด๋๋ฆฌ์ ์ฃผ์ ์์ฑํ ๋ ํ์ํ ํด์ ํจ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ตฌ์ ๐
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ๊ด๋ จ ๋ฉํ ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์!
์ค์ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์์๊ฒ ์ง์ ๋ฐฐ์ฐ๋ฉด ์ฝ์ง ์๊ฐ์ ์์ฒญ ์ค์ผ ์ ์๊ฑฐ๋ ์ ใ ใ
๐ ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑํ๊ธฐ
์ด์ ์ง์ง ํต์ฌ์ธ ERC-20 ํ ํฐ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ์์ฑํด๋ณผ๊ฒ์!
์ค๋งํธ ์ปจํธ๋ํธ๋ Solidity๋ผ๋ ์ธ์ด๋ก ์์ฑํ๋๋ฐ, ์๋ฐ์คํฌ๋ฆฝํธ๋ ๋น์ทํด์ ๋ฐฐ์ฐ๊ธฐ ์ฌ์์ ๐
์ค๋งํธ ์ปจํธ๋ํธ๋ ํ๋ฒ ๋ฐฐํฌํ๋ฉด ์์ ์ด ๋ถ๊ฐ๋ฅํด์!
๊ทธ๋์ ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ํ ์คํธํ ํ์ ๋ฉ์ธ๋ท์ ๋ฐฐํฌํด์ผ ํฉ๋๋ค!
์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ์ ์ํด Hardhat์ด๋ Truffle ๊ฐ์ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ ์ ์์ด์.
์ฌ๊ธฐ์๋ ์์ฆ ํซํ Hardhat์ ์ฌ์ฉํด๋ณผ๊ฒ์!
npm install --save-dev hardhat
npx hardhat init
์ด๊ธฐํ ๊ณผ์ ์์ "Create a JavaScript project"๋ฅผ ์ ํํ๋ฉด ๋ผ์!
๊ทธ๋ฌ๋ฉด contracts, scripts, test ํด๋๊ฐ ์๋์ผ๋ก ์์ฑ๋ฉ๋๋ค ๐
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ํ ํฐ ์ปจํธ๋ํธ๋ฅผ ์์ฑํด๋ณผ๊ฒ์!
OpenZeppelin ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ์์ ํ๊ณ ๊ฒ์ฆ๋ ERC-20 ๊ตฌํ์ ์ฝ๊ฒ ๊ฐ์ ธ์ฌ ์ ์์ด์ ๐ก๏ธ
npm install @openzeppelin/contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
// ํ ํฐ ์ด๊ธฐ ๋ฐํ๋ (1์ต ๊ฐ)
uint256 private constant INITIAL_SUPPLY = 100000000 * 10**18;
constructor() ERC20("MyAwesomeToken", "MAT") Ownable(msg.sender) {
// ์ปจํธ๋ํธ ๋ฐฐํฌ์์๊ฒ ์ ์ฒด ํ ํฐ ๋ฐํ
_mint(msg.sender, INITIAL_SUPPLY);
}
// ํ ํฐ ์ถ๊ฐ ๋ฐํ ํจ์ (์์ ์๋ง ๊ฐ๋ฅ)
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
// ํ ํฐ ์๊ฐ ํจ์
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}
์ด ์ฝ๋ ์ง์ง ๊ฐ๋จํ์ฃ ? ใ
ใ
ใ
OpenZeppelin ๋๋ถ์ ๋ณต์กํ ERC-20 ๋ก์ง์ ์ง์ ๊ตฌํํ ํ์๊ฐ ์์ด์!
์ฝ๋ ์ค๋ช
:
โข ERC20 - ๊ธฐ๋ณธ ERC-20 ๊ธฐ๋ฅ ์ ๊ณต
โข Ownable - ์์ ์ ๊ถํ ๊ด๋ฆฌ ๊ธฐ๋ฅ
โข constructor - ํ ํฐ ์ด๋ฆ๊ณผ ์ฌ๋ณผ ์ค์ , ์ด๊ธฐ ๋ฐํ
โข mint - ์ถ๊ฐ ํ ํฐ ๋ฐํ (์์ ์๋ง ๊ฐ๋ฅ)
โข burn - ํ ํฐ ์๊ฐ ๊ธฐ๋ฅ
1. ํ ํฐ ์ด๋ฆ - "MyAwesomeToken" ๋ถ๋ถ์ ์ํ๋ ์ด๋ฆ์ผ๋ก ๋ณ๊ฒฝ
2. ํ ํฐ ์ฌ๋ณผ - "MAT" ๋ถ๋ถ์ ์ํ๋ ํฐ์ปค๋ก ๋ณ๊ฒฝ (๋ณดํต 3-4๊ธ์)
3. ์ด๊ธฐ ๋ฐํ๋ - INITIAL_SUPPLY ๊ฐ ์กฐ์
4. ์์์ ์๋ฆฌ - ๊ธฐ๋ณธ 18์๋ฆฌ (์ด๋๋ฆฌ์ ํ์ค)
์๋ฅผ ๋ค์ด "์ฌ๋ฅ์ฝ์ธ(JAENUNG)"์ ๋ง๋ ๋ค๋ฉด:
ERC20("JaenungCoin", "JAENUNG") ์ด๋ ๊ฒ ์์ ํ๋ฉด ๋ผ์! ๐ช
๐งช ์ค๋งํธ ์ปจํธ๋ํธ ํ ์คํธํ๊ธฐ
๋ฐฐํฌํ๊ธฐ ์ ์ ๊ผญ! ํ
์คํธ๋ฅผ ํด์ผ ํด์!
๋ฒ๊ทธ๊ฐ ์๋ ์ปจํธ๋ํธ๋ฅผ ๋ฐฐํฌํ๋ฉด ๋์ด ๋ ์๊ฐ ์ ์๊ฑฐ๋ ์ ๐ฑ
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken Contract", function () {
let myToken;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const MyToken = await ethers.getContractFactory("MyToken");
myToken = await MyToken.deploy();
});
describe("๋ฐฐํฌ ํ
์คํธ", function () {
it("์ฌ๋ฐ๋ฅธ ์ด๋ฆ๊ณผ ์ฌ๋ณผ์ด ์ค์ ๋์ด์ผ ํจ", async function () {
expect(await myToken.name()).to.equal("MyAwesomeToken");
expect(await myToken.symbol()).to.equal("MAT");
});
it("์ด๊ธฐ ๋ฐํ๋์ด ์์ ์์๊ฒ ํ ๋น๋์ด์ผ ํจ", async function () {
const ownerBalance = await myToken.balanceOf(owner.address);
expect(await myToken.totalSupply()).to.equal(ownerBalance);
});
});
describe("ํ ํฐ ์ ์ก ํ
์คํธ", function () {
it("๊ณ์ ๊ฐ ํ ํฐ ์ ์ก์ด ๊ฐ๋ฅํด์ผ ํจ", async function () {
await myToken.transfer(addr1.address, 1000);
const addr1Balance = await myToken.balanceOf(addr1.address);
expect(addr1Balance).to.equal(1000);
});
it("์์ก ๋ถ์กฑ ์ ์ ์ก ์คํจํด์ผ ํจ", async function () {
const initialOwnerBalance = await myToken.balanceOf(owner.address);
await expect(
myToken.connect(addr1).transfer(owner.address, 1)
).to.be.reverted;
expect(await myToken.balanceOf(owner.address)).to.equal(
initialOwnerBalance
);
});
});
describe("Mint ๊ธฐ๋ฅ ํ
์คํธ", function () {
it("์์ ์๋ ์ ํ ํฐ์ ๋ฐํํ ์ ์์ด์ผ ํจ", async function () {
await myToken.mint(addr1.address, 5000);
expect(await myToken.balanceOf(addr1.address)).to.equal(5000);
});
it("์์ ์๊ฐ ์๋ ๊ณ์ ์ ํ ํฐ ๋ฐํ ๋ถ๊ฐ", async function () {
await expect(
myToken.connect(addr1).mint(addr2.address, 5000)
).to.be.reverted;
});
});
});
ํ ์คํธ ์คํ์ ์ด๋ ๊ฒ ํด์:
npx hardhat test
๋ชจ๋ ํ
์คํธ๊ฐ ํต๊ณผํ๋ฉด ์ด๋ก์ ์ฒดํฌ๋งํฌ๊ฐ ์ญ ๋์์! โ
โ
โ
ํ๋๋ผ๋ ์คํจํ๋ฉด ์ฝ๋๋ฅผ ๋ค์ ํ์ธํด์ผ ํฉ๋๋ค ใ
ใ
๐ ํ ์คํธ๋ท์ ๋ฐฐํฌํ๊ธฐ
์ด์ ์ง์ง ๋ธ๋ก์ฒด์ธ์ ๋ฐฐํฌํ ์ฐจ๋ก์์!
์ฒ์์ ํ
์คํธ๋ท(Sepolia, Goerli ๋ฑ)์ ๋ฐฐํฌํด์ ํ
์คํธํ๋ ๊ฒ ์ข์์ ๐ฏ
๋จผ์ ํ๋ก์ ํธ ๋ฃจํธ์ .env ํ์ผ์ ๋ง๋ค์ด์:
PRIVATE_KEY=์ฌ๊ธฐ์_๋ฉํ๋ง์คํฌ_๊ฐ์ธํค_์
๋ ฅ
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID
ETHERSCAN_API_KEY=์ด๋์ค์บ_API_ํค
๊ฐ์ธํค๋ ์ ๋ ๊นํ๋ธ์ ์ฌ๋ฆฌ๋ฉด ์ ๋ผ์!
.gitignore์ .env ํ์ผ์ ๊ผญ ์ถ๊ฐํ์ธ์!
๊ฐ์ธํค๊ฐ ๋ ธ์ถ๋๋ฉด ์ง๊ฐ์ ๋ชจ๋ ์์ฐ์ ์์ ์ ์์ต๋๋ค! ๐
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY],
chainId: 11155111
},
hardhat: {
chainId: 1337
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
const hre = require("hardhat");
async function main() {
console.log("๐ ํ ํฐ ๋ฐฐํฌ๋ฅผ ์์ํฉ๋๋ค...");
// ์ปจํธ๋ํธ ํฉํ ๋ฆฌ ๊ฐ์ ธ์ค๊ธฐ
const MyToken = await hre.ethers.getContractFactory("MyToken");
// ๋ฐฐํฌ ์คํ
console.log("โณ ๋ฐฐํฌ ์ค...");
const myToken = await MyToken.deploy();
await myToken.waitForDeployment();
const address = await myToken.getAddress();
console.log("โ
MyToken ๋ฐฐํฌ ์๋ฃ!");
console.log("๐ ์ปจํธ๋ํธ ์ฃผ์:", address);
// ๋ฐฐํฌ ์ ๋ณด ์ถ๋ ฅ
console.log("\n๐ ํ ํฐ ์ ๋ณด:");
console.log("์ด๋ฆ:", await myToken.name());
console.log("์ฌ๋ณผ:", await myToken.symbol());
console.log("์ด ๋ฐํ๋:", await myToken.totalSupply());
console.log("\n๐ ๋ฐฐํฌ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋์์ต๋๋ค!");
console.log("์ด๋์ค์บ์์ ํ์ธ: https://sepolia.etherscan.io/address/" + address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
๋ฐฐํฌํ๋ ค๋ฉด ๊ฐ์ค๋น๋ก ์ธ ํ
์คํธ๋ท ETH๊ฐ ํ์ํด์!
Sepolia Faucet์์ ๋ฌด๋ฃ๋ก ๋ฐ์ ์ ์์ด์:
โข Sepolia Faucet - https://sepoliafaucet.com/
โข Alchemy Faucet - https://sepoliafaucet.com/
โข Infura Faucet - https://www.infura.io/faucet/sepolia
๋ฉํ๋ง์คํฌ ์ฃผ์๋ฅผ ์ ๋ ฅํ๋ฉด 0.5 ETH ์ ๋ ๋ฐ์ ์ ์์ด์!
ํ๋ฃจ์ ํ ๋ฒ์ฉ ๋ฐ์ ์ ์์ผ๋ ํ์ํ๋ฉด ๋งค์ผ ๋ฐ์ผ์ธ์ ใ ใ ใ
npx hardhat run scripts/deploy.js --network sepolia
์ง์! ๐ ์ด์ ๋น์ ์ ํ ํฐ์ด ๋ธ๋ก์ฒด์ธ์ ์ฌ๋ผ๊ฐ์ด์!
์ถ๋ ฅ๋ ์ปจํธ๋ํธ ์ฃผ์๋ฅผ ์ ์ ์ฅํด๋์ธ์. ๋์ค์ ํ์ํฉ๋๋ค!
๋ฐฐํฌ ํ ์ด๋์ค์บ์์ ์ปจํธ๋ํธ๋ฅผ verify(๊ฒ์ฆ)ํ๋ฉด ์์ค์ฝ๋๊ฐ ๊ณต๊ฐ๋์ด ์ ๋ขฐ๋๊ฐ ์ฌ๋ผ๊ฐ์!
npx hardhat verify --network sepolia ์ปจํธ๋ํธ์ฃผ์ ๋ช
๋ น์ด๋ก ๊ฐ๋จํ ํ ์ ์์ด์!
๐ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ ์ฐ๊ฒฐํ๊ธฐ
๋๋์ด ๋ผ๋ผ๋ฒจ ํํธ์์! ใ
ใ
ใ
์ด์ ๋ผ๋ผ๋ฒจ ๋ฐฑ์๋์์ ๋ฐฐํฌํ ํ ํฐ๊ณผ ์ํธ์์ฉํ๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ์ ๐ฏ
๋ผ๋ผ๋ฒจ์์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ์๋น์ค ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์:
<?php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
class Web3Service
{
private $web3;
private $contract;
private $contractAddress;
private $abi;
public function __construct()
{
// Infura ๋๋ Alchemy RPC URL ์ฌ์ฉ
$rpcUrl = env('ETHEREUM_RPC_URL');
$this->web3 = new Web3($rpcUrl);
$this->contractAddress = env('TOKEN_CONTRACT_ADDRESS');
$this->abi = json_decode(file_get_contents(storage_path('app/abi/MyToken.json')), true);
$this->contract = new Contract($rpcUrl, $this->abi);
}
/**
* ํ ํฐ ์์ก ์กฐํ
*/
public function getBalance($address)
{
$balance = null;
$this->contract->at($this->contractAddress)->call('balanceOf', $address, function ($err, $result) use (&$balance) {
if ($err !== null) {
throw new \Exception('์์ก ์กฐํ ์คํจ: ' . $err->getMessage());
}
$balance = $result[0]->toString();
});
// Wei๋ฅผ Ether๋ก ๋ณํ (18 decimals)
return bcdiv($balance, bcpow('10', '18'), 18);
}
/**
* ํ ํฐ ์ ์ก
*/
public function transfer($fromPrivateKey, $toAddress, $amount)
{
$eth = $this->web3->eth;
// Wei๋ก ๋ณํ
$amountInWei = bcmul($amount, bcpow('10', '18'));
// ํธ๋์ญ์
๋ฐ์ดํฐ ์์ฑ
$data = $this->contract->at($this->contractAddress)->getData('transfer', $toAddress, $amountInWei);
// ๊ฐ์ค ์ถ์
$gas = null;
$eth->estimateGas([
'to' => $this->contractAddress,
'data' => $data
], function ($err, $result) use (&$gas) {
if ($err !== null) {
throw new \Exception('๊ฐ์ค ์ถ์ ์คํจ: ' . $err->getMessage());
}
$gas = $result->toString();
});
// ํธ๋์ญ์
์ ์ก
// ์ค์ ๊ตฌํ์์๋ ์๋ช
๋ฐ ์ ์ก ๋ก์ง ์ถ๊ฐ ํ์
return [
'success' => true,
'gas' => $gas,
'data' => $data
];
}
/**
* ํ ํฐ ์ด ๋ฐํ๋ ์กฐํ
*/
public function getTotalSupply()
{
$supply = null;
$this->contract->at($this->contractAddress)->call('totalSupply', function ($err, $result) use (&$supply) {
if ($err !== null) {
throw new \Exception('์ด ๋ฐํ๋ ์กฐํ ์คํจ: ' . $err->getMessage());
}
$supply = $result[0]->toString();
});
return bcdiv($supply, bcpow('10', '18'), 18);
}
/**
* ํ ํฐ ์ด๋ฆ ์กฐํ
*/
public function getTokenName()
{
$name = null;
$this->contract->at($this->contractAddress)->call('name', function ($err, $result) use (&$name) {
if ($err !== null) {
throw new \Exception('์ด๋ฆ ์กฐํ ์คํจ: ' . $err->getMessage());
}
$name = $result[0];
});
return $name;
}
/**
* ํ ํฐ ์ฌ๋ณผ ์กฐํ
*/
public function getTokenSymbol()
{
$symbol = null;
$this->contract->at($this->contractAddress)->call('symbol', function ($err, $result) use (&$symbol) {
if ($err !== null) {
throw new \Exception('์ฌ๋ณผ ์กฐํ ์คํจ: ' . $err->getMessage());
}
$symbol = $result[0];
});
return $symbol;
}
}
๋ผ๋ผ๋ฒจ์ .env ํ์ผ์ ๋ธ๋ก์ฒด์ธ ๊ด๋ จ ์ค์ ์ ์ถ๊ฐํด์:
ETHEREUM_RPC_URL=https://sepolia.infura.io/v3/YOUR_PROJECT_ID
TOKEN_CONTRACT_ADDRESS=0x๋ฐฐํฌํ_์ปจํธ๋ํธ_์ฃผ์
ETHEREUM_CHAIN_ID=11155111
php artisan make:controller TokenController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\Web3Service;
class TokenController extends Controller
{
private $web3Service;
public function __construct(Web3Service $web3Service)
{
$this->web3Service = $web3Service;
}
/**
* ํ ํฐ ์ ๋ณด ์กฐํ
*/
public function getTokenInfo()
{
try {
$info = [
'name' => $this->web3Service->getTokenName(),
'symbol' => $this->web3Service->getTokenSymbol(),
'totalSupply' => $this->web3Service->getTotalSupply(),
];
return response()->json([
'success' => true,
'data' => $info
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
/**
* ์ฃผ์๋ณ ์์ก ์กฐํ
*/
public function getBalance(Request $request)
{
$request->validate([
'address' => 'required|string|size:42'
]);
try {
$balance = $this->web3Service->getBalance($request->address);
return response()->json([
'success' => true,
'address' => $request->address,
'balance' => $balance
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
/**
* ํ ํฐ ์ ์ก
*/
public function transfer(Request $request)
{
$request->validate([
'from_private_key' => 'required|string',
'to_address' => 'required|string|size:42',
'amount' => 'required|numeric|min:0'
]);
try {
$result = $this->web3Service->transfer(
$request->from_private_key,
$request->to_address,
$request->amount
);
return response()->json([
'success' => true,
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
}
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TokenController;
Route::prefix('token')->group(function () {
Route::get('/info', [TokenController::class, 'getTokenInfo']);
Route::post('/balance', [TokenController::class, 'getBalance']);
Route::post('/transfer', [TokenController::class, 'transfer']);
});
์ด์ API ์๋ํฌ์ธํธ๊ฐ ์ค๋น๋์ด์! ๐
http://localhost:8000/api/token/info ๊ฐ์ ์ฃผ์๋ก ์ ๊ทผํ ์ ์์ด์!
๐จ ํ๋ก ํธ์๋ ๊ตฌํํ๊ธฐ
๋ฐฑ์๋ API๊ฐ ์ค๋น๋์ผ๋ ์ด์ ์ฌ์ฉ์๊ฐ ๋ณผ ํ๋ฉด์ ๋ง๋ค์ด๋ณผ๊ฒ์!
Vue.js๋ React๋ฅผ ์ธ ์๋ ์์ง๋ง, ์ฌ๊ธฐ์๋ ๊ฐ๋จํ๊ฒ Blade ํ
ํ๋ฆฟ์ผ๋ก ํด๋ณผ๊ฒ์ ๐
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ํ ํฐ ๋์๋ณด๋</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 800px; margin: 0 auto; }
.card { background: white; border-radius: 12px; padding: 25px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.card-title { font-size: 24px; font-weight: bold; color: #2d3748; margin-bottom: 20px; }
.info-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #e2e8f0; }
.info-label { font-weight: 600; color: #4a5568; }
.info-value { color: #667eea; font-weight: bold; }
.input-group { margin-bottom: 15px; }
.input-group label { display: block; margin-bottom: 5px; font-weight: 600; color: #4a5568; }
.input-group input { width: 100%; padding: 10px; border: 2px solid #e2e8f0; border-radius: 8px; font-size: 14px; }
.btn { padding: 12px 24px; border: none; border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 16px; }
.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
.btn-primary:hover { opacity: 0.9; }
.loading { text-align: center; padding: 20px; color: #667eea; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1 class="card-title">๐ช ํ ํฐ ์ ๋ณด</h1>
<div id="tokenInfo" class="loading">๋ก๋ฉ ์ค...</div>
</div>
<div class="card">
<h2 class="card-title">๐ฐ ์์ก ์กฐํ</h2>
<div class="input-group">
<label>์ง๊ฐ ์ฃผ์</label>
<input type="text" id="addressInput" placeholder="0x...">
</div>
<button class="btn btn-primary" onclick="checkBalance()">์์ก ํ์ธ</button>
<div id="balanceResult" style="margin-top: 15px;"></div>
</div>
<div class="card">
<h2 class="card-title">๐ค ํ ํฐ ์ ์ก</h2>
<div class="input-group">
<label>๋ฐ๋ ์ฃผ์</label>
<input type="text" id="toAddress" placeholder="0x...">
</div>
<div class="input-group">
<label>์ ์ก ์๋</label>
<input type="number" id="amount" placeholder="0.0">
</div>
<button class="btn btn-primary" onclick="transferToken()">์ ์กํ๊ธฐ</button>
<div id="transferResult" style="margin-top: 15px;"></div>
</div>
</div>
<script>
// ํ์ด์ง ๋ก๋ ์ ํ ํฐ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
window.onload = function() {
loadTokenInfo();
};
async function loadTokenInfo() {
try {
const response = await axios.get('/api/token/info');
const data = response.data.data;
document.getElementById('tokenInfo').innerHTML = `
<div class="info-row">
<span class="info-label">ํ ํฐ ์ด๋ฆ</span>
<span class="info-value">${data.name}</span>
</div>
<div class="info-row">
<span class="info-label">ํ ํฐ ์ฌ๋ณผ</span>
<span class="info-value">${data.symbol}</span>
</div>
<div class="info-row">
<span class="info-label">์ด ๋ฐํ๋</span>
<span class="info-value">${parseFloat(data.totalSupply).toLocaleString()} ${data.symbol}</span>
</div>
`;
} catch (error) {
document.getElementById('tokenInfo').innerHTML =
'<p style="color: red;">์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค.</p>';
}
}
async function checkBalance() {
const address = document.getElementById('addressInput').value;
if (!address || !address.startsWith('0x')) {
alert('์ฌ๋ฐ๋ฅธ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์!');
return;
}
try {
const response = await axios.post('/api/token/balance', { address });
const balance = response.data.balance;
document.getElementById('balanceResult').innerHTML = `
<div style="padding: 15px; background: #f0f4ff; border-radius: 8px;">
<strong>์์ก:</strong> ${parseFloat(balance).toLocaleString()} ํ ํฐ
</div>
`;
} catch (error) {
document.getElementById('balanceResult').innerHTML =
'<p style="color: red;">์์ก ์กฐํ ์คํจ</p>';
}
}
async function transferToken() {
const toAddress = document.getElementById('toAddress').value;
const amount = document.getElementById('amount').value;
if (!toAddress || !amount) {
alert('๋ชจ๋ ํ๋๋ฅผ ์
๋ ฅํด์ฃผ์ธ์!');
return;
}
try {
// ์ค์ ๋ก๋ MetaMask ์ฐ๋ ํ์
alert('์ค์ ์ ์ก ๊ธฐ๋ฅ์ MetaMask ์ฐ๋์ด ํ์ํฉ๋๋ค!');
// const response = await axios.post('/api/token/transfer', {
// to_address: toAddress,
// amount: amount
// });
document.getElementById('transferResult').innerHTML = `
<div style="padding: 15px; background: #d1f2eb; border-radius: 8px;">
<strong>์ ์ก ์ค๋น ์๋ฃ!</strong> MetaMask๋ก ์๋ช
ํ์ธ์.
</div>
`;
} catch (error) {
document.getElementById('transferResult').innerHTML =
'<p style="color: red;">์ ์ก ์คํจ</p>';
}
}
</script>
</body>
</html>
<?php
use Illuminate\Support\Facades\Route;
Route::get('/token/dashboard', function () {
return view('token.dashboard');
});
์ด์ http://localhost:8000/token/dashboard๋ก ์ ์ํ๋ฉด
๊น๋ํ ํ ํฐ ๋์๋ณด๋๋ฅผ ๋ณผ ์ ์์ด์! ๐จ
๐ฆ MetaMask ์ฐ๋ํ๊ธฐ
์ง์ง ๋ธ๋ก์ฒด์ธ ์ฑ์ฒ๋ผ ๋ง๋ค๋ ค๋ฉด MetaMask ์ฐ๋์ด ํ์์์!
์ฌ์ฉ์๊ฐ ์ง์ ์ง๊ฐ์ผ๋ก ์๋ช
ํ๊ณ ํธ๋์ญ์
์ ๋ณด๋ผ ์ ์๊ฒ ํด๋ณผ๊ฒ์ ๐
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
<script>
let provider;
let signer;
let contract;
const contractAddress = '0x๋ฐฐํฌํ_์ปจํธ๋ํธ_์ฃผ์';
const contractABI = [
// ABI๋ ์ปดํ์ผ ํ ์์ฑ๋ JSON์์ ๊ฐ์ ธ์ค๊ธฐ
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"function name() view returns (string)",
"function symbol() view returns (string)"
];
// MetaMask ์ฐ๊ฒฐ
async function connectWallet() {
if (typeof window.ethereum !== 'undefined') {
try {
// ์ง๊ฐ ์ฐ๊ฒฐ ์์ฒญ
await window.ethereum.request({ method: 'eth_requestAccounts' });
provider = new ethers.providers.Web3Provider(window.ethereum);
signer = provider.getSigner();
contract = new ethers.Contract(contractAddress, contractABI, signer);
const address = await signer.getAddress();
console.log('์ฐ๊ฒฐ๋ ์ฃผ์:', address);
alert('์ง๊ฐ ์ฐ๊ฒฐ ์ฑ๊ณต! ๐');
// ์์ก ์๋ ์กฐํ
await updateBalance(address);
} catch (error) {
console.error('์ฐ๊ฒฐ ์คํจ:', error);
alert('์ง๊ฐ ์ฐ๊ฒฐ์ ์คํจํ์ต๋๋ค.');
}
} else {
alert('MetaMask๋ฅผ ์ค์นํด์ฃผ์ธ์!');
window.open('https://metamask.io/download/', '_blank');
}
}
// ์์ก ์กฐํ
async function updateBalance(address) {
try {
const balance = await contract.balanceOf(address);
const formattedBalance = ethers.utils.formatEther(balance);
document.getElementById('userBalance').innerHTML = `
<strong>๋ด ์์ก:</strong> ${parseFloat(formattedBalance).toLocaleString()} ํ ํฐ
`;
} catch (error) {
console.error('์์ก ์กฐํ ์คํจ:', error);
}
}
// ํ ํฐ ์ ์ก
async function sendToken() {
const toAddress = document.getElementById('sendToAddress').value;
const amount = document.getElementById('sendAmount').value;
if (!toAddress || !amount) {
alert('์ฃผ์์ ์๋์ ์
๋ ฅํด์ฃผ์ธ์!');
return;
}
try {
// Wei๋ก ๋ณํ
const amountInWei = ethers.utils.parseEther(amount);
// ํธ๋์ญ์
์ ์ก
const tx = await contract.transfer(toAddress, amountInWei);
alert('ํธ๋์ญ์
์ ์ก ์ค... โณ');
// ํธ๋์ญ์
์๋ฃ ๋๊ธฐ
await tx.wait();
alert('์ ์ก ์๋ฃ! โ
');
// ์์ก ์
๋ฐ์ดํธ
const address = await signer.getAddress();
await updateBalance(address);
} catch (error) {
console.error('์ ์ก ์คํจ:', error);
alert('์ ์ก์ ์คํจํ์ต๋๋ค: ' + error.message);
}
}
// ๋คํธ์ํฌ ๋ณ๊ฒฝ ๊ฐ์ง
window.ethereum.on('chainChanged', () => {
window.location.reload();
});
// ๊ณ์ ๋ณ๊ฒฝ ๊ฐ์ง
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
alert('์ง๊ฐ ์ฐ๊ฒฐ์ด ํด์ ๋์์ต๋๋ค.');
} else {
updateBalance(accounts[0]);
}
});
</script>
์ด์ ์ฌ์ฉ์๊ฐ MetaMask๋ก ์ง์ ํธ๋์ญ์
์ ์๋ช
ํ ์ ์์ด์!
๋ผ๋ผ๋ฒจ ๋ฐฑ์๋๋ ์ฝ๊ธฐ ์ ์ฉ ์์
๋ง ํ๊ณ , ์ฐ๊ธฐ ์์
์ ํด๋ผ์ด์ธํธ์์ ์ฒ๋ฆฌํ๋ ๊ฑฐ์ฃ ใ
ใ
ใ
๊ฐ์ธํค๋ฅผ ์๋ฒ์ ์ ์ฅํ๋ ๊ฑด ๋งค์ฐ ์ํํด์!
ํญ์ ์ฌ์ฉ์๊ฐ ์์ ์ ์ง๊ฐ(MetaMask)์ผ๋ก ์๋ช ํ๋๋ก ๊ตฌํํ์ธ์.
์๋ฒ๋ ์ฝ๊ธฐ ์์ ๊ณผ ๋ฐ์ดํฐ ์ ์ฅ๋ง ๋ด๋นํ๋ ๊ฒ ์์ ํฉ๋๋ค! ๐
๐ ๊ฑฐ๋ ๋ด์ญ ์ ์ฅํ๊ธฐ
๋ธ๋ก์ฒด์ธ์ ๋ชจ๋ ๊ฑฐ๋๋ ์๊ตฌ์ ์ผ๋ก ๊ธฐ๋ก๋์ง๋ง,
์ฌ์ฉ์ ํธ์๋ฅผ ์ํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ์ ์ฅํด๋๋ฉด ์ข์์!
๋น ๋ฅธ ์กฐํ์ ํต๊ณ ๋ถ์์ด ๊ฐ๋ฅํ๊ฑฐ๋ ์ ๐
php artisan make:migration create_token_transactions_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('token_transactions', function (Blueprint $table) {
$table->id();
$table->string('transaction_hash', 66)->unique();
$table->string('from_address', 42);
$table->string('to_address', 42);
$table->decimal('amount', 36, 18);
$table->string('status', 20)->default('pending');
$table->integer('block_number')->nullable();
$table->decimal('gas_used', 20, 0)->nullable();
$table->decimal('gas_price', 20, 0)->nullable();
$table->timestamp('confirmed_at')->nullable();
$table->timestamps();
$table->index('from_address');
$table->index('to_address');
$table->index('status');
});
}
public function down()
{
Schema::dropIfExists('token_transactions');
}
};
php artisan migrate
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TokenTransaction extends Model
{
protected $fillable = [
'transaction_hash',
'from_address',
'to_address',
'amount',
'status',
'block_number',
'gas_used',
'gas_price',
'confirmed_at'
];
protected $casts = [
'amount' => 'decimal:18',
'gas_used' => 'decimal:0',
'gas_price' => 'decimal:0',
'confirmed_at' => 'datetime'
];
// ์ํ๋ณ ์กฐํ
public function scopePending($query)
{
return $query->where('status', 'pending');
}
public function scopeConfirmed($query)
{
return $query->where('status', 'confirmed');
}
public function scopeFailed($query)
{
return $query->where('status', 'failed');
}
// ์ฃผ์๋ณ ์กฐํ
public function scopeByAddress($query, $address)
{
return $query->where('from_address', $address)
->orWhere('to_address', $address);
}
}
<?php
namespace App\Services;
use App\Models\TokenTransaction;
use Carbon\Carbon;
class TransactionService
{
private $web3Service;
public function __construct(Web3Service $web3Service)
{
$this->web3Service = $web3Service;
}
/**
* ํธ๋์ญ์
์ ์ฅ
*/
public function saveTransaction($txHash, $fromAddress, $toAddress, $amount)
{
return TokenTransaction::create([
'transaction_hash' => $txHash,
'from_address' => $fromAddress,
'to_address' => $toAddress,
'amount' => $amount,
'status' => 'pending'
]);
}
/**
* ํธ๋์ญ์
์ํ ์
๋ฐ์ดํธ
*/
public function updateTransactionStatus($txHash)
{
$transaction = TokenTransaction::where('transaction_hash', $txHash)->first();
if (!$transaction) {
return null;
}
try {
// ๋ธ๋ก์ฒด์ธ์์ ํธ๋์ญ์
์ ๋ณด ์กฐํ
$receipt = $this->web3Service->getTransactionReceipt($txHash);
if ($receipt) {
$transaction->update([
'status' => $receipt['status'] ? 'confirmed' : 'failed',
'block_number' => hexdec($receipt['blockNumber']),
'gas_used' => hexdec($receipt['gasUsed']),
'confirmed_at' => Carbon::now()
]);
}
return $transaction;
} catch (\Exception $e) {
return null;
}
}
/**
* ์ฌ์ฉ์๋ณ ๊ฑฐ๋ ๋ด์ญ ์กฐํ
*/
public function getUserTransactions($address, $limit = 20)
{
return TokenTransaction::byAddress($address)
->orderBy('created_at', 'desc')
->limit($limit)
->get();
}
/**
* ๋๊ธฐ ์ค์ธ ํธ๋์ญ์
ํ์ธ
*/
public function checkPendingTransactions()
{
$pending = TokenTransaction::pending()->get();
foreach ($pending as $tx) {
$this->updateTransactionStatus($tx->transaction_hash);
}
}
}
์ด์ ๋ชจ๋ ํ ํฐ ๊ฑฐ๋๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ธฐ๋ก๋ผ์!
์ฌ์ฉ์๋ ์์ ์ ๊ฑฐ๋ ๋ด์ญ์ ๋น ๋ฅด๊ฒ ํ์ธํ ์ ์์ฃ ๐
โก ์ค์๊ฐ ์ด๋ฒคํธ ๋ฆฌ์ค๋
๋ธ๋ก์ฒด์ธ์์ ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ๋ฉด
๋ ์ญ๋์ ์ธ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค ์ ์์ด์!
์๋ฅผ ๋ค์ด ํ ํฐ์ด ์ ์ก๋๋ฉด ์๋์ผ๋ก ์๋ฆผ์ ๋ณด๋ด๋ ๊ฑฐ์ฃ ๐
<?php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
class EventListenerService
{
private $web3;
private $contract;
public function __construct()
{
$rpcUrl = env('ETHEREUM_RPC_URL');
$this->web3 = new Web3($rpcUrl);
$abi = json_decode(file_get_contents(storage_path('app/abi/MyToken.json')), true);
$this->contract = new Contract($rpcUrl, $abi);
}
/**
* Transfer ์ด๋ฒคํธ ๋ฆฌ์ค๋
*/
public function listenTransferEvents($fromBlock = 'latest')
{
$contractAddress = env('TOKEN_CONTRACT_ADDRESS');
// Transfer ์ด๋ฒคํธ ํํฐ
$filter = [
'fromBlock' => $fromBlock,
'toBlock' => 'latest',
'address' => $contractAddress,
'topics' => [
// Transfer(address,address,uint256) ์ด๋ฒคํธ ์๊ทธ๋์ฒ
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
]
];
$this->web3->eth->getLogs($filter, function ($err, $logs) {
if ($err !== null) {
\Log::error('์ด๋ฒคํธ ์กฐํ ์คํจ: ' . $err->getMessage());
return;
}
foreach ($logs as $log) {
$this->processTransferEvent($log);
}
});
}
/**
* Transfer ์ด๋ฒคํธ ์ฒ๋ฆฌ
*/
private function processTransferEvent($log)
{
// ์ด๋ฒคํธ ๋ฐ์ดํฐ ํ์ฑ
$from = '0x' . substr($log->topics[1], 26);
$to = '0x' . substr($log->topics[2], 26);
$amount = hexdec($log->data);
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
\App\Models\TokenTransaction::create([
'transaction_hash' => $log->transactionHash,
'from_address' => $from,
'to_address' => $to,
'amount' => bcdiv($amount, bcpow('10', '18'), 18),
'status' => 'confirmed',
'block_number' => hexdec($log->blockNumber),
'confirmed_at' => now()
]);
// ์๋ฆผ ๋ฐ์ก ๋ฑ ์ถ๊ฐ ์ฒ๋ฆฌ
$this->sendNotification($from, $to, $amount);
}
/**
* ์๋ฆผ ๋ฐ์ก
*/
private function sendNotification($from, $to, $amount)
{
// ์ด๋ฉ์ผ, ํธ์ ์๋ฆผ ๋ฑ ๊ตฌํ
\Log::info("ํ ํฐ ์ ์ก ๊ฐ์ง: {$from} -> {$to}, ์๋: {$amount}");
}
}
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
// 1๋ถ๋ง๋ค ๋๊ธฐ ์ค์ธ ํธ๋์ญ์
ํ์ธ
$schedule->call(function () {
$service = app(\App\Services\TransactionService::class);
$service->checkPendingTransactions();
})->everyMinute();
// 5๋ถ๋ง๋ค ์๋ก์ด ์ด๋ฒคํธ ํ์ธ
$schedule->call(function () {
$service = app(\App\Services\EventListenerService::class);
$service->listenTransferEvents();
})->everyFiveMinutes();
}
}
์ค์ผ์ค๋ฌ๋ฅผ ์คํํ๋ ค๋ฉด:
php artisan schedule:work
์ด์ ๋ธ๋ก์ฒด์ธ์ ๋ชจ๋ ํ ํฐ ์ ์ก์ด ์๋์ผ๋ก ๊ฐ์ง๋๊ณ ์ ์ฅ๋ผ์! ๐ฏ
๐ก๏ธ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๋ธ๋ก์ฒด์ธ ์ฑ์ ๋๊ณผ ์ง๊ฒฐ๋๊ธฐ ๋๋ฌธ์ ๋ณด์์ด ์ ๋ง ์ค์ํด์!
๊ผญ ์ง์ผ์ผ ํ ๋ณด์ ์์น๋ค์ ์ ๋ฆฌํด๋ณผ๊ฒ์ ๐
| ๋ณด์ ํญ๋ชฉ | ์ค๋ช | ๊ตฌํ ๋ฐฉ๋ฒ |
|---|---|---|
| ๊ฐ์ธํค ๊ด๋ฆฌ | ์ ๋ ์๋ฒ์ ์ ์ฅ ๊ธ์ง | MetaMask ๋ฑ ํด๋ผ์ด์ธํธ ์ง๊ฐ ์ฌ์ฉ |
| ํ๊ฒฝ ๋ณ์ | .env ํ์ผ ๋ณด์ | .gitignore ์ถ๊ฐ, ์ํธํ ์ ์ฅ |
| API ์ธ์ฆ | ๋ฌด๋จ ์ ๊ทผ ๋ฐฉ์ง | Laravel Sanctum, JWT ํ ํฐ |
| Rate Limiting | ๊ณผ๋ํ ์์ฒญ ์ฐจ๋จ | Laravel ๋ฏธ๋ค์จ์ด ํ์ฉ |
| ์ ๋ ฅ ๊ฒ์ฆ | ์ ์์ ๋ฐ์ดํฐ ์ฐจ๋จ | Validation Rules ์๊ฒฉํ ์ ์ฉ |
| ์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ์ฌ | ์ฝ๋ ์ทจ์ฝ์ ์ ๊ฒ | ์ ๋ฌธ ๊ฐ์ฌ ์ ์ฒด ์๋ขฐ |
โ ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ํ ์คํธ ์๋ฃ
โ ์ค๋งํธ ์ปจํธ๋ํธ ๋ณด์ ๊ฐ์ฌ ์๋ฃ
โ ๊ฐ์ธํค ๊ด๋ฆฌ ๋ฐฉ์ ์ฌํ์ธ
โ ์๋ฌ ํธ๋ค๋ง ์๋ฒฝํ ๊ตฌํ
โ ๊ฐ์ค๋น ์ต์ ํ ์๋ฃ
โ ๋ฐฑ์ ๋ฐ ๋ณต๊ตฌ ๊ณํ ์๋ฆฝ
โ ๋ฒ์ ๊ฒํ ์๋ฃ (ํ ํฐ ๋ฐํ ๊ด๋ จ ๊ท์ ํ์ธ)
ํ๋๋ผ๋ ๋น ๋จ๋ฆฌ๋ฉด ํฐ ์์ค์ด ๋ฐ์ํ ์ ์์ด์! ๐ฐ
1. Multi-Sig ์ง๊ฐ ์ฌ์ฉ
์ค์ํ ํ ํฐ ๊ด๋ฆฌ๋ ๋ค์ค ์๋ช ์ง๊ฐ์ผ๋ก!
Gnosis Safe ๊ฐ์ ์๋น์ค ํ์ฉํ์ธ์.
2. ํ์๋ฝ ๊ตฌํ
์ค์ํ ๋ณ๊ฒฝ์ฌํญ์ ์ผ์ ์๊ฐ ํ์ ์คํ๋๋๋ก ์ค์
3. ๊ธด๊ธ ์ ์ง ๊ธฐ๋ฅ
๋ฌธ์ ๋ฐ์ ์ ์ปจํธ๋ํธ๋ฅผ ์ผ์ ์ ์งํ ์ ์๋ pause() ํจ์ ๊ตฌํ
4. ์ ๊ทธ๋ ์ด๋ ๊ฐ๋ฅํ ์ปจํธ๋ํธ
Proxy ํจํด์ผ๋ก ๋ฒ๊ทธ ์์ ๊ฐ๋ฅํ๊ฒ ์ค๊ณ
5. ๋ชจ๋ํฐ๋ง ์์คํ
์ด์ ๊ฑฐ๋ ๊ฐ์ง ๋ฐ ์ค์๊ฐ ์๋ฆผ ์์คํ ๊ตฌ์ถ
๐ ์ฑ๋ฅ ์ต์ ํ
๋ธ๋ก์ฒด์ธ ํต์ ์ ๋๋ฆด ์ ์์ด์ ์ต์ ํ๊ฐ ์ค์ํด์!
์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๋ ๋ช ๊ฐ์ง ํ์ ์๋ ค๋๋ฆด๊ฒ์ โก
1. ์บ์ฑ ํ์ฉ
์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ๋ Redis์ ์บ์ฑํ์ธ์!
// ํ ํฐ ์ ๋ณด ์บ์ฑ (1์๊ฐ)
$tokenInfo = Cache::remember('token_info', 3600, function () {
return [
'name' => $this->web3Service->getTokenName(),
'symbol' => $this->web3Service->getTokenSymbol(),
'totalSupply' => $this->web3Service->getTotalSupply()
];
});2. ๋ฐฐ์น ์ฒ๋ฆฌ
์ฌ๋ฌ ์ฃผ์์ ์์ก์ ํ ๋ฒ์ ์กฐํํ๋ Multicall ํจํด ์ฌ์ฉ
3. ๋น๋๊ธฐ ์ฒ๋ฆฌ
์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ์์ ์ Queue๋ก ์ฒ๋ฆฌ
// ํธ๋์ญ์
ํ์ธ ์์
์ ํ๋ก ์ฒ๋ฆฌ
dispatch(new CheckTransactionJob($txHash))->delay(now()->addMinutes(1));4. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ฑ
์์ฃผ ๊ฒ์ํ๋ ์ปฌ๋ผ์ ์ธ๋ฑ์ค ์ถ๊ฐ
5. RPC ๋ ธ๋ ์ ํ
๋น ๋ฅด๊ณ ์์ ์ ์ธ ๋ ธ๋ ์ฌ์ฉ (Infura, Alchemy, QuickNode ๋ฑ)
๐ฐ ๊ฐ์ค๋น ์ต์ ํ
์ด๋๋ฆฌ์ ๊ฐ์ค๋น๋ ๋น์ ์ ์์ด์! ๐ฑ
์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๋ฉด ๊ฐ์ค๋น๋ฅผ ๋ง์ด ์ ์ฝํ ์ ์์ด์!
1. ๋ณ์ ํ์ ์ต์ ํ
uint256 ๋์ uint128 ์ฌ์ฉ (ํ์ํ ๊ฒฝ์ฐ)2. ์ํ ๋ณ์ ์ต์ํ
๋ธ๋ก์ฒด์ธ์ ์ ์ฅํ๋ ๋ฐ์ดํฐ๋ฅผ ์ต์ํํ์ธ์
3. ์ด๋ฒคํธ ํ์ฉ
์ ์ฅ ๋์ ์ด๋ฒคํธ๋ก ๋ก๊ทธ ๋จ๊ธฐ๊ธฐ (ํจ์ฌ ์ ๋ ด!)
4. ๋ฐฐ์น ์ ์ก
์ฌ๋ฌ ๊ฑด์ ํ ๋ฒ์ ์ฒ๋ฆฌํ๋ ํจ์ ๊ตฌํ
function batchTransfer(address[] memory recipients, uint256[] memory amounts) public {
require(recipients.length == amounts.length, "Length mismatch");
for (uint i = 0; i < recipients.length; i++) {
_transfer(msg.sender, recipients[i], amounts[i]);
}
}5. Layer 2 ์๋ฃจ์ ๊ณ ๋ ค
Polygon, Arbitrum ๊ฐ์ L2์ ๋ฐฐํฌํ๋ฉด ๊ฐ์ค๋น๊ฐ 1/100 ์์ค! ๐
๐ ์ค์ ํ์ฉ ์ฌ๋ก
์ด์ ๋ฐฐ์ด ๋ด์ฉ์ ์ค์ ๋ก ์ด๋ป๊ฒ ํ์ฉํ ์ ์๋์ง ์์๋ณผ๊ฒ์!
๋ค์ํ ๋น์ฆ๋์ค ๋ชจ๋ธ์ ์ ์ฉํ ์ ์์ด์ ๐ก
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฌ์ฉ์ ํ๋์ ๋ฐ๋ผ ํ ํฐ์ ๋ณด์์ผ๋ก ์ง๊ธ!
โข ๊ธ ์์ฑ ์ 10 ํ ํฐ
โข ๋๊ธ ์์ฑ ์ 2 ํ ํฐ
โข ์ถ์ฒ ๋ฐ์ผ๋ฉด 5 ํ ํฐ
ํ ํฐ์ ํ๋ซํผ ๋ด์์ ํ๋ฆฌ๋ฏธ์ ๊ธฐ๋ฅ ์ด์ฉ๊ถ์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅ!
๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋๋ ํฌ๋ช ํ๊ณ ์กฐ์ ๋ถ๊ฐ๋ฅํ์ฃ ใ ใ ใ
ERC-20 ํ ํฐ์ ๊ฒฐ์ ์๋จ์ผ๋ก ์ฌ์ฉํ๋ NFT ๊ฑฐ๋์!
โข ์ํ ๊ตฌ๋งค ์ ํ ํฐ์ผ๋ก ๊ฒฐ์
โข ํ๋งค ์์๋ฃ๋ฅผ ํ ํฐ์ผ๋ก ์ง์
โข ์คํ ์ดํนํ๋ฉด ๊ฑฐ๋ ์์๋ฃ ํ ์ธ
๋ผ๋ผ๋ฒจ๋ก ๋ฐฑ์๋ ๊ตฌ์ถํ๊ณ , ํ ํฐ ๊ฒฐ์ ์์คํ ์ฐ๋!
ํ ํฐ ๋ณด์ ์๊ฐ ํ๋ก์ ํธ ์์ฌ๊ฒฐ์ ์ ์ฐธ์ฌ!
โข 1 ํ ํฐ = 1 ํฌํ๊ถ
โข ์ ์ ์์ฑ ๋ฐ ํฌํ
โข ์๋ ์คํ๋๋ ์ค๋งํธ ์ปจํธ๋ํธ
๋ผ๋ผ๋ฒจ๋ก ํฌํ UI ์ ๊ณต, ๋ธ๋ก์ฒด์ธ์์ ์ค์ ํฌํ ์ฒ๋ฆฌ!
ํ๋ก์ ํธ ํ์์์๊ฒ ํ ํฐ ๋ฐํ!
โข ๋ชฉํ ๊ธ์ก ๋ฌ์ฑ ์ ์๋ ๋ถ๋ฐฐ
โข ํ ํฐ์ผ๋ก ํ๋ก์ ํธ ํํ ์ ๊ณต
โข 2์ฐจ ๊ฑฐ๋ ๊ฐ๋ฅ (์ ๋์ฑ ํ๋ณด)
ํฌ๋ช ํ ์๊ธ ๊ด๋ฆฌ์ ์๋ํ๋ ๋ถ๋ฐฐ ์์คํ !
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ค๋๋ผ ์๊ณ ๋ง์ผ์
จ์ด์! ๐๐๐
๋ผ๋ผ๋ฒจ์์ ERC-20 ํ ํฐ์ ์์ฑํ๊ณ ๋ฐฐํฌํ๋ ์ ๊ณผ์ ์ ๋ค๋ค๋ดค๋๋ฐ์,
์๊ฐ๋ณด๋ค ๋ณต์กํ์ง ์์ฃ ? ใ
ใ
ใ
๋ฌผ๋ก ์ค์ ํ๋ก๋์
ํ๊ฒฝ์์๋ ๋ ๋ง์ ๊ณ ๋ ค์ฌํญ์ด ์์ง๋ง,
์ด ๊ฐ์ด๋๊ฐ ์ฌ๋ฌ๋ถ์ ๋ธ๋ก์ฒด์ธ ํ๋ก์ ํธ ์์์ ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ด์! ๐
ํต์ฌ ์ ๋ฆฌ:
โ
ERC-20์ ์ด๋๋ฆฌ์ ํ ํฐ์ ํ์ค ๊ท๊ฒฉ
โ
OpenZeppelin์ผ๋ก ์์ ํ ์ปจํธ๋ํธ ๊ตฌํ
โ
Hardhat์ผ๋ก ๊ฐ๋ฐ ๋ฐ ๋ฐฐํฌ
โ
๋ผ๋ผ๋ฒจ๋ก ๋ฐฑ์๋ API ๊ตฌ์ถ
โ
Web3.php๋ก ๋ธ๋ก์ฒด์ธ ํต์
โ
MetaMask๋ก ์ฌ์ฉ์ ์ง๊ฐ ์ฐ๋
โ
๋ณด์๊ณผ ์ต์ ํ๋ ํ์!
๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์ด ์ฒ์์ด๋ผ๋ฉด ์ฌ๋ฅ๋ท์์ ๊ฒฝํ ๋ง์ ๋ฉํ ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์!
์ค์ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์์๊ฒ ๋ฐฐ์ฐ๋ฉด ์ํ์ฐฉ์ค๋ฅผ ๋ง์ด ์ค์ผ ์ ์๊ฑฐ๋ ์ ๐
์ฌ๋ฌ๋ถ๋ ๋ฉ์ง ํ ํฐ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์ธ์!
Web3 ์ธ์์์ ๋ง๋์! ๐โจ
๐ ์ถํํฉ๋๋ค!
๋น์ ์ ์ด์ ๋ผ๋ผ๋ฒจ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์์
๋๋ค! ๐
๊ณ์ ํ์ตํ๊ณ , ์คํํ๊ณ , ๋ฉ์ง ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์ธ์!
Happy Coding! ๐ป๐
๐ ์ด ๊ธ์ด ๋์์ด ๋์
จ๋์?
์ฌ๋ฅ๋ท์์ ๋ ๋ง์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ์ง์์ ๊ณต์ ํ๊ณ ์์ด์!
๊ถ๊ธํ ์ ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด์ฃผ์ธ์ ๐
ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ, ์ฌ๋ฅ๋ท! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.