This commit is contained in:
Beomhee.Lee 2025-12-16 12:05:05 +09:00
parent df2eade8c8
commit fb07e0b010
11 changed files with 1951 additions and 0 deletions

82
main.go Normal file
View File

@ -0,0 +1,82 @@
//go:build !Develop
package main
import (
"AngkorWalletScanning/model"
"AngkorWalletScanning/src"
"AngkorWalletScanning/umlog"
"AngkorWalletScanning/umsql"
"fmt"
"os"
"time"
)
var (
MODE string = "" // Release, Info, Debug
DB_USER string = ""
DB_PASS string = ""
DB_HOST string = ""
DB_PORT string = ""
DB_NAME string = ""
KEY string = ""
CODE_STRC model.CodeStrc
)
func main() {
var header model.Header
if len(os.Args) != 1 {
DB_USER = os.Args[1]
DB_PASS = os.Args[2]
DB_HOST = os.Args[3]
DB_PORT = os.Args[4]
DB_NAME = os.Args[5]
KEY = os.Args[6]
MODE = os.Args[7]
header.AppKey = os.Args[8]
header.SecretKey = os.Args[9]
}
err := umlog.Init("/var/tmp/AnkWalletScanning.log", "7208765670:AAEN35cnQPeoDho33QvrchhVzuwnUcyDZdk", CODE_STRC.MESSAGE_ID)
if err != nil {
fmt.Println("umlog.Init Error : " + err.Error())
}
if MODE == "Release" {
umlog.SetLevel("E")
} else if MODE == "Info" {
umlog.SetLevel("I")
} else {
umlog.SetLevel("D")
}
walletDB := umsql.DbConnect(DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME)
defer umsql.DbDisconnect(walletDB)
_, CODE_STRC = src.GetCodeInfo(walletDB, KEY)
umlog.Info("main\nCode Info Loaded: %+v", CODE_STRC)
// 트랜잭션 모니터링 시작 (백그라운드 고루틴)
// CYBX 토큰 컨트랙트 주소는 환경에 맞게 설정 필요
var contractAddress string
contractAddress = CODE_STRC.CYBX_MAINNET
// 백그라운드에서 트랜잭션 모니터링 시작
// go src.StartTransactionMonitor(
// src.StartTransactionMonitor(
// walletDB, // DB 연결
// contractAddress, // CYBX 토큰 컨트랙트 주소
// 10*time.Second, // 10초마다 체크
// 40, // 한 번에 40명 체크
// NETTYPE, // MAINNET 또는 TESTNET
// CODE_STRC, // 코드 정보
// )
src.StartTransactionMonitorAlchemy(
walletDB,
header,
contractAddress,
3*time.Second,
15,
CODE_STRC)
}

16
model/code.go Normal file
View File

@ -0,0 +1,16 @@
package model
type CodeStrc struct {
CIPHERKEY string
CIPHERIVKEY string
MESSAGE_TOKEN string
MESSAGE_ID string
SENDBIRD_KEY string
SENDBIRD_VERSION string
SENDBIRD_TOKEN string
ETHERSCAN_TOKEN string
CYBX_MAINNET string
CYBX_TESTNET string
ALCHEMY_MAINNET string
ALCHEMY_TESTNET string
}

34
model/common.go Normal file
View File

@ -0,0 +1,34 @@
package model
type Header struct {
AppKey string `form:"appKey" json:"appKey" example:"adpsofimerpifgarpo8rt320"`
SecretKey string `form:"secretKey" json:"secretKey" example:"sdfjlsdkfjsldkfjlsdkjflsdkjflsdkjflsdkj"`
}
type ServerUrlStrc struct {
AAuthUrl string `form:"aauthUrl" json:"aauthUrl" example:"http://aauth.angkorlife.com"`
LifeUrl string `form:"lifeUrl" json:"lifeUrl" example:"http://life.angkorlife.com"`
WattUrl string `form:"wattUrl" json:"wattUrl" example:"http://watt.angkorlife.com"`
}
type UserInfoStrc struct {
AngkorId string `form:"angkorId" json:"angkorId" example:"akDEvQ1693297580"`
UserId string `form:"userId" json:"userId" example:"user01"`
UserAngkorId string `form:"userAngkorId" json:"userAngkorId" example:"alice"`
PhoneNumber string `form:"phoneNumber" json:"phoneNumber" example:"8551012345678"`
Email string `form:"email" json:"email" example:"user01@example.com"`
}
type AuthInitStrc struct {
GrantType string `form:"grantType" json:"grantType" example:"session"`
UserAngkorId string `form:"userAngkorId" json:"userAngkorId" example:"alice"`
}
type AuthKeyStrc struct {
TokenType string `json:"tokenType" example:"bearer"`
AuthKey string `json:"authKey" example:"adpsofimerpifgarpo8rt320"`
}
type WattInfoStrc struct {
WattAmount string `form:"wattAmount" json:"wattAmount" example:"0.0"`
}

28
model/default.go Normal file
View File

@ -0,0 +1,28 @@
package model
type RequestDefaultStrc struct {
GrantType string `form:"grantType" json:"grantType" example:"session"`
AngkorId string `form:"angkorid" json:"angkorid" example:"ak123214"`
}
type ResponseDefaultModel struct {
Code int `json:"code" example:"200"`
Message string `json:"message" example:"success"`
}
type DefaultErrorModel struct {
Code int `json:"code" example:"400"`
Message string `json:"message" example:"message"`
}
type ApplicationInfo struct {
AppId string `json:"appId" example:"11"`
ApplicationName string `json:"appName" example:"angkorlife"`
ApplicationCompany string `json:"appCompany" example:"digital angkor"`
ApplicationIcon string `json:"appIcon" example:"https://icon.angkorlifes.com/images/angkorlife.jpg"`
ApplicationConsent string `json:"appConsent" example:"127"`
ApplicationCallbackUrl string `json:"appCallback" example:"https://callback.angkorlifes.com/receive"`
ServiceCategory string `json:"serviceCategory" example:"game"`
AppleBundleIDs string `json:"appleBundleIDs" example:"com.angkorlifes"`
AndroidPackageName string `json:"androidPackageName" example:"com.angkorlifes"`
}

693
src/alchemy.go Normal file
View File

@ -0,0 +1,693 @@
package src
import (
"AngkorWalletScanning/model"
"AngkorWalletScanning/umlog"
"AngkorWalletScanning/umsql"
"bytes"
"context"
"crypto/tls"
"database/sql"
"encoding/json"
"fmt"
"io"
"math/big"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"golang.org/x/time/rate"
)
// Alchemy API Rate Limiter (무료 티어: 500 CU/s, eth_getLogs=75 CU → 약 6.67 req/s)
var alchemyApiLimiter = rate.NewLimiter(rate.Limit(6.5), 7)
// HTTP 클라이언트 초기화 함수
func initAlchemyHTTPClient() *http.Client {
transport := &http.Transport{
Dial: (&net.Dialer{
Timeout: 30 * time.Second, // 10초 → 30초로 증가
KeepAlive: 30 * time.Second,
Resolver: &net.Resolver{
PreferGo: false, // 시스템 DNS resolver 사용
},
}).Dial,
TLSHandshakeTimeout: 15 * time.Second, // 10초 → 15초로 증가
ResponseHeaderTimeout: 15 * time.Second, // 10초 → 15초로 증가
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: false,
},
DisableKeepAlives: false,
// DNS 캐시 시간 연장
MaxConnsPerHost: 10,
}
// 환경 변수 또는 시스템 프록시 사용
if proxyURL := os.Getenv("HTTPS_PROXY"); proxyURL != "" {
umlog.Debug("[Alchemy] Using proxy from HTTPS_PROXY: %s\n", proxyURL)
if proxy, err := url.Parse(proxyURL); err == nil {
transport.Proxy = http.ProxyURL(proxy)
}
} else if proxyURL := os.Getenv("HTTP_PROXY"); proxyURL != "" {
umlog.Debug("[Alchemy] Using proxy from HTTP_PROXY: %s\n", proxyURL)
if proxy, err := url.Parse(proxyURL); err == nil {
transport.Proxy = http.ProxyURL(proxy)
}
} else {
// 시스템 기본 프록시 사용
transport.Proxy = http.ProxyFromEnvironment
}
return &http.Client{
Timeout: 60 * time.Second, // 30초 → 60초로 증가 (전체 요청 타임아웃)
Transport: transport,
}
}
// HTTP 클라이언트 (타임아웃 및 연결 설정)
var alchemyHTTPClient = initAlchemyHTTPClient()
// ERC20 Transfer Event Topic (Transfer 이벤트 시그니처 해시)
const transferEventTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
// Alchemy JSON-RPC Request/Response Structures
type AlchemyRequest struct {
JsonRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params []interface{} `json:"params"`
ID int `json:"id"`
}
type AlchemyResponse struct {
JsonRPC string `json:"jsonrpc"`
ID int `json:"id"`
Result map[string]interface{} `json:"result"`
Error *AlchemyError `json:"error,omitempty"`
}
type AlchemyError struct {
Code int `json:"code"`
Message string `json:"message"`
}
type AlchemyLog struct {
Address string `json:"address"`
Topics []string `json:"topics"`
Data string `json:"data"`
BlockNumber string `json:"blockNumber"`
TransactionHash string `json:"transactionHash"`
TransactionIndex string `json:"transactionIndex"`
BlockHash string `json:"blockHash"`
LogIndex string `json:"logIndex"`
Removed bool `json:"removed"`
}
type AlchemyTransferInfo struct {
BlockNumber int64
TransactionHash string
From string
To string
Value string
TokenAddress string
Timestamp int64
}
/*
**************************************************************
desc : Alchemy API를 통해 특정 주소의 ERC20 토큰 Transfer 이벤트 조회
parameter
- address : 조회할 지갑 주소
- contractAddress : 토큰 컨트랙트 주소 (CYBX)
- fromBlock : 시작 블록 (hex 형식)
- toBlock : 종료 블록 (hex 형식, "latest" 가능)
- netType : 네트워크 타입 ("MAINNET" 또는 "TESTNET")
- CODE_STRC : 설정 정보
return
- []AlchemyTransferInfo : Transfer 이벤트 목록
- error : 에러 정보
**************************************************************
*/
func getAlchemyTokenTransfers(address string, contractAddress string, fromBlock string, toBlock string, CODE_STRC model.CodeStrc) ([]AlchemyTransferInfo, error) {
// 입력 값 검증
if address == "" || len(address) < 40 {
return nil, fmt.Errorf("invalid wallet address: %s", address)
}
if contractAddress == "" || len(contractAddress) < 40 {
return nil, fmt.Errorf("invalid contract address: %s", contractAddress)
}
// Rate limiter 체크
ctx := context.Background()
err := alchemyApiLimiter.Wait(ctx)
if err != nil {
return nil, fmt.Errorf("rate limiter error: %v", err)
}
// eth_getLogs 요청 파라미터 구성
// Transfer(address indexed from, address indexed to, uint256 value)
// topic[0] = Transfer 이벤트 시그니처
// topic[1] = from 주소 (32바이트 패딩)
// topic[2] = to 주소 (32바이트 패딩)
// 입금 트랜잭션 필터: to = 내 주소
paddedAddress := "0x" + strings.Repeat("0", 24) + strings.TrimPrefix(strings.ToLower(address), "0x")
filterParams := map[string]interface{}{
"fromBlock": fromBlock,
"toBlock": toBlock,
"address": contractAddress,
"topics": []interface{}{
transferEventTopic, // Transfer 이벤트
nil, // from (any)
paddedAddress, // to (내 주소)
},
}
request := AlchemyRequest{
JsonRPC: "2.0",
Method: "eth_getLogs",
Params: []interface{}{filterParams},
ID: 1,
}
// JSON 요청 생성
requestBody, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %v", err)
}
// HTTP POST 요청
umlog.Debug("Request - Address: %s, From: %s, To: %s", address, fromBlock, toBlock)
// umlog.Debug("[getAlchemyTokenTransfers] Padded address (topic[2]): %s\n", paddedAddress)
// umlog.Debug("[getAlchemyTokenTransfers] Request body: %s", string(requestBody))
req, err := http.NewRequest("POST", CODE_STRC.ALCHEMY_MAINNET, bytes.NewBuffer(requestBody))
if err != nil {
umlog.Warn("Failed to create request: %v\n", err)
return nil, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// 재시도 로직 (DNS 에러 대응)
var resp *http.Response
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
resp, err = alchemyHTTPClient.Do(req)
if err == nil {
break // 성공
}
// DNS 에러인 경우 재시도
if attempt < maxRetries {
umlog.Debug("Attempt %d failed: %v, retrying in 2s...", attempt, err)
time.Sleep(2 * time.Second)
// 요청 바디 재설정 (한 번 읽히면 소진되므로)
req.Body = io.NopCloser(bytes.NewBuffer(requestBody))
continue
}
// 최종 실패
umlog.Warn("HTTP request failed after %d attempts: %v\n", maxRetries, err)
return nil, fmt.Errorf("http request failed: %v", err)
}
defer resp.Body.Close()
// umlog.Debug("Response status: %d", resp.StatusCode)
// 응답 읽기
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %v", err)
}
// umlog.Debug("Response body: %s\n", string(body))
// JSON 파싱
var alchemyResp struct {
JsonRPC string `json:"jsonrpc"`
ID int `json:"id"`
Result []AlchemyLog `json:"result"`
Error *AlchemyError `json:"error,omitempty"`
}
err = json.Unmarshal(body, &alchemyResp)
if err != nil {
return nil, fmt.Errorf("failed to parse json: %v", err)
}
// API 에러 체크
if alchemyResp.Error != nil {
return nil, fmt.Errorf("alchemy api error: %s (code: %d)", alchemyResp.Error.Message, alchemyResp.Error.Code)
}
// Transfer 이벤트 파싱
var transfers []AlchemyTransferInfo
for _, log := range alchemyResp.Result {
if len(log.Topics) < 3 {
continue // Invalid Transfer event
}
umlog.Debug("Log entry: %+v\n", log)
// from 주소 추출 (topic[1])
fromAddr := "0x" + log.Topics[1][26:] // 24개의 0 제거
// to 주소 추출 (topic[2])
toAddr := "0x" + log.Topics[2][26:] // 24개의 0 제거
// value 추출 (data)
value := strings.TrimPrefix(log.Data, "0x")
// 블록 번호 파싱
blockNum := new(big.Int)
blockNum.SetString(strings.TrimPrefix(log.BlockNumber, "0x"), 16)
// 타임스탬프는 별도 조회 필요 (블록 정보에서)
timestamp := time.Now().Unix() // 기본값, 필요시 eth_getBlockByNumber 호출
transfers = append(transfers, AlchemyTransferInfo{
BlockNumber: blockNum.Int64(),
TransactionHash: log.TransactionHash,
From: fromAddr,
To: toAddr,
Value: value,
TokenAddress: log.Address,
Timestamp: timestamp,
})
}
return transfers, nil
}
/*
**************************************************************
desc : 현재 블록 번호 조회 (Alchemy API)
parameter
- netType : 네트워크 타입 ("MAINNET" 또는 "TESTNET")
- CODE_STRC : 설정 정보
return
- int64 : 현재 블록 번호
**************************************************************
*/
func getCurrentBlockNumberAlchemy(CODE_STRC model.CodeStrc) int64 {
// Alchemy API URL
alchemyURL := CODE_STRC.ALCHEMY_MAINNET
if alchemyURL == "" {
umlog.Error("Alchemy API URL not configured")
return 0
}
request := AlchemyRequest{
JsonRPC: "2.0",
Method: "eth_blockNumber",
Params: []interface{}{},
ID: 1,
}
requestBody, err := json.Marshal(request)
if err != nil {
umlog.Warn("Failed to marshal request: %v\n", err)
return 0
}
umlog.Info("Requesting: %s", alchemyURL)
req, err := http.NewRequest("POST", alchemyURL, bytes.NewBuffer(requestBody))
if err != nil {
umlog.Warn("Failed to create request: %v\n", err)
return 0
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := alchemyHTTPClient.Do(req)
if err != nil {
umlog.Warn("HTTP request failed: %v\n", err)
return 0
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result struct {
JsonRPC string `json:"jsonrpc"`
ID int `json:"id"`
Result string `json:"result"`
}
json.Unmarshal(body, &result)
blockNum := new(big.Int)
blockNum.SetString(strings.TrimPrefix(result.Result, "0x"), 16)
return blockNum.Int64()
}
/*
**************************************************************
desc : 해당 트랜잭션에 대해 이미 알림을 보냈는지 확인
parameter
- walletDBConn : Wallet DB 핸들러
- txHash : 트랜잭션 해시
return
- bool : 이미 알림 보냈으면 true
**************************************************************
*/
func isTransactionNotified(walletDBConn *sql.DB, txHash string) bool {
query := "SELECT COUNT(*) FROM ank_wallet_notification WHERE tx_hash = ?"
var count int
res, _, err := umsql.SqlSelect(walletDBConn, query, txHash)
if err != nil {
umlog.Error("SELECT Error(%s): %s", err.Error(), query)
return false
}
defer res.Close()
if res.Next() {
if err := res.Scan(&count); err != nil {
umlog.Error("SCAN Error(%s): %s", err.Error(), query)
return false
}
}
return count > 0
}
/*
**************************************************************
desc : 특정 지갑의 입금 트랜잭션 체크 (Alchemy API 사용)
parameter
- walletDBConn : Wallet DB 핸들러
- userId : 사용자 ID
- walletAddress : 지갑 주소
- contractAddress : CYBX 토큰 컨트랙트 주소
- lastCheckTime : 마지막 체크 시각
- netType : 네트워크 타입
- CODE_STRC : 설정 정보
return
- int : 새로 발견된 입금 건수
- error : 에러 정보
**************************************************************
*/
func checkWalletWithAlchemy(walletDBConn *sql.DB, header model.Header, userId int, walletAddress string, contractAddress string, lastCheckTime time.Time, currentBlock int64, CODE_STRC model.CodeStrc) (int, error) {
// 현재 블록은 호출자로부터 받음 (배치 시작 시 한 번만 조회)
// 입력 값 검증
if walletAddress == "" {
umlog.Warn("User %d has empty wallet address, skipping\n", userId)
return 0, nil
}
if contractAddress == "" {
umlog.Warn("Empty contract address, skipping\n")
return 0, fmt.Errorf("contract address is empty")
}
// Alchemy 무료 티어 제한: 한 번에 최대 10 블록만 조회 가능
const maxBlockRange = 9 // 10블록이 아니라 9블록 차이 (0부터 9까지 = 10개)
// 마지막 체크 시각을 블록 번호로 변환 (BSC는 약 3초당 1블록)
timeDiff := time.Since(lastCheckTime)
blocksToCheck := int64(timeDiff.Seconds() / 3)
// umlog.Debug("Time diff: %v, Calculated blocks: %d\n", timeDiff, blocksToCheck)
// Alchemy 무료 티어 제한 적용 (최소 1블록은 체크)
if blocksToCheck < 1 {
blocksToCheck = 1
}
if blocksToCheck > maxBlockRange {
blocksToCheck = maxBlockRange
umlog.Debug("Limiting block range to %d due to Alchemy free tier\n", maxBlockRange)
}
startBlock := currentBlock - blocksToCheck
if startBlock < 0 {
startBlock = 0
}
// 블록 번호를 hex로 변환
fromBlockHex := fmt.Sprintf("0x%x", startBlock)
toBlockHex := fmt.Sprintf("0x%x", currentBlock)
umlog.Debug("Checking blocks %d to %d (range: %d)\n", startBlock, currentBlock, blocksToCheck)
// Alchemy API로 Transfer 이벤트 조회
transfers, err := getAlchemyTokenTransfers(walletAddress, contractAddress, fromBlockHex, toBlockHex, CODE_STRC)
if err != nil {
umlog.Warn("Failed to get transfers for user %d: %v\n", userId, err)
return 0, err
}
newTransactionCount := 0
for _, transfer := range transfers {
umlog.Debug("transfer info: %v\n", transfer)
// 트랜잭션 시각 체크 (Timestamp 사용)
txTime := time.Unix(transfer.Timestamp, 0)
// 마지막 체크 이후의 트랜잭션만 처리
if txTime.Before(lastCheckTime) {
continue
}
// 이미 알림 보낸 트랜잭션인지 확인
if isTransactionNotified(walletDBConn, transfer.TransactionHash) {
continue
}
// 금액 계산 (18 decimals for most tokens, CYBX는 확인 필요)
amount := calculateTokenAmountFromHex(transfer.Value, "18")
// 알림 전송
sendIncomingTransactionNotification(walletDBConn, header, userId, transfer.From, transfer.To, amount, transfer.TransactionHash)
// 알림 기록 저장
saveNotificationRecord(walletDBConn, userId, transfer.TransactionHash, transfer.From, transfer.To, amount, "CYBX")
newTransactionCount++
umlog.Info("checkWalletWithAlchemy\nNew incoming tx detected - User: %d, From: %s, Amount: %s CYBX, Hash: %s",
userId, transfer.From, amount, transfer.TransactionHash)
}
return newTransactionCount, nil
}
/*
**************************************************************
desc : Hex 값으로 토큰 금액 계산 (decimal 적용)
parameter
- hexValue : 토큰 raw value (hex 문자열)
- decimal : 토큰 decimal
return
- string : 사람이 읽을 있는 금액
**************************************************************
*/
func calculateTokenAmountFromHex(hexValue string, decimal string) string {
// hex를 big.Int로 변환
valueInt := new(big.Int)
if strings.HasPrefix(hexValue, "0x") {
hexValue = strings.TrimPrefix(hexValue, "0x")
}
valueInt.SetString(hexValue, 16)
// decimal을 int로 변환
decimalInt, _ := strconv.Atoi(decimal)
// 10^decimal 계산
divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimalInt)), nil)
// 나눗셈
result := new(big.Float).Quo(
new(big.Float).SetInt(valueInt),
new(big.Float).SetInt(divisor),
)
// 소수점 4자리까지 표시
return result.Text('f', 4)
}
/*
**************************************************************
desc : 모든 활성 사용자의 지갑 체크 (Alchemy API 사용)
parameter
- walletDBConn : Wallet DB 핸들러
- contractAddress : CYBX 토큰 컨트랙트 주소
- batchSize : 번에 처리할 사용자
- netType : 네트워크 타입
- CODE_STRC : 설정 정보
**************************************************************
*/
func checkAllWalletsWithAlchemy(walletDBConn *sql.DB, header model.Header, contractAddress string, batchSize int, CODE_STRC model.CodeStrc) {
startTime := time.Now()
// 배치 시작 시 현재 블록 번호 한 번만 조회
currentBlock := getCurrentBlockNumberAlchemy(CODE_STRC)
if currentBlock == 0 {
umlog.Error("Failed to get current block number")
return
}
umlog.Debug("Current block: %d - will be used for all wallets in this batch", currentBlock)
// 활성 사용자 목록 조회 (최근 활동 순)
query := `
SELECT id, address, checkedAt
FROM ank_wallet_user
WHERE address > ''
ORDER BY checkedAt ASC
LIMIT ?
`
rows, err := walletDBConn.Query(query, batchSize)
if err != nil {
umlog.Error("checkAllWalletsWithAlchemy\nDB query failed: %v", err)
return
}
defer rows.Close()
totalChecked := 0
totalNewTransactions := 0
// 요청 분산 간격 계산 (batchSize개를 3초에 균등 분산)
// 예: 12개 → 3000ms ÷ 12 = 250ms 간격
delayBetweenRequests := time.Duration(3000/batchSize) * time.Millisecond
for rows.Next() {
var userId int
var walletAddress string
var lastCheckStr string
err := rows.Scan(&userId, &walletAddress, &lastCheckStr)
if err != nil {
umlog.Error("checkAllWalletsWithAlchemy\nRow scan error: %v", err)
continue
}
// 마지막 체크 시각 파싱
lastCheckTime, _ := time.Parse("2006-01-02 15:04:05", lastCheckStr)
// 개별 지갑 체크 (현재 블록 번호를 파라미터로 전달)
newTxCount, err := checkWalletWithAlchemy(walletDBConn, header, userId, walletAddress, contractAddress, lastCheckTime, currentBlock, CODE_STRC)
if err != nil {
// 에러 로그는 checkWalletWithAlchemy 내부에서 이미 출력됨
continue
}
// 마지막 체크 시각 업데이트
updateLastCheckTime(walletDBConn, userId)
totalChecked++
totalNewTransactions += newTxCount
// 다음 요청 전 대기 (마지막 요청 후에는 대기 불필요)
if totalChecked < batchSize {
time.Sleep(delayBetweenRequests)
}
}
elapsed := time.Since(startTime)
umlog.Info("checkAllWalletsWithAlchemy\nBatch completed - Checked: %d wallets, New transactions: %d, Time: %.2f seconds",
totalChecked, totalNewTransactions, elapsed.Seconds())
}
/*
**************************************************************
desc : 트랜잭션 모니터링 시작 (Alchemy API 사용)
parameter
- walletDBConn : Wallet DB 핸들러
- contractAddress : CYBX 토큰 컨트랙트 주소
- checkInterval : 체크 주기
- batchSize : 번에 처리할 사용자
- netType : 네트워크 타입
- CODE_STRC : 설정 정보
**************************************************************
*/
func StartTransactionMonitorAlchemy(walletDBConn *sql.DB, header model.Header, contractAddress string, checkInterval time.Duration, batchSize int, CODE_STRC model.CodeStrc) {
umlog.Debug("[StartTransactionMonitorAlchemy] Starting monitor - Interval: %v, Batch size: %d\n", checkInterval, batchSize)
umlog.Debug("[StartTransactionMonitorAlchemy] Contract Address: %s\n", contractAddress)
umlog.Debug("[StartTransactionMonitorAlchemy] Alchemy URL: %s\n", CODE_STRC.ALCHEMY_MAINNET)
ticker := time.NewTicker(checkInterval)
defer ticker.Stop()
// 즉시 한 번 실행
checkAllWalletsWithAlchemy(walletDBConn, header, contractAddress, batchSize, CODE_STRC)
// 주기적 실행
for range ticker.C {
checkAllWalletsWithAlchemy(walletDBConn, header, contractAddress, batchSize, CODE_STRC)
}
}
/*
**************************************************************
desc : 입금 알림 전송 (FCM Push + Telegram)
parameter
- walletDBConn : Wallet DB 핸들러
- userId : 사용자 ID
- fromAddress : 발신 주소
- toAddress : 수신 주소
- amount : 금액
- txHash : 트랜잭션 해시
**************************************************************
*/
func sendIncomingTransactionNotification(walletDBConn *sql.DB, header model.Header, userId int, fromAddress string, toAddress string, amount string, txHash string) {
// FCM 토큰 조회
var fcmToken, angkorId string
query := "SELECT fcm_token, angkorid FROM ank_wallet_user WHERE id = ?"
umlog.Debug("authCode count query: %s", query)
rows, _, err := umsql.SqlSelect(walletDBConn, query, userId)
if err != nil {
umlog.Error("SELECT Error(%s): %s", err.Error(), query)
return
}
defer rows.Close()
if rows.Next() {
if scanErr := rows.Scan(&fcmToken, &angkorId); scanErr != nil {
umlog.Error("SELECT Scan Error(%s): %s", scanErr.Error(), query)
return
}
}
// FCM Push 알림 (FCM 토큰이 있는 경우)
// if fcmToken.Valid && fcmToken.String != "" {
// sendFCMPushNotification(fcmToken.String, amount, fromAddress, txHash)
// }
// Telegram 알림 (관리자용)
messageboxy := fmt.Sprintf(
"💰 Deposit notification(from Alchemy)\nFrom: %s\nTo: %s\nAmount: %s CYBX\nTxHash: %s",
fromAddress, toAddress, amount, txHash)
message := map[string]string{"message": messageboxy}
jsonData, err := json.Marshal(message)
if err != nil {
umlog.Warn("SendMessage Error(%s): %v", err.Error(), message)
}
SendMessage(header, angkorId, string(jsonData))
}

149
src/common.go Normal file
View File

@ -0,0 +1,149 @@
package src
import (
"AngkorWalletScanning/model"
"AngkorWalletScanning/umlog"
"AngkorWalletScanning/umsql"
"bytes"
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
)
/*
**************************************************************
desc : DB에서 코드값을 읽어온다.
parameter
- dbConn : DB 핸들러
- key : AES 복호화
return
- int : 상태 코드
- model.CodeStrc: 코드 구조체
error code
- 4001202 : DB 연결이 nil
- 4001213 : DB 조회 오류
- 4001215 : DB 스캔 오류
**************************************************************
*/
func GetCodeInfo(dbConn *sql.DB, key string) (int, model.CodeStrc) {
var status int = 200
var code_strc model.CodeStrc
// DB 연결 검증
if dbConn == nil {
umlog.Error("DB connection is nil")
return 4001202, code_strc
}
// 매개변수 검증
if len(key) == 0 {
umlog.Error("Decryption key is empty")
return 4001213, code_strc
}
// SQL 쿼리 (매개변수 바인딩 사용)
query := `SELECT code_name, AES_DECRYPT(UNHEX(code_value), ?) AS code_value
FROM angkor_code
WHERE code_number IN ('001001', '001002', '001010', '001011', '001003', '001004', '001005', '100001', '100002', '100003', '100004', '100005')`
res, _, err := umsql.SqlSelect(dbConn, query, key)
if err != nil {
umlog.Error("SELECT Error(%s): %s", err.Error(), query)
return 4001213, code_strc
}
defer res.Close()
for res.Next() {
var code_name, code_value string
if err := res.Scan(&code_name, &code_value); err != nil {
umlog.Error("SCAN Error(%s): %s", err.Error(), query)
return 4001215, code_strc
}
// 코드명에 따라 구조체 필드 설정
switch code_name {
case "CIPHERKEY":
code_strc.CIPHERKEY = code_value
case "CIPHERIVKEY":
code_strc.CIPHERIVKEY = code_value
case "MESSAGE_TOKEN":
code_strc.MESSAGE_TOKEN = code_value
case "MESSAGE_ID":
code_strc.MESSAGE_ID = code_value
case "SENDBIRD_KEY":
code_strc.SENDBIRD_KEY = code_value
case "SENDBIRD_TOKEN":
code_strc.SENDBIRD_TOKEN = code_value
case "SENDBIRD_VERSION":
code_strc.SENDBIRD_VERSION = code_value
case "ETHERSCAN":
code_strc.ETHERSCAN_TOKEN = code_value
case "CYBX_MAINNET":
code_strc.CYBX_MAINNET = code_value
case "CYBX_TESTNET":
code_strc.CYBX_TESTNET = code_value
case "ALCHEMY_MAINNET":
code_strc.ALCHEMY_MAINNET = code_value
case "ALCHEMY_TESTNET":
code_strc.ALCHEMY_TESTNET = code_value
default:
umlog.Warn("Unknown code_name: %s", code_name)
}
}
return status, code_strc
}
func SendMessage(header model.Header, angkorId string, message string) error {
// Media 서버에서 사용자 정보 조회
fullUrl := "https://aauth.angkorlifes.com/message/v1/chat"
var sendBody = map[string]any{
"grantType": "session",
"angkorId": angkorId,
"messageType": "WAL",
"notiMessage": message,
}
sendBodyByte, err := json.Marshal(sendBody)
if err != nil {
umlog.Warn("JSON marshal error: %s", err.Error())
return err
}
req, err := http.NewRequest("POST", fullUrl, bytes.NewBuffer(sendBodyByte))
if err != nil {
umlog.Warn("Error creating request: => %s : %s", fullUrl, err.Error())
return err
}
//Content-Type 헤더 추가
req.Header.Add("Content-Type", "application/json")
req.Header.Add("AppKey", header.AppKey)
req.Header.Add("Authorization", header.SecretKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
umlog.Warn("Error: => %s", err.Error())
return err
}
defer resp.Body.Close()
umlog.Debug("StatusCode: => %d", resp.StatusCode)
// 응답 본문 읽기
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
umlog.Warn("Error reading response body: %s", err.Error())
return err
}
if resp.StatusCode != 200 {
// 실패 응답
umlog.Warn("User info retrieval failed - Status: %d, Response: %s", resp.StatusCode, string(resBody))
return fmt.Errorf("User info retrieval failed - Status: %s, Response: %s", strconv.Itoa(resp.StatusCode), string(resBody))
}
return nil
}

464
src/etherscan.go Normal file
View File

@ -0,0 +1,464 @@
package src
import (
"AngkorWalletScanning/model"
"AngkorWalletScanning/umlog"
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"os"
"strconv"
"strings"
"time"
"golang.org/x/time/rate"
)
// BSCScan API Rate Limiter (5 requests per second for free tier)
var bscApiLimiter = rate.NewLimiter(rate.Limit(5), 5)
// BSCScan API Response Structures
type BSCScanTransaction struct {
BlockNumber string `json:"blockNumber"`
TimeStamp string `json:"timeStamp"`
Hash string `json:"hash"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
ContractAddress string `json:"contractAddress"`
TokenName string `json:"tokenName"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimal string `json:"tokenDecimal"`
TransactionIndex string `json:"transactionIndex"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
GasUsed string `json:"gasUsed"`
CumulativeGasUsed string `json:"cumulativeGasUsed"`
Input string `json:"input"`
Confirmations string `json:"confirmations"`
}
type BSCScanResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Result []BSCScanTransaction `json:"result"`
}
/*
**************************************************************
desc : BSCScan API를 통해 특정 주소의 토큰 트랜잭션 조회
parameter
- address : 조회할 지갑 주소
- contractAddress : 토큰 컨트랙트 주소 (CYBX)
- startBlock : 시작 블록 번호 (0 = 처음부터)
- netType : 네트워크 타입 ("MAINNET" 또는 "TESTNET")
return
- []BSCScanTransaction : 트랜잭션 목록
- error : 에러 정보
**************************************************************
*/
func getBSCScanTokenTransactions(address string, contractAddress string, startBlock int64, netType string, CODE_STRC model.CodeStrc) ([]BSCScanTransaction, error) {
// Rate limiter 체크 (5 requests per second)
ctx := context.Background()
err := bscApiLimiter.Wait(ctx)
if err != nil {
return nil, fmt.Errorf("rate limiter error: %v", err)
}
// chainid 결정 (MAINNET: 56, TESTNET: 97)
chainId := "56" // BSC Mainnet
if netType == "TESTNET" {
chainId = "97" // BSC Testnet
}
// Etherscan API V2 URL 구성
url := fmt.Sprintf(
"https://api.etherscan.io/v2/api?chainid=%s&module=account&action=tokentx&contractaddress=%s&address=%s&startblock=%d&endblock=999999999&sort=desc&apikey=%s",
chainId,
contractAddress,
address,
startBlock,
CODE_STRC.ETHERSCAN_TOKEN,
)
// HTTP GET 요청
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("http request failed: %v", err)
}
defer resp.Body.Close()
// 응답 읽기
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %v", err)
}
// JSON 파싱
var bscResp BSCScanResponse
err = json.Unmarshal(body, &bscResp)
if err != nil {
return nil, fmt.Errorf("failed to parse json: %v", err)
}
// API 응답 상태 체크
if bscResp.Status != "1" {
if bscResp.Message == "No transactions found" {
return []BSCScanTransaction{}, nil
}
return nil, fmt.Errorf("bscscan api error: %s", bscResp.Message)
}
return bscResp.Result, nil
}
/*
**************************************************************
desc : 특정 지갑의 입금 트랜잭션 체크 알림 전송
parameter
- walletDBConn : Wallet DB 핸들러
- userId : 사용자 ID
- walletAddress : 지갑 주소
- contractAddress : CYBX 토큰 컨트랙트 주소
- lastCheckTime : 마지막 체크 시각
- netType : 네트워크 타입 ("MAINNET" 또는 "TESTNET")
return
- int : 새로 발견된 입금 건수
- error : 에러 정보
**************************************************************
*/
func checkWalletWithAPI(walletDBConn *sql.DB, header model.Header, userId int, walletAddress string, contractAddress string, lastCheckTime time.Time, netType string, CODE_STRC model.CodeStrc) (int, error) {
// 마지막 체크 시각을 블록 번호로 변환 (BSC는 약 3초당 1블록)
timeDiff := time.Since(lastCheckTime)
blocksToCheck := int64(timeDiff.Seconds() / 3) // 3초당 1블록
// 최소 100블록은 체크 (약 5분)
if blocksToCheck < 100 {
blocksToCheck = 100
}
// 현재 블록에서 역산
currentBlock := getCurrentBlockNumber(netType)
startBlock := currentBlock - blocksToCheck
if startBlock < 0 {
startBlock = 0
}
// BSCScan API로 트랜잭션 조회
transactions, err := getBSCScanTokenTransactions(walletAddress, contractAddress, startBlock, netType, CODE_STRC)
if err != nil {
umlog.Error("checkWalletWithAPI\nFailed to get transactions for user %d: %v", userId, err)
return 0, err
}
newTransactionCount := 0
// 입금 트랜잭션 필터링 (To == 내 지갑 주소)
for _, tx := range transactions {
// 소문자로 변환하여 비교
if strings.ToLower(tx.To) != strings.ToLower(walletAddress) {
continue // 출금 트랜잭션은 무시
}
// 트랜잭션 시각 체크
txTimestamp, _ := strconv.ParseInt(tx.TimeStamp, 10, 64)
txTime := time.Unix(txTimestamp, 0)
// 마지막 체크 이후의 트랜잭션만 처리
if txTime.Before(lastCheckTime) {
continue
}
// 이미 알림 보낸 트랜잭션인지 확인
if isTransactionNotified(walletDBConn, tx.Hash) {
continue
}
// 금액 계산 (토큰 decimal 적용)
amount := calculateTokenAmount(tx.Value, tx.TokenDecimal)
// 알림 전송
sendIncomingTransactionNotification(walletDBConn, header, userId, tx.From, walletAddress, amount, tx.Hash)
// 알림 기록 저장
saveNotificationRecord(walletDBConn, userId, tx.Hash, tx.From, tx.To, amount, tx.TokenSymbol)
newTransactionCount++
umlog.Info("checkWalletWithAPI\nNew incoming tx detected - User: %d, From: %s, Amount: %s %s, Hash: %s",
userId, tx.From, amount, tx.TokenSymbol, tx.Hash)
}
return newTransactionCount, nil
}
/*
**************************************************************
desc : 현재 BSC 블록 번호 조회
parameter
- netType : 네트워크 타입 ("MAINNET" 또는 "TESTNET")
return
- int64 : 현재 블록 번호
**************************************************************
*/
func getCurrentBlockNumber(netType string) int64 {
apiKey := os.Getenv("BSCSCAN_API_KEY")
if apiKey == "" {
return 0
}
// chainid 결정 (MAINNET: 56, TESTNET: 97)
chainId := "56" // BSC Mainnet
if netType == "TESTNET" {
chainId = "97" // BSC Testnet
}
url := fmt.Sprintf("https://api.etherscan.io/v2/api?chainid=%s&module=proxy&action=eth_blockNumber&apikey=%s", chainId, apiKey)
resp, err := http.Get(url)
if err != nil {
return 0
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0
}
var result map[string]interface{}
json.Unmarshal(body, &result)
if blockHex, ok := result["result"].(string); ok {
blockNum := new(big.Int)
blockNum.SetString(blockHex[2:], 16) // "0x" 제거 후 16진수 파싱
return blockNum.Int64()
}
return 0
}
/*
**************************************************************
desc : 토큰 금액 계산 (decimal 적용)
parameter
- value : 토큰 raw value (wei 단위)
- decimal : 토큰 decimal
return
- string : 사람이 읽을 있는 금액
**************************************************************
*/
func calculateTokenAmount(value string, decimal string) string {
// value를 big.Int로 변환
valueInt := new(big.Int)
valueInt.SetString(value, 10)
// decimal을 int로 변환
decimalInt, _ := strconv.Atoi(decimal)
// 10^decimal 계산
divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimalInt)), nil)
// 나눗셈
result := new(big.Float).Quo(
new(big.Float).SetInt(valueInt),
new(big.Float).SetInt(divisor),
)
// 소수점 4자리까지 표시
return result.Text('f', 4)
}
/*
**************************************************************
desc : 알림 기록을 DB에 저장
parameter
- walletDBConn : Wallet DB 핸들러
- userId : 사용자 ID
- txHash : 트랜잭션 해시
- txFrom : 발신 주소
- txTo : 수신 주소
- amount : 금액
- token : 토큰 심볼
**************************************************************
*/
func saveNotificationRecord(walletDBConn *sql.DB, userId int, txHash string, txFrom string, txTo string, amount string, token string) {
query := `
INSERT INTO ank_wallet_notification
(user_id, tx_hash, tx_from, tx_to, amount, token, notified_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())
`
_, err := walletDBConn.Exec(query, userId, txHash, txFrom, txTo, amount, token)
if err != nil {
umlog.Error("saveNotificationRecord", fmt.Sprintf("Failed to save notification: %v", err))
}
}
/*
**************************************************************
desc : 사용자의 마지막 체크 시각 업데이트
parameter
- walletDBConn : Wallet DB 핸들러
- userId : 사용자 ID
**************************************************************
*/
func updateLastCheckTime(walletDBConn *sql.DB, userId int) {
query := "UPDATE ank_wallet_user SET checkedAt = NOW() WHERE id = ?"
_, err := walletDBConn.Exec(query, userId)
if err != nil {
umlog.Error("updateLastCheckTime\nFailed to update last_check_time for user %d: %v", userId, err)
}
}
/*
**************************************************************
desc : FCM Push 알림 전송
parameter
- fcmToken : FCM 토큰
- amount : 입금 금액
- fromAddress : 발신 주소
- txHash : 트랜잭션 해시
**************************************************************
*/
func sendFCMPushNotification(fcmToken string, amount string, fromAddress string, txHash string) {
// TODO: Firebase Admin SDK를 사용하여 FCM Push 알림 구현
// 여기서는 구조만 제공하고, 실제 구현은 프로젝트 환경에 맞게 작성 필요
umlog.Info("sendFCMPushNotification\nSending FCM push to token: %s, Amount: %s CYBX", fcmToken[:20]+"...", amount)
// Example:
// message := &messaging.Message{
// Token: fcmToken,
// Notification: &messaging.Notification{
// Title: "입금 알림",
// Body: fmt.Sprintf("%s CYBX가 입금되었습니다.", amount),
// },
// Data: map[string]string{
// "type": "incoming_transaction",
// "amount": amount,
// "from": fromAddress,
// "txHash": txHash,
// },
// }
//
// _, err := fcmClient.Send(ctx, message)
// if err != nil {
// umlog.Error("sendFCMPushNotification", fmt.Sprintf("FCM send failed: %v", err))
// }
}
/*
**************************************************************
desc : 모든 활성 사용자의 지갑 체크 (배치 처리)
parameter
- walletDBConn : Wallet DB 핸들러
- contractAddress : CYBX 토큰 컨트랙트 주소
- batchSize : 번에 처리할 사용자 (기본 300)
- netType : 네트워크 타입 ("MAINNET" 또는 "TESTNET")
**************************************************************
*/
func checkAllWalletsForIncomingTransactions(walletDBConn *sql.DB, header model.Header, contractAddress string, batchSize int, netType string, CODE_STRC model.CodeStrc) {
startTime := time.Now()
// 활성 사용자 목록 조회 (최근 활동 순)
query := `
SELECT id, address, checkedAt
FROM ank_wallet_user
WHERE address > ''
ORDER BY checkedAt ASC
LIMIT ?
`
rows, err := walletDBConn.Query(query, batchSize)
if err != nil {
umlog.Error("checkAllWalletsForIncomingTransactions\nDB query failed: %v", err)
return
}
defer rows.Close()
totalChecked := 0
totalNewTransactions := 0
for rows.Next() {
var userId int
var walletAddress string
var lastCheckStr string
err := rows.Scan(&userId, &walletAddress, &lastCheckStr)
if err != nil {
umlog.Error("checkAllWalletsForIncomingTransactions\nRow scan error: %v", err)
continue
}
// 마지막 체크 시각 파싱
lastCheckTime, _ := time.Parse("2006-01-02 15:04:05", lastCheckStr)
// 개별 지갑 체크
newTxCount, err := checkWalletWithAPI(walletDBConn, header, userId, walletAddress, contractAddress, lastCheckTime, netType, CODE_STRC)
if err != nil {
// 에러 로그는 checkWalletWithAPI 내부에서 이미 출력됨
continue
}
// 마지막 체크 시각 업데이트
updateLastCheckTime(walletDBConn, userId)
totalChecked++
totalNewTransactions += newTxCount
}
elapsed := time.Since(startTime)
umlog.Info("checkAllWalletsForIncomingTransactions\nBatch completed - Checked: %d wallets, New transactions: %d, Time: %.2f seconds",
totalChecked, totalNewTransactions, elapsed.Seconds())
}
/*
**************************************************************
desc : 트랜잭션 모니터링 시작 (백그라운드 고루틴)
parameter
- walletDBConn : Wallet DB 핸들러
- contractAddress : CYBX 토큰 컨트랙트 주소
- checkInterval : 체크 주기 (: 5)
- batchSize : 번에 처리할 사용자
- netType : 네트워크 타입 ("MAINNET" 또는 "TESTNET")
**************************************************************
*/
func StartTransactionMonitor(walletDBConn *sql.DB, header model.Header, contractAddress string, checkInterval time.Duration, batchSize int, netType string, CODE_STRC model.CodeStrc) {
umlog.Info("StartTransactionMonitor: Transaction monitor started - Network: %s, Interval: %v, Batch size: %d", netType, checkInterval, batchSize)
ticker := time.NewTicker(checkInterval)
defer ticker.Stop()
// 즉시 한 번 실행
checkAllWalletsForIncomingTransactions(walletDBConn, header, contractAddress, batchSize, netType, CODE_STRC)
// // 주기적 실행
// for range ticker.C {
// checkAllWalletsForIncomingTransactions(walletDBConn, header, contractAddress, batchSize, netType, CODE_STRC)
// }
}

103
umlog/umlog.go Normal file
View File

@ -0,0 +1,103 @@
package umlog
import (
"AngkorWalletScanning/ummessage"
"fmt"
"log"
"os"
"runtime"
"strconv"
"strings"
)
type Logger struct {
Debug *log.Logger
Info *log.Logger
Warn *log.Logger
Error *log.Logger
}
var (
logFile *os.File
umLogger Logger
log_level string = ""
)
// 패키지 외부에서 사용하기 위해서는 첫문자는 대문자로...
func Init(logfile string, messageToken string, messageId string) error {
var err error
logFile, err = os.OpenFile(logfile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
return err
}
umLogger.Debug = log.New(logFile, "[DEBUG] ", log.Ldate|log.Ltime)
umLogger.Info = log.New(logFile, "[INFO] ", log.Ldate|log.Ltime)
umLogger.Warn = log.New(logFile, "[WARN] ", log.Ldate|log.Ltime)
umLogger.Error = log.New(logFile, "[ERROR] ", log.Ldate|log.Ltime)
ummessage.InitTelegramBot(messageToken, messageId)
return nil
}
func Close() {
logFile.Close()
}
func SetLevel(level string) {
switch level {
case "I":
log_level = "info"
case "W":
log_level = "warn"
case "E":
log_level = "error"
default:
log_level = "debug"
}
}
func debugInfo() (string, string, string) {
funcname, filename, line, _ := runtime.Caller(2)
functionname := runtime.FuncForPC(funcname).Name()
filenames := strings.Split(filename, "/")
return filenames[len(filenames)-1], functionname, strconv.Itoa(line)
}
func Message(title string, body string) {
chatId := int64(-5029236432)
token := "7108014537:AAHU7299mCzHsBJp1KmPMp7tZEGfGsQ-ffM"
ummessage.SendMessageDirect(chatId, token, title, body)
}
func Error(format string, args ...interface{}) {
if log_level == "error" || log_level == "warn" || log_level == "info" || log_level == "debug" {
filename, function, line := debugInfo()
message := fmt.Sprintf(format, args...)
umLogger.Error.Println("[" + filename + "][" + function + "][" + line + "] " + message)
ummessage.SendMessage("["+filename+"]["+function+"]["+line+"]", message)
}
}
func Warn(format string, args ...interface{}) {
if log_level == "warn" || log_level == "info" || log_level == "debug" {
filename, _, line := debugInfo()
message := fmt.Sprintf(format, args...)
umLogger.Warn.Println("[" + filename + "][" + line + "] " + message)
}
}
func Info(format string, args ...interface{}) {
if log_level == "info" || log_level == "debug" {
filename, _, line := debugInfo()
message := fmt.Sprintf(format, args...)
umLogger.Info.Println("[" + filename + "][" + line + "] " + message)
}
}
func Debug(format string, args ...interface{}) {
if log_level == "debug" {
filename, function, line := debugInfo()
message := fmt.Sprintf(format, args...)
umLogger.Debug.Println("[" + filename + "][" + function + "][" + line + "] " + message)
}
}

73
ummessage/ummessage.go Normal file
View File

@ -0,0 +1,73 @@
package ummessage
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
var telegramToken string
var telegramChatId string
func InitTelegramBot(token string, chatId string) {
telegramToken = token
telegramChatId = chatId
}
type SendMessageModel struct {
Parse_mode string
Chat_id string
Disable_web_page_preview bool
Text string
}
func SendMessage(title string, body string) {
message := `<b>` + title + `</b>
<b>발생시간(UTC)</b> : <code>` + time.Now().Format("2006-01-02 15:04:05") + `</code>
<b>내용</b> :
<pre>` + body + `</pre>`
messageBody, _ := json.Marshal(map[string]any{
"chat_id": telegramChatId,
"text": message,
"parse_mode": "html",
})
sendUrl := "https://api.telegram.org/bot" + telegramToken + "/sendMessage"
resp, err := http.Post(sendUrl, "application/json", bytes.NewBuffer(messageBody))
if err != nil {
fmt.Println(err.Error())
}
defer resp.Body.Close()
// 결과 출력
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
}
}
func SendMessageDirect(chatId int64, telegramToken string, title string, body string) {
message := `<b>` + title + `</b>
<b>발생시간(UTC)</b> : <code>` + time.Now().Format("2006-01-02 15:04:05") + `</code>
<b>내용</b> :
` + body
messageBody, _ := json.Marshal(map[string]any{
"chat_id": chatId,
"text": message,
"parse_mode": "html",
})
sendUrl := "https://api.telegram.org/bot" + telegramToken + "/sendMessage"
resp, err := http.Post(sendUrl, "application/json", bytes.NewBuffer(messageBody))
if err != nil {
panic(err)
}
defer resp.Body.Close()
// 결과 출력
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
}
}

100
umredis/umredis-cluster.go Normal file
View File

@ -0,0 +1,100 @@
package umredis
import (
"context"
"time"
"AngkorWalletScanning/umlog"
"github.com/redis/go-redis/v9"
)
// version : 1.0.0
// date : 2022-10-19
// writer : lee.beomhee
// modify : lee.beomhee (2022-10-19)
func RedisClusterConnect(host []string, password string) *redis.ClusterClient {
var rdb *redis.ClusterClient
if len(password) > 0 {
rdb = redis.NewClusterClient(&redis.ClusterOptions{
Addrs: host,
Password: password,
})
} else {
rdb = redis.NewClusterClient(&redis.ClusterOptions{
Addrs: host,
})
}
return rdb
}
func RedisClusterDisConnect(cluster *redis.ClusterClient) {
umlog.Debug("REDIS DISCONNECTION")
cluster.Close()
}
func ClusterGetKeys(cluster *redis.ClusterClient, keys string) ([]string, error) {
return cluster.Keys(context.Background(), keys+"*").Result()
}
func ClusterGetValue(cluster *redis.ClusterClient, key string) (string, error) {
return cluster.Get(context.Background(), key).Result()
}
func ClusterHGetValue(cluster *redis.ClusterClient, key string, field string) (string, error) {
return cluster.HGet(context.Background(), key, field).Result()
}
func ClusterHGetAllValue(cluster *redis.ClusterClient, key string) (map[string]string, error) {
return cluster.HGetAll(context.Background(), key).Result()
}
func ClusterSetValue(cluster *redis.ClusterClient, key string, value any) error {
_, err := cluster.Set(context.Background(), key, value, 0).Result()
return err
}
func ClusterHMSetValue(cluster *redis.ClusterClient, key string, value any) error {
_, err := cluster.HMSet(context.Background(), key, value).Result()
return err
}
func ClusterHSetValue(cluster *redis.ClusterClient, key string, field string, value any) error {
_, err := cluster.HSet(context.Background(), key, field, value).Result()
return err
}
func ClusterHINCRBY(cluster *redis.ClusterClient, key string, field string, value int64) error {
_, err := cluster.HIncrBy(context.Background(), key, field, value).Result()
return err
}
func ClusterRemoveValue(cluster *redis.ClusterClient, key string, field string) error {
_, err := cluster.HDel(context.Background(), key, field).Result()
return err
}
func ClusterRemoveKey(cluster *redis.ClusterClient, key string) error {
_, err := cluster.Del(context.Background(), key).Result()
return err
}
func ClusterRemoveKeys(cluster *redis.ClusterClient, keys string) error {
findKeys, err := cluster.Keys(context.Background(), keys+"*").Result()
if err == nil {
for _, key := range findKeys {
_, err = cluster.Del(context.Background(), key).Result()
if err != nil {
break
}
}
}
return err
}
func ClusterExpireKey(cluster *redis.ClusterClient, key string, sec time.Duration) error {
_, err := cluster.Expire(context.Background(), key, sec).Result()
return err
}

209
umsql/umsql.go Normal file
View File

@ -0,0 +1,209 @@
package umsql
import (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"time"
"AngkorWalletScanning/model"
"AngkorWalletScanning/umlog"
_ "github.com/go-sql-driver/mysql"
)
// type DB_handler struct {
// conn *sql.DB
// }
func DbConnect(host string, port string, user string, password string, dbname string) *sql.DB {
conn, connect_err := sql.Open("mysql", user+":"+password+"@tcp("+host+":"+port+")/"+dbname)
if connect_err != nil {
umlog.Error(connect_err.Error())
panic(connect_err.Error())
}
conn.SetConnMaxLifetime(time.Minute * 3)
conn.SetMaxOpenConns(1000)
conn.SetMaxIdleConns(1000)
return conn
}
func DbDisconnect(connA *sql.DB) {
umlog.Debug("DataBase DISCONNECTION")
err := connA.Close()
if err != nil {
umlog.Warn("DataBase DISCONNECTION Error A: " + err.Error())
}
}
func DbBegin(conn *sql.DB) (*sql.Tx, error) {
if conn != nil {
return conn.Begin()
} else {
return nil, errors.New("db connecter is nil")
}
}
func DbRollback(tx *sql.Tx) error {
return tx.Rollback()
}
func DbCommit(tx *sql.Tx) error {
return tx.Commit()
}
func SqlSelect(conn *sql.DB, query string, args ...any) (*sql.Rows, []string, error) {
var rows *sql.Rows
var err error
if conn != nil {
if len(args) == 0 {
rows, err = conn.Query(query)
} else {
rows, err = conn.Query(query, args...)
}
if err != nil {
umlog.Error(err.Error())
return nil, nil, err
}
cols, err := rows.Columns()
if err != nil {
umlog.Error(err.Error())
return nil, nil, err
}
return rows, cols, nil
}
umlog.Error("db connecter is nil")
return nil, nil, errors.New("db connecter is nil")
}
func SqlTxExecute(tx *sql.Tx, query string, args ...any) (int64, error) {
var result sql.Result
var err error
if tx != nil {
if len(args) == 0 {
result, err = tx.Exec(query)
} else {
result, err = tx.Exec(query, args...)
}
if err != nil {
umlog.Error(err.Error())
return -1, err
}
n, err := result.RowsAffected()
if err != nil {
umlog.Error(err.Error())
return -1, err
}
return n, nil
} else {
umlog.Error("tx is nil")
return -1, errors.New("tx is nil")
}
}
func SqlExecute(conn *sql.DB, query string, args ...any) (int64, error) {
var result sql.Result
var err error
if conn != nil {
if len(args) == 0 {
result, err = conn.Exec(query)
} else {
result, err = conn.Exec(query, args...)
}
if err != nil {
umlog.Error(err.Error())
return -1, err
}
n, err := result.RowsAffected()
if err != nil {
umlog.Error(err.Error())
return -1, err
}
return n, nil
}
umlog.Error("db connecter is nil")
return -1, errors.New("db connecter is nil")
}
func SqlStatement(conn *sql.DB, sql string, param [][]string) error {
stmt, err := conn.Prepare(sql)
if err != nil {
umlog.Error(err.Error())
return err
}
defer stmt.Close()
for i, sub_val := range param {
fmt.Println(sub_val)
err = sqlStatementExec(stmt, param[i], len(param[i]))
if err != nil {
umlog.Error(err.Error())
return err
}
}
return nil
}
func sqlStatementExec(stmt *sql.Stmt, param []string, param_count int) error {
args := make([]interface{}, param_count)
for i, v := range param {
args[i] = v
}
_, err := stmt.Exec(args...)
return err
}
func SqlTransaction(conn *sql.DB, maxRetries int, txFunc func(*sql.Tx) error) model.DefaultErrorModel {
var err error
var value string
for i := 0; i <= maxRetries; i++ {
tx, beginErr := conn.Begin()
if beginErr != nil {
return model.DefaultErrorModel{Code: 4001201, Message: fmt.Sprintf("failed to begin transaction: %v", beginErr)}
}
err = txFunc(tx)
if err != nil {
// 트랜잭션 롤백
_ = tx.Rollback()
// 1205 오류인지 확인
if isLockWaitTimeout(err) {
umlog.Info(value + ": Transaction lock timeout (1205), retrying... (" + strconv.Itoa(i+1) + "/" + strconv.Itoa(maxRetries) + ")")
time.Sleep(time.Duration(100*(i+1)) * time.Millisecond) // simple backoff
continue
}
return model.DefaultErrorModel{Code: 4001201, Message: fmt.Sprintf("transaction failed: %v", err)}
}
// 커밋 시도
commitErr := tx.Commit()
if commitErr != nil {
// 커밋 중 에러가 1205일 수도 있음
if isLockWaitTimeout(commitErr) {
umlog.Info(value + ": Commit failed due to lock timeout, retrying... (" + strconv.Itoa(i+1) + "/" + strconv.Itoa(maxRetries) + ")")
time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
continue
}
umlog.Error(value + ": commit error: " + commitErr.Error())
return model.DefaultErrorModel{Code: 4001201, Message: fmt.Sprintf("commit error: %v", commitErr)}
}
// 성공
return model.DefaultErrorModel{Code: 200, Message: ""}
}
umlog.Error(value + ": Transaction failed after " + strconv.Itoa(maxRetries) + " retries: " + err.Error())
return model.DefaultErrorModel{Code: 4001201, Message: fmt.Sprintf("%s: Transaction failed after %d retries: %w", value, maxRetries, err)}
}
func isLockWaitTimeout(err error) bool {
// mysql 드라이버의 오류 메시지에서 1205 감지
return err != nil && (strings.Contains(err.Error(), "Error 1205") || strings.Contains(err.Error(), "Lock wait timeout"))
}