์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ํŒŒ์ด์ฌ์œผ๋กœ ์ง€๊ฐ‘ ๊ฐ„ ํ† ํฐ ์Šค์™‘ ์ž๋™ํ™” ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿš€ ํŒŒ์ด์ฌ์œผ๋กœ ์ง€๊ฐ‘ ๊ฐ„ ํ† ํฐ ์Šค์™‘ ์ž๋™ํ™” ๊ตฌํ˜„ํ•˜๊ธฐ

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์‹ค์ „ ๊ฐ€์ด๋“œ ๐Ÿ’Ž

๐ŸŽฏ ์™œ ํ† ํฐ ์Šค์™‘ ์ž๋™ํ™”๊ฐ€ ํ•„์š”ํ• ๊นŒ?

์•ผ, ์†”์งํžˆ ๋งํ•ด์„œ ์•”ํ˜ธํ™”ํ ๊ฑฐ๋ž˜ํ•˜๋‹ค ๋ณด๋ฉด ์ง„์งœ ๊ท€์ฐฎ์€ ์ˆœ๊ฐ„๋“ค ๋งŽ์ง€ ์•Š์•„? ๐Ÿค”

ํŠนํžˆ DeFi(ํƒˆ์ค‘์•™ํ™” ๊ธˆ์œต) ์ƒํƒœ๊ณ„์—์„œ ํ™œ๋™ํ•˜๋‹ค ๋ณด๋ฉด, ์—ฌ๋Ÿฌ ์ง€๊ฐ‘ ์‚ฌ์ด์—์„œ ํ† ํฐ์„ ์˜ฎ๊ธฐ๊ณ , ์Šค์™‘ํ•˜๊ณ , ๋˜ ๋‹ค์‹œ ์˜ฎ๊ธฐ๊ณ ... ์ด๋Ÿฐ ๋ฐ˜๋ณต ์ž‘์—…์ด ์ •๋ง ๋งŽ์•„. ์ˆ˜๋™์œผ๋กœ ํ•˜๋‚˜ํ•˜๋‚˜ ์ฒ˜๋ฆฌํ•˜๋‹ค ๋ณด๋ฉด ์‹œ๊ฐ„๋„ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ณ , ์‹ค์ˆ˜ํ•  ๊ฐ€๋Šฅ์„ฑ๋„ ๋†’์•„์ง€์ง€.

๋” ํฐ ๋ฌธ์ œ๋Š” ํƒ€์ด๋ฐ์ด์•ผ. ์•”ํ˜ธํ™”ํ ์‹œ์žฅ์€ 24์‹œ๊ฐ„ ์‰ฌ์ง€ ์•Š๊ณ  ์›€์ง์ด๊ฑฐ๋“ ? ์ƒˆ๋ฒฝ 3์‹œ์— ์ข‹์€ ์Šค์™‘ ๊ธฐํšŒ๊ฐ€ ์™”๋Š”๋ฐ, ์ž๊ณ  ์žˆ์œผ๋ฉด ๊ทธ๋ƒฅ ๋‚ ๋ ค๋ฒ„๋ฆฌ๋Š” ๊ฑฐ์ง€. ๐Ÿ˜ด

๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ ํŒŒ์ด์ฌ์œผ๋กœ ์ด ๋ชจ๋“  ๊ฑธ ์ž๋™ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ค„๊ฒŒ. ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์‹ค๋ ฅ์„ ํ‚ค์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๊ด€๋ จ ๊ฐ•์˜๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ!
Wallet A Wallet B SWAP Token Transfer Automated Process

๐Ÿ”ง ํ•„์š”ํ•œ ๊ธฐ์ˆ  ์Šคํƒ๊ณผ ์ค€๋น„๋ฌผ

๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์—, ๋ญ˜ ์ค€๋น„ํ•ด์•ผ ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž. ๊ฑฑ์ • ๋งˆ, ์ƒ๊ฐ๋ณด๋‹ค ๋ณต์žกํ•˜์ง€ ์•Š์•„! ๐Ÿ˜Š

๐Ÿ“ฆ ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

โ€ข Web3.py: ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข eth-account: ์ง€๊ฐ‘ ๊ด€๋ฆฌ์™€ ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…์„ ์œ„ํ•œ ๋„๊ตฌ
โ€ข python-dotenv: ๋ฏผ๊ฐํ•œ ์ •๋ณด(๊ฐœ์ธํ‚ค ๋“ฑ)๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌ
โ€ข requests: API ํ˜ธ์ถœ์„ ์œ„ํ•œ HTTP ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข asyncio: ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ํšจ์œจ์„ฑ ๊ทน๋Œ€ํ™”
๐ŸŽ“ ํ•„์š”ํ•œ ๋ฐฐ๊ฒฝ ์ง€์‹

์†”์งํžˆ ๋งํ•˜๋ฉด, ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ด ์ฒ˜์Œ์ด๋ผ๋ฉด ์ข€ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผ์˜ค๋ฉด ์ถฉ๋ถ„ํžˆ ํ•  ์ˆ˜ ์žˆ์–ด! ๋‹ค์Œ ๊ฐœ๋…๋“ค์„ ์•Œ๊ณ  ์žˆ์œผ๋ฉด ํ›จ์”ฌ ์ˆ˜์›”ํ•ด:

1. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๊ธฐ์ดˆ
์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋Š” ๋ธ”๋ก์ฒด์ธ ์œ„์—์„œ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜๋Š” ํ”„๋กœ๊ทธ๋žจ์ด์•ผ. ํ† ํฐ ์Šค์™‘๋„ ๊ฒฐ๊ตญ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์˜ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฑฐ๊ฑฐ๋“ .

2. ERC-20 ํ† ํฐ ํ‘œ์ค€
์ด๋”๋ฆฌ์›€์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” ํ† ํฐ ํ‘œ์ค€์ด์•ผ. transfer, approve, allowance ๊ฐ™์€ ํ•จ์ˆ˜๋“ค์„ ์ดํ•ดํ•ด์•ผ ํ•ด.

3. ๊ฐ€์Šค(Gas) ๊ฐœ๋…
๋ธ”๋ก์ฒด์ธ์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์‹คํ–‰ํ•˜๋ ค๋ฉด ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๋‚ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๊ฒŒ ๋ฐ”๋กœ ๊ฐ€์Šค์•ผ. ๊ฐ€์Šค ๊ฐ€๊ฒฉ ์„ค์ •์„ ์ž˜๋ชปํ•˜๋ฉด ํŠธ๋žœ์žญ์…˜์ด ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ๋น„์‹ธ์งˆ ์ˆ˜ ์žˆ์–ด.

4. DEX(ํƒˆ์ค‘์•™ํ™” ๊ฑฐ๋ž˜์†Œ) ์ž‘๋™ ์›๋ฆฌ
Uniswap, PancakeSwap ๊ฐ™์€ DEX๊ฐ€ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ์•Œ์•„์•ผ ํ•ด. AMM(Automated Market Maker) ๋ฐฉ์‹์„ ์ดํ•ดํ•˜๋ฉด ๊ธˆ์ƒ์ฒจํ™”! ๐Ÿ’ฏ
๐Ÿ’ก ์ดˆ๋ณด์ž ํŒ

์ฒ˜์Œ๋ถ€ํ„ฐ ๋ฉ”์ธ๋„ท์—์„œ ์‹ค์Šตํ•˜์ง€ ๋งˆ! ํ…Œ์ŠคํŠธ๋„ท(Goerli, Sepolia ๋“ฑ)์—์„œ ์ถฉ๋ถ„ํžˆ ์—ฐ์Šตํ•œ ๋‹ค์Œ์— ์‹ค์ „์— ํˆฌ์ž…ํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด. ์‹ค์ˆ˜๋กœ ์ง„์งœ ๋ˆ ๋‚ ๋ฆฌ๋ฉด... ๐Ÿ˜ฑ

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ…ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋”ฉ ํ™˜๊ฒฝ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๋‹จ๊ณ„๋ณ„๋กœ ์ฒœ์ฒœํžˆ ๋”ฐ๋ผ์™€ ๋ด.

Step 1: ๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ

ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ํ™˜๊ฒฝ์„ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ข‹์•„. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ .

python -m venv swap_automation
source swap_automation/bin/activate  # Windows: swap_automation\Scripts\activate
Step 2: ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

pip install web3 eth-account python-dotenv requests aiohttp
๊ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋ญ ํ•˜๋Š” ๊ฑด์ง€ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ•˜๋ฉด:
โ€ข web3: ๋ธ”๋ก์ฒด์ธ๊ณผ ๋Œ€ํ™”ํ•˜๋Š” ํ†ต์—ญ์‚ฌ ๐Ÿ—ฃ๏ธ
โ€ข eth-account: ์ง€๊ฐ‘ ๊ด€๋ฆฌ์ž ๐Ÿ‘›
โ€ข python-dotenv: ๋น„๋ฐ€ ์ •๋ณด ๋ณด๊ด€ํ•จ ๐Ÿ”
โ€ข aiohttp: ๋น ๋ฅธ ๋น„๋™๊ธฐ HTTP ์š”์ฒญ โšก
Step 3: ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ

๊น”๋”ํ•œ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋Š” ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ›จ์”ฌ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค˜.

swap_automation/
โ”œโ”€โ”€ .env                 # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ (๊ฐœ์ธํ‚ค, API ํ‚ค ๋“ฑ)
โ”œโ”€โ”€ config.py           # ์„ค์ • ํŒŒ์ผ
โ”œโ”€โ”€ wallet_manager.py   # ์ง€๊ฐ‘ ๊ด€๋ฆฌ ๋ชจ๋“ˆ
โ”œโ”€โ”€ swap_engine.py      # ์Šค์™‘ ๋กœ์ง ํ•ต์‹ฌ
โ”œโ”€โ”€ price_monitor.py    # ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง
โ”œโ”€โ”€ main.py            # ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ
โ””โ”€โ”€ utils.py           # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋“ค
Step 4: .env ํŒŒ์ผ ์„ค์ •

์ ˆ๋Œ€๋กœ ๊ฐœ์ธํ‚ค๋ฅผ ์ฝ”๋“œ์— ์ง์ ‘ ๋„ฃ์œผ๋ฉด ์•ˆ ๋ผ! GitHub์— ์˜ฌ๋ ธ๋‹ค๊ฐ€ ํ•ดํ‚น๋‹นํ•˜๋Š” ์‚ฌ๋ก€๊ฐ€ ์ง„์งœ ๋งŽ๊ฑฐ๋“ . ๐Ÿ˜จ

# .env ํŒŒ์ผ
PRIVATE_KEY_A=your_wallet_a_private_key_here
PRIVATE_KEY_B=your_wallet_b_private_key_here
INFURA_API_KEY=your_infura_api_key
ETHERSCAN_API_KEY=your_etherscan_api_key
RPC_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID
โš ๏ธ ๋ณด์•ˆ ๊ฒฝ๊ณ 

.env ํŒŒ์ผ์€ ์ ˆ๋Œ€ Git์— ์ปค๋ฐ‹ํ•˜์ง€ ๋งˆ! .gitignore์— ๊ผญ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด. ๊ทธ๋ฆฌ๊ณ  ๊ฐœ์ธํ‚ค๋Š” ์ ˆ๋Œ€ ๋ˆ„๊ตฌ์—๊ฒŒ๋„ ๊ณต์œ ํ•˜์ง€ ๋ง ๊ฒƒ!

๐Ÿ’ผ ์ง€๊ฐ‘ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž! ๋จผ์ € ์ง€๊ฐ‘์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ชจ๋“ˆ๋ถ€ํ„ฐ ๋งŒ๋“ค ๊ฑฐ์•ผ.

wallet_manager.py ์ž‘์„ฑ

from web3 import Web3
from eth_account import Account
from dotenv import load_dotenv
import os

load_dotenv()

class WalletManager:
    def __init__(self, rpc_url):
        """
        ์ง€๊ฐ‘ ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™”
        rpc_url: ๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ ์—ฐ๊ฒฐ ์ฃผ์†Œ
        """
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        
        # ์—ฐ๊ฒฐ ํ™•์ธ
        if not self.w3.is_connected():
            raise Exception("๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ ์—ฐ๊ฒฐ ์‹คํŒจ! ๐Ÿ˜ข")
        
        print(f"โœ… ๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ ์„ฑ๊ณต! ํ˜„์žฌ ๋ธ”๋ก: {self.w3.eth.block_number}")
        
        # ์ง€๊ฐ‘ ๋กœ๋“œ
        self.wallet_a = self._load_wallet(os.getenv('PRIVATE_KEY_A'))
        self.wallet_b = self._load_wallet(os.getenv('PRIVATE_KEY_B'))
    
    def _load_wallet(self, private_key):
        """๊ฐœ์ธํ‚ค๋กœ๋ถ€ํ„ฐ ์ง€๊ฐ‘ ๊ฐ์ฒด ์ƒ์„ฑ"""
        if not private_key:
            raise ValueError("๊ฐœ์ธํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์–ด์š”!")
        
        account = Account.from_key(private_key)
        return {
            'address': account.address,
            'private_key': private_key,
            'account': account
        }
    
    def get_balance(self, wallet_type='A', token_address=None):
        """
        ์ง€๊ฐ‘ ์ž”์•ก ์กฐํšŒ
        wallet_type: 'A' ๋˜๋Š” 'B'
        token_address: None์ด๋ฉด ETH, ์ฃผ์†Œ ์ž…๋ ฅ์‹œ ํ•ด๋‹น ํ† ํฐ
        """
        wallet = self.wallet_a if wallet_type == 'A' else self.wallet_b
        
        if token_address is None:
            # ETH ์ž”์•ก ์กฐํšŒ
            balance_wei = self.w3.eth.get_balance(wallet['address'])
            balance_eth = self.w3.from_wei(balance_wei, 'ether')
            return float(balance_eth)
        else:
            # ERC-20 ํ† ํฐ ์ž”์•ก ์กฐํšŒ
            token_contract = self._get_token_contract(token_address)
            balance = token_contract.functions.balanceOf(wallet['address']).call()
            decimals = token_contract.functions.decimals().call()
            return balance / (10 ** decimals)
    
    def _get_token_contract(self, token_address):
        """ERC-20 ํ† ํฐ ์ปจํŠธ๋ž™ํŠธ ๊ฐ์ฒด ๋ฐ˜ํ™˜"""
        # ํ‘œ์ค€ ERC-20 ABI (๊ฐ„์†Œํ™” ๋ฒ„์ „)
        erc20_abi = [
            {
                "constant": True,
                "inputs": [{"name": "_owner", "type": "address"}],
                "name": "balanceOf",
                "outputs": [{"name": "balance", "type": "uint256"}],
                "type": "function"
            },
            {
                "constant": True,
                "inputs": [],
                "name": "decimals",
                "outputs": [{"name": "", "type": "uint8"}],
                "type": "function"
            },
            {
                "constant": False,
                "inputs": [
                    {"name": "_spender", "type": "address"},
                    {"name": "_value", "type": "uint256"}
                ],
                "name": "approve",
                "outputs": [{"name": "", "type": "bool"}],
                "type": "function"
            },
            {
                "constant": True,
                "inputs": [
                    {"name": "_owner", "type": "address"},
                    {"name": "_spender", "type": "address"}
                ],
                "name": "allowance",
                "outputs": [{"name": "", "type": "uint256"}],
                "type": "function"
            }
        ]
        
        return self.w3.eth.contract(
            address=Web3.to_checksum_address(token_address),
            abi=erc20_abi
        )

์ด ์ฝ”๋“œ๊ฐ€ ํ•˜๋Š” ์ผ์„ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•˜๋ฉด:

1. ๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ ๐Ÿ”—
Web3 ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ด๋”๋ฆฌ์›€ ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐํ•ด. Infura ๊ฐ™์€ ๋…ธ๋“œ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ง์ ‘ ๋…ธ๋“œ๋ฅผ ์šด์˜ํ•˜์ง€ ์•Š์•„๋„ ๋ผ.

2. ์ง€๊ฐ‘ ๋กœ๋“œ ๐Ÿ‘›
๊ฐœ์ธํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ง€๊ฐ‘ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด. ์ด ๊ฐ์ฒด๋กœ ํŠธ๋žœ์žญ์…˜์— ์„œ๋ช…ํ•  ์ˆ˜ ์žˆ์–ด.

3. ์ž”์•ก ์กฐํšŒ ๐Ÿ’ฐ
ETH๋“  ํ† ํฐ์ด๋“  ํ˜„์žฌ ์ž”์•ก์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด. ํ† ํฐ์˜ ๊ฒฝ์šฐ decimals๋ฅผ ๊ณ ๋ คํ•ด์„œ ์‹ค์ œ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ค˜์•ผ ํ•ด.

4. ํ† ํฐ ์ปจํŠธ๋ž™ํŠธ ์ธํ„ฐํŽ˜์ด์Šค ๐Ÿ“„
ERC-20 ํ‘œ์ค€ ABI๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ† ํฐ ์ปจํŠธ๋ž™ํŠธ์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด.
๐Ÿ’ก Pro Tip

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ „์ฒด ERC-20 ABI๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์œ„ ์ฝ”๋“œ๋Š” ์„ค๋ช…์„ ์œ„ํ•ด ํ•„์š”ํ•œ ํ•จ์ˆ˜๋งŒ ํฌํ•จ์‹œํ‚จ ๊ฑฐ์•ผ. Etherscan์—์„œ ์ปจํŠธ๋ž™ํŠธ ABI๋ฅผ ๋ณต์‚ฌํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ!

๐Ÿ”„ ์Šค์™‘ ์—”์ง„ ํ•ต์‹ฌ ๋กœ์ง ๊ตฌํ˜„

์ž, ์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ! ์‹ค์ œ๋กœ ํ† ํฐ์„ ์Šค์™‘ํ•˜๋Š” ๋กœ์ง์„ ๋งŒ๋“ค์–ด๋ณด์ž. ์—ฌ๊ธฐ์„œ๋Š” Uniswap V2๋ฅผ ์˜ˆ์‹œ๋กœ ๋“ค๊ฒŒ. ๐Ÿฆ„

swap_engine.py ์ž‘์„ฑ

from web3 import Web3
from eth_account import Account
import time
import json

class SwapEngine:
    def __init__(self, wallet_manager, router_address, router_abi):
        """
        ์Šค์™‘ ์—”์ง„ ์ดˆ๊ธฐํ™”
        wallet_manager: WalletManager ์ธ์Šคํ„ด์Šค
        router_address: Uniswap Router ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ
        router_abi: Router ABI
        """
        self.wm = wallet_manager
        self.w3 = wallet_manager.w3
        
        # Uniswap Router ์ปจํŠธ๋ž™ํŠธ
        self.router = self.w3.eth.contract(
            address=Web3.to_checksum_address(router_address),
            abi=router_abi
        )
        
        # WETH ์ฃผ์†Œ (Wrapped ETH)
        self.weth_address = self.router.functions.WETH().call()
        
        print(f"๐Ÿฆ„ Uniswap Router ์—ฐ๊ฒฐ ์™„๋ฃŒ!")
        print(f"WETH ์ฃผ์†Œ: {self.weth_address}")
    
    def approve_token(self, token_address, wallet_type='A', amount=None):
        """
        ํ† ํฐ ์‚ฌ์šฉ ์Šน์ธ (approve)
        DEX๊ฐ€ ๋‚ด ํ† ํฐ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ๊ฐ€ํ•˜๋Š” ๊ณผ์ •
        """
        wallet = self.wm.wallet_a if wallet_type == 'A' else self.wm.wallet_b
        token_contract = self.wm._get_token_contract(token_address)
        
        # amount๊ฐ€ None์ด๋ฉด ๋ฌด์ œํ•œ ์Šน์ธ
        if amount is None:
            amount = 2**256 - 1  # ์ตœ๋Œ€๊ฐ’
        else:
            decimals = token_contract.functions.decimals().call()
            amount = int(amount * (10 ** decimals))
        
        # ํ˜„์žฌ ์Šน์ธ๋Ÿ‰ ํ™•์ธ
        current_allowance = token_contract.functions.allowance(
            wallet['address'],
            self.router.address
        ).call()
        
        if current_allowance >= amount:
            print(f"โœ… ์ด๋ฏธ ์ถฉ๋ถ„ํ•œ ์Šน์ธ๋Ÿ‰์ด ์žˆ์–ด์š”: {current_allowance}")
            return True
        
        # approve ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ
        approve_txn = token_contract.functions.approve(
            self.router.address,
            amount
        ).build_transaction({
            'from': wallet['address'],
            'gas': 100000,
            'gasPrice': self.w3.eth.gas_price,
            'nonce': self.w3.eth.get_transaction_count(wallet['address'])
        })
        
        # ํŠธ๋žœ์žญ์…˜ ์„œ๋ช… ๋ฐ ์ „์†ก
        signed_txn = self.w3.eth.account.sign_transaction(
            approve_txn,
            wallet['private_key']
        )
        
        tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
        print(f"๐Ÿ“ Approve ํŠธ๋žœ์žญ์…˜ ์ „์†ก: {tx_hash.hex()}")
        
        # ํŠธ๋žœ์žญ์…˜ ์™„๋ฃŒ ๋Œ€๊ธฐ
        receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
        
        if receipt['status'] == 1:
            print(f"โœ… Approve ์„ฑ๊ณต!")
            return True
        else:
            print(f"โŒ Approve ์‹คํŒจ!")
            return False
    
    def get_amounts_out(self, amount_in, path):
        """
        ์Šค์™‘ ์‹œ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ํ† ํฐ ์–‘ ๊ณ„์‚ฐ
        amount_in: ์ž…๋ ฅ ํ† ํฐ ์–‘
        path: ์Šค์™‘ ๊ฒฝ๋กœ [token_in, token_out]
        """
        # ํ† ํฐ decimals ๊ฐ€์ ธ์˜ค๊ธฐ
        token_in_contract = self.wm._get_token_contract(path[0])
        decimals = token_in_contract.functions.decimals().call()
        amount_in_wei = int(amount_in * (10 ** decimals))
        
        # Uniswap์—์„œ ์˜ˆ์ƒ ์ถœ๋ ฅ๋Ÿ‰ ์กฐํšŒ
        amounts = self.router.functions.getAmountsOut(
            amount_in_wei,
            path
        ).call()
        
        # ์ถœ๋ ฅ ํ† ํฐ decimals๋กœ ๋ณ€ํ™˜
        token_out_contract = self.wm._get_token_contract(path[-1])
        out_decimals = token_out_contract.functions.decimals().call()
        amount_out = amounts[-1] / (10 ** out_decimals)
        
        return amount_out
    
    def execute_swap(self, amount_in, amount_out_min, path, wallet_type='A', deadline_minutes=20):
        """
        ์‹ค์ œ ์Šค์™‘ ์‹คํ–‰
        amount_in: ์ž…๋ ฅ ํ† ํฐ ์–‘
        amount_out_min: ์ตœ์†Œ ์ถœ๋ ฅ ํ† ํฐ ์–‘ (์Šฌ๋ฆฌํ”ผ์ง€ ๋ณดํ˜ธ)
        path: ์Šค์™‘ ๊ฒฝ๋กœ
        wallet_type: ์‚ฌ์šฉํ•  ์ง€๊ฐ‘
        deadline_minutes: ํŠธ๋žœ์žญ์…˜ ์œ ํšจ ์‹œ๊ฐ„
        """
        wallet = self.wm.wallet_a if wallet_type == 'A' else self.wm.wallet_b
        
        # ํ† ํฐ decimals ์ฒ˜๋ฆฌ
        token_in_contract = self.wm._get_token_contract(path[0])
        token_out_contract = self.wm._get_token_contract(path[-1])
        
        in_decimals = token_in_contract.functions.decimals().call()
        out_decimals = token_out_contract.functions.decimals().call()
        
        amount_in_wei = int(amount_in * (10 ** in_decimals))
        amount_out_min_wei = int(amount_out_min * (10 ** out_decimals))
        
        # Deadline ์„ค์ • (ํ˜„์žฌ ์‹œ๊ฐ„ + ์ง€์ • ๋ถ„)
        deadline = int(time.time()) + (deadline_minutes * 60)
        
        print(f"\n๐Ÿ”„ ์Šค์™‘ ์ค€๋น„ ์ค‘...")
        print(f"์ž…๋ ฅ: {amount_in} {path[0][:6]}...")
        print(f"์ตœ์†Œ ์ถœ๋ ฅ: {amount_out_min} {path[-1][:6]}...")
        
        # ์Šค์™‘ ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ
        swap_txn = self.router.functions.swapExactTokensForTokens(
            amount_in_wei,
            amount_out_min_wei,
            path,
            wallet['address'],
            deadline
        ).build_transaction({
            'from': wallet['address'],
            'gas': 250000,  # ๊ฐ€์Šค ๋ฆฌ๋ฐ‹
            'gasPrice': self.w3.eth.gas_price,
            'nonce': self.w3.eth.get_transaction_count(wallet['address'])
        })
        
        # ๊ฐ€์Šค ์˜ˆ์ธก (์„ ํƒ์‚ฌํ•ญ)
        try:
            estimated_gas = self.w3.eth.estimate_gas(swap_txn)
            swap_txn['gas'] = int(estimated_gas * 1.2)  # 20% ์—ฌ์œ 
            print(f"โ›ฝ ์˜ˆ์ƒ ๊ฐ€์Šค: {estimated_gas}")
        except Exception as e:
            print(f"โš ๏ธ ๊ฐ€์Šค ์˜ˆ์ธก ์‹คํŒจ, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ: {e}")
        
        # ํŠธ๋žœ์žญ์…˜ ์„œ๋ช…
        signed_txn = self.w3.eth.account.sign_transaction(
            swap_txn,
            wallet['private_key']
        )
        
        # ํŠธ๋žœ์žญ์…˜ ์ „์†ก
        tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
        print(f"\n๐Ÿ“ค ์Šค์™‘ ํŠธ๋žœ์žญ์…˜ ์ „์†ก!")
        print(f"TX Hash: {tx_hash.hex()}")
        
        # ํŠธ๋žœ์žญ์…˜ ์™„๋ฃŒ ๋Œ€๊ธฐ
        print("โณ ๋ธ”๋ก ํ™•์ธ ๋Œ€๊ธฐ ์ค‘...")
        receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
        
        if receipt['status'] == 1:
            print(f"\nโœ… ์Šค์™‘ ์„ฑ๊ณต! ๐ŸŽ‰")
            print(f"๊ฐ€์Šค ์‚ฌ์šฉ๋Ÿ‰: {receipt['gasUsed']}")
            print(f"๋ธ”๋ก ๋ฒˆํ˜ธ: {receipt['blockNumber']}")
            return {
                'success': True,
                'tx_hash': tx_hash.hex(),
                'gas_used': receipt['gasUsed']
            }
        else:
            print(f"\nโŒ ์Šค์™‘ ์‹คํŒจ! ๐Ÿ˜ข")
            return {
                'success': False,
                'tx_hash': tx_hash.hex()
            }
    
    def swap_with_slippage_protection(self, amount_in, path, wallet_type='A', 
                                     slippage_percent=0.5):
        """
        ์Šฌ๋ฆฌํ”ผ์ง€ ๋ณดํ˜ธ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” ์Šค์™‘
        slippage_percent: ํ—ˆ์šฉ ์Šฌ๋ฆฌํ”ผ์ง€ (%)
        """
        # ์˜ˆ์ƒ ์ถœ๋ ฅ๋Ÿ‰ ๊ณ„์‚ฐ
        expected_out = self.get_amounts_out(amount_in, path)
        
        # ์Šฌ๋ฆฌํ”ผ์ง€ ์ ์šฉํ•œ ์ตœ์†Œ ์ถœ๋ ฅ๋Ÿ‰
        amount_out_min = expected_out * (1 - slippage_percent / 100)
        
        print(f"\n๐Ÿ’ฑ ์Šฌ๋ฆฌํ”ผ์ง€ ๋ณดํ˜ธ ์Šค์™‘")
        print(f"์˜ˆ์ƒ ์ถœ๋ ฅ: {expected_out:.6f}")
        print(f"์ตœ์†Œ ์ถœ๋ ฅ: {amount_out_min:.6f} (์Šฌ๋ฆฌํ”ผ์ง€ {slippage_percent}%)")
        
        # ์Šค์™‘ ์‹คํ–‰
        return self.execute_swap(amount_in, amount_out_min, path, wallet_type)

์™€, ์ฝ”๋“œ๊ฐ€ ์ข€ ๊ธธ์ง€? ํ•˜์ง€๋งŒ ํ•˜๋‚˜ํ•˜๋‚˜ ๋œฏ์–ด๋ณด๋ฉด ๊ทธ๋ ‡๊ฒŒ ์–ด๋ ต์ง€ ์•Š์•„! ํ•ต์‹ฌ ๊ฐœ๋…๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ. ๐Ÿ“š

๐Ÿ”‘ ํ•ต์‹ฌ ๊ฐœ๋… ์„ค๋ช…

1. Approve (์Šน์ธ)
DEX๊ฐ€ ๋‚ด ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋จผ์ € ์Šน์ธ์„ ๋ฐ›์•„์•ผ ํ•ด. ์ด๊ฒŒ ์—†์œผ๋ฉด ์Šค์™‘์ด ๋ถˆ๊ฐ€๋Šฅํ•ด. ํ•œ ๋ฒˆ ์Šน์ธํ•˜๋ฉด ์—ฌ๋Ÿฌ ๋ฒˆ ์Šค์™‘ํ•  ์ˆ˜ ์žˆ์–ด.

2. getAmountsOut (์˜ˆ์ƒ ์ถœ๋ ฅ๋Ÿ‰)
์Šค์™‘ํ•˜๊ธฐ ์ „์— ์–ผ๋งˆ๋‚˜ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š”์ง€ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•ด. ์ด๊ฑธ๋กœ ๊ฑฐ๋ž˜๊ฐ€ ์ด๋“์ธ์ง€ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ์ง€.

3. Slippage (์Šฌ๋ฆฌํ”ผ์ง€)
์˜ˆ์ƒ ๊ฐ€๊ฒฉ๊ณผ ์‹ค์ œ ์ฒด๊ฒฐ ๊ฐ€๊ฒฉ์˜ ์ฐจ์ด์•ผ. ๋ณ€๋™์„ฑ์ด ํฐ ์‹œ์žฅ์—์„œ๋Š” ์Šฌ๋ฆฌํ”ผ์ง€๊ฐ€ ํด ์ˆ˜ ์žˆ์–ด์„œ, ์ตœ์†Œ ์ถœ๋ ฅ๋Ÿ‰์„ ์„ค์ •ํ•ด์„œ ๋ณดํ˜ธํ•ด์•ผ ํ•ด.

4. Deadline (๋งˆ๊ฐ ์‹œ๊ฐ„)
ํŠธ๋žœ์žญ์…˜์ด ๋„ˆ๋ฌด ์˜ค๋ž˜ ๋Œ€๊ธฐํ•˜๋ฉด ๊ฐ€๊ฒฉ์ด ๋งŽ์ด ๋ณ€ํ•  ์ˆ˜ ์žˆ์–ด. ๊ทธ๋ž˜์„œ ์œ ํšจ ์‹œ๊ฐ„์„ ์„ค์ •ํ•˜๋Š” ๊ฑฐ์•ผ.

5. Gas Estimation (๊ฐ€์Šค ์˜ˆ์ธก)
ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰์— ํ•„์š”ํ•œ ๊ฐ€์Šค๋ฅผ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•ด. ๋ถ€์กฑํ•˜๋ฉด ์‹คํŒจํ•˜๊ณ , ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ๋‚ญ๋น„๋‹ˆ๊นŒ ์ ์ ˆํ•œ ๊ฐ’์„ ์ฐพ๋Š” ๊ฒŒ ์ค‘์š”ํ•ด.

๐Ÿ“Š ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ•

์Šค์™‘์„ ์ž๋™ํ™”ํ•˜๋ ค๋ฉด ๊ฐ€๊ฒฉ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•ด์•ผ ํ•ด. ์ข‹์€ ๊ธฐํšŒ๋ฅผ ๋†“์น˜์ง€ ์•Š์œผ๋ ค๋ฉด ๋ง์ด์•ผ! ๐Ÿ‘€

price_monitor.py ์ž‘์„ฑ

import asyncio
import aiohttp
from datetime import datetime
import json

class PriceMonitor:
    def __init__(self, swap_engine):
        """
        ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ ์ดˆ๊ธฐํ™”
        swap_engine: SwapEngine ์ธ์Šคํ„ด์Šค
        """
        self.swap_engine = swap_engine
        self.price_history = {}
        self.alert_conditions = []
        
    def add_alert_condition(self, token_pair, condition_type, threshold, callback):
        """
        ๊ฐ€๊ฒฉ ์•Œ๋ฆผ ์กฐ๊ฑด ์ถ”๊ฐ€
        token_pair: (token_a, token_b) ํŠœํ”Œ
        condition_type: 'above' ๋˜๋Š” 'below'
        threshold: ์ž„๊ณ„๊ฐ’
        callback: ์กฐ๊ฑด ์ถฉ์กฑ์‹œ ์‹คํ–‰ํ•  ํ•จ์ˆ˜
        """
        self.alert_conditions.append({
            'pair': token_pair,
            'type': condition_type,
            'threshold': threshold,
            'callback': callback,
            'triggered': False
        })
        print(f"๐Ÿ”” ์•Œ๋ฆผ ์กฐ๊ฑด ์ถ”๊ฐ€: {token_pair} {condition_type} {threshold}")
    
    async def get_current_price(self, token_in, token_out, amount=1.0):
        """
        ํ˜„์žฌ ๊ฐ€๊ฒฉ ์กฐํšŒ (๋น„๋™๊ธฐ)
        """
        try:
            price = self.swap_engine.get_amounts_out(
                amount,
                [token_in, token_out]
            )
            return price
        except Exception as e:
            print(f"โŒ ๊ฐ€๊ฒฉ ์กฐํšŒ ์‹คํŒจ: {e}")
            return None
    
    async def monitor_price(self, token_in, token_out, interval=10):
        """
        ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง (๋น„๋™๊ธฐ)
        interval: ์ฒดํฌ ๊ฐ„๊ฒฉ (์ดˆ)
        """
        pair_key = f"{token_in[:6]}.../{token_out[:6]}..."
        
        print(f"\n๐Ÿ‘๏ธ ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘: {pair_key}")
        print(f"์ฒดํฌ ๊ฐ„๊ฒฉ: {interval}์ดˆ")
        
        while True:
            try:
                price = await self.get_current_price(token_in, token_out)
                
                if price is not None:
                    timestamp = datetime.now().strftime("%H:%M:%S")
                    
                    # ๊ฐ€๊ฒฉ ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ
                    if pair_key not in self.price_history:
                        self.price_history[pair_key] = []
                    
                    self.price_history[pair_key].append({
                        'timestamp': timestamp,
                        'price': price
                    })
                    
                    # ์ตœ๊ทผ 100๊ฐœ๋งŒ ์œ ์ง€
                    if len(self.price_history[pair_key]) > 100:
                        self.price_history[pair_key].pop(0)
                    
                    print(f"[{timestamp}] {pair_key}: {price:.6f}")
                    
                    # ์•Œ๋ฆผ ์กฐ๊ฑด ์ฒดํฌ
                    await self._check_alert_conditions(
                        (token_in, token_out),
                        price
                    )
                
                await asyncio.sleep(interval)
                
            except Exception as e:
                print(f"โŒ ๋ชจ๋‹ˆํ„ฐ๋ง ์˜ค๋ฅ˜: {e}")
                await asyncio.sleep(interval)
    
    async def _check_alert_conditions(self, token_pair, current_price):
        """์•Œ๋ฆผ ์กฐ๊ฑด ํ™•์ธ ๋ฐ ์‹คํ–‰"""
        for condition in self.alert_conditions:
            if condition['pair'] != token_pair:
                continue
            
            if condition['triggered']:
                continue
            
            should_trigger = False
            
            if condition['type'] == 'above' and current_price > condition['threshold']:
                should_trigger = True
            elif condition['type'] == 'below' and current_price < condition['threshold']:
                should_trigger = True
            
            if should_trigger:
                print(f"\n๐Ÿšจ ์•Œ๋ฆผ ์กฐ๊ฑด ์ถฉ์กฑ!")
                print(f"ํŽ˜์–ด: {token_pair}")
                print(f"ํ˜„์žฌ ๊ฐ€๊ฒฉ: {current_price}")
                print(f"์กฐ๊ฑด: {condition['type']} {condition['threshold']}")
                
                condition['triggered'] = True
                
                # ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ์‹คํ–‰
                if asyncio.iscoroutinefunction(condition['callback']):
                    await condition['callback'](token_pair, current_price)
                else:
                    condition['callback'](token_pair, current_price)
    
    def get_price_statistics(self, pair_key):
        """๊ฐ€๊ฒฉ ํ†ต๊ณ„ ๊ณ„์‚ฐ"""
        if pair_key not in self.price_history:
            return None
        
        prices = [item['price'] for item in self.price_history[pair_key]]
        
        if not prices:
            return None
        
        return {
            'current': prices[-1],
            'min': min(prices),
            'max': max(prices),
            'avg': sum(prices) / len(prices),
            'change': ((prices[-1] - prices[0]) / prices[0] * 100) if len(prices) > 1 else 0
        }
    
    async def monitor_multiple_pairs(self, pairs, interval=10):
        """
        ์—ฌ๋Ÿฌ ํ† ํฐ ํŽ˜์–ด ๋™์‹œ ๋ชจ๋‹ˆํ„ฐ๋ง
        pairs: [(token_in, token_out), ...] ๋ฆฌ์ŠคํŠธ
        """
        tasks = []
        for token_in, token_out in pairs:
            task = asyncio.create_task(
                self.monitor_price(token_in, token_out, interval)
            )
            tasks.append(task)
        
        await asyncio.gather(*tasks)

์ด ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์˜ ์žฅ์ ์€ ๋ญ˜๊นŒ? ๐Ÿค”

1. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ โšก
asyncio๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์—ฌ๋Ÿฌ ํ† ํฐ ํŽ˜์–ด๋ฅผ ๋™์‹œ์— ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด. ํ•˜๋‚˜์”ฉ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒดํฌํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ํšจ์œจ์ ์ด์ง€!

2. ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๐Ÿ””
ํŠน์ • ๊ฐ€๊ฒฉ ์กฐ๊ฑด์ด ์ถฉ์กฑ๋˜๋ฉด ์ž๋™์œผ๋กœ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๊ฑฐ๋‚˜ ์Šค์™‘์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. 24์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ๊ฐ€๋Šฅํ•ด์ง€๋Š” ๊ฑฐ์•ผ.

3. ๊ฐ€๊ฒฉ ํžˆ์Šคํ† ๋ฆฌ ๐Ÿ“ˆ
๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด์„œ ํ†ต๊ณ„๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ์–ด. ์ด๊ฑธ๋กœ ๋” ๋˜‘๋˜‘ํ•œ ๊ฑฐ๋ž˜ ์ „๋žต์„ ์„ธ์šธ ์ˆ˜ ์žˆ์ง€.

4. ํ™•์žฅ ๊ฐ€๋Šฅ์„ฑ ๐Ÿš€
์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋‹ค์–‘ํ•œ ์ž๋™ํ™” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. ํ…”๋ ˆ๊ทธ๋žจ ์•Œ๋ฆผ, ์ด๋ฉ”์ผ ๋ฐœ์†ก, ์ž๋™ ์Šค์™‘ ๋“ฑ ๋ญ๋“ ์ง€ ๊ฐ€๋Šฅํ•ด!

๐ŸŽฎ ๋ฉ”์ธ ์‹คํ–‰ ๋กœ์ง ์ž‘์„ฑ

์ž, ์ด์ œ ๋ชจ๋“  ๋ชจ๋“ˆ์„ ํ†ตํ•ฉํ•ด์„œ ์‹ค์ œ๋กœ ๋™์ž‘ํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐ŸŽฏ

main.py ์ž‘์„ฑ

import asyncio
from wallet_manager import WalletManager
from swap_engine import SwapEngine
from price_monitor import PriceMonitor
import json
import os
from dotenv import load_dotenv

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ
load_dotenv()

# Uniswap V2 Router ์ •๋ณด
UNISWAP_ROUTER_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"

# ๊ฐ„์†Œํ™”๋œ Uniswap Router ABI (ํ•„์š”ํ•œ ํ•จ์ˆ˜๋งŒ)
UNISWAP_ROUTER_ABI = json.loads('''[
    {
        "inputs": [],
        "name": "WETH",
        "outputs": [{"internalType": "address", "name": "", "type": "address"}],
        "stateMutability": "pure",
        "type": "function"
    },
    {
        "inputs": [
            {"internalType": "uint256", "name": "amountIn", "type": "uint256"},
            {"internalType": "address[]", "name": "path", "type": "address[]"}
        ],
        "name": "getAmountsOut",
        "outputs": [{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {"internalType": "uint256", "name": "amountIn", "type": "uint256"},
            {"internalType": "uint256", "name": "amountOutMin", "type": "uint256"},
            {"internalType": "address[]", "name": "path", "type": "address[]"},
            {"internalType": "address", "name": "to", "type": "address"},
            {"internalType": "uint256", "name": "deadline", "type": "uint256"}
        ],
        "name": "swapExactTokensForTokens",
        "outputs": [{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]''')

# ํ† ํฐ ์ฃผ์†Œ (์˜ˆ์‹œ - ์‹ค์ œ ์ฃผ์†Œ๋กœ ๋ณ€๊ฒฝ ํ•„์š”)
USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
DAI_ADDRESS = "0x6B175474E89094C44Da98b954EedeAC495271d0F"

async def auto_swap_on_price_condition(token_pair, current_price):
    """
    ๊ฐ€๊ฒฉ ์กฐ๊ฑด ์ถฉ์กฑ์‹œ ์ž๋™ ์Šค์™‘ ์‹คํ–‰
    """
    print(f"\n๐Ÿค– ์ž๋™ ์Šค์™‘ ์‹œ์ž‘!")
    
    token_in, token_out = token_pair
    amount_to_swap = 100  # ์Šค์™‘ํ•  ์–‘ (์‹ค์ œ๋กœ๋Š” ๋™์ ์œผ๋กœ ๊ณ„์‚ฐ)
    
    # ์Šค์™‘ ์‹คํ–‰
    result = swap_engine.swap_with_slippage_protection(
        amount_in=amount_to_swap,
        path=[token_in, token_out],
        wallet_type='A',
        slippage_percent=1.0
    )
    
    if result['success']:
        print(f"โœ… ์ž๋™ ์Šค์™‘ ์™„๋ฃŒ! TX: {result['tx_hash']}")
    else:
        print(f"โŒ ์ž๋™ ์Šค์™ ์‹คํŒจ!")

async def main():
    """๋ฉ”์ธ ์‹คํ–‰ ํ•จ์ˆ˜"""
    print("=" * 60)
    print("๐Ÿš€ ํ† ํฐ ์Šค์™‘ ์ž๋™ํ™” ์‹œ์Šคํ…œ ์‹œ์ž‘!")
    print("=" * 60)
    
    # 1. ์ง€๊ฐ‘ ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™”
    print("\n๐Ÿ“ฑ ์ง€๊ฐ‘ ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™” ์ค‘...")
    wallet_manager = WalletManager(os.getenv('RPC_URL'))
    
    # ์ง€๊ฐ‘ ์ž”์•ก ํ™•์ธ
    print(f"\n๐Ÿ’ฐ Wallet A ์ž”์•ก:")
    eth_balance = wallet_manager.get_balance('A')
    print(f"  ETH: {eth_balance:.4f}")
    
    # 2. ์Šค์™‘ ์—”์ง„ ์ดˆ๊ธฐํ™”
    print("\n๐Ÿ”ง ์Šค์™‘ ์—”์ง„ ์ดˆ๊ธฐํ™” ์ค‘...")
    global swap_engine
    swap_engine = SwapEngine(
        wallet_manager,
        UNISWAP_ROUTER_ADDRESS,
        UNISWAP_ROUTER_ABI
    )
    
    # 3. ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ ์ดˆ๊ธฐํ™”
    print("\n๐Ÿ“Š ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘...")
    price_monitor = PriceMonitor(swap_engine)
    
    # 4. ์•Œ๋ฆผ ์กฐ๊ฑด ์„ค์ •
    print("\nโš™๏ธ ์•Œ๋ฆผ ์กฐ๊ฑด ์„ค์ • ์ค‘...")
    
    # ์˜ˆ์‹œ: USDC/DAI ๊ฐ€๊ฒฉ์ด 1.01 ์ด์ƒ์ด๋ฉด ์ž๋™ ์Šค์™‘
    price_monitor.add_alert_condition(
        token_pair=(USDC_ADDRESS, DAI_ADDRESS),
        condition_type='above',
        threshold=1.01,
        callback=auto_swap_on_price_condition
    )
    
    # 5. ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘
    print("\n๐Ÿ‘๏ธ ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘!")
    print("Ctrl+C๋ฅผ ๋ˆŒ๋Ÿฌ ์ข…๋ฃŒํ•˜์„ธ์š”.\n")
    
    try:
        # ์—ฌ๋Ÿฌ ํŽ˜์–ด ๋™์‹œ ๋ชจ๋‹ˆํ„ฐ๋ง
        await price_monitor.monitor_multiple_pairs(
            pairs=[
                (USDC_ADDRESS, DAI_ADDRESS),
                # ํ•„์š”ํ•œ ๋งŒํผ ํŽ˜์–ด ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
            ],
            interval=30  # 30์ดˆ๋งˆ๋‹ค ์ฒดํฌ
        )
    except KeyboardInterrupt:
        print("\n\nโน๏ธ ํ”„๋กœ๊ทธ๋žจ ์ข…๋ฃŒ!")
        
        # ํ†ต๊ณ„ ์ถœ๋ ฅ
        print("\n๐Ÿ“ˆ ๊ฐ€๊ฒฉ ํ†ต๊ณ„:")
        for pair_key in price_monitor.price_history:
            stats = price_monitor.get_price_statistics(pair_key)
            if stats:
                print(f"\n{pair_key}:")
                print(f"  ํ˜„์žฌ: {stats['current']:.6f}")
                print(f"  ์ตœ์ €: {stats['min']:.6f}")
                print(f"  ์ตœ๊ณ : {stats['max']:.6f}")
                print(f"  ํ‰๊ท : {stats['avg']:.6f}")
                print(f"  ๋ณ€๋™: {stats['change']:.2f}%")

if __name__ == "__main__":
    asyncio.run(main())

์ด์ œ ํ”„๋กœ๊ทธ๋žจ์„ ์‹คํ–‰ํ•˜๋ฉด ์ด๋Ÿฐ ํ๋ฆ„์œผ๋กœ ๋™์ž‘ํ•ด:

1๋‹จ๊ณ„: ์ดˆ๊ธฐํ™” ๐Ÿ”ง
์ง€๊ฐ‘ ์—ฐ๊ฒฐ โ†’ ์Šค์™‘ ์—”์ง„ ์ค€๋น„ โ†’ ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ ์„ค์ •

2๋‹จ๊ณ„: ๋ชจ๋‹ˆํ„ฐ๋ง ๐Ÿ‘€
์„ค์ •ํ•œ ํ† ํฐ ํŽ˜์–ด์˜ ๊ฐ€๊ฒฉ์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ฒดํฌ

3๋‹จ๊ณ„: ์กฐ๊ฑด ํ™•์ธ ๐ŸŽฏ
์•Œ๋ฆผ ์กฐ๊ฑด์ด ์ถฉ์กฑ๋˜๋Š”์ง€ ๊ณ„์† ํ™•์ธ

4๋‹จ๊ณ„: ์ž๋™ ์‹คํ–‰ ๐Ÿค–
์กฐ๊ฑด์ด ์ถฉ์กฑ๋˜๋ฉด ์ž๋™์œผ๋กœ ์Šค์™‘ ์‹คํ–‰!

5๋‹จ๊ณ„: ํ†ต๊ณ„ ์ˆ˜์ง‘ ๐Ÿ“Š
๋ชจ๋“  ๊ฐ€๊ฒฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด์„œ ๋‚˜์ค‘์— ๋ถ„์„ ๊ฐ€๋Šฅ
์ž๋™ํ™” ์‹œ์Šคํ…œ ํ”Œ๋กœ์šฐ 1. ์ดˆ๊ธฐํ™” ์ง€๊ฐ‘ ์—ฐ๊ฒฐ 2. ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ€๊ฒฉ ์ถ”์  3. ์กฐ๊ฑด ํ™•์ธ ์•Œ๋ฆผ ์ฒดํฌ 4. ์‹คํ–‰ ์ž๋™ ์Šค์™‘ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ ์Šฌ๋ฆฌํ”ผ์ง€ ๋ณดํ˜ธ ๊ฐ€์Šค ์ตœ์ ํ™” ์‹ค์‹œ๊ฐ„ ๊ฐ€๊ฒฉ ์ถ”์  ์†์‹ค ๋ฐฉ์ง€ ๋น„์šฉ ์ ˆ๊ฐ 24/7 ์ž๋™ ์šด์˜ ์‚ฌ๋žŒ์˜ ๊ฐœ์ž… ์—†์ด ์ง€์†์ ์œผ๋กœ ๋™์ž‘

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

์ž๋™ํ™” ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์—ˆ๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ์•ผ. ์‹ค์ œ๋กœ ์šด์˜ํ•˜๋ ค๋ฉด ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ์ •๋ง ์ค‘์š”ํ•ด! ๐Ÿ’ช

1. ๊ฐ€์Šค ๋น„์šฉ ์ตœ์ ํ™”

๋ธ”๋ก์ฒด์ธ์—์„œ ๊ฐ€์žฅ ํฐ ๋น„์šฉ์€ ๋ฐ”๋กœ ๊ฐ€์Šค์•ผ. ๋˜‘๋˜‘ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฉด ์ˆ˜์ต๋ณด๋‹ค ๋น„์šฉ์ด ๋” ํด ์ˆ˜๋„ ์žˆ์–ด. ๐Ÿ˜ฑ

๊ฐ€์Šค ๊ฐ€๊ฒฉ ๋™์  ์กฐ์ • ์ฝ”๋“œ

def get_optimal_gas_price(self):
    """
    ํ˜„์žฌ ๋„คํŠธ์›Œํฌ ์ƒํ™ฉ์— ๋งž๋Š” ์ตœ์  ๊ฐ€์Šค ๊ฐ€๊ฒฉ ๊ณ„์‚ฐ
    """
    # ํ˜„์žฌ ๊ฐ€์Šค ๊ฐ€๊ฒฉ ์กฐํšŒ
    current_gas = self.w3.eth.gas_price
    
    # ์ตœ๊ทผ ๋ธ”๋ก๋“ค์˜ ๊ฐ€์Šค ๊ฐ€๊ฒฉ ๋ถ„์„
    latest_block = self.w3.eth.block_number
    gas_prices = []
    
    for i in range(5):  # ์ตœ๊ทผ 5๊ฐœ ๋ธ”๋ก
        block = self.w3.eth.get_block(latest_block - i)
        if 'baseFeePerGas' in block:
            gas_prices.append(block['baseFeePerGas'])
    
    if gas_prices:
        avg_gas = sum(gas_prices) // len(gas_prices)
        # ํ‰๊ท ๋ณด๋‹ค 10% ๋†’๊ฒŒ ์„ค์ • (๋น ๋ฅธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด)
        optimal_gas = int(avg_gas * 1.1)
    else:
        optimal_gas = current_gas
    
    # ์ตœ๋Œ€ ๊ฐ€์Šค ๊ฐ€๊ฒฉ ์ œํ•œ (๋„ˆ๋ฌด ๋น„์‹ธ๋ฉด ์‹คํ–‰ ์•ˆ ํ•จ)
    max_gas_price = self.w3.to_wei(100, 'gwei')
    
    return min(optimal_gas, max_gas_price)

2. RPC ์š”์ฒญ ์ตœ์ ํ™”

๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ์— ๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์†๋„๊ฐ€ ๋А๋ ค์ง€๊ณ , ๋ฌด๋ฃŒ ํ”Œ๋žœ์ด๋ฉด ์ œํ•œ์— ๊ฑธ๋ฆด ์ˆ˜๋„ ์žˆ์–ด.

๐ŸŽฏ RPC ์ตœ์ ํ™” ํŒ

โ€ข ๋ฐฐ์น˜ ์š”์ฒญ ์‚ฌ์šฉ: ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์š”์ฒญ์„ ํ•œ ๋ฒˆ์— ๋ณด๋‚ด๊ธฐ
โ€ข ์บ์‹ฑ ํ™œ์šฉ: ์ž์ฃผ ์กฐํšŒํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ๋กœ์ปฌ์— ์ €์žฅ
โ€ข ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ: HTTP๋ณด๋‹ค ๋น ๋ฅธ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 
โ€ข ์—ฌ๋Ÿฌ RPC ์—”๋“œํฌ์ธํŠธ: ํ•˜๋‚˜๊ฐ€ ๋А๋ฆฌ๋ฉด ๋‹ค๋ฅธ ๊ฑธ๋กœ ์ „ํ™˜
class OptimizedRPCManager:
    def __init__(self, rpc_urls):
        """์—ฌ๋Ÿฌ RPC ์—”๋“œํฌ์ธํŠธ ๊ด€๋ฆฌ"""
        self.rpc_urls = rpc_urls
        self.current_index = 0
        self.w3_instances = [Web3(Web3.HTTPProvider(url)) for url in rpc_urls]
        self.cache = {}
        self.cache_ttl = 10  # ์บ์‹œ ์œ ํšจ ์‹œ๊ฐ„ (์ดˆ)
    
    def get_w3(self):
        """ํ˜„์žฌ ํ™œ์„ฑ Web3 ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜"""
        return self.w3_instances[self.current_index]
    
    def switch_rpc(self):
        """๋‹ค์Œ RPC๋กœ ์ „ํ™˜"""
        self.current_index = (self.current_index + 1) % len(self.rpc_urls)
        print(f"๐Ÿ”„ RPC ์ „ํ™˜: {self.rpc_urls[self.current_index]}")
    
    def get_with_cache(self, key, fetch_function, ttl=None):
        """์บ์‹ฑ์„ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ์กฐํšŒ"""
        import time
        
        if ttl is None:
            ttl = self.cache_ttl
        
        now = time.time()
        
        # ์บ์‹œ์— ์žˆ๊ณ  ์œ ํšจํ•˜๋ฉด ๋ฐ˜ํ™˜
        if key in self.cache:
            cached_data, cached_time = self.cache[key]
            if now - cached_time < ttl:
                return cached_data
        
        # ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋˜๋ฉด ์ƒˆ๋กœ ์กฐํšŒ
        data = fetch_function()
        self.cache[key] = (data, now)
        return data

3. ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ๊ณผ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

์—ฌ๋Ÿฌ ์ž‘์—…์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•˜๋ฉด ์ „์ฒด ์‹œ์Šคํ…œ ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ. ํŠนํžˆ ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ™์€ I/O ์ž‘์—…์— ํšจ๊ณผ์ ์ด์•ผ! ๐Ÿš€

import asyncio
import aiohttp
from concurrent.futures import ThreadPoolExecutor

class AsyncSwapSystem:
    def __init__(self):
        self.executor = ThreadPoolExecutor(max_workers=5)
    
    async def fetch_multiple_prices(self, token_pairs):
        """์—ฌ๋Ÿฌ ํ† ํฐ ํŽ˜์–ด์˜ ๊ฐ€๊ฒฉ์„ ๋™์‹œ์— ์กฐํšŒ"""
        async with aiohttp.ClientSession() as session:
            tasks = []
            for token_in, token_out in token_pairs:
                task = self.fetch_price_async(session, token_in, token_out)
                tasks.append(task)
            
            results = await asyncio.gather(*tasks, return_exceptions=True)
            return results
    
    async def fetch_price_async(self, session, token_in, token_out):
        """๋น„๋™๊ธฐ ๊ฐ€๊ฒฉ ์กฐํšŒ"""
        try:
            # API ํ˜ธ์ถœ ๋˜๋Š” ๋ธ”๋ก์ฒด์ธ ์กฐํšŒ
            # ์‹ค์ œ ๊ตฌํ˜„์€ ์‚ฌ์šฉํ•˜๋Š” DEX์— ๋”ฐ๋ผ ๋‹ค๋ฆ„
            price = await self.get_price_from_api(session, token_in, token_out)
            return {
                'pair': (token_in, token_out),
                'price': price,
                'success': True
            }
        except Exception as e:
            return {
                'pair': (token_in, token_out),
                'error': str(e),
                'success': False
            }
    
    async def parallel_swap_execution(self, swap_tasks):
        """์—ฌ๋Ÿฌ ์Šค์™‘์„ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰"""
        results = await asyncio.gather(*swap_tasks, return_exceptions=True)
        
        successful = [r for r in results if r.get('success')]
        failed = [r for r in results if not r.get('success')]
        
        print(f"โœ… ์„ฑ๊ณต: {len(successful)}๊ฐœ")
        print(f"โŒ ์‹คํŒจ: {len(failed)}๊ฐœ")
        
        return results

๐Ÿ›ก๏ธ ๋ณด์•ˆ๊ณผ ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ

์ž๋™ํ™” ์‹œ์Šคํ…œ์€ ํŽธ๋ฆฌํ•˜์ง€๋งŒ, ๋ณด์•ˆ์— ๋” ์‹ ๊ฒฝ ์จ์•ผ ํ•ด. ํ•œ ๋ฒˆ ์‹ค์ˆ˜ํ•˜๋ฉด ๋ชจ๋“  ์ž์‚ฐ์„ ์žƒ์„ ์ˆ˜๋„ ์žˆ๊ฑฐ๋“ ! ๐Ÿ˜ฐ

โš ๏ธ ํ•„์ˆ˜ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ๊ฐœ์ธํ‚ค๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๊ธฐ
  • โœ… .env ํŒŒ์ผ์€ Git์— ์ปค๋ฐ‹ํ•˜์ง€ ์•Š๊ธฐ
  • โœ… ์„œ๋ฒ„์—์„œ ์‹คํ–‰ํ•  ๋•Œ๋Š” ๋ฐฉํ™”๋ฒฝ ์„ค์ •
  • โœ… ์ •๊ธฐ์ ์œผ๋กœ ๋กœ๊ทธ ํ™•์ธํ•˜๊ธฐ
  • โœ… ๋น„์ •์ƒ์ ์ธ ํŠธ๋žœ์žญ์…˜ ๊ฐ์ง€ ์‹œ์Šคํ…œ ๊ตฌ์ถ•
  • โœ… ์ตœ๋Œ€ ๊ฑฐ๋ž˜ ๊ธˆ์•ก ์ œํ•œ ์„ค์ •
  • โœ… ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ถฉ๋ถ„ํžˆ ๊ฒ€์ฆ ํ›„ ๋ฉ”์ธ๋„ท ์‚ฌ์šฉ
์Šค๋งˆํŠธํ•œ ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ ์ฝ”๋“œ

class RiskManager:
    def __init__(self, max_daily_loss=1000, max_single_trade=100):
        """
        ๋ฆฌ์Šคํฌ ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™”
        max_daily_loss: ์ผ์ผ ์ตœ๋Œ€ ์†์‹ค ํ•œ๋„ (USD)
        max_single_trade: ๋‹จ์ผ ๊ฑฐ๋ž˜ ์ตœ๋Œ€ ๊ธˆ์•ก (USD)
        """
        self.max_daily_loss = max_daily_loss
        self.max_single_trade = max_single_trade
        self.daily_pnl = 0  # ์ผ์ผ ์†์ต
        self.trade_history = []
        self.emergency_stop = False
    
    def check_trade_allowed(self, trade_amount_usd):
        """๊ฑฐ๋ž˜ ์‹คํ–‰ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ"""
        # ๊ธด๊ธ‰ ์ •์ง€ ์ƒํƒœ ํ™•์ธ
        if self.emergency_stop:
            print("๐Ÿšจ ๊ธด๊ธ‰ ์ •์ง€ ์ƒํƒœ! ๊ฑฐ๋ž˜ ๋ถˆ๊ฐ€")
            return False
        
        # ๋‹จ์ผ ๊ฑฐ๋ž˜ ๊ธˆ์•ก ํ™•์ธ
        if trade_amount_usd > self.max_single_trade:
            print(f"โš ๏ธ ๊ฑฐ๋ž˜ ๊ธˆ์•ก ์ดˆ๊ณผ: {trade_amount_usd} > {self.max_single_trade}")
            return False
        
        # ์ผ์ผ ์†์‹ค ํ•œ๋„ ํ™•์ธ
        if self.daily_pnl < -self.max_daily_loss:
            print(f"๐Ÿ›‘ ์ผ์ผ ์†์‹ค ํ•œ๋„ ๋„๋‹ฌ: {self.daily_pnl}")
            self.emergency_stop = True
            return False
        
        return True
    
    def record_trade(self, trade_result):
        """๊ฑฐ๋ž˜ ๊ฒฐ๊ณผ ๊ธฐ๋ก"""
        self.trade_history.append(trade_result)
        
        if 'pnl' in trade_result:
            self.daily_pnl += trade_result['pnl']
        
        # ์ด์ƒ ํŒจํ„ด ๊ฐ์ง€
        self._detect_anomalies()
    
    def _detect_anomalies(self):
        """๋น„์ •์ƒ์ ์ธ ๊ฑฐ๋ž˜ ํŒจํ„ด ๊ฐ์ง€"""
        if len(self.trade_history) < 5:
            return
        
        # ์ตœ๊ทผ 5๊ฐœ ๊ฑฐ๋ž˜ ๋ถ„์„
        recent_trades = self.trade_history[-5:]
        failed_count = sum(1 for t in recent_trades if not t.get('success'))
        
        # ์—ฐ์† ์‹คํŒจ๊ฐ€ 3๋ฒˆ ์ด์ƒ์ด๋ฉด ๊ฒฝ๊ณ 
        if failed_count >= 3:
            print("๐Ÿšจ ๊ฒฝ๊ณ : ์—ฐ์† ๊ฑฐ๋ž˜ ์‹คํŒจ ๊ฐ์ง€!")
            print("์‹œ์Šคํ…œ ์ ๊ฒ€์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
            # ์—ฌ๊ธฐ์„œ ํ…”๋ ˆ๊ทธ๋žจ ์•Œ๋ฆผ ๋“ฑ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Œ
    
    def reset_daily_stats(self):
        """์ผ์ผ ํ†ต๊ณ„ ์ดˆ๊ธฐํ™” (๋งค์ผ ์ž์ •์— ์‹คํ–‰)"""
        print(f"\n๐Ÿ“Š ์ผ์ผ ๋ฆฌํฌํŠธ")
        print(f"์ด ๊ฑฐ๋ž˜: {len(self.trade_history)}๊ฑด")
        print(f"์†์ต: ${self.daily_pnl:.2f}")
        
        self.daily_pnl = 0
        self.trade_history = []
        self.emergency_stop = False

์Šฌ๋ฆฌํ”ผ์ง€ ๋ณดํ˜ธ ๊ฐ•ํ™”

์‹œ์žฅ ๋ณ€๋™์„ฑ์ด ํด ๋•Œ๋Š” ์Šฌ๋ฆฌํ”ผ์ง€๊ฐ€ ์˜ˆ์ƒ๋ณด๋‹ค ํ›จ์”ฌ ํด ์ˆ˜ ์žˆ์–ด. ๋™์ ์œผ๋กœ ์Šฌ๋ฆฌํ”ผ์ง€๋ฅผ ์กฐ์ •ํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

def calculate_dynamic_slippage(self, token_pair, base_slippage=0.5):
    """
    ์‹œ์žฅ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ์Šฌ๋ฆฌํ”ผ์ง€ ๊ณ„์‚ฐ
    """
    # ์ตœ๊ทผ ๊ฐ€๊ฒฉ ๋ณ€๋™์„ฑ ๊ณ„์‚ฐ
    price_history = self.get_recent_prices(token_pair, minutes=10)
    
    if len(price_history) < 2:
        return base_slippage
    
    # ํ‘œ์ค€ํŽธ์ฐจ๋กœ ๋ณ€๋™์„ฑ ์ธก์ •
    import statistics
    volatility = statistics.stdev(price_history)
    avg_price = statistics.mean(price_history)
    
    # ๋ณ€๋™์„ฑ ๋น„์œจ
    volatility_ratio = (volatility / avg_price) * 100
    
    # ๋ณ€๋™์„ฑ์ด ๋†’์œผ๋ฉด ์Šฌ๋ฆฌํ”ผ์ง€ ์ฆ๊ฐ€
    if volatility_ratio > 2.0:
        adjusted_slippage = base_slippage * 2
        print(f"โš ๏ธ ๋†’์€ ๋ณ€๋™์„ฑ ๊ฐ์ง€: {volatility_ratio:.2f}%")
        print(f"์Šฌ๋ฆฌํ”ผ์ง€ ์กฐ์ •: {base_slippage}% โ†’ {adjusted_slippage}%")
    elif volatility_ratio > 1.0:
        adjusted_slippage = base_slippage * 1.5
    else:
        adjusted_slippage = base_slippage
    
    # ์ตœ๋Œ€ ์Šฌ๋ฆฌํ”ผ์ง€ ์ œํ•œ
    return min(adjusted_slippage, 5.0)

๐Ÿ“ฑ ์‹ค์ „ ๋ฐฐํฌ์™€ ์šด์˜

์ฝ”๋“œ๋ฅผ ๋‹ค ์ž‘์„ฑํ–ˆ์œผ๋ฉด ์ด์ œ ์‹ค์ œ๋กœ ๋ฐฐํฌํ•ด์„œ ์šด์˜ํ•ด์•ผ์ง€! ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ์•ˆ์ •์ ์œผ๋กœ 24์‹œ๊ฐ„ ๋Œ๋ฆด ์ˆ˜ ์žˆ์„๊นŒ? ๐Ÿค”

1. ํด๋ผ์šฐ๋“œ ์„œ๋ฒ„ ๋ฐฐํฌ

๊ฐœ์ธ ์ปดํ“จํ„ฐ๋ฅผ 24์‹œ๊ฐ„ ์ผœ๋‘๋Š” ๊ฑด ๋น„ํšจ์œจ์ ์ด์•ผ. ํด๋ผ์šฐ๋“œ ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ํ›จ์”ฌ ๋‚˜์•„!

์„œ๋น„์Šค ์žฅ์  ๋‹จ์  ์ถ”์ฒœ๋„
AWS EC2 ์•ˆ์ •์ , ๋‹ค์–‘ํ•œ ์˜ต์…˜ ์ดˆ๋ณด์ž์—๊ฒŒ ๋ณต์žกํ•จ โญโญโญโญ
DigitalOcean ๊ฐ„๋‹จํ•œ UI, ์ €๋ ด ๊ธฐ๋Šฅ์ด ์ œํ•œ์  โญโญโญโญโญ
Google Cloud ๋ฌด๋ฃŒ ํฌ๋ ˆ๋”ง ์ œ๊ณต ๊ฐ€๊ฒฉ ์ฒด๊ณ„ ๋ณต์žก โญโญโญ
Heroku ๋ฐฐํฌ ๋งค์šฐ ์‰ฌ์›€ ๋น„์Œˆ, ์„ฑ๋Šฅ ์ œํ•œ โญโญโญ

2. Docker ์ปจํ…Œ์ด๋„ˆํ™”

Docker๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์–ด๋–ค ํ™˜๊ฒฝ์—์„œ๋“  ๋™์ผํ•˜๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. ๋ฐฐํฌ๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ง€์ง€! ๐Ÿณ

Dockerfile ์ž‘์„ฑ

# Dockerfile
FROM python:3.9-slim

WORKDIR /app

# ์˜์กด์„ฑ ์„ค์น˜
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
ENV PYTHONUNBUFFERED=1

# ์‹คํ–‰
CMD ["python", "main.py"]
docker-compose.yml ์ž‘์„ฑ

# docker-compose.yml
version: '3.8'

services:
  swap-bot:
    build: .
    container_name: token-swap-automation
    restart: unless-stopped
    env_file:
      - .env
    volumes:
      - ./logs:/app/logs
    networks:
      - swap-network

  # ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ (์„ ํƒ์‚ฌํ•ญ)
  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - swap-network

networks:
  swap-network:
    driver: bridge

3. ๋กœ๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง

์‹œ์Šคํ…œ์ด ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋ ค๋ฉด ๋กœ๊ทธ๊ฐ€ ํ•„์ˆ˜์•ผ! ๐Ÿ“

import logging
from logging.handlers import RotatingFileHandler
import json
from datetime import datetime

class SwapLogger:
    def __init__(self, log_file='swap_bot.log'):
        """๋กœ๊ฑฐ ์ดˆ๊ธฐํ™”"""
        self.logger = logging.getLogger('SwapBot')
        self.logger.setLevel(logging.INFO)
        
        # ํŒŒ์ผ ํ•ธ๋“ค๋Ÿฌ (์ตœ๋Œ€ 10MB, 5๊ฐœ ๋ฐฑ์—…)
        file_handler = RotatingFileHandler(
            log_file,
            maxBytes=10*1024*1024,
            backupCount=5
        )
        
        # ์ฝ˜์†” ํ•ธ๋“ค๋Ÿฌ
        console_handler = logging.StreamHandler()
        
        # ํฌ๋งท ์„ค์ •
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        )
        file_handler.setFormatter(formatter)
        console_handler.setFormatter(formatter)
        
        self.logger.addHandler(file_handler)
        self.logger.addHandler(console_handler)
    
    def log_swap(self, swap_data):
        """์Šค์™‘ ์‹คํ–‰ ๋กœ๊ทธ"""
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'type': 'SWAP',
            'data': swap_data
        }
        self.logger.info(json.dumps(log_entry))
    
    def log_price(self, token_pair, price):
        """๊ฐ€๊ฒฉ ๋กœ๊ทธ"""
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'type': 'PRICE',
            'pair': token_pair,
            'price': price
        }
        self.logger.info(json.dumps(log_entry))
    
    def log_error(self, error_msg, exception=None):
        """์—๋Ÿฌ ๋กœ๊ทธ"""
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'type': 'ERROR',
            'message': error_msg,
            'exception': str(exception) if exception else None
        }
        self.logger.error(json.dumps(log_entry))

4. ํ…”๋ ˆ๊ทธ๋žจ ์•Œ๋ฆผ ์—ฐ๋™

์ค‘์š”ํ•œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ฆ‰์‹œ ์•Œ๋ฆผ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด! ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡์„ ๋งŒ๋“ค์–ด๋ณด์ž. ๐Ÿ“ฒ

import requests
import asyncio

class TelegramNotifier:
    def __init__(self, bot_token, chat_id):
        """
        ํ…”๋ ˆ๊ทธ๋žจ ์•Œ๋ฆผ ์ดˆ๊ธฐํ™”
        bot_token: ๋ด‡ ํ† ํฐ (@BotFather์—์„œ ๋ฐœ๊ธ‰)
        chat_id: ์ฑ„ํŒ…๋ฐฉ ID
        """
        self.bot_token = bot_token
        self.chat_id = chat_id
        self.api_url = f"https://api.telegram.org/bot{bot_token}"
    
    async def send_message(self, message, parse_mode='HTML'):
        """๋ฉ”์‹œ์ง€ ์ „์†ก"""
        url = f"{self.api_url}/sendMessage"
        data = {
            'chat_id': self.chat_id,
            'text': message,
            'parse_mode': parse_mode
        }
        
        try:
            response = requests.post(url, data=data)
            return response.json()
        except Exception as e:
            print(f"ํ…”๋ ˆ๊ทธ๋žจ ์ „์†ก ์‹คํŒจ: {e}")
            return None
    
    async def notify_swap_success(self, swap_result):
        """์Šค์™‘ ์„ฑ๊ณต ์•Œ๋ฆผ"""
        message = f"""
๐ŸŽ‰ <b>์Šค์™‘ ์„ฑ๊ณต!</b>

๐Ÿ’ฑ ๊ฑฐ๋ž˜ ํ•ด์‹œ: <code>{swap_result['tx_hash'][:10]}...</code>
โ›ฝ ๊ฐ€์Šค ์‚ฌ์šฉ: {swap_result['gas_used']}
๐Ÿ’ฐ ๊ธˆ์•ก: {swap_result.get('amount', 'N/A')}

โœ… ๊ฑฐ๋ž˜๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!
        """
        await self.send_message(message)
    
    async def notify_error(self, error_msg):
        """์—๋Ÿฌ ์•Œ๋ฆผ"""
        message = f"""
๐Ÿšจ <b>์—๋Ÿฌ ๋ฐœ์ƒ!</b>

โŒ {error_msg}

์‹œ์Šคํ…œ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.
        """
        await self.send_message(message)
    
    async def notify_daily_report(self, stats):
        """์ผ์ผ ๋ฆฌํฌํŠธ"""
        message = f"""
๐Ÿ“Š <b>์ผ์ผ ๊ฑฐ๋ž˜ ๋ฆฌํฌํŠธ</b>

๐Ÿ“ˆ ์ด ๊ฑฐ๋ž˜: {stats['total_trades']}๊ฑด
โœ… ์„ฑ๊ณต: {stats['successful']}๊ฑด
โŒ ์‹คํŒจ: {stats['failed']}๊ฑด
๐Ÿ’ต ์ด ์†์ต: ${stats['pnl']:.2f}

๊ณ„์† ์—ด์‹ฌํžˆ ์ผํ•˜๊ณ  ์žˆ์–ด์š”! ๐Ÿ’ช
        """
        await self.send_message(message)
๐Ÿ’ก ์šด์˜ ํŒ

์‹ค์ œ ์šด์˜ํ•  ๋•Œ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ด‡์„ ๋™์‹œ์— ๋Œ๋ฆฌ๋Š” ๊ฒƒ๋„ ์ข‹์€ ์ „๋žต์ด์•ผ. ํ•˜๋‚˜๋Š” ๋ณด์ˆ˜์ ์œผ๋กœ, ํ•˜๋‚˜๋Š” ๊ณต๊ฒฉ์ ์œผ๋กœ ์„ค์ •ํ•ด์„œ ๋ฆฌ์Šคํฌ๋ฅผ ๋ถ„์‚ฐ์‹œํ‚ฌ ์ˆ˜ ์žˆ๊ฑฐ๋“ !

๐ŸŽ“ ๊ณ ๊ธ‰ ์ „๋žต๊ณผ ์ตœ์ ํ™”

๊ธฐ๋ณธ์ ์ธ ์ž๋™ํ™”๋ฅผ ๋„˜์–ด์„œ, ๋” ๋˜‘๋˜‘ํ•œ ์ „๋žต์„ ๊ตฌํ˜„ํ•ด๋ณด์ž! ์ด ๋ถ€๋ถ„์€ ์ข€ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์ง€๋งŒ, ์ˆ˜์ต๋ฅ ์„ ํฌ๊ฒŒ ๋†’์ผ ์ˆ˜ ์žˆ์–ด. ๐Ÿš€

1. ์ฐจ์ต๊ฑฐ๋ž˜ (Arbitrage) ์ „๋žต

๊ฐ™์€ ํ† ํฐ์ด ์„œ๋กœ ๋‹ค๋ฅธ DEX์—์„œ ๋‹ค๋ฅธ ๊ฐ€๊ฒฉ์— ๊ฑฐ๋ž˜๋  ๋•Œ๊ฐ€ ์žˆ์–ด. ์ด ์ฐจ์ด๋ฅผ ์ด์šฉํ•ด์„œ ์ˆ˜์ต์„ ๋‚ผ ์ˆ˜ ์žˆ์ง€!

class ArbitrageEngine:
    def __init__(self, dex_list):
        """
        ์ฐจ์ต๊ฑฐ๋ž˜ ์—”์ง„
        dex_list: [(dex_name, router_address, router_abi), ...]
        """
        self.dexes = {}
        for name, address, abi in dex_list:
            self.dexes[name] = {
                'router': self.w3.eth.contract(address=address, abi=abi)
            }
    
    async def find_arbitrage_opportunities(self, token_a, token_b, amount=1.0):
        """
        ์ฐจ์ต๊ฑฐ๋ž˜ ๊ธฐํšŒ ํƒ์ƒ‰
        """
        opportunities = []
        
        # ๋ชจ๋“  DEX ํŽ˜์–ด์—์„œ ๊ฐ€๊ฒฉ ์กฐํšŒ
        prices = {}
        for dex_name, dex_info in self.dexes.items():
            try:
                price = await self.get_price_from_dex(
                    dex_name,
                    token_a,
                    token_b,
                    amount
                )
                prices[dex_name] = price
            except Exception as e:
                print(f"โŒ {dex_name} ๊ฐ€๊ฒฉ ์กฐํšŒ ์‹คํŒจ: {e}")
        
        # ๊ฐ€๊ฒฉ ์ฐจ์ด ๋ถ„์„
        if len(prices) < 2:
            return opportunities
        
        dex_names = list(prices.keys())
        for i in range(len(dex_names)):
            for j in range(i + 1, len(dex_names)):
                dex1, dex2 = dex_names[i], dex_names[j]
                price1, price2 = prices[dex1], prices[dex2]
                
                # ๊ฐ€๊ฒฉ ์ฐจ์ด ๊ณ„์‚ฐ
                if price1 > price2:
                    profit_percent = ((price1 - price2) / price2) * 100
                    buy_dex, sell_dex = dex2, dex1
                    buy_price, sell_price = price2, price1
                else:
                    profit_percent = ((price2 - price1) / price1) * 100
                    buy_dex, sell_dex = dex1, dex2
                    buy_price, sell_price = price1, price2
                
                # ์ˆ˜์ต์„ฑ ์žˆ๋Š” ๊ธฐํšŒ๋งŒ ์ €์žฅ (๊ฐ€์Šค๋น„ ๊ณ ๋ ค)
                if profit_percent > 1.0:  # 1% ์ด์ƒ ์ฐจ์ด
                    opportunities.append({
                        'buy_dex': buy_dex,
                        'sell_dex': sell_dex,
                        'buy_price': buy_price,
                        'sell_price': sell_price,
                        'profit_percent': profit_percent,
                        'estimated_profit': (sell_price - buy_price) * amount
                    })
        
        # ์ˆ˜์ต๋ฅ  ๋†’์€ ์ˆœ์œผ๋กœ ์ •๋ ฌ
        opportunities.sort(key=lambda x: x['profit_percent'], reverse=True)
        
        return opportunities
    
    async def execute_arbitrage(self, opportunity, amount):
        """
        ์ฐจ์ต๊ฑฐ๋ž˜ ์‹คํ–‰
        """
        print(f"\n๐Ÿ’Ž ์ฐจ์ต๊ฑฐ๋ž˜ ์‹คํ–‰!")
        print(f"๋งค์ˆ˜: {opportunity['buy_dex']} @ {opportunity['buy_price']}")
        print(f"๋งค๋„: {opportunity['sell_dex']} @ {opportunity['sell_price']}")
        print(f"์˜ˆ์ƒ ์ˆ˜์ต: {opportunity['profit_percent']:.2f}%")
        
        # 1๋‹จ๊ณ„: ์ €๋ ดํ•œ DEX์—์„œ ๋งค์ˆ˜
        buy_result = await self.swap_on_dex(
            opportunity['buy_dex'],
            token_a,
            token_b,
            amount
        )
        
        if not buy_result['success']:
            print("โŒ ๋งค์ˆ˜ ์‹คํŒจ!")
            return {'success': False}
        
        # 2๋‹จ๊ณ„: ๋น„์‹ผ DEX์—์„œ ๋งค๋„
        sell_result = await self.swap_on_dex(
            opportunity['sell_dex'],
            token_b,
            token_a,
            buy_result['amount_out']
        )
        
        if not sell_result['success']:
            print("โš ๏ธ ๋งค๋„ ์‹คํŒจ! ํ† ํฐ์ด ๋ฌถ์˜€์Šต๋‹ˆ๋‹ค.")
            return {'success': False, 'stuck_tokens': buy_result['amount_out']}
        
        # ์‹ค์ œ ์ˆ˜์ต ๊ณ„์‚ฐ
        actual_profit = sell_result['amount_out'] - amount
        print(f"โœ… ์ฐจ์ต๊ฑฐ๋ž˜ ์™„๋ฃŒ! ์‹ค์ œ ์ˆ˜์ต: {actual_profit:.6f}")
        
        return {
            'success': True,
            'profit': actual_profit,
            'buy_tx': buy_result['tx_hash'],
            'sell_tx': sell_result['tx_hash']
        }

2. ํ”Œ๋ž˜์‹œ๋ก (Flash Loan) ํ™œ์šฉ

ํ”Œ๋ž˜์‹œ๋ก ์€ ๋‹ด๋ณด ์—†์ด ๊ฑฐ์•ก์„ ๋นŒ๋ ค์„œ ํ•œ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ๊ฐš๋Š” ๊ธฐ๋ฒ•์ด์•ผ. ์ฐจ์ต๊ฑฐ๋ž˜์— ํ™œ์šฉํ•˜๋ฉด ์ž๋ณธ ์—†์ด๋„ ํฐ ์ˆ˜์ต์„ ๋‚ผ ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ฐ

โšก ํ”Œ๋ž˜์‹œ๋ก  ์ž‘๋™ ์›๋ฆฌ

1. ํ”„๋กœํ† ์ฝœ์—์„œ ๊ฑฐ์•ก ๋Œ€์ถœ (์˜ˆ: 100 ETH)
2. ๋Œ€์ถœ๊ธˆ์œผ๋กœ ์ฐจ์ต๊ฑฐ๋ž˜ ์‹คํ–‰
3. ์ˆ˜์ต๊ธˆ ํฌํ•จํ•ด์„œ ์ฆ‰์‹œ ์ƒํ™˜
4. ๋‚จ์€ ์ˆ˜์ต์€ ๋‚ด๊ฐ€ ๊ฐ€์ ธ๊ฐ

๋ชจ๋“  ๊ณผ์ •์ด ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์ผ์–ด๋‚˜๊ธฐ ๋•Œ๋ฌธ์—, ์ƒํ™˜ํ•˜์ง€ ๋ชปํ•˜๋ฉด ์ „์ฒด ํŠธ๋žœ์žญ์…˜์ด ์ทจ์†Œ๋ผ. ๊ทธ๋ž˜์„œ ๋ฆฌ์Šคํฌ๊ฐ€ ์—†์–ด!
class FlashLoanArbitrage:
    def __init__(self, aave_pool_address, aave_pool_abi):
        """
        ํ”Œ๋ž˜์‹œ๋ก  ์ฐจ์ต๊ฑฐ๋ž˜ ์—”์ง„
        Aave ํ”„๋กœํ† ์ฝœ ์‚ฌ์šฉ
        """
        self.aave_pool = self.w3.eth.contract(
            address=aave_pool_address,
            abi=aave_pool_abi
        )
    
    def create_flashloan_contract(self):
        """
        ํ”Œ๋ž˜์‹œ๋ก  ์‹คํ–‰ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ
        (Solidity ์ฝ”๋“œ๋ฅผ ๋ฏธ๋ฆฌ ์ž‘์„ฑํ•ด์•ผ ํ•จ)
        """
        # ์‹ค์ œ๋กœ๋Š” Solidity๋กœ ์ž‘์„ฑํ•œ ์ปจํŠธ๋ž™ํŠธ๋ฅผ ๋ฐฐํฌํ•ด์•ผ ํ•ด
        # ์—ฌ๊ธฐ์„œ๋Š” ๊ฐœ๋…๋งŒ ์„ค๋ช…
        
        flashloan_contract_code = """
        // SPDX-License-Identifier: MIT
        pragma solidity ^0.8.0;
        
        import "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
        
        contract FlashLoanArbitrage is FlashLoanSimpleReceiverBase {
            address payable owner;
            
            constructor(address _addressProvider) 
                FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider)) 
            {
                owner = payable(msg.sender);
            }
            
            function executeOperation(
                address asset,
                uint256 amount,
                uint256 premium,
                address initiator,
                bytes calldata params
            ) external override returns (bool) {
                // 1. ์ฐจ์ต๊ฑฐ๋ž˜ ๋กœ์ง ์‹คํ–‰
                // 2. ์ˆ˜์ต ํ™•์ธ
                // 3. ๋Œ€์ถœ๊ธˆ + ์ˆ˜์ˆ˜๋ฃŒ ์ƒํ™˜
                
                uint256 amountOwed = amount + premium;
                IERC20(asset).approve(address(POOL), amountOwed);
                
                return true;
            }
            
            function requestFlashLoan(address _token, uint256 _amount) public {
                address receiverAddress = address(this);
                address asset = _token;
                uint256 amount = _amount;
                bytes memory params = "";
                uint16 referralCode = 0;
                
                POOL.flashLoanSimple(
                    receiverAddress,
                    asset,
                    amount,
                    params,
                    referralCode
                );
            }
        }
        """
        
        print("๐Ÿ“ ํ”Œ๋ž˜์‹œ๋ก  ์ปจํŠธ๋ž™ํŠธ ๋ฐฐํฌ ํ•„์š”")
        print("Remix๋‚˜ Hardhat์„ ์‚ฌ์šฉํ•ด์„œ ๋ฐฐํฌํ•˜์„ธ์š”!")

3. MEV(Maximal Extractable Value) ์ „๋žต

MEV๋Š” ๋ธ”๋ก ์ƒ์„ฑ ๊ณผ์ •์—์„œ ํŠธ๋žœ์žญ์…˜ ์ˆœ์„œ๋ฅผ ์กฐ์ž‘ํ•ด์„œ ์ด์ต์„ ์–ป๋Š” ๊ณ ๊ธ‰ ๊ธฐ๋ฒ•์ด์•ผ. ํ”„๋ก ํŠธ๋Ÿฌ๋‹, ๋ฐฑ๋Ÿฌ๋‹, ์ƒŒ๋“œ์œ„์น˜ ๊ณต๊ฒฉ ๋“ฑ์ด ์žˆ์–ด.

โš ๏ธ ์œค๋ฆฌ์  ๊ณ ๋ ค์‚ฌํ•ญ

MEV ์ „๋žต ์ค‘ ์ผ๋ถ€๋Š” ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผํ•ด๋ฅผ ์ค„ ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์ƒŒ๋“œ์œ„์น˜ ๊ณต๊ฒฉ์€ ๋…ผ๋ž€์˜ ์—ฌ์ง€๊ฐ€ ์žˆ์œผ๋‹ˆ, ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์œค๋ฆฌ์  ์ธก๋ฉด์„ ๊ผญ ๊ณ ๋ คํ•ด์•ผ ํ•ด!
class MEVStrategy:
    def __init__(self, flashbots_relay_url):
        """
        MEV ์ „๋žต ์—”์ง„
        Flashbots๋ฅผ ํ†ตํ•ด ํ”„๋ผ์ด๋น— ํŠธ๋žœ์žญ์…˜ ์ „์†ก
        """
        self.flashbots_url = flashbots_relay_url
    
    async def detect_profitable_tx(self, pending_tx):
        """
        ์ˆ˜์ต์„ฑ ์žˆ๋Š” ํŠธ๋žœ์žญ์…˜ ๊ฐ์ง€
        """
        # ๋Œ€๊ธฐ ์ค‘์ธ ํŠธ๋žœ์žญ์…˜ ๋ถ„์„
        if self.is_large_swap(pending_tx):
            # ํฐ ์Šค์™‘ ํŠธ๋žœ์žญ์…˜ ๋ฐœ๊ฒฌ
            opportunity = await self.calculate_sandwich_opportunity(pending_tx)
            
            if opportunity['profitable']:
                return opportunity
        
        return None
    
    def is_large_swap(self, tx):
        """ํฐ ์Šค์™‘ ํŠธ๋žœ์žญ์…˜์ธ์ง€ ํ™•์ธ"""
        # ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ
        # Uniswap swapExactTokensForTokens ํ•จ์ˆ˜ ํ˜ธ์ถœ์ธ์ง€ ํ™•์ธ
        # ๊ธˆ์•ก์ด ์ถฉ๋ถ„ํžˆ ํฐ์ง€ ํ™•์ธ
        pass
    
    async def send_bundle_to_flashbots(self, transactions):
        """
        Flashbots๋ฅผ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜ ๋ฒˆ๋“ค ์ „์†ก
        """
        bundle = {
            'txs': transactions,
            'blockNumber': self.w3.eth.block_number + 1,
            'minTimestamp': 0,
            'maxTimestamp': 0
        }
        
        # Flashbots API ํ˜ธ์ถœ
        # ์‹ค์ œ ๊ตฌํ˜„์€ Flashbots ๋ฌธ์„œ ์ฐธ๊ณ 
        print("๐Ÿ“ฆ Flashbots ๋ฒˆ๋“ค ์ „์†ก")

๐Ÿ” ๋””๋ฒ„๊น…๊ณผ ๋ฌธ์ œ ํ•ด๊ฒฐ

๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ๋‹น์—ฐํžˆ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์ด์Šˆ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ดค์–ด! ๐Ÿ› ๏ธ

๐Ÿ› ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋“ค

1. "Insufficient funds for gas"
โ†’ ์ง€๊ฐ‘์— ETH๊ฐ€ ๋ถ€์กฑํ•ด. ๊ฐ€์Šค๋น„๋ฅผ ๋‚ผ ์ˆ˜ ์žˆ์„ ๋งŒํผ ETH๋ฅผ ์ถฉ์ „ํ•ด์•ผ ํ•ด.

2. "Transaction underpriced"
โ†’ ๊ฐ€์Šค ๊ฐ€๊ฒฉ์ด ๋„ˆ๋ฌด ๋‚ฎ์•„. ํ˜„์žฌ ๋„คํŠธ์›Œํฌ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๊ฐ€์Šค ๊ฐ€๊ฒฉ์„ ๋†’์—ฌ์•ผ ํ•ด.

3. "Execution reverted"
โ†’ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ. ์Šฌ๋ฆฌํ”ผ์ง€ ์„ค์ •์ด ๋„ˆ๋ฌด ๋‚ฎ๊ฑฐ๋‚˜, approve๊ฐ€ ์•ˆ ๋์„ ์ˆ˜ ์žˆ์–ด.

4. "Nonce too low"
โ†’ ์ด๋ฏธ ์‚ฌ์šฉ๋œ nonce๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•จ. ์ตœ์‹  nonce๋ฅผ ๋‹ค์‹œ ์กฐํšŒํ•ด์•ผ ํ•ด.

5. "Replacement transaction underpriced"
โ†’ ๊ฐ™์€ nonce์˜ ํŠธ๋žœ์žญ์…˜์„ ๊ต์ฒดํ•˜๋ ค๋ฉด ๊ฐ€์Šค ๊ฐ€๊ฒฉ์„ ์ตœ์†Œ 10% ์ด์ƒ ๋†’์—ฌ์•ผ ํ•ด.
๋””๋ฒ„๊น… ๋„๊ตฌ ํ™œ์šฉ

class DebugHelper:
    def __init__(self, w3):
        self.w3 = w3
    
    def analyze_failed_transaction(self, tx_hash):
        """
        ์‹คํŒจํ•œ ํŠธ๋žœ์žญ์…˜ ๋ถ„์„
        """
        print(f"\n๐Ÿ” ํŠธ๋žœ์žญ์…˜ ๋ถ„์„: {tx_hash}")
        
        # ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ์กฐํšŒ
        tx = self.w3.eth.get_transaction(tx_hash)
        receipt = self.w3.eth.get_transaction_receipt(tx_hash)
        
        print(f"\n๐Ÿ“‹ ๊ธฐ๋ณธ ์ •๋ณด:")
        print(f"  From: {tx['from']}")
        print(f"  To: {tx['to']}")
        print(f"  Value: {self.w3.from_wei(tx['value'], 'ether')} ETH")
        print(f"  Gas Price: {self.w3.from_wei(tx['gasPrice'], 'gwei')} Gwei")
        print(f"  Gas Limit: {tx['gas']}")
        
        print(f"\n๐Ÿ“Š ์‹คํ–‰ ๊ฒฐ๊ณผ:")
        print(f"  Status: {'์„ฑ๊ณต' if receipt['status'] == 1 else '์‹คํŒจ'}")
        print(f"  Gas Used: {receipt['gasUsed']}")
        print(f"  Block: {receipt['blockNumber']}")
        
        # ์‹คํŒจ ์›์ธ ์ถ”์ •
        if receipt['status'] == 0:
            print(f"\nโŒ ์‹คํŒจ ์›์ธ ๋ถ„์„:")
            
            # ๊ฐ€์Šค ๋ถ€์กฑ
            if receipt['gasUsed'] == tx['gas']:
                print("  โ€ข ๊ฐ€์Šค ๋ถ€์กฑ (Out of Gas)")
                print("  โ†’ ๊ฐ€์Šค ๋ฆฌ๋ฐ‹์„ ๋†’์—ฌ๋ณด์„ธ์š”")
            
            # ํŠธ๋žœ์žญ์…˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์œผ๋กœ ์ƒ์„ธ ์—๋Ÿฌ ํ™•์ธ
            try:
                self.w3.eth.call(tx, receipt['blockNumber'] - 1)
            except Exception as e:
                print(f"  โ€ข ์—๋Ÿฌ ๋ฉ”์‹œ์ง€: {str(e)}")
    
    def simulate_transaction(self, tx_params):
        """
        ํŠธ๋žœ์žญ์…˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (์‹ค์ œ ์ „์†ก ์ „ ํ…Œ์ŠคํŠธ)
        """
        print("\n๐Ÿงช ํŠธ๋žœ์žญ์…˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜...")
        
        try:
            # eth_call๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
            result = self.w3.eth.call(tx_params)
            print("โœ… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์„ฑ๊ณต!")
            print(f"์˜ˆ์ƒ ๊ฒฐ๊ณผ: {result.hex()}")
            
            # ๊ฐ€์Šค ์˜ˆ์ธก
            estimated_gas = self.w3.eth.estimate_gas(tx_params)
            print(f"์˜ˆ์ƒ ๊ฐ€์Šค: {estimated_gas}")
            
            return True
        except Exception as e:
            print(f"โŒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํŒจ: {str(e)}")
            return False
    
    def check_token_approval(self, token_address, owner, spender):
        """ํ† ํฐ ์Šน์ธ ์ƒํƒœ ํ™•์ธ"""
        token = self.w3.eth.contract(
            address=token_address,
            abi=ERC20_ABI
        )
        
        allowance = token.functions.allowance(owner, spender).call()
        decimals = token.functions.decimals().call()
        
        print(f"\n๐Ÿ” ์Šน์ธ ์ƒํƒœ:")
        print(f"  Owner: {owner}")
        print(f"  Spender: {spender}")
        print(f"  Allowance: {allowance / (10 ** decimals)}")
        
        return allowance

๐Ÿ“š ์ถ”๊ฐ€ ํ•™์Šต ๋ฆฌ์†Œ์Šค

์—ฌ๊ธฐ๊นŒ์ง€ ๋”ฐ๋ผ์™”๋‹ค๋ฉด ์ •๋ง ๋Œ€๋‹จํ•ด! ๐Ÿ‘ ํ•˜์ง€๋งŒ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์€ ๊ณ„์† ๋ฐฐ์›Œ์•ผ ํ•˜๋Š” ๋ถ„์•ผ์•ผ. ๋” ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๋ฆฌ์†Œ์Šค๋“ค์„ ์ถ”์ฒœํ•ด:

๐ŸŽ“ ํ•™์Šต ์ž๋ฃŒ

๊ณต์‹ ๋ฌธ์„œ
โ€ข Web3.py ๊ณต์‹ ๋ฌธ์„œ: https://web3py.readthedocs.io/
โ€ข Uniswap V2 ๋ฌธ์„œ: https://docs.uniswap.org/protocol/V2/
โ€ข Ethereum ๊ฐœ๋ฐœ์ž ๋ฌธ์„œ: https://ethereum.org/developers

์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข Ethereum Stack Exchange: ๊ธฐ์ˆ  ์งˆ๋ฌธ
โ€ข Reddit r/ethdev: ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข Discord ์„œ๋ฒ„๋“ค: ์‹ค์‹œ๊ฐ„ ์†Œํ†ต

์‹ค์Šต ํ”Œ๋žซํผ
โ€ข Remix IDE: ๋ธŒ๋ผ์šฐ์ €์—์„œ Solidity ๊ฐœ๋ฐœ
โ€ข Hardhat: ํ”„๋กœํŽ˜์…”๋„ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ
โ€ข Tenderly: ํŠธ๋žœ์žญ์…˜ ๋””๋ฒ„๊น… ๋„๊ตฌ
๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ์‹ค๋ ฅ์„ ๋” ํ‚ค์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋“ค์˜ ๋ฉ˜ํ† ๋ง์„ ๋ฐ›์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ์‹ค์ „ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์ง์ ‘ ๋ฐฐ์šธ ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐ŸŽฏ

์‹ค์ „ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด

๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ด๋Ÿฐ ํ”„๋กœ์ ํŠธ๋“ค์„ ๋งŒ๋“ค์–ด๋ณผ ์ˆ˜ ์žˆ์–ด:

1. ๋ฉ€ํ‹ฐ์ฒด์ธ ์Šค์™‘ ๋ด‡ ๐ŸŒ
์ด๋”๋ฆฌ์›€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ BSC, Polygon, Arbitrum ๋“ฑ ์—ฌ๋Ÿฌ ์ฒด์ธ์—์„œ ๋™์‹œ์— ์ž‘๋™ํ•˜๋Š” ๋ด‡

2. ์œ ๋™์„ฑ ๋งˆ์ด๋‹ ์ž๋™ํ™” ๐Ÿ’ง
์ตœ์ ์˜ ์œ ๋™์„ฑ ํ’€์„ ์ฐพ์•„์„œ ์ž๋™์œผ๋กœ ์˜ˆ์น˜ํ•˜๊ณ  ์ˆ˜ํ™•ํ•˜๋Š” ์‹œ์Šคํ…œ

3. NFT ์Šค๋‚˜์ดํ•‘ ๋ด‡ ๐ŸŽจ
์ƒˆ๋กœ ๋ฏผํŒ…๋˜๋Š” NFT๋ฅผ ๋น ๋ฅด๊ฒŒ ๊ตฌ๋งคํ•˜๋Š” ๋ด‡

4. ๊ฐ€์Šค ๊ฐ€๊ฒฉ ์ตœ์ ํ™” ์„œ๋น„์Šค โ›ฝ
๊ฐ€์Šค ๊ฐ€๊ฒฉ์ด ์ €๋ ดํ•  ๋•Œ๋ฅผ ์˜ˆ์ธกํ•ด์„œ ํŠธ๋žœ์žญ์…˜์„ ์‹คํ–‰ํ•˜๋Š” ์„œ๋น„์Šค

5. ํฌํŠธํด๋ฆฌ์˜ค ๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ ๋ด‡ ๐Ÿ“Š
์„ค์ •ํ•œ ๋น„์œจ์— ๋งž์ถฐ ์ž๋™์œผ๋กœ ์ž์‚ฐ์„ ์žฌ์กฐ์ •ํ•˜๋Š” ๋ด‡

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ๐ŸŽ‰

์šฐ๋ฆฌ๋Š” ํŒŒ์ด์ฌ์œผ๋กœ ํ† ํฐ ์Šค์™‘ ์ž๋™ํ™” ์‹œ์Šคํ…œ์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ์‚ดํŽด๋ดค์–ด. ์ง€๊ฐ‘ ๊ด€๋ฆฌ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์Šค์™‘ ์—”์ง„, ๊ฐ€๊ฒฉ ๋ชจ๋‹ˆํ„ฐ๋ง, ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ, ๊ทธ๋ฆฌ๊ณ  ๊ณ ๊ธ‰ ์ „๋žต๊นŒ์ง€ ๋‹ค๋ค˜์ง€.

๐Ÿ’ก ํ•ต์‹ฌ ํฌ์ธํŠธ ์ •๋ฆฌ
  • โœ… ๋ณด์•ˆ์ด ์ตœ์šฐ์„ : ๊ฐœ์ธํ‚ค ๊ด€๋ฆฌ๋ฅผ ์ฒ ์ €ํžˆ ํ•˜๊ณ , ํ…Œ์ŠคํŠธ๋„ท์—์„œ ์ถฉ๋ถ„ํžˆ ๊ฒ€์ฆ
  • โœ… ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ: ์ตœ๋Œ€ ์†์‹ค ํ•œ๋„๋ฅผ ์„ค์ •ํ•˜๊ณ , ์ด์ƒ ํŒจํ„ด์„ ๊ฐ์ง€ํ•˜๋Š” ์‹œ์Šคํ…œ ๊ตฌ์ถ•
  • โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๊ฐ€์Šค๋น„๋ฅผ ์ ˆ์•ฝํ•˜๊ณ , ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ํšจ์œจ์„ฑ ํ–ฅ์ƒ
  • โœ… ์ง€์†์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง: ๋กœ๊ทธ๋ฅผ ๊ผผ๊ผผํžˆ ํ™•์ธํ•˜๊ณ , ์•Œ๋ฆผ ์‹œ์Šคํ…œ ํ™œ์šฉ
  • โœ… ๊ณ„์† ํ•™์Šตํ•˜๊ธฐ: ๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์€ ๋น ๋ฅด๊ฒŒ ๋ฐœ์ „ํ•˜๋‹ˆ ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ๋”ฐ๋ผ๊ฐ€์•ผ ํ•ด

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์€ ์–ด๋ ต์ง€๋งŒ, ์ •๋ง ์žฌ๋ฏธ์žˆ๊ณ  ๋ณด๋žŒ์ฐฌ ๋ถ„์•ผ์•ผ. ์ž๋™ํ™” ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๋ฉด์„œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์‹ค๋ ฅ๋„ ๋Š˜๊ณ , ๋ธ”๋ก์ฒด์ธ์— ๋Œ€ํ•œ ์ดํ•ด๋„ ๊นŠ์–ด์ง€๊ฑฐ๋“ . ๐Ÿ’ช

์ฒ˜์Œ์—๋Š” ์ž‘์€ ๊ธˆ์•ก์œผ๋กœ ์‹œ์ž‘ํ•ด์„œ, ์‹œ์Šคํ…œ์ด ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ๊ฑธ ํ™•์ธํ•œ ๋‹ค์Œ์— ์ ์ฐจ ๊ทœ๋ชจ๋ฅผ ํ‚ค์›Œ๊ฐ€๋Š” ๊ฒŒ ์ข‹์•„. ๊ทธ๋ฆฌ๊ณ  ์ ˆ๋Œ€ ํˆฌ์žํ•  ์ˆ˜ ์—†๋Š” ๋ˆ์œผ๋กœ ์‹คํ—˜ํ•˜์ง€ ๋งˆ! ๐Ÿ˜Š

ํ˜น์‹œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๊ฑฐ๋‚˜, ๋” ๊นŠ์ด ์žˆ๋Š” ๋‚ด์šฉ์„ ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•˜๋‹ˆ๊นŒ!

์ž, ์ด์ œ ๋‹น์‹ ๋„ ๋ธ”๋ก์ฒด์ธ ์ž๋™ํ™” ์ „๋ฌธ๊ฐ€๊ฐ€ ๋  ์ค€๋น„๊ฐ€ ๋์–ด! ๐Ÿš€

ํ–‰์šด์„ ๋นŒ์–ด, ๊ทธ๋ฆฌ๊ณ  ํ•ดํ”ผ ์ฝ”๋”ฉ! ๐ŸŽŠ

๐ŸŒŸ ๋‹น์‹ ์˜ ๋ธ”๋ก์ฒด์ธ ์—ฌ์ •์„ ์‘์›ํ•ฉ๋‹ˆ๋‹ค!

Keep Building, Keep Learning! ๐Ÿ’Ž

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.