Ver3.0 ๐ ๋ผ๋ผ๋ฒจ๊ณผ ์๋ฆฌ๋ํฐ๋ก ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์๋น์ค ๋ง๋ค๊ธฐ: ์ค์ ๊ฐ์ด๋

๐ ๋ผ๋ผ๋ฒจ๊ณผ ์๋ฆฌ๋ํฐ๋ก ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์๋น์ค ๋ง๋ค๊ธฐ: ์ค์ ๊ฐ์ด๋
์น2.0๊ณผ ์น3.0์ ์๋ฒฝํ ๋ง๋จ, ์ค๋ฌด์์ ๋ฐ๋ก ์ฐ๋ ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์์คํ ๊ตฌ์ถํ๊ธฐ
๐ก ์๋ ! ์ค๋์ ์ ๋ง ํซํ ์ฃผ์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด. ๋ฐ๋ก ๋ผ๋ผ๋ฒจ(Laravel)๊ณผ ์๋ฆฌ๋ํฐ(Solidity)๋ฅผ ํ์ฉํด์ ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์๋น์ค๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ด์ผ. ์์ฆ NFT ๋ง์ผํ๋ ์ด์ค๋ ํฌ๋ฆฝํ ๊ฒฐ์ ์์คํ ์ด ๋์ธ์์? ๊ทผ๋ฐ ๋ง์ ๋ง๋ค๋ ค๊ณ ํ๋ฉด ์ด๋์๋ถํฐ ์์ํด์ผ ํ ์ง ๋ง๋งํ๋๋ผ๊ณ . ๊ทธ๋์ ๋ด๊ฐ ์ง์ ๊ฒฝํํ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์น๊ตฌํํ ์ค๋ช ํ๋ฏ์ด ์ฝ๊ฒ ํ์ด๋ณผ๊ฒ! ๐
๐ฏ ์ ๋ผ๋ผ๋ฒจ๊ณผ ์๋ฆฌ๋ํฐ๋ฅผ ํจ๊ป ์ฌ์ฉํ ๊น?
๋จผ์ ์ด ์กฐํฉ์ด ์ ์ข์์ง๋ถํฐ ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ์ฌ์ค ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์์คํ ์ ๋ง๋ค ๋ ๊ฐ์ฅ ํฐ ๊ณ ๋ฏผ์ด ๋ญ๋๋ฉด, "์์ ๋ธ๋ก์ฒด์ธ๋ง์ผ๋ก ํ ๊น? ์๋๋ฉด ๊ธฐ์กด ์น ๊ธฐ์ ๊ณผ ์์๊น?"์ผ. ๋ต์ ๋ช ํํด. ๋ ๋ค ์จ์ผ ํด! ๐ช
๐ฅ ๋ผ๋ผ๋ฒจ์ ์ญํ
๋ผ๋ผ๋ฒจ์ PHP ๊ธฐ๋ฐ์ ์น ํ๋ ์์ํฌ์ธ๋ฐ, ์ ๋ง ๊ฐ๋ ฅํ ์น๊ตฌ์ผ. ์ฌ์ฉ์ ์ธ์ฆ, ์ธ์
๊ด๋ฆฌ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฒ๋ฆฌ, API ์๋ํฌ์ธํธ ๊ตฌ์ถ ๊ฐ์ ์ ํต์ ์ธ ์น ์๋น์ค ๊ธฐ๋ฅ์ ๋ด๋นํด. ๋ธ๋ก์ฒด์ธ์ ๋๋ฆฌ๊ณ ๋น์ฉ์ด ๋ค๊ธฐ ๋๋ฌธ์, ๋ชจ๋ ๊ฑธ ๋ธ๋ก์ฒด์ธ์ ์ฌ๋ฆด ์ ์๊ฑฐ๋ . ์๋ฅผ ๋ค์ด ์ฌ์ฉ์ ํ๋กํ ์ ๋ณด, ์ฃผ๋ฌธ ๋ด์ญ, ์ํ ์ ๋ณด ๊ฐ์ ๊ฑด ์ผ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ ๊ฒ ํจ์ฌ ํจ์จ์ ์ด์ง.
๋ผ๋ผ๋ฒจ์ ์ฅ์ ์ ์ ๋ฆฌํ๋ฉด:
โ
Eloquent ORM์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ด ์ ๋ง ์ฌ์
โ
๋ฏธ๋ค์จ์ด๋ก ์ธ์ฆ/๊ถํ ๊ด๋ฆฌ๊ฐ ๊น๋ํด
โ
ํ(Queue) ์์คํ
์ผ๋ก ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ
โ
์ด๋ฒคํธ/๋ฆฌ์ค๋๋ก ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ ๊ฐ์ง ๋ฐ ์ฒ๋ฆฌ
โ
ํ๋ถํ ์ํ๊ณ์ ํจํค์ง๋ค
โ๏ธ ์๋ฆฌ๋ํฐ์ ์ญํ
์๋ฆฌ๋ํฐ๋ ์ด๋๋ฆฌ์ ๋ธ๋ก์ฒด์ธ์์ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ์์ฑํ๋ ์ธ์ด์ผ. ์ฌ๊ธฐ์ ์ค์ ๊ฒฐ์ ๋ก์ง, ํ ํฐ ์ ์ก, ์๊ธ ๊ด๋ฆฌ ๊ฐ์ ํต์ฌ ๊ธ์ต ๊ธฐ๋ฅ์ ๊ตฌํํด. ๋ธ๋ก์ฒด์ธ์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ ํฌ๋ช
์ฑ๊ณผ ๋ถ๋ณ์ฑ์ด์์? ๊ฒฐ์ ๋ด์ญ์ด ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋๋ฉด ๋๊ตฌ๋ ์กฐ์ํ ์ ์๊ณ , ๋ชจ๋ ๊ฑฐ๋๊ฐ ํฌ๋ช
ํ๊ฒ ๊ณต๊ฐ๋ผ.
์๋ฆฌ๋ํฐ๋ก ๊ตฌํํ๋ ๊ฒ๋ค:
โ
๊ฒฐ์ ์ปจํธ๋ํธ: ETH๋ ERC-20 ํ ํฐ ๋ฐ๊ธฐ
โ
์์คํฌ๋ก ์์คํ
: ์์ ํ ๊ฑฐ๋๋ฅผ ์ํ ์ค๊ฐ ๋ณด๊ด
โ
ํ๋ถ ๋ก์ง: ์กฐ๊ฑด๋ถ ์๊ธ ๋ฐํ
โ
์์๋ฃ ๋ถ๋ฐฐ: ํ๋ซํผ ์์๋ฃ ์๋ ์ ์ฐ
โ
์ด๋ฒคํธ ๋ฐ์: ๊ฒฐ์ ์๋ฃ ์ ์ด๋ฒคํธ emit
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ธํ ํด์ผ ํด. ์๊ฐ๋ณด๋ค ์ค๋นํ ๊ฒ ๋ง์ผ๋๊น ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผ์๋ด! ๐
1๏ธโฃ ๋ผ๋ผ๋ฒจ ์ค์น ๋ฐ ์ค์
๋ผ๋ผ๋ฒจ์ Composer๋ฅผ ํตํด ์ค์นํ ์ ์์ด. ์ต์ ๋ฒ์ (Laravel 10 ์ด์)์ ์ฌ์ฉํ๋ ๊ฑธ ์ถ์ฒํด.
composer create-project laravel/laravel blockchain-payment
cd blockchain-payment
php artisan serve
์ด๋ ๊ฒ ํ๋ฉด ๊ธฐ๋ณธ ๋ผ๋ผ๋ฒจ ํ๋ก์ ํธ๊ฐ ์์ฑ๋ผ. http://localhost:8000์ ์ ์ํ๋ฉด ๋ผ๋ผ๋ฒจ ์ฐ์ปด ํ์ด์ง๊ฐ ๋ณด์ผ ๊ฑฐ์ผ.
2๏ธโฃ ํ์ ํจํค์ง ์ค์น
๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ค๋ฉด Web3 ๊ด๋ จ ํจํค์ง๊ฐ ํ์ํด. PHP์์๋ web3.php ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ง์ด ์จ.
composer require sc0vu/web3.php
composer require guzzlehttp/guzzle
composer require predis/predis
web3.php: ์ด๋๋ฆฌ์ ๋
ธ๋์ ํต์
guzzle: HTTP ํด๋ผ์ด์ธํธ (API ํธ์ถ์ฉ)
predis: Redis ์บ์ฑ (ํธ๋์ญ์
์ํ ์ถ์ ์ฉ)
3๏ธโฃ ์๋ฆฌ๋ํฐ ๊ฐ๋ฐ ํ๊ฒฝ
์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ๋ฐ์ ์ํด์๋ ๋ณ๋์ ํด์ฒด์ธ์ด ํ์ํด. ์์ฆ์ Hardhat์ด๋ Foundry๋ฅผ ๋ง์ด ์ฐ๋๋ฐ, ๋๋ Hardhat์ ์ถ์ฒํด. ์ด๋ณด์๋ ์ฐ๊ธฐ ์ฝ๊ฑฐ๋ !
mkdir smart-contracts
cd smart-contracts
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init
Hardhat ์ด๊ธฐํ ์ "Create a JavaScript project"๋ฅผ ์ ํํ๋ฉด ๋ผ. ๊ทธ๋ฌ๋ฉด ๊ธฐ๋ณธ ํ๋ก์ ํธ ๊ตฌ์กฐ๊ฐ ์์ฑ๋ผ.
4๏ธโฃ ํ ์คํธ๋ท ์ค์
์ค์ ์ด๋๋ฆฌ์ ๋ฉ์ธ๋ท์์ ํ ์คํธํ๋ฉด ์ง์ง ๋์ด ๋ค์ด๊ฐ๋๊น, ํ ์คํธ๋ท์ ์ฌ์ฉํด์ผ ํด. Sepolia๋ Goerli ๊ฐ์ ํ ์คํธ๋ท์์ ๊ฐ๋ฐํ์!
ํ์ํ ๊ฒ๋ค:
๐ MetaMask ์ง๊ฐ: ํ
์คํธ ๊ณ์ ์์ฑ
๐ง Faucet: ํ
์คํธ ETH ๋ฐ๊ธฐ (๋ฌด๋ฃ!)
๐ Infura/Alchemy: ์ด๋๋ฆฌ์ ๋
ธ๋ ์ ๊ทผ (API ํค ๋ฐ๊ธ)
๐ Etherscan: ํธ๋์ญ์
ํ์ธ์ฉ
Infura๋ Alchemy์ ๊ฐ์
ํ๋ฉด ๋ฌด๋ฃ๋ก API ํค๋ฅผ ๋ฐ์ ์ ์์ด. ์ด๊ฑธ ๋ผ๋ผ๋ฒจ .env ํ์ผ์ ์ ์ฅํด๋์.
ETHEREUM_NETWORK=sepolia
INFURA_API_KEY=your_infura_key_here
CONTRACT_ADDRESS=
WALLET_PRIVATE_KEY=your_test_wallet_private_key
๐ ์ค๋งํธ ์ปจํธ๋ํธ ์์ฑํ๊ธฐ
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์๋ฆฌ๋ํฐ ์ฝ๋๋ฅผ ์์ฑํด๋ณผ ์๊ฐ์ด์ผ! ๊ฒฐ์ ์์คํ ์ ํต์ฌ์ธ ์ค๋งํธ ์ปจํธ๋ํธ๋ฅผ ๋ง๋ค์ด๋ณด์. ๐
๐จ ๊ธฐ๋ณธ ๊ฒฐ์ ์ปจํธ๋ํธ ๊ตฌ์กฐ
๋จผ์ ๊ฐ๋จํ ๊ฒฐ์ ์ปจํธ๋ํธ๋ถํฐ ์์ํด๋ณผ๊ฒ. ์ด ์ปจํธ๋ํธ๋ ETH๋ฅผ ๋ฐ๊ณ , ํ๋งค์์๊ฒ ์ ๋ฌํ๊ณ , ํ๋ซํผ ์์๋ฃ๋ฅผ ๋ผ๋ ๊ธฐ๋ฅ์ ๊ฐ์ง๊ณ ์์ด.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract PaymentContract {
address public owner;
uint256 public platformFeePercent = 3; // 3% ์์๋ฃ
struct Payment {
address buyer;
address seller;
uint256 amount;
uint256 timestamp;
bool completed;
bool refunded;
}
mapping(uint256 => Payment) public payments;
uint256 public paymentCounter;
event PaymentCreated(
uint256 indexed paymentId,
address indexed buyer,
address indexed seller,
uint256 amount
);
event PaymentCompleted(uint256 indexed paymentId);
event PaymentRefunded(uint256 indexed paymentId);
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this");
_;
}
constructor() {
owner = msg.sender;
}
function createPayment(address _seller) external payable returns (uint256) {
require(msg.value > 0, "Payment amount must be greater than 0");
require(_seller != address(0), "Invalid seller address");
uint256 paymentId = paymentCounter++;
payments[paymentId] = Payment({
buyer: msg.sender,
seller: _seller,
amount: msg.value,
timestamp: block.timestamp,
completed: false,
refunded: false
});
emit PaymentCreated(paymentId, msg.sender, _seller, msg.value);
return paymentId;
}
function completePayment(uint256 _paymentId) external {
Payment storage payment = payments[_paymentId];
require(!payment.completed, "Payment already completed");
require(!payment.refunded, "Payment was refunded");
require(
msg.sender == payment.seller || msg.sender == owner,
"Only seller or owner can complete"
);
uint256 platformFee = (payment.amount * platformFeePercent) / 100;
uint256 sellerAmount = payment.amount - platformFee;
payment.completed = true;
payable(payment.seller).transfer(sellerAmount);
payable(owner).transfer(platformFee);
emit PaymentCompleted(_paymentId);
}
function refundPayment(uint256 _paymentId) external onlyOwner {
Payment storage payment = payments[_paymentId];
require(!payment.completed, "Cannot refund completed payment");
require(!payment.refunded, "Payment already refunded");
payment.refunded = true;
payable(payment.buyer).transfer(payment.amount);
emit PaymentRefunded(_paymentId);
}
function updatePlatformFee(uint256 _newFee) external onlyOwner {
require(_newFee <= 10, "Fee cannot exceed 10%");
platformFeePercent = _newFee;
}
function getPayment(uint256 _paymentId) external view returns (
address buyer,
address seller,
uint256 amount,
uint256 timestamp,
bool completed,
bool refunded
) {
Payment memory payment = payments[_paymentId];
return (
payment.buyer,
payment.seller,
payment.amount,
payment.timestamp,
payment.completed,
payment.refunded
);
}
}
์ด ์ปจํธ๋ํธ์ ํต์ฌ ๊ธฐ๋ฅ์ ์ค๋ช
ํ๋ฉด:
1. createPayment(): ๊ตฌ๋งค์๊ฐ ETH๋ฅผ ๋ณด๋ด๋ฉด์ ๊ฒฐ์ ๋ฅผ ์์ฑํด. ์ด๋ ํ๋งค์ ์ฃผ์๋ฅผ ์ง์ ํ๊ณ , ๋์ ์ปจํธ๋ํธ์ ์ผ๋จ ๋ณด๊ด๋ผ. ์์คํฌ๋ก ๋ฐฉ์์ด์ง!
2. completePayment(): ๊ฑฐ๋๊ฐ ์๋ฃ๋๋ฉด ํ๋งค์๋ ํ๋ซํผ ๊ด๋ฆฌ์๊ฐ ์ด ํจ์๋ฅผ ํธ์ถํด. ๊ทธ๋ฌ๋ฉด ์์๋ฃ๋ฅผ ์ ์ธํ ๊ธ์ก์ด ํ๋งค์์๊ฒ ์ ์ก๋ผ.
3. refundPayment(): ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ๊ด๋ฆฌ์๊ฐ ํ๋ถ์ ์ฒ๋ฆฌํ ์ ์์ด. ๊ตฌ๋งค์์๊ฒ ์ ์ก ๋ฐํ!
4. ์ด๋ฒคํธ(Event): ๋ชจ๋ ์ค์ํ ์ก์
๋ง๋ค ์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ. ๋ผ๋ผ๋ฒจ์์ ์ด๊ฑธ ๊ฐ์งํด์ DB์ ๊ธฐ๋กํ ๊ฑฐ์ผ.
๐ ERC-20 ํ ํฐ ๊ฒฐ์ ์ง์
ETH๋ง ๋ฐ๋ ๊ฒ ์๋๋ผ USDT, USDC ๊ฐ์ ์คํ ์ด๋ธ์ฝ์ธ๋ ๋ฐ๊ณ ์ถ๋ค๋ฉด? ERC-20 ํ ํฐ์ ์ง์ํ๋ ์ปจํธ๋ํธ๋ฅผ ๋ง๋ค์ด์ผ ํด!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}
contract TokenPaymentContract {
address public owner;
uint256 public platformFeePercent = 3;
struct TokenPayment {
address buyer;
address seller;
address tokenAddress;
uint256 amount;
uint256 timestamp;
bool completed;
bool refunded;
}
mapping(uint256 => TokenPayment) public payments;
uint256 public paymentCounter;
event TokenPaymentCreated(
uint256 indexed paymentId,
address indexed buyer,
address indexed seller,
address tokenAddress,
uint256 amount
);
event TokenPaymentCompleted(uint256 indexed paymentId);
event TokenPaymentRefunded(uint256 indexed paymentId);
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
constructor() {
owner = msg.sender;
}
function createTokenPayment(
address _seller,
address _tokenAddress,
uint256 _amount
) external returns (uint256) {
require(_amount > 0, "Amount must be greater than 0");
require(_seller != address(0), "Invalid seller");
require(_tokenAddress != address(0), "Invalid token");
IERC20 token = IERC20(_tokenAddress);
require(
token.transferFrom(msg.sender, address(this), _amount),
"Token transfer failed"
);
uint256 paymentId = paymentCounter++;
payments[paymentId] = TokenPayment({
buyer: msg.sender,
seller: _seller,
tokenAddress: _tokenAddress,
amount: _amount,
timestamp: block.timestamp,
completed: false,
refunded: false
});
emit TokenPaymentCreated(paymentId, msg.sender, _seller, _tokenAddress, _amount);
return paymentId;
}
function completeTokenPayment(uint256 _paymentId) external {
TokenPayment storage payment = payments[_paymentId];
require(!payment.completed, "Already completed");
require(!payment.refunded, "Was refunded");
require(
msg.sender == payment.seller || msg.sender == owner,
"Unauthorized"
);
uint256 platformFee = (payment.amount * platformFeePercent) / 100;
uint256 sellerAmount = payment.amount - platformFee;
payment.completed = true;
IERC20 token = IERC20(payment.tokenAddress);
require(token.transfer(payment.seller, sellerAmount), "Transfer to seller failed");
require(token.transfer(owner, platformFee), "Transfer fee failed");
emit TokenPaymentCompleted(_paymentId);
}
function refundTokenPayment(uint256 _paymentId) external onlyOwner {
TokenPayment storage payment = payments[_paymentId];
require(!payment.completed, "Cannot refund completed");
require(!payment.refunded, "Already refunded");
payment.refunded = true;
IERC20 token = IERC20(payment.tokenAddress);
require(token.transfer(payment.buyer, payment.amount), "Refund failed");
emit TokenPaymentRefunded(_paymentId);
}
}
ERC-20 ํ ํฐ ๊ฒฐ์ ์ ํต์ฌ์ transferFrom() ํจ์์ผ. ์ฌ์ฉ์๊ฐ ๋จผ์ ์ปจํธ๋ํธ์ approve๋ฅผ ํด์ค์ผ ํ๊ณ , ๊ทธ ๋ค์์ ์ปจํธ๋ํธ๊ฐ ์ฌ์ฉ์ ์ง๊ฐ์์ ํ ํฐ์ ๊ฐ์ ธ์ฌ ์ ์์ด. ์ด ๋ถ๋ถ์ ํ๋ก ํธ์๋์์ ์ฒ๋ฆฌํด์ผ ํด!
๐จ ์ปจํธ๋ํธ ์ปดํ์ผ ๋ฐ ๋ฐฐํฌ
์ปจํธ๋ํธ๋ฅผ ์์ฑํ์ผ๋ฉด ์ด์ ์ปดํ์ผํ๊ณ ํ ์คํธ๋ท์ ๋ฐฐํฌํด์ผ ํด.
require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: `https://sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
const PaymentContract = await hre.ethers.getContractFactory("PaymentContract");
const payment = await PaymentContract.deploy();
await payment.waitForDeployment();
const address = await payment.getAddress();
console.log("PaymentContract deployed to:", address);
// TokenPaymentContract๋ ๋ฐฐํฌ
const TokenPaymentContract = await hre.ethers.getContractFactory("TokenPaymentContract");
const tokenPayment = await TokenPaymentContract.deploy();
await tokenPayment.waitForDeployment();
const tokenAddress = await tokenPayment.getAddress();
console.log("TokenPaymentContract deployed to:", tokenAddress);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
npx hardhat compile
npx hardhat run scripts/deploy.js --network sepolia
๋ฐฐํฌ๊ฐ ์๋ฃ๋๋ฉด ์ปจํธ๋ํธ ์ฃผ์๊ฐ ์ถ๋ ฅ๋ผ. ์ด ์ฃผ์๋ฅผ ๋ผ๋ผ๋ฒจ .env ํ์ผ์ ์ ์ฅํด๋ฌ์ผ ํด!
๐ ๋ผ๋ผ๋ฒจ๊ณผ ๋ธ๋ก์ฒด์ธ ์ฐ๋ํ๊ธฐ
์, ์ด์ ๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ์ด์ผ! ๋ผ๋ผ๋ฒจ ๋ฐฑ์๋์์ ์ค๋งํธ ์ปจํธ๋ํธ์ ํต์ ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์. ์ด๊ฒ ํต์ฌ์ด๊ฑฐ๋ ! ๐ฏ
๐ฆ Web3 ์๋น์ค ํด๋์ค ๋ง๋ค๊ธฐ
๋จผ์ ๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ์ ์ฉ ์๋น์ค ํด๋์ค๋ฅผ ๋ง๋ค์ด์ผ ํด. ์ด๋ ๊ฒ ํ๋ฉด ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ด ๋์์ง๊ณ ์ ์ง๋ณด์๊ฐ ์ฌ์์ ธ.
<?php
namespace App\Services;
use Web3\Web3;
use Web3\Contract;
use Web3\Utils;
use Illuminate\Support\Facades\Log;
class Web3Service
{
protected $web3;
protected $contract;
protected $contractAddress;
protected $abi;
public function __construct()
{
$providerUrl = 'https://sepolia.infura.io/v3/' . env('INFURA_API_KEY');
$this->web3 = new Web3($providerUrl);
$this->contractAddress = env('CONTRACT_ADDRESS');
// ABI๋ ์ปดํ์ผ ํ ์์ฑ๋ JSON ํ์ผ์์ ๊ฐ์ ธ์์ผ ํด
$this->abi = json_decode(file_get_contents(storage_path('contracts/PaymentContract.json')), true);
$this->contract = new Contract($providerUrl, $this->abi);
}
public function createPayment($sellerAddress, $amount)
{
try {
$amountInWei = Utils::toWei($amount, 'ether');
// ํธ๋์ญ์
๋ฐ์ดํฐ ์์ฑ
$data = $this->contract->at($this->contractAddress)->getData('createPayment', $sellerAddress);
return [
'to' => $this->contractAddress,
'data' => $data,
'value' => '0x' . dechex($amountInWei->toString())
];
} catch (\Exception $e) {
Log::error('Create payment error: ' . $e->getMessage());
throw $e;
}
}
public function getPaymentDetails($paymentId)
{
try {
$payment = null;
$this->contract->at($this->contractAddress)->call('getPayment', $paymentId, function ($err, $result) use (&$payment) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$payment = $result;
});
return [
'buyer' => $payment[0],
'seller' => $payment[1],
'amount' => Utils::fromWei($payment[2], 'ether'),
'timestamp' => $payment[3],
'completed' => $payment[4],
'refunded' => $payment[5]
];
} catch (\Exception $e) {
Log::error('Get payment details error: ' . $e->getMessage());
throw $e;
}
}
public function completePayment($paymentId, $privateKey)
{
try {
// ์ค์ ๋ก๋ ์๋ฒ์์ ์ง์ ํธ๋์ญ์
์ ๋ณด๋ด๋ ๊ฑด ๋ณด์์ ์ํํ ์ ์์ด
// ๋์ ์๋ช
๋ ํธ๋์ญ์
์ ํ๋ก ํธ์๋์์ ๋ฐ์์ ์ฒ๋ฆฌํ๋ ๊ฒ ๋์
$data = $this->contract->at($this->contractAddress)->getData('completePayment', $paymentId);
return [
'to' => $this->contractAddress,
'data' => $data
];
} catch (\Exception $e) {
Log::error('Complete payment error: ' . $e->getMessage());
throw $e;
}
}
public function listenToPaymentEvents($fromBlock = 'latest')
{
try {
$filter = [
'fromBlock' => $fromBlock,
'toBlock' => 'latest',
'address' => $this->contractAddress,
'topics' => []
];
$this->web3->eth->getLogs($filter, function ($err, $logs) {
if ($err !== null) {
Log::error('Event listening error: ' . $err->getMessage());
return;
}
foreach ($logs as $log) {
$this->processEvent($log);
}
});
} catch (\Exception $e) {
Log::error('Listen to events error: ' . $e->getMessage());
throw $e;
}
}
protected function processEvent($log)
{
// ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง
// PaymentCreated, PaymentCompleted, PaymentRefunded ๋ฑ
Log::info('Blockchain event received', ['log' => $log]);
}
public function getTransactionReceipt($txHash)
{
try {
$receipt = null;
$this->web3->eth->getTransactionReceipt($txHash, function ($err, $result) use (&$receipt) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$receipt = $result;
});
return $receipt;
} catch (\Exception $e) {
Log::error('Get transaction receipt error: ' . $e->getMessage());
throw $e;
}
}
public function estimateGas($transactionData)
{
try {
$gas = null;
$this->web3->eth->estimateGas($transactionData, function ($err, $result) use (&$gas) {
if ($err !== null) {
throw new \Exception($err->getMessage());
}
$gas = $result;
});
return $gas;
} catch (\Exception $e) {
Log::error('Estimate gas error: ' . $e->getMessage());
throw $e;
}
}
}
์ด ์๋น์ค ํด๋์ค๋ ๋ธ๋ก์ฒด์ธ๊ณผ์ ๋ชจ๋ ์ํธ์์ฉ์ ๋ด๋นํด. ์ฃผ์ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ณด๋ฉด:
createPayment(): ๊ฒฐ์ ํธ๋์ญ์
๋ฐ์ดํฐ๋ฅผ ์์ฑํด. ์ค์ ์ ์ก์ ํ๋ก ํธ์๋์์ MetaMask๋ฅผ ํตํด ํด.
getPaymentDetails(): ํน์ ๊ฒฐ์ ์ ์์ธ ์ ๋ณด๋ฅผ ๋ธ๋ก์ฒด์ธ์์ ์กฐํํด.
listenToPaymentEvents(): ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํด์ DB์ ๋๊ธฐํํ ์ ์์ด.
getTransactionReceipt(): ํธ๋์ญ์
์ด ์ฑ๊ณตํ๋์ง ํ์ธํด.
estimateGas(): ๊ฐ์ค๋น๋ฅผ ๋ฏธ๋ฆฌ ๊ณ์ฐํด์ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค ์ ์์ด.
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ ์ค๊ณ
๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ๋ฅผ ๋ผ๋ผ๋ฒจ DB์๋ ์ ์ฅํด์ผ ํด. ์๋ํ๋ฉด ๋ธ๋ก์ฒด์ธ ์กฐํ๋ ๋๋ฆฌ๊ณ ๋น์ฉ์ด ๋ค๊ธฐ ๋๋ฌธ์ด์ผ. ๋น ๋ฅธ ์กฐํ๋ฅผ ์ํด DB์ ์บ์ฑํ๋ ๊ฑฐ์ง!
<?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('payments', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('blockchain_payment_id')->nullable();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('seller_id')->constrained('users')->onDelete('cascade');
$table->string('buyer_address', 42);
$table->string('seller_address', 42);
$table->decimal('amount', 20, 8);
$table->string('currency', 10)->default('ETH');
$table->string('token_address', 42)->nullable();
$table->string('transaction_hash', 66)->nullable();
$table->enum('status', ['pending', 'processing', 'completed', 'refunded', 'failed'])->default('pending');
$table->text('description')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamp('refunded_at')->nullable();
$table->timestamps();
$table->index('blockchain_payment_id');
$table->index('transaction_hash');
$table->index('status');
});
}
public function down()
{
Schema::dropIfExists('payments');
}
};
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Payment extends Model
{
protected $fillable = [
'blockchain_payment_id',
'user_id',
'seller_id',
'buyer_address',
'seller_address',
'amount',
'currency',
'token_address',
'transaction_hash',
'status',
'description',
'completed_at',
'refunded_at'
];
protected $casts = [
'amount' => 'decimal:8',
'completed_at' => 'datetime',
'refunded_at' => 'datetime'
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function seller(): BelongsTo
{
return $this->belongsTo(User::class, 'seller_id');
}
public function isPending(): bool
{
return $this->status === 'pending';
}
public function isCompleted(): bool
{
return $this->status === 'completed';
}
public function isRefunded(): bool
{
return $this->status === 'refunded';
}
public function markAsCompleted(): void
{
$this->update([
'status' => 'completed',
'completed_at' => now()
]);
}
public function markAsRefunded(): void
{
$this->update([
'status' => 'refunded',
'refunded_at' => now()
]);
}
}
์ด๋ ๊ฒ ํ๋ฉด ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ์ DB ๋ฐ์ดํฐ๋ฅผ ๋๊ธฐํํ ์ ์์ด. blockchain_payment_id๊ฐ ๋ธ๋ก์ฒด์ธ์ ๊ฒฐ์ ID์ ๋งคํ๋๋ ๊ฑฐ์ง!
๐ฎ ์ปจํธ๋กค๋ฌ ๊ตฌํ
์ด์ ์ค์ API ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค์ด๋ณด์. ํ๋ก ํธ์๋์์ ํธ์ถํ ์ ์๋ REST API์ผ.
<?php
namespace App\Http\Controllers;
use App\Models\Payment;
use App\Services\Web3Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class PaymentController extends Controller
{
protected $web3Service;
public function __construct(Web3Service $web3Service)
{
$this->web3Service = $web3Service;
}
public function createPayment(Request $request)
{
$validated = $request->validate([
'seller_id' => 'required|exists:users,id',
'seller_address' => 'required|string|size:42',
'amount' => 'required|numeric|min:0.001',
'currency' => 'required|in:ETH,USDT,USDC',
'description' => 'nullable|string|max:500'
]);
try {
DB::beginTransaction();
// DB์ ๊ฒฐ์ ๋ ์ฝ๋ ์์ฑ
$payment = Payment::create([
'user_id' => Auth::id(),
'seller_id' => $validated['seller_id'],
'buyer_address' => $request->user()->wallet_address,
'seller_address' => $validated['seller_address'],
'amount' => $validated['amount'],
'currency' => $validated['currency'],
'status' => 'pending',
'description' => $validated['description'] ?? null
]);
// ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
๋ฐ์ดํฐ ์์ฑ
$txData = $this->web3Service->createPayment(
$validated['seller_address'],
$validated['amount']
);
DB::commit();
return response()->json([
'success' => true,
'payment_id' => $payment->id,
'transaction_data' => $txData,
'message' => 'Payment created. Please sign the transaction in your wallet.'
]);
} catch (\Exception $e) {
DB::rollBack();
Log::error('Payment creation failed: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Failed to create payment: ' . $e->getMessage()
], 500);
}
}
public function confirmPayment(Request $request, $id)
{
$validated = $request->validate([
'transaction_hash' => 'required|string|size:66',
'blockchain_payment_id' => 'required|integer'
]);
try {
$payment = Payment::findOrFail($id);
if ($payment->user_id !== Auth::id()) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 403);
}
// ํธ๋์ญ์
์์์ฆ ํ์ธ
$receipt = $this->web3Service->getTransactionReceipt($validated['transaction_hash']);
if (!$receipt || !isset($receipt->status) || $receipt->status !== '0x1') {
return response()->json([
'success' => false,
'message' => 'Transaction failed or not found'
], 400);
}
// DB ์
๋ฐ์ดํธ
$payment->update([
'blockchain_payment_id' => $validated['blockchain_payment_id'],
'transaction_hash' => $validated['transaction_hash'],
'status' => 'processing'
]);
return response()->json([
'success' => true,
'message' => 'Payment confirmed successfully'
]);
} catch (\Exception $e) {
Log::error('Payment confirmation failed: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Failed to confirm payment: ' . $e->getMessage()
], 500);
}
}
public function completePayment(Request $request, $id)
{
try {
$payment = Payment::findOrFail($id);
// ํ๋งค์๋ ๊ด๋ฆฌ์๋ง ์๋ฃ ๊ฐ๋ฅ
if ($payment->seller_id !== Auth::id() && !Auth::user()->isAdmin()) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 403);
}
if ($payment->status !== 'processing') {
return response()->json([
'success' => false,
'message' => 'Payment is not in processing status'
], 400);
}
// ๋ธ๋ก์ฒด์ธ์์ ์๋ฃ ํธ๋์ญ์
๋ฐ์ดํฐ ์์ฑ
$txData = $this->web3Service->completePayment($payment->blockchain_payment_id);
return response()->json([
'success' => true,
'transaction_data' => $txData,
'message' => 'Please sign the completion transaction'
]);
} catch (\Exception $e) {
Log::error('Payment completion failed: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Failed to complete payment: ' . $e->getMessage()
], 500);
}
}
public function getPaymentDetails($id)
{
try {
$payment = Payment::with(['user', 'seller'])->findOrFail($id);
// ๊ถํ ์ฒดํฌ
if ($payment->user_id !== Auth::id() && $payment->seller_id !== Auth::id() && !Auth::user()->isAdmin()) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 403);
}
// ๋ธ๋ก์ฒด์ธ์์ ์ต์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
if ($payment->blockchain_payment_id) {
$blockchainData = $this->web3Service->getPaymentDetails($payment->blockchain_payment_id);
// DB์ ๋ธ๋ก์ฒด์ธ ๋ฐ์ดํฐ ๋๊ธฐํ
if ($blockchainData['completed'] && !$payment->isCompleted()) {
$payment->markAsCompleted();
} elseif ($blockchainData['refunded'] && !$payment->isRefunded()) {
$payment->markAsRefunded();
}
}
return response()->json([
'success' => true,
'payment' => $payment
]);
} catch (\Exception $e) {
Log::error('Get payment details failed: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Failed to get payment details: ' . $e->getMessage()
], 500);
}
}
public function listPayments(Request $request)
{
try {
$query = Payment::with(['user', 'seller'])
->where(function($q) {
$q->where('user_id', Auth::id())
->orWhere('seller_id', Auth::id());
});
if ($request->has('status')) {
$query->where('status', $request->status);
}
if ($request->has('currency')) {
$query->where('currency', $request->currency);
}
$payments = $query->orderBy('created_at', 'desc')
->paginate(20);
return response()->json([
'success' => true,
'payments' => $payments
]);
} catch (\Exception $e) {
Log::error('List payments failed: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Failed to list payments: ' . $e->getMessage()
], 500);
}
}
}
์ด ์ปจํธ๋กค๋ฌ์ ํ๋ฆ์ ์ ๋ฆฌํ๋ฉด:
1. createPayment(): DB์ ๊ฒฐ์ ๋ ์ฝ๋๋ฅผ ๋ง๋ค๊ณ , ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
๋ฐ์ดํฐ๋ฅผ ์์ฑํด์ ํ๋ก ํธ์๋๋ก ๋ฐํํด.
2. confirmPayment(): ํ๋ก ํธ์๋์์ ํธ๋์ญ์
์ ์ ์กํ ํ, ํธ๋์ญ์
ํด์๋ฅผ ๋ฐ์์ ์์์ฆ์ ํ์ธํ๊ณ DB๋ฅผ ์
๋ฐ์ดํธํด.
3. completePayment(): ํ๋งค์๊ฐ ์๋น์ค๋ฅผ ์ ๊ณตํ ํ ๊ฒฐ์ ๋ฅผ ์๋ฃํ๋ ํธ๋์ญ์
์ ์์ฑํด.
4. getPaymentDetails(): ๋ธ๋ก์ฒด์ธ๊ณผ DB ๋ฐ์ดํฐ๋ฅผ ๋๊ธฐํํ๋ฉด์ ์ต์ ์ ๋ณด๋ฅผ ๋ฐํํด.
5. listPayments(): ์ฌ์ฉ์์ ๊ฒฐ์ ๋ด์ญ์ ์กฐํํด.
โก ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ๋ฐ ์ด๋ฒคํธ ์ฒ๋ฆฌ
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์ ์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ธฐ ๋๋ฌธ์, ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ํ์์ผ. ๋ผ๋ผ๋ฒจ์ Queue ์์คํ ์ ํ์ฉํด๋ณด์! ๐
๐จ Job ํด๋์ค ์์ฑ
ํธ๋์ญ์ ์ํ๋ฅผ ์ฒดํฌํ๊ณ DB๋ฅผ ์ ๋ฐ์ดํธํ๋ Job์ ๋ง๋ค์ด๋ณด์.
<?php
namespace App\Jobs;
use App\Models\Payment;
use App\Services\Web3Service;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CheckTransactionStatus implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $payment;
public $tries = 5;
public $backoff = [60, 120, 300, 600, 1200]; // ์ฌ์๋ ๊ฐ๊ฒฉ (์ด)
public function __construct(Payment $payment)
{
$this->payment = $payment;
}
public function handle(Web3Service $web3Service)
{
try {
if (!$this->payment->transaction_hash) {
Log::warning("Payment {$this->payment->id} has no transaction hash");
return;
}
// ํธ๋์ญ์
์์์ฆ ์กฐํ
$receipt = $web3Service->getTransactionReceipt($this->payment->transaction_hash);
if (!$receipt) {
// ์์ง ๋ง์ด๋ ์๋จ, ๋์ค์ ๋ค์ ์๋
$this->release(30);
return;
}
// ํธ๋์ญ์
์ฑ๊ณต ์ฌ๋ถ ํ์ธ
if ($receipt->status === '0x1') {
// ์ฑ๊ณต
if ($this->payment->status === 'pending') {
$this->payment->update(['status' => 'processing']);
Log::info("Payment {$this->payment->id} transaction confirmed");
}
// ๋ธ๋ก์ฒด์ธ์์ ๊ฒฐ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
if ($this->payment->blockchain_payment_id) {
$blockchainData = $web3Service->getPaymentDetails($this->payment->blockchain_payment_id);
// ์๋ฃ ์ํ ๋๊ธฐํ
if ($blockchainData['completed'] && !$this->payment->isCompleted()) {
$this->payment->markAsCompleted();
Log::info("Payment {$this->payment->id} marked as completed");
// ์๋ฃ ์๋ฆผ ๋ฐ์ก
event(new \App\Events\PaymentCompleted($this->payment));
}
// ํ๋ถ ์ํ ๋๊ธฐํ
if ($blockchainData['refunded'] && !$this->payment->isRefunded()) {
$this->payment->markAsRefunded();
Log::info("Payment {$this->payment->id} marked as refunded");
// ํ๋ถ ์๋ฆผ ๋ฐ์ก
event(new \App\Events\PaymentRefunded($this->payment));
}
}
} else {
// ์คํจ
$this->payment->update(['status' => 'failed']);
Log::error("Payment {$this->payment->id} transaction failed");
// ์คํจ ์๋ฆผ ๋ฐ์ก
event(new \App\Events\PaymentFailed($this->payment));
}
} catch (\Exception $e) {
Log::error("CheckTransactionStatus job failed for payment {$this->payment->id}: " . $e->getMessage());
if ($this->attempts() >= $this->tries) {
// ์ต๋ ์ฌ์๋ ํ์ ์ด๊ณผ
$this->payment->update(['status' => 'failed']);
} else {
// ์ฌ์๋
$this->release($this->backoff[$this->attempts() - 1] ?? 600);
}
}
}
}
์ด Job์ ํธ๋์ญ์ ์ํ๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ์ฒดํฌํด์ DB๋ฅผ ์ ๋ฐ์ดํธํด. ์คํจํ๋ฉด ์๋์ผ๋ก ์ฌ์๋ํ๊ณ , ์ต๋ 5๋ฒ๊น์ง ์๋ํด.
๐ช ์ด๋ฒคํธ ๋ฐ ๋ฆฌ์ค๋ ์์ฑ
๊ฒฐ์ ์๋ฃ๋ ํ๋ถ ๊ฐ์ ์ค์ํ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ์๋ฆผ์ ๋ณด๋ด์ผ ํด. ๋ผ๋ผ๋ฒจ์ ์ด๋ฒคํธ ์์คํ ์ ํ์ฉํ์!
<?php
namespace App\Events;
use App\Models\Payment;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PaymentCompleted implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $payment;
public function __construct(Payment $payment)
{
$this->payment = $payment;
}
public function broadcastOn()
{
return [
new Channel('payment.' . $this->payment->id),
new Channel('user.' . $this->payment->user_id),
new Channel('user.' . $this->payment->seller_id)
];
}
public function broadcastAs()
{
return 'payment.completed';
}
public function broadcastWith()
{
return [
'payment_id' => $this->payment->id,
'amount' => $this->payment->amount,
'currency' => $this->payment->currency,
'completed_at' => $this->payment->completed_at
];
}
}
<?php
namespace App\Listeners;
use App\Events\PaymentCompleted;
use App\Notifications\PaymentCompletedNotification;
use Illuminate\Support\Facades\Log;
class SendPaymentCompletedNotification
{
public function handle(PaymentCompleted $event)
{
try {
$payment = $event->payment;
// ๊ตฌ๋งค์์๊ฒ ์๋ฆผ
$payment->user->notify(new PaymentCompletedNotification($payment, 'buyer'));
// ํ๋งค์์๊ฒ ์๋ฆผ
$payment->seller->notify(new PaymentCompletedNotification($payment, 'seller'));
Log::info("Payment completed notifications sent for payment {$payment->id}");
} catch (\Exception $e) {
Log::error("Failed to send payment completed notification: " . $e->getMessage());
}
}
}
์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฒฐ์ ์๋ฃ ์ ์๋์ผ๋ก ์ด๋ฉ์ผ, ํธ์ ์๋ฆผ, ์น์์ผ ๋ธ๋ก๋์บ์คํธ ๋ฑ์ ๋ณด๋ผ ์ ์์ด. ์ฝ๋๊ฐ ๊น๋ํ๊ฒ ๋ถ๋ฆฌ๋์ง!
โฐ ์ค์ผ์ค๋ฌ ์ค์
์ฃผ๊ธฐ์ ์ผ๋ก ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํ๊ณ ์ฒ๋ฆฌํ๋ ์ค์ผ์ค๋ฌ๋ฅผ ์ค์ ํ์.
<?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๋ถ๋ง๋ค pending ์ํ์ธ ๊ฒฐ์ ๋ค์ ํธ๋์ญ์
์ํ ์ฒดํฌ
$schedule->call(function () {
$payments = \App\Models\Payment::where('status', 'pending')
->whereNotNull('transaction_hash')
->where('created_at', '>', now()->subHours(24))
->get();
foreach ($payments as $payment) {
\App\Jobs\CheckTransactionStatus::dispatch($payment);
}
})->everyMinute();
// 5๋ถ๋ง๋ค processing ์ํ์ธ ๊ฒฐ์ ๋ค ์ฒดํฌ
$schedule->call(function () {
$payments = \App\Models\Payment::where('status', 'processing')
->where('created_at', '>', now()->subDays(7))
->get();
foreach ($payments as $payment) {
\App\Jobs\CheckTransactionStatus::dispatch($payment);
}
})->everyFiveMinutes();
// 10๋ถ๋ง๋ค ๋ธ๋ก์ฒด์ธ ์ด๋ฒคํธ ๋ฆฌ์ค๋
$schedule->call(function () {
$web3Service = app(\App\Services\Web3Service::class);
$web3Service->listenToPaymentEvents();
})->everyTenMinutes();
// ๋งค์ผ ์์ ์ ์ค๋๋ failed ๊ฒฐ์ ์ ๋ฆฌ
$schedule->call(function () {
\App\Models\Payment::where('status', 'failed')
->where('created_at', '<', now()->subDays(30))
->delete();
})->daily();
}
}
์ค์ผ์ค๋ฌ๋ฅผ ์คํํ๋ ค๋ฉด ์๋ฒ์ cron์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ๋ฑ๋กํด์ผ ํด:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
์ด๋ ๊ฒ ํ๋ฉด ์๋์ผ๋ก ํธ๋์ญ์ ์ํ๋ฅผ ์ถ์ ํ๊ณ DB๋ฅผ ๋๊ธฐํํ ์ ์์ด!
๐จ ํ๋ก ํธ์๋ ํตํฉ
๋ฐฑ์๋๊ฐ ์ค๋น๋์ผ๋ ์ด์ ํ๋ก ํธ์๋์์ MetaMask์ ์ฐ๋ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์. Vue.js ์์ ๋ก ์ค๋ช ํ ๊ฒ! ๐ป
๐ฆ MetaMask ์ฐ๊ฒฐ
๋จผ์ ์ฌ์ฉ์์ ์ง๊ฐ์ ์ฐ๊ฒฐํ๋ ๊ธฐ๋ฅ๋ถํฐ ๊ตฌํํด์ผ ํด.
import { ref, computed } from 'vue';
import { ethers } from 'ethers';
export function useWeb3() {
const account = ref(null);
const chainId = ref(null);
const provider = ref(null);
const signer = ref(null);
const isConnected = computed(() => !!account.value);
const connectWallet = async () => {
try {
if (!window.ethereum) {
throw new Error('MetaMask is not installed');
}
// ๊ณ์ ์์ฒญ
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
account.value = accounts[0];
// Provider ๋ฐ Signer ์ค์
provider.value = new ethers.BrowserProvider(window.ethereum);
signer.value = await provider.value.getSigner();
// ์ฒด์ธ ID ๊ฐ์ ธ์ค๊ธฐ
const network = await provider.value.getNetwork();
chainId.value = network.chainId.toString();
// ๊ณ์ ๋ณ๊ฒฝ ๊ฐ์ง
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
disconnectWallet();
} else {
account.value = accounts[0];
}
});
// ๋คํธ์ํฌ ๋ณ๊ฒฝ ๊ฐ์ง
window.ethereum.on('chainChanged', () => {
window.location.reload();
});
return account.value;
} catch (error) {
console.error('Failed to connect wallet:', error);
throw error;
}
};
const disconnectWallet = () => {
account.value = null;
chainId.value = null;
provider.value = null;
signer.value = null;
};
const switchNetwork = async (targetChainId) => {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${parseInt(targetChainId).toString(16)}` }]
});
} catch (error) {
if (error.code === 4902) {
// ๋คํธ์ํฌ๊ฐ ์ถ๊ฐ๋์ง ์์ ๊ฒฝ์ฐ
throw new Error('Please add this network to MetaMask');
}
throw error;
}
};
const getBalance = async (address) => {
try {
if (!provider.value) {
throw new Error('Provider not initialized');
}
const balance = await provider.value.getBalance(address || account.value);
return ethers.formatEther(balance);
} catch (error) {
console.error('Failed to get balance:', error);
throw error;
}
};
return {
account,
chainId,
provider,
signer,
isConnected,
connectWallet,
disconnectWallet,
switchNetwork,
getBalance
};
}
์ด composable์ MetaMask ์ฐ๊ฒฐ, ๊ณ์ ๊ด๋ฆฌ, ๋คํธ์ํฌ ์ ํ ๋ฑ์ ๊ธฐ๋ฅ์ ์ ๊ณตํด. Vue ์ปดํฌ๋ํธ์์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์์ด!
๐ณ ๊ฒฐ์ ์ปดํฌ๋ํธ ๊ตฌํ
์ค์ ๊ฒฐ์ ๋ฅผ ์ฒ๋ฆฌํ๋ Vue ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด๋ณด์.
<template>
<div class="payment-form">
<h2>๋ธ๋ก์ฒด์ธ ๊ฒฐ์ </h2>
<div v-if="!isConnected" class="connect-wallet">
<button @click="handleConnect" class="btn-primary">
๐ฆ MetaMask ์ฐ๊ฒฐ
</button>
</div>
<div v-else class="payment-details">
<div class="wallet-info">
<p>์ฐ๊ฒฐ๋ ์ง๊ฐ: {{ shortenAddress(account) }}</p>
<p>์์ก: {{ balance }} ETH</p>
</div>
<form @submit.prevent="handlePayment">
<div class="form-group">
<label>ํ๋งค์ ์ฃผ์</label>
<input
v-model="form.sellerAddress"
type="text"
placeholder="0x..."
required
/>
</div>
<div class="form-group">
<label>๊ฒฐ์ ๊ธ์ก (ETH)</label>
<input
v-model="form.amount"
type="number"
step="0.001"
min="0.001"
required
/>
</div>
<div class="form-group">
<label>์ค๋ช
(์ ํ)</label>
<textarea
v-model="form.description"
rows="3"
placeholder="๊ฒฐ์ ๋ด์ฉ์ ์
๋ ฅํ์ธ์"
></textarea>
</div>
<div v-if="estimatedGas" class="gas-estimate">
<p>์์ ๊ฐ์ค๋น: {{ estimatedGas }} ETH</p>
<p>์ด ๊ธ์ก: {{ totalAmount }} ETH</p>
</div>
<button
type="submit"
class="btn-primary"
:disabled="isProcessing"
>
{{ isProcessing ? '์ฒ๋ฆฌ ์ค...' : '๊ฒฐ์ ํ๊ธฐ' }}
</button>
</form>
<div v-if="transactionHash" class="transaction-info">
<p>โ
ํธ๋์ญ์
์ ์ก ์๋ฃ!</p>
<a
:href="`https://sepolia.etherscan.io/tx/${transactionHash}`"
target="_blank"
>
Etherscan์์ ํ์ธ
</a>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ethers } from 'ethers';
import { useWeb3 } from '../composables/useWeb3';
import axios from 'axios';
const {
account,
isConnected,
connectWallet,
signer,
getBalance
} = useWeb3();
const form = ref({
sellerAddress: '',
amount: '',
description: ''
});
const balance = ref('0');
const estimatedGas = ref(null);
const isProcessing = ref(false);
const transactionHash = ref(null);
const CONTRACT_ADDRESS = import.meta.env.VITE_CONTRACT_ADDRESS;
const CONTRACT_ABI = [
"function createPayment(address _seller) external payable returns (uint256)",
"event PaymentCreated(uint256 indexed paymentId, address indexed buyer, address indexed seller, uint256 amount)"
];
const totalAmount = computed(() => {
if (!form.value.amount || !estimatedGas.value) return '0';
return (parseFloat(form.value.amount) + parseFloat(estimatedGas.value)).toFixed(6);
});
const handleConnect = async () => {
try {
await connectWallet();
await updateBalance();
} catch (error) {
alert('์ง๊ฐ ์ฐ๊ฒฐ ์คํจ: ' + error.message);
}
};
const updateBalance = async () => {
try {
balance.value = await getBalance();
} catch (error) {
console.error('Failed to get balance:', error);
}
};
const estimateGas = async () => {
try {
if (!form.value.sellerAddress || !form.value.amount) return;
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer.value);
const amountInWei = ethers.parseEther(form.value.amount);
const gasEstimate = await contract.createPayment.estimateGas(
form.value.sellerAddress,
{ value: amountInWei }
);
const gasPrice = await signer.value.provider.getFeeData();
const gasCost = gasEstimate * gasPrice.gasPrice;
estimatedGas.value = ethers.formatEther(gasCost);
} catch (error) {
console.error('Failed to estimate gas:', error);
}
};
const handlePayment = async () => {
try {
isProcessing.value = true;
// 1. ๋ฐฑ์๋์ ๊ฒฐ์ ์์ฑ ์์ฒญ
const response = await axios.post('/api/payments', {
seller_id: 1, // ์ค์ ๋ก๋ ๋์ ์ผ๋ก ๋ฐ์์ผ ํจ
seller_address: form.value.sellerAddress,
amount: form.value.amount,
currency: 'ETH',
description: form.value.description
});
const { payment_id, transaction_data } = response.data;
// 2. ์ค๋งํธ ์ปจํธ๋ํธ ํธ์ถ
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer.value);
const tx = await contract.createPayment(
form.value.sellerAddress,
{
value: ethers.parseEther(form.value.amount),
gasLimit: 300000
}
);
transactionHash.value = tx.hash;
// 3. ํธ๋์ญ์
๋๊ธฐ
const receipt = await tx.wait();
// 4. ์ด๋ฒคํธ์์ paymentId ์ถ์ถ
const event = receipt.logs.find(log => {
try {
const parsed = contract.interface.parseLog(log);
return parsed.name === 'PaymentCreated';
} catch {
return false;
}
});
let blockchainPaymentId = null;
if (event) {
const parsed = contract.interface.parseLog(event);
blockchainPaymentId = parsed.args.paymentId.toString();
}
// 5. ๋ฐฑ์๋์ ํธ๋์ญ์
ํ์ธ ์์ฒญ
await axios.post(`/api/payments/${payment_id}/confirm`, {
transaction_hash: tx.hash,
blockchain_payment_id: blockchainPaymentId
});
alert('๊ฒฐ์ ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋์์ต๋๋ค!');
// ํผ ์ด๊ธฐํ
form.value = {
sellerAddress: '',
amount: '',
description: ''
};
await updateBalance();
} catch (error) {
console.error('Payment failed:', error);
alert('๊ฒฐ์ ์คํจ: ' + error.message);
} finally {
isProcessing.value = false;
}
};
const shortenAddress = (address) => {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
onMounted(async () => {
if (isConnected.value) {
await updateBalance();
}
});
</script>
<style scoped>
.payment-form {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.wallet-info {
background: #f0f0f0;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.gas-estimate {
background: #fff3cd;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
.btn-primary {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
}
.transaction-info {
margin-top: 20px;
padding: 15px;
background: #d4edda;
border-radius: 8px;
}
.transaction-info a {
color: #155724;
text-decoration: underline;
}
</style>
์ด ์ปดํฌ๋ํธ๋ ์์ ํ ๊ฒฐ์ ํ๋ก์ฐ๋ฅผ ๊ตฌํํด:
1๏ธโฃ MetaMask ์ฐ๊ฒฐ
2๏ธโฃ ์์ก ํ์ธ
3๏ธโฃ ๊ฐ์ค๋น ์์ธก
4๏ธโฃ ๋ฐฑ์๋์ ๊ฒฐ์ ์์ฑ
5๏ธโฃ ์ค๋งํธ ์ปจํธ๋ํธ ํธ์ถ
6๏ธโฃ ํธ๋์ญ์
๋๊ธฐ ๋ฐ ํ์ธ
7๏ธโฃ ๋ฐฑ์๋์ ๊ฒฐ๊ณผ ์ ์ก
โข ํ๋ก ํธ์๋์์ ์ ๋ ํ๋ผ์ด๋น ํค๋ฅผ ๋ค๋ฃจ์ง ๋ง!
โข ๋ชจ๋ ํธ๋์ญ์ ์ ์ฌ์ฉ์๊ฐ MetaMask์์ ์ง์ ์๋ช ํด์ผ ํด
โข ๋ฐฑ์๋ API๋ ๋ฐ๋์ ์ธ์ฆ/๊ถํ ์ฒดํฌ๋ฅผ ํด์ผ ํด
โข XSS, CSRF ๊ณต๊ฒฉ ๋ฐฉ์ด๋ฅผ ์ฒ ์ ํ ํด์ผ ํด
๐ก๏ธ ๋ณด์ ๋ฐ ์ต์ ํ
๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์์คํ ์์ ๋ณด์์ ์ ๋ง ์ค์ํด! ์ค์ ๋์ด ์ค๊ฐ๋ ๊ฑฐ๋๊น ์ฒ ์ ํ๊ฒ ์ค๋นํด์ผ ํด. ๐
๐ ์ค๋งํธ ์ปจํธ๋ํธ ๋ณด์
1. Reentrancy ๊ณต๊ฒฉ ๋ฐฉ์ด
์ธ๋ถ ์ปจํธ๋ํธ๋ฅผ ํธ์ถํ๊ธฐ ์ ์ ์ํ๋ฅผ ๋จผ์ ์
๋ฐ์ดํธํด์ผ ํด. Checks-Effects-Interactions ํจํด์ ๋ฐ๋ฅด์!
function completePayment(uint256 _paymentId) external {
Payment storage payment = payments[_paymentId];
// 1. Checks (๊ฒ์ฆ)
require(!payment.completed, "Already completed");
require(msg.sender == payment.seller, "Unauthorized");
// 2. Effects (์ํ ๋ณ๊ฒฝ)
payment.completed = true;
uint256 amount = payment.amount;
// 3. Interactions (์ธ๋ถ ํธ์ถ)
payable(payment.seller).transfer(amount);
}
2. Integer Overflow/Underflow ๋ฐฉ์ง
Solidity 0.8.0 ์ด์์์๋ ์๋์ผ๋ก ์ฒดํฌ๋์ง๋ง, ๋ช
์์ ์ผ๋ก SafeMath๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๋ ์ข์.
3. Access Control
OpenZeppelin์ Ownable, AccessControl ๊ฐ์ ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์.
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
contract PaymentContract is Ownable, ReentrancyGuard, Pausable {
// nonReentrant ๋ชจ๋ํ์ด์ด๋ก ์ฌ์ง์
๊ณต๊ฒฉ ๋ฐฉ์ด
function completePayment(uint256 _paymentId)
external
nonReentrant
whenNotPaused
{
// ๊ตฌํ...
}
// ๊ธด๊ธ ์ํฉ ์ ์ปจํธ๋ํธ ์ผ์ ์ ์ง
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
4. ๊ฐ์ค ์ต์ ํ
โข Storage ๋ณ์ ์ฌ์ฉ์ ์ต์ํํด (๋น์ธ!)
โข ๋ฐ๋ณต๋ฌธ์์ ๋ฐฐ์ด ๊ธธ์ด๋ฅผ ์บ์ฑํด
โข uint256 ๋์ uint8, uint16 ๊ฐ์ ์์ ํ์
์ฌ์ฉ (packing ๊ฐ๋ฅํ ๋)
โข ๋ถํ์ํ ์ด๋ฒคํธ ๋ฐ์ ์ค์ด๊ธฐ
๐ ๋ฐฑ์๋ ๋ณด์
1. API ์ธ์ฆ ๋ฐ ๊ถํ ๊ด๋ฆฌ
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class VerifyPaymentOwnership
{
public function handle(Request $request, Closure $next)
{
$paymentId = $request->route('id');
$payment = \App\Models\Payment::find($paymentId);
if (!$payment) {
return response()->json(['error' => 'Payment not found'], 404);
}
$user = $request->user();
// ๋ณธ์ธ ๋๋ ํ๋งค์ ๋๋ ๊ด๋ฆฌ์๋ง ์ ๊ทผ ๊ฐ๋ฅ
if ($payment->user_id !== $user->id &&
$payment->seller_id !== $user->id &&
!$user->isAdmin()) {
return response()->json(['error' => 'Unauthorized'], 403);
}
return $next($request);
}
}
2. Rate Limiting
๋ธ๋ก์ฒด์ธ API ํธ์ถ์ ๋น์ฉ์ด ๋ค ์ ์์ผ๋ Rate Limiting์ ๊ผญ ์ค์ ํด์ผ ํด.
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::post('/payments', [PaymentController::class, 'createPayment']);
Route::get('/payments', [PaymentController::class, 'listPayments']);
});
3. ์
๋ ฅ ๊ฒ์ฆ
์ด๋๋ฆฌ์ ์ฃผ์ ํ์, ๊ธ์ก ๋ฒ์ ๋ฑ์ ์ฒ ์ ํ ๊ฒ์ฆํด์ผ ํด.
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class EthereumAddress implements Rule
{
public function passes($attribute, $value)
{
// 0x๋ก ์์ํ๊ณ 40์๋ฆฌ 16์ง์์ธ์ง ํ์ธ
return preg_match('/^0x[a-fA-F0-9]{40}$/', $value);
}
public function message()
{
return 'The :attribute must be a valid Ethereum address.';
}
}
// ์ฌ์ฉ
$request->validate([
'seller_address' => ['required', new EthereumAddress]
]);
4. ํ๊ฒฝ ๋ณ์ ๋ณด์
์ ๋๋ก ํ๋ผ์ด๋น ํค๋ API ํค๋ฅผ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง! .env ํ์ผ์ ์ฌ์ฉํ๊ณ , ํ๋ก๋์
์์๋ ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌํด.
โก ์ฑ๋ฅ ์ต์ ํ
1. ์บ์ฑ ์ ๋ต
๋ธ๋ก์ฒด์ธ ์กฐํ๋ ๋๋ฆฌ๋๊น Redis๋ก ์บ์ฑํ์!
<?php
use Illuminate\Support\Facades\Cache;
public function getPaymentDetails($paymentId)
{
$cacheKey = "payment:{$paymentId}";
return Cache::remember($cacheKey, 300, function () use ($paymentId) {
return $this->web3Service->getPaymentDetails($paymentId);
});
}
// ๊ฒฐ์ ์๋ฃ ์ ์บ์ ๋ฌดํจํ
public function markAsCompleted()
{
$this->update(['status' => 'completed', 'completed_at' => now()]);
Cache::forget("payment:{$this->id}");
}
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ฑ
์์ฃผ ์กฐํํ๋ ์ปฌ๋ผ์ ์ธ๋ฑ์ค๋ฅผ ์ถ๊ฐํด์ผ ํด.
$table->index(['user_id', 'status']);
$table->index('transaction_hash');
$table->index('created_at');
3. ํ ์์คํ
ํ์ฉ
๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
์ฒ๋ฆฌ๋ ์๊ฐ์ด ๊ฑธ๋ฆฌ๋๊น ํ๋ก ๋น๋๊ธฐ ์ฒ๋ฆฌํด์ผ ํด.
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
],
4. ๋ฐฐ์น ์ฒ๋ฆฌ
์ฌ๋ฌ ํธ๋์ญ์
์ ํ ๋ฒ์ ์ฒ๋ฆฌํ๋ฉด ํจ์จ์ ์ด์ผ.
$payments = Payment::where('status', 'pending')
->whereNotNull('transaction_hash')
->limit(100)
->get();
$jobs = $payments->map(function ($payment) {
return new CheckTransactionStatus($payment);
});
Bus::batch($jobs)->dispatch();
๐งช ํ ์คํธ ๋ฐ ๋๋ฒ๊น
๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ์์ ํ ์คํธ๋ ์ ๋ง ์ค์ํด! ์ค์ํ๋ฉด ์ง์ง ๋์ ์์ ์ ์๊ฑฐ๋ . ์ฒ ์ ํ๊ฒ ํ ์คํธํ์! ๐ฌ
๐จ ์ค๋งํธ ์ปจํธ๋ํธ ํ ์คํธ
Hardhat์ผ๋ก ์ปจํธ๋ํธ๋ฅผ ํ ์คํธํด๋ณด์. JavaScript๋ก ์์ฑํ ์ ์์ด์ ํธํด!
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("PaymentContract", function () {
let paymentContract;
let owner, buyer, seller;
beforeEach(async function () {
[owner, buyer, seller] = await ethers.getSigners();
const PaymentContract = await ethers.getContractFactory("PaymentContract");
paymentContract = await PaymentContract.deploy();
await paymentContract.waitForDeployment();
});
describe("createPayment", function () {
it("should create a payment successfully", async function () {
const amount = ethers.parseEther("1.0");
const tx = await paymentContract.connect(buyer).createPayment(
seller.address,
{ value: amount }
);
const receipt = await tx.wait();
// ์ด๋ฒคํธ ํ์ธ
const event = receipt.logs.find(log => {
try {
const parsed = paymentContract.interface.parseLog(log);
return parsed.name === 'PaymentCreated';
} catch {
return false;
}
});
expect(event).to.not.be.undefined;
const parsed = paymentContract.interface.parseLog(event);
expect(parsed.args.buyer).to.equal(buyer.address);
expect(parsed.args.seller).to.equal(seller.address);
expect(parsed.args.amount).to.equal(amount);
});
it("should fail with zero amount", async function () {
await expect(
paymentContract.connect(buyer).createPayment(seller.address, { value: 0 })
).to.be.revertedWith("Payment amount must be greater than 0");
});
it("should fail with invalid seller address", async function () {
const amount = ethers.parseEther("1.0");
await expect(
paymentContract.connect(buyer).createPayment(
ethers.ZeroAddress,
{ value: amount }
)
).to.be.revertedWith("Invalid seller address");
});
});
describe("completePayment", function () {
let paymentId;
const amount = ethers.parseEther("1.0");
beforeEach(async function () {
const tx = await paymentContract.connect(buyer).createPayment(
seller.address,
{ value: amount }
);
const receipt = await tx.wait();
const event = receipt.logs.find(log => {
try {
const parsed = paymentContract.interface.parseLog(log);
return parsed.name === 'PaymentCreated';
} catch {
return false;
}
});
const parsed = paymentContract.interface.parseLog(event);
paymentId = parsed.args.paymentId;
});
it("should complete payment and transfer funds", async function () {
const sellerBalanceBefore = await ethers.provider.getBalance(seller.address);
const ownerBalanceBefore = await ethers.provider.getBalance(owner.address);
await paymentContract.connect(seller).completePayment(paymentId);
const sellerBalanceAfter = await ethers.provider.getBalance(seller.address);
const ownerBalanceAfter = await ethers.provider.getBalance(owner.address);
const platformFee = amount * 3n / 100n;
const sellerAmount = amount - platformFee;
// ๊ฐ์ค๋น ๋๋ฌธ์ ์ ํํ ๋น๊ต๋ ์ด๋ ต์ง๋ง, ๋๋ต์ ์ผ๋ก ํ์ธ
expect(sellerBalanceAfter).to.be.gt(sellerBalanceBefore);
expect(ownerBalanceAfter).to.be.gt(ownerBalanceBefore);
});
it("should fail if already completed", async function () {
await paymentContract.connect(seller).completePayment(paymentId);
await expect(
paymentContract.connect(seller).completePayment(paymentId)
).to.be.revertedWith("Payment already completed");
});
it("should fail if unauthorized", async function () {
await expect(
paymentContract.connect(buyer).completePayment(paymentId)
).to.be.revertedWith("Only seller or owner can complete");
});
});
describe("refundPayment", function () {
let paymentId;
const amount = ethers.parseEther("1.0");
beforeEach(async function () {
const tx = await paymentContract.connect(buyer).createPayment(
seller.address,
{ value: amount }
);
const receipt = await tx.wait();
const event = receipt.logs.find(log => {
try {
const parsed = paymentContract.interface.parseLog(log);
return parsed.name === 'PaymentCreated';
} catch {
return false;
}
});
const parsed = paymentContract.interface.parseLog(event);
paymentId = parsed.args.paymentId;
});
it("should refund payment successfully", async function () {
const buyerBalanceBefore = await ethers.provider.getBalance(buyer.address);
await paymentContract.connect(owner).refundPayment(paymentId);
const buyerBalanceAfter = await ethers.provider.getBalance(buyer.address);
expect(buyerBalanceAfter).to.be.gt(buyerBalanceBefore);
});
it("should fail if not owner", async function () {
await expect(
paymentContract.connect(seller).refundPayment(paymentId)
).to.be.revertedWith("Only owner can call this");
});
});
});
ํ ์คํธ ์คํ:
npx hardhat test
npx hardhat coverage # ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ํ์ธ
100% ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๋ชฉํ๋ก ํ์! ํนํ ๋๊ณผ ๊ด๋ จ๋ ๋ก์ง์ ๋ชจ๋ ๊ฒฝ์ฐ์ ์๋ฅผ ํ ์คํธํด์ผ ํด.
๐งช ๋ผ๋ผ๋ฒจ ํ ์คํธ
๋ฐฑ์๋ API๋ ์ฒ ์ ํ๊ฒ ํ ์คํธํด์ผ ํด.
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Models\Payment;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PaymentTest extends TestCase
{
use RefreshDatabase;
protected $user;
protected $seller;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create([
'wallet_address' => '0x1234567890123456789012345678901234567890'
]);
$this->seller = User::factory()->create([
'wallet_address' => '0x0987654321098765432109876543210987654321'
]);
}
public function test_create_payment_successfully()
{
$response = $this->actingAs($this->user)->postJson('/api/payments', [
'seller_id' => $this->seller->id,
'seller_address' => $this->seller->wallet_address,
'amount' => 1.5,
'currency' => 'ETH',
'description' => 'Test payment'
]);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'payment_id',
'transaction_data',
'message'
]);
$this->assertDatabaseHas('payments', [
'user_id' => $this->user->id,
'seller_id' => $this->seller->id,
'amount' => 1.5,
'status' => 'pending'
]);
}
public function test_create_payment_validation_fails()
{
$response = $this->actingAs($this->user)->postJson('/api/payments', [
'seller_id' => $this->seller->id,
'seller_address' => 'invalid_address',
'amount' => -1,
'currency' => 'INVALID'
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['seller_address', 'amount', 'currency']);
}
public function test_confirm_payment_successfully()
{
$payment = Payment::factory()->create([
'user_id' => $this->user->id,
'seller_id' => $this->seller->id,
'status' => 'pending'
]);
$response = $this->actingAs($this->user)->postJson("/api/payments/{$payment->id}/confirm", [
'transaction_hash' => '0x' . str_repeat('a', 64),
'blockchain_payment_id' => 1
]);
$response->assertStatus(200);
$payment->refresh();
$this->assertEquals('processing', $payment->status);
$this->assertNotNull($payment->transaction_hash);
}
public function test_unauthorized_user_cannot_confirm_payment()
{
$payment = Payment::factory()->create([
'user_id' => $this->seller->id,
'seller_id' => $this->user->id
]);
$response = $this->actingAs($this->user)->postJson("/api/payments/{$payment->id}/confirm", [
'transaction_hash' => '0x' . str_repeat('a', 64),
'blockchain_payment_id' => 1
]);
$response->assertStatus(403);
}
public function test_list_payments_returns_user_payments_only()
{
Payment::factory()->count(5)->create(['user_id' => $this->user->id]);
Payment::factory()->count(3)->create(['seller_id' => $this->user->id]);
Payment::factory()->count(2)->create(); // ๋ค๋ฅธ ์ฌ์ฉ์
$response = $this->actingAs($this->user)->getJson('/api/payments');
$response->assertStatus(200)
->assertJsonCount(8, 'payments.data');
}
}
ํ ์คํธ ์คํ:
php artisan test
php artisan test --coverage # ์ปค๋ฒ๋ฆฌ์ง ํ์ธ
๐ ๋ฐฐํฌ ๋ฐ ์ด์
๊ฐ๋ฐ์ด ๋๋ฌ์ผ๋ฉด ์ด์ ์ค์ ์๋น์ค๋ก ๋ฐฐํฌํด์ผ์ง! ํ๋ก๋์ ํ๊ฒฝ ์ค์ ์ ์์๋ณด์. ๐
๐ฆ ๋ฉ์ธ๋ท ๋ฐฐํฌ ์ค๋น
ํ ์คํธ๋ท์์ ์ถฉ๋ถํ ํ ์คํธํ๋ค๋ฉด ์ด์ ๋ฉ์ธ๋ท์ ๋ฐฐํฌํ ์ฐจ๋ก์ผ. ํ์ง๋ง ์กฐ์ฌํด์ผ ํด!
๋ฐฐํฌ ์ ์ฒดํฌ๋ฆฌ์คํธ:
โ
์ค๋งํธ ์ปจํธ๋ํธ ๊ฐ์ฌ(Audit) ์๋ฃ
โ
๋ชจ๋ ํ
์คํธ ํต๊ณผ
โ
๊ฐ์ค ์ต์ ํ ์๋ฃ
โ
๊ธด๊ธ ์ ์ง(Pause) ๊ธฐ๋ฅ ๊ตฌํ
โ
์
๊ทธ๋ ์ด๋ ๊ฐ๋ฅ์ฑ ๊ณ ๋ ค (Proxy ํจํด)
โ
์ถฉ๋ถํ ETH ์ค๋น (๋ฐฐํฌ ๊ฐ์ค๋น)
โ
๋ฐฑ์
๋ฐ ๋กค๋ฐฑ ๊ณํ ์๋ฆฝ
require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config();
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
mainnet: {
url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`,
accounts: [process.env.MAINNET_PRIVATE_KEY],
gasPrice: 'auto',
gas: 'auto'
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
# ๋ฐฐํฌ ์ ์๋ฎฌ๋ ์ด์
npx hardhat run scripts/deploy.js --network mainnet --dry-run
# ์ค์ ๋ฐฐํฌ
npx hardhat run scripts/deploy.js --network mainnet
# Etherscan์ ์์ค์ฝ๋ ๊ฒ์ฆ
npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS
โข ๋ฐฐํฌ๋ ๋๋๋ฆด ์ ์์ด! ์ ์คํ๊ฒ ํด์ผ ํด
โข ๊ฐ์ค๋น๊ฐ ๋น์ ์ ์์ผ๋ ๊ฐ์ค ๊ฐ๊ฒฉ์ด ๋ฎ์ ๋ ๋ฐฐํฌํ์
โข ๋ฐฐํฌ ํ ๋ฐ๋์ Etherscan์์ ์์ค์ฝ๋๋ฅผ ๊ฒ์ฆํด์ผ ์ ๋ขฐ๋๊ฐ ๋์์ ธ
โข ์ฒซ ๋ฐฐํฌ ํ ์์ ๊ธ์ก์ผ๋ก ๋จผ์ ํ ์คํธํด๋ณด์
๐ฅ๏ธ ๋ผ๋ผ๋ฒจ ํ๋ก๋์ ๋ฐฐํฌ
๋ฐฑ์๋๋ ํ๋ก๋์ ํ๊ฒฝ์ ๋ง๊ฒ ์ค์ ํด์ผ ํด.
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com
DB_CONNECTION=mysql
DB_HOST=your-db-host
DB_DATABASE=your-database
DB_USERNAME=your-username
DB_PASSWORD=your-secure-password
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=your-redis-host
REDIS_PASSWORD=your-redis-password
ETHEREUM_NETWORK=mainnet
INFURA_API_KEY=your-infura-key
CONTRACT_ADDRESS=your-deployed-contract-address
# ์ ๋๋ก ๋ฉ์ธ๋ท ํ๋ผ์ด๋น ํค๋ฅผ ์ฌ๊ธฐ์ ๋ฃ์ง ๋ง!
# ์๋ฒ ์ฌ์ด๋ ์๋ช
์ด ํ์ํ๋ฉด AWS KMS ๊ฐ์ ๋ณด์ ์๋ฃจ์
์ฌ์ฉ
#!/bin/bash
# ์ ์ฅ์์์ ์ต์ ์ฝ๋ ๊ฐ์ ธ์ค๊ธฐ
git pull origin main
# ์์กด์ฑ ์ค์น
composer install --no-dev --optimize-autoloader
# ์บ์ ์ต์ ํ
php artisan config:cache
php artisan route:cache
php artisan view:cache
# ๋ง์ด๊ทธ๋ ์ด์
์คํ
php artisan migrate --force
# ํ ์์ปค ์ฌ์์
php artisan queue:restart
# ์ค์ผ์ค๋ฌ ํ์ธ
php artisan schedule:list
echo "Deployment completed!"
์๋ฒ ์ค์ :
โข Nginx/Apache: ์น ์๋ฒ ์ค์
โข PHP-FPM: PHP ํ๋ก์ธ์ค ๊ด๋ฆฌ
โข Supervisor: ํ ์์ปค ๊ด๋ฆฌ
โข Redis: ์บ์ฑ ๋ฐ ํ
โข MySQL/PostgreSQL: ๋ฐ์ดํฐ๋ฒ ์ด์ค
โข SSL ์ธ์ฆ์: HTTPS ํ์!
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/your/project/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/path/to/your/project/storage/logs/worker.log
stopwaitsecs=3600
๐ ๋ชจ๋ํฐ๋ง ๋ฐ ๋ก๊น
ํ๋ก๋์ ์์๋ ๋ชจ๋ํฐ๋ง์ด ์ ๋ง ์ค์ํด! ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ๋น ๋ฅด๊ฒ ๋์ํด์ผ ํ๊ฑฐ๋ .
๋ชจ๋ํฐ๋ง ๋๊ตฌ:
๐ Laravel Telescope: ๊ฐ๋ฐ ํ๊ฒฝ์์ ๋๋ฒ๊น
๐ Laravel Horizon: ํ ๋ชจ๋ํฐ๋ง
๐จ Sentry: ์๋ฌ ์ถ์
๐ New Relic/DataDog: APM (Application Performance Monitoring)
๐ Grafana + Prometheus: ๋ฉํธ๋ฆญ ์๊ฐํ
๐ PagerDuty/Opsgenie: ์๋ฆผ ๊ด๋ฆฌ
composer require sentry/sentry-laravel
# config/sentry.php
return [
'dsn' => env('SENTRY_LARAVEL_DSN'),
'traces_sample_rate' => 1.0,
'environment' => env('APP_ENV'),
];
<?php
use Illuminate\Support\Facades\Log;
// ๊ฒฐ์ ๊ด๋ จ ์ค์ ์ด๋ฒคํธ ๋ก๊น
Log::channel('payments')->info('Payment created', [
'payment_id' => $payment->id,
'user_id' => $payment->user_id,
'amount' => $payment->amount,
'currency' => $payment->currency
]);
// ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
๋ก๊น
Log::channel('blockchain')->info('Transaction sent', [
'tx_hash' => $txHash,
'from' => $fromAddress,
'to' => $toAddress,
'value' => $value
]);
// ์๋ฌ ๋ก๊น
Log::channel('slack')->error('Payment processing failed', [
'payment_id' => $payment->id,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString()
]);
๐ ๋ฐฑ์ ๋ฐ ์ฌํด ๋ณต๊ตฌ
๋ง์ฝ์ ๋๋นํ ๋ฐฑ์ ์ ๋ต๋ ํ์์ผ!
๋ฐฑ์
์ ๋ต:
๐พ ๋ฐ์ดํฐ๋ฒ ์ด์ค: ๋งค์ผ ์๋ ๋ฐฑ์
, 7์ผ์น ๋ณด๊ด
๐ ํ์ผ ์์คํ
: ์ฃผ๊ธฐ์ ์ค๋
์ท
๐ ํ๊ฒฝ ๋ณ์: ์์ ํ ๊ณณ์ ์ํธํ ์ ์ฅ
๐ ์ค๋งํธ ์ปจํธ๋ํธ: ์์ค์ฝ๋ ๋ฐ ABI ๋ฒ์ ๊ด๋ฆฌ
๐๏ธ ๋ก๊ทธ: ์ฅ๊ธฐ ๋ณด๊ด (์ต์ 1๋
)
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
DB_NAME="your_database"
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฑ์
mysqldump -u $DB_USER -p$DB_PASS $DB_NAME | gzip > $BACKUP_DIR/db_$DATE.sql.gz
# ํ์ผ ๋ฐฑ์
tar -czf $BACKUP_DIR/files_$DATE.tar.gz /path/to/your/project/storage
# S3์ ์
๋ก๋
aws s3 cp $BACKUP_DIR/db_$DATE.sql.gz s3://your-bucket/backups/
aws s3 cp $BACKUP_DIR/files_$DATE.tar.gz s3://your-bucket/backups/
# 7์ผ ์ด์ ๋ ๋ฐฑ์
์ญ์
find $BACKUP_DIR -name "*.gz" -mtime +7 -delete
echo "Backup completed: $DATE"
๐ฐ ๋น์ฉ ์ต์ ํ ๋ฐ ์์ต ๋ชจ๋ธ
๋ธ๋ก์ฒด์ธ ์๋น์ค๋ฅผ ์ด์ํ๋ ค๋ฉด ๋น์ฉ ๊ด๋ฆฌ๊ฐ ์ ๋ง ์ค์ํด. ๊ฐ์ค๋น๊ฐ ์ฅ๋ ์๋๊ฑฐ๋ ! ๐ธ
โฝ ๊ฐ์ค๋น ์ต์ ํ ์ ๋ต
1. ๋ฐฐ์น ์ฒ๋ฆฌ
์ฌ๋ฌ ํธ๋์ญ์
์ ํ๋๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌํ๋ฉด ๊ฐ์ค๋น๋ฅผ ์ ์ฝํ ์ ์์ด.
function batchCompletePayments(uint256[] calldata _paymentIds) external onlyOwner {
for (uint256 i = 0; i < _paymentIds.length; i++) {
_completePayment(_paymentIds[i]);
}
}
function _completePayment(uint256 _paymentId) internal {
// ๊ฐ๋ณ ๊ฒฐ์ ์๋ฃ ๋ก์ง
}
2. Layer 2 ์๋ฃจ์
ํ์ฉ
Polygon, Arbitrum, Optimism ๊ฐ์ L2๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ์ค๋น๊ฐ 1/100 ์์ค์ผ๋ก ์ค์ด๋ค์ด!
<?php
class Web3Service
{
protected function getProvider($network)
{
$providers = [
'ethereum' => 'https://mainnet.infura.io/v3/' . env('INFURA_API_KEY'),
'polygon' => 'https://polygon-rpc.com',
'arbitrum' => 'https://arb1.arbitrum.io/rpc',
'optimism' => 'https://mainnet.optimism.io'
];
return $providers[$network] ?? $providers['ethereum'];
}
}
3. ๊ฐ์ค ๊ฐ๊ฒฉ ๋ชจ๋ํฐ๋ง
๊ฐ์ค ๊ฐ๊ฒฉ์ด ๋ฎ์ ๋ ํธ๋์ญ์
์ ๋ณด๋ด๋ฉด ๋น์ฉ์ ์ ์ฝํ ์ ์์ด.
public function getOptimalGasPrice()
{
$gasPrice = null;
$this->web3->eth->gasPrice(function ($err, $price) use (&$gasPrice) {
if ($err === null) {
$gasPrice = $price;
}
});
// Gwei๋ก ๋ณํ
$gasPriceGwei = hexdec($gasPrice->toString()) / 1e9;
// 50 Gwei ์ดํ์ผ ๋๋ง ์ ์ก
if ($gasPriceGwei <= 50) {
return $gasPrice;
}
return null; // ๊ฐ์ค๋น๊ฐ ๋๋ฌด ๋น์ธ๋ฉด ๋๊ธฐ
}
4. ์คํ์ฒด์ธ ์ฒ๋ฆฌ
๋ชจ๋ ๊ฑธ ๋ธ๋ก์ฒด์ธ์ ์ฌ๋ฆด ํ์๋ ์์ด. ์ค์ํ ๊ฒ๋ง ์จ์ฒด์ธ์!
| ๋ฐ์ดํฐ ์ข ๋ฅ | ์ ์ฅ ์์น | ์ด์ |
|---|---|---|
| ๊ฒฐ์ ๊ธ์ก, ์ฃผ์ | ์จ์ฒด์ธ | ๋ถ๋ณ์ฑ, ํฌ๋ช ์ฑ ํ์ |
| ์ฌ์ฉ์ ํ๋กํ | ์คํ์ฒด์ธ | ์์ฃผ ๋ณ๊ฒฝ, ๊ฐ์ธ์ ๋ณด |
| ์ํ ์ค๋ช , ์ด๋ฏธ์ง | ์คํ์ฒด์ธ/IPFS | ์ฉ๋ ํผ, ๊ฐ์ค๋น ์ ์ฝ |
| ์ฑํ , ๋๊ธ | ์คํ์ฒด์ธ | ์ค์๊ฐ์ฑ, ๋๋ ๋ฐ์ดํฐ |
๐ต ์์ต ๋ชจ๋ธ
๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์๋น์ค๋ก ์ด๋ป๊ฒ ์์ต์ ๋ผ ์ ์์๊น?
1. ํ๋ซํผ ์์๋ฃ
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๋ชจ๋ธ์ด์ผ. ๊ฑฐ๋ ๊ธ์ก์ ์ผ์ ๋น์จ์ ์์๋ฃ๋ก ๋ฐ๋ ๊ฑฐ์ง.
// ์ค๋งํธ ์ปจํธ๋ํธ์์ ์๋ ์ ์ฐ
uint256 platformFeePercent = 3; // 3%
uint256 platformFee = (amount * platformFeePercent) / 100;
uint256 sellerAmount = amount - platformFee;
payable(seller).transfer(sellerAmount);
payable(platformOwner).transfer(platformFee);
2. ํ๋ฆฌ๋ฏธ์ ๊ธฐ๋ฅ
โข ๋น ๋ฅธ ์ ์ฐ (์ฆ์ ์ถ๊ธ)
โข ๋์ ๊ฑฐ๋ ํ๋
โข ์ฐ์ ๊ณ ๊ฐ ์ง์
โข ๋ถ์ ๋์๋ณด๋
โข API ์ ๊ทผ
3. ๊ตฌ๋
๋ชจ๋ธ
์์ ์ก์ผ๋ก ๋ฌด์ ํ ๊ฑฐ๋ ๋๋ ๋ฎ์ ์์๋ฃ ์ ๊ณต
<?php
class Subscription extends Model
{
const PLAN_FREE = 'free';
const PLAN_BASIC = 'basic';
const PLAN_PRO = 'pro';
const PLAN_ENTERPRISE = 'enterprise';
public function getFeeRate()
{
$rates = [
self::PLAN_FREE => 5.0, // 5%
self::PLAN_BASIC => 3.0, // 3%
self::PLAN_PRO => 2.0, // 2%
self::PLAN_ENTERPRISE => 1.0 // 1%
];
return $rates[$this->plan] ?? $rates[self::PLAN_FREE];
}
}
4. ํ ํฐ ์ด์ฝ๋
ธ๋ฏธ
์์ฒด ํ ํฐ์ ๋ฐํํด์ ์ํ๊ณ๋ฅผ ๋ง๋ค ์๋ ์์ด. ํ ํฐ ํ๋์๊ฒ ์์๋ฃ ํ ์ธ, ๊ฑฐ๋ฒ๋์ค ๊ถํ ๋ฑ์ ์ ๊ณตํ๋ ๊ฑฐ์ง.
๐ ์ค์ ์์ : ๊ฐ๋จํ NFT ๋ง์ผํ๋ ์ด์ค
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ์ข ํฉํด์ ๊ฐ๋จํ NFT ๋ง์ผํ๋ ์ด์ค๋ฅผ ๋ง๋ค์ด๋ณด์! ์ค์ ๋ก ๋์๊ฐ๋ ์์ ์ผ. ๐จ
๐ผ๏ธ NFT ๊ฒฐ์ ์ปจํธ๋ํธ
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTMarketplace is ReentrancyGuard, Ownable {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool active;
}
mapping(uint256 => Listing) public listings;
uint256 public listingCounter;
uint256 public platformFeePercent = 2; // 2%
event NFTListed(
uint256 indexed listingId,
address indexed seller,
address indexed nftContract,
uint256 tokenId,
uint256 price
);
event NFTSold(
uint256 indexed listingId,
address indexed buyer,
address indexed seller,
uint256 price
);
event ListingCancelled(uint256 indexed listingId);
constructor() Ownable(msg.sender) {}
function listNFT(
address _nftContract,
uint256 _tokenId,
uint256 _price
) external returns (uint256) {
require(_price > 0, "Price must be greater than 0");
IERC721 nft = IERC721(_nftContract);
require(nft.ownerOf(_tokenId) == msg.sender, "Not the owner");
require(
nft.isApprovedForAll(msg.sender, address(this)) ||
nft.getApproved(_tokenId) == address(this),
"Marketplace not approved"
);
uint256 listingId = listingCounter++;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: _nftContract,
tokenId: _tokenId,
price: _price,
active: true
});
emit NFTListed(listingId, msg.sender, _nftContract, _tokenId, _price);
return listingId;
}
function buyNFT(uint256 _listingId) external payable nonReentrant {
Listing storage listing = listings[_listingId];
require(listing.active, "Listing not active");
require(msg.value >= listing.price, "Insufficient payment");
listing.active = false;
uint256 platformFee = (listing.price * platformFeePercent) / 100;
uint256 sellerAmount = listing.price - platformFee;
IERC721(listing.nftContract).safeTransferFrom(
listing.seller,
msg.sender,
listing.tokenId
);
payable(listing.seller).transfer(sellerAmount);
payable(owner()).transfer(platformFee);
if (msg.value > listing.price) {
payable(msg.sender).transfer(msg.value - listing.price);
}
emit NFTSold(_listingId, msg.sender, listing.seller, listing.price);
}
function cancelListing(uint256 _listingId) external {
Listing storage listing = listings[_listingId];
require(listing.active, "Listing not active");
require(listing.seller == msg.sender, "Not the seller");
listing.active = false;
emit ListingCancelled(_listingId);
}
function updatePlatformFee(uint256 _newFee) external onlyOwner {
require(_newFee <= 10, "Fee too high");
platformFeePercent = _newFee;
}
}
๐ง ๋ผ๋ผ๋ฒจ ํตํฉ
<?php
namespace App\Http\Controllers;
use App\Models\NFTListing;
use App\Services\Web3Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class NFTMarketplaceController extends Controller
{
protected $web3Service;
public function __construct(Web3Service $web3Service)
{
$this->web3Service = $web3Service;
}
public function listNFT(Request $request)
{
$validated = $request->validate([
'nft_contract' => 'required|string|size:42',
'token_id' => 'required|integer',
'price' => 'required|numeric|min:0.001',
'title' => 'required|string|max:200',
'description' => 'nullable|string|max:1000',
'image_url' => 'required|url'
]);
try {
// DB์ ๋ฆฌ์คํ
์์ฑ
$listing = NFTListing::create([
'user_id' => Auth::id(),
'nft_contract' => $validated['nft_contract'],
'token_id' => $validated['token_id'],
'price' => $validated['price'],
'title' => $validated['title'],
'description' => $validated['description'] ?? null,
'image_url' => $validated['image_url'],
'status' => 'pending'
]);
// ๋ธ๋ก์ฒด์ธ ํธ๋์ญ์
๋ฐ์ดํฐ ์์ฑ
$txData = $this->web3Service->createNFTListing(
$validated['nft_contract'],
$validated['token_id'],
$validated['price']
);
return response()->json([
'success' => true,
'listing_id' => $listing->id,
'transaction_data' => $txData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
public function getListings(Request $request)
{
$query = NFTListing::where('status', 'active');
if ($request->has('min_price')) {
$query->where('price', '>=', $request->min_price);
}
if ($request->has('max_price')) {
$query->where('price', '<=', $request->max_price);
}
if ($request->has('search')) {
$query->where(function($q) use ($request) {
$q->where('title', 'like', "%{$request->search}%")
->orWhere('description', 'like', "%{$request->search}%");
});
}
$listings = $query->orderBy('created_at', 'desc')
->paginate(20);
return response()->json([
'success' => true,
'listings' => $listings
]);
}
public function buyNFT(Request $request, $id)
{
try {
$listing = NFTListing::findOrFail($id);
if ($listing->status !== 'active') {
return response()->json([
'success' => false,
'message' => 'Listing is not active'
], 400);
}
// ๋ธ๋ก์ฒด์ธ ๊ตฌ๋งค ํธ๋์ญ์
๋ฐ์ดํฐ ์์ฑ
$txData = $this->web3Service->buyNFT(
$listing->blockchain_listing_id,
$listing->price
);
return response()->json([
'success' => true,
'transaction_data' => $txData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
}
๐จ ํ๋ก ํธ์๋ NFT ๊ฐค๋ฌ๋ฆฌ
<template>
<div class="nft-gallery">
<div class="filters">
<input
v-model="filters.search"
type="text"
placeholder="๊ฒ์..."
@input="searchNFTs"
/>
<div class="price-range">
<input
v-model="filters.minPrice"
type="number"
placeholder="์ต์ ๊ฐ๊ฒฉ"
/>
<input
v-model="filters.maxPrice"
type="number"
placeholder="์ต๋ ๊ฐ๊ฒฉ"
/>
<button @click="applyFilters">์ ์ฉ</button>
</div>
</div>
<div class="nft-grid">
<div
v-for="nft in nfts"
:key="nft.id"
class="nft-card"
>
<img :src="nft.image_url" :alt="nft.title" />
<div class="nft-info">
<h3>{{ nft.title }}</h3>
<p class="description">{{ nft.description }}</p>
<div class="price-section">
<span class="price">{{ nft.price }} ETH</span>
<button
@click="buyNFT(nft)"
class="btn-buy"
:disabled="!isConnected"
>
๊ตฌ๋งคํ๊ธฐ
</button>
</div>
</div>
</div>
</div>
<div v-if="loading" class="loading">
๋ก๋ฉ ์ค...
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useWeb3 } from '../composables/useWeb3';
import axios from 'axios';
import { ethers } from 'ethers';
const { isConnected, signer } = useWeb3();
const nfts = ref([]);
const loading = ref(false);
const filters = ref({
search: '',
minPrice: null,
maxPrice: null
});
const MARKETPLACE_ADDRESS = import.meta.env.VITE_MARKETPLACE_ADDRESS;
const MARKETPLACE_ABI = [
"function buyNFT(uint256 _listingId) external payable"
];
const loadNFTs = async () => {
try {
loading.value = true;
const params = new URLSearchParams();
if (filters.value.search) params.append('search', filters.value.search);
if (filters.value.minPrice) params.append('min_price', filters.value.minPrice);
if (filters.value.maxPrice) params.append('max_price', filters.value.maxPrice);
const response = await axios.get(`/api/nft-listings?${params}`);
nfts.value = response.data.listings.data;
} catch (error) {
console.error('Failed to load NFTs:', error);
alert('NFT ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋๋ฐ ์คํจํ์ต๋๋ค.');
} finally {
loading.value = false;
}
};
const buyNFT = async (nft) => {
try {
if (!isConnected.value) {
alert('๋จผ์ ์ง๊ฐ์ ์ฐ๊ฒฐํด์ฃผ์ธ์.');
return;
}
// ๋ฐฑ์๋์์ ํธ๋์ญ์
๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
const response = await axios.post(`/api/nft-listings/${nft.id}/buy`);
const { transaction_data } = response.data;
// ์ค๋งํธ ์ปจํธ๋ํธ ํธ์ถ
const contract = new ethers.Contract(
MARKETPLACE_ADDRESS,
MARKETPLACE_ABI,
signer.value
);
const tx = await contract.buyNFT(
nft.blockchain_listing_id,
{
value: ethers.parseEther(nft.price.toString()),
gasLimit: 300000
}
);
alert('ํธ๋์ญ์
์ด ์ ์ก๋์์ต๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์...');
await tx.wait();
alert('NFT ๊ตฌ๋งค๊ฐ ์๋ฃ๋์์ต๋๋ค!');
// ๋ชฉ๋ก ์๋ก๊ณ ์นจ
await loadNFTs();
} catch (error) {
console.error('Failed to buy NFT:', error);
alert('NFT ๊ตฌ๋งค์ ์คํจํ์ต๋๋ค: ' + error.message);
}
};
const searchNFTs = debounce(() => {
loadNFTs();
}, 500);
const applyFilters = () => {
loadNFTs();
};
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
onMounted(() => {
loadNFTs();
});
</script>
<style scoped>
.nft-gallery {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.filters {
margin-bottom: 30px;
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.filters input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
flex: 1;
min-width: 200px;
}
.price-range {
display: flex;
gap: 10px;
}
.nft-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 25px;
}
.nft-card {
border: 1px solid #e0e0e0;
border-radius: 12px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.nft-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
}
.nft-card img {
width: 100%;
height: 280px;
object-fit: cover;
}
.nft-info {
padding: 15px;
}
.nft-info h3 {
margin: 0 0 10px 0;
font-size: 18px;
}
.description {
color: #666;
font-size: 14px;
margin-bottom: 15px;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
}
.price-section {
display: flex;
justify-content: space-between;
align-items: center;
}
.price {
font-size: 20px;
font-weight: bold;
color: #667eea;
}
.btn-buy {
padding: 8px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
.btn-buy:hover {
background: #5568d3;
}
.btn-buy:disabled {
background: #ccc;
cursor: not-allowed;
}
.loading {
text-align: center;
padding: 40px;
font-size: 18px;
color: #666;
}
</style>
์ด๋ ๊ฒ ํ๋ฉด ์์ ํ ์๋ํ๋ NFT ๋ง์ผํ๋ ์ด์ค๊ฐ ์์ฑ๋ผ! ์ฌ์ฉ์๋ NFT๋ฅผ ๋ฑ๋กํ๊ณ , ๋ค๋ฅธ ์ฌ๋๋ค์ ETH๋ก ๊ตฌ๋งคํ ์ ์์ด. ๋ชจ๋ ๊ฑฐ๋๋ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋ก๋๊ณ , ํ๋ซํผ์ ์๋์ผ๋ก ์์๋ฃ๋ฅผ ๋ฐ์ง. ๐
๐ฏ ๋ง๋ฌด๋ฆฌ ๋ฐ ๋ค์ ๋จ๊ณ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด. ๋ผ๋ผ๋ฒจ๊ณผ ์๋ฆฌ๋ํฐ๋ก ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์๋น์ค๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ฒ์๋ถํฐ ๋๊น์ง ๋ค๋ค๋ดค์ด. ์ ๋ฆฌํด๋ณผ๊น? ๐
โ ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ด ๊ฒ๋ค
1๏ธโฃ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถ: ๋ผ๋ผ๋ฒจ, Hardhat, MetaMask ์ค์
2๏ธโฃ ์ค๋งํธ ์ปจํธ๋ํธ: ์๋ฆฌ๋ํฐ๋ก ๊ฒฐ์ ๋ก์ง ๊ตฌํ
3๏ธโฃ ๋ฐฑ์๋ ํตํฉ: Web3.php๋ก ๋ธ๋ก์ฒด์ธ ์ฐ๋
4๏ธโฃ ํ๋ก ํธ์๋: Vue.js์ ethers.js๋ก ์ฌ์ฉ์ ์ธํฐํ์ด์ค
5๏ธโฃ ๋ณด์: ์ค๋งํธ ์ปจํธ๋ํธ ๋ฐ ๋ฐฑ์๋ ๋ณด์ ๊ฐํ
6๏ธโฃ ์ต์ ํ: ๊ฐ์ค๋น ์ ๊ฐ ๋ฐ ์ฑ๋ฅ ๊ฐ์
7๏ธโฃ ๋ฐฐํฌ: ํ
์คํธ๋ท์์ ๋ฉ์ธ๋ท๊น์ง
8๏ธโฃ ์ด์: ๋ชจ๋ํฐ๋ง, ๋ฐฑ์
, ์ฌํด ๋ณต๊ตฌ
9๏ธโฃ ์ค์ ์์ : NFT ๋ง์ผํ๋ ์ด์ค ๊ตฌํ
๐ ๋ค์ ๋จ๊ณ๋ก ๋์๊ฐ๊ธฐ
์ด์ ๊ธฐ๋ณธ์ ๋ง์คํฐํ์ผ๋, ๋ ๋ฐ์ ์์ผ๋ณด์!
๊ณ ๊ธ ๊ธฐ๋ฅ ์ถ๊ฐ:
โข ๋ค์ค ์๋ช
์ง๊ฐ (Multisig)
โข ํ์๋ฝ (Timelock) ๊ธฐ๋ฅ
โข DAO ๊ฑฐ๋ฒ๋์ค
โข ์คํ
์ดํน ์์คํ
โข ํฌ๋ก์ค์ฒด์ธ ๋ธ๋ฆฟ์ง
๋ ๊ณต๋ถํ ๊ฒ๋ค:
โข Solidity ๊ณ ๊ธ ํจํด (Proxy, Diamond)
โข DeFi ํ๋กํ ์ฝ (Uniswap, Aave ๋ฑ)
โข Zero-Knowledge Proofs
โข MEV (Maximal Extractable Value)
โข ๋ณด์ ๊ฐ์ฌ (Audit) ๋ฐฉ๋ฒ๋ก
์ปค๋ฎค๋ํฐ ์ฐธ์ฌ:
โข GitHub์์ ์คํ์์ค ํ๋ก์ ํธ ๊ธฐ์ฌ
โข Ethereum Stack Exchange์์ ์ง๋ฌธ/๋ต๋ณ
โข ํด์ปคํค ์ฐธ๊ฐ
โข ๋ธ๋ก์ฒด์ธ ์ปจํผ๋ฐ์ค ์ฐธ์
์ด์ ๋น์ ๋ ๋ธ๋ก์ฒด์ธ ๊ฒฐ์ ์๋น์ค๋ฅผ ๋ง๋ค ์ ์์ด! ๐
์ค์ ์์ ๋ถ๋ชํ๋ฉด์ ๋ฐฐ์ฐ๋ ๊ฒ ๊ฐ์ฅ ๋น ๋ฅธ ๋ฐฉ๋ฒ์ด์ผ.
์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ ํค์๋๊ฐ๋ณด์!
๐ ์ฌ๋ฅ๋ท์์ ๋ธ๋ก์ฒด์ธ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๊ฑฐ๋,
์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์๋ณด๋ ๊ฑด ์ด๋?
ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ๊ฐ ๊ธฐ๋ค๋ฆฌ๊ณ ์์ด! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.