Add files via upload
This commit is contained in:
parent
537a825786
commit
49f5486782
36 changed files with 1189 additions and 0 deletions
2
Web3-_--main/README.md
Normal file
2
Web3-_--main/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Web3 (^_^)
|
||||
Web3 OnlyFans — это децентрализованная платформа для монетизации контента, которая объединяет инфлюенсеров и подписчиков. Платформа использует технологии блокчейна для обеспечения прозрачности, безопасности, приватности и прямых финансовых взаимодействий без посредников.
|
118
Web3-_--main/contracts/market/Market.go
Normal file
118
Web3-_--main/contracts/market/Market.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package contract
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/std"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
ownerKey = "o"
|
||||
tokenKey = "t"
|
||||
nftKey = "n"
|
||||
sellPrefix = "forSale_"
|
||||
)
|
||||
|
||||
// MarketDeployData структура для _deploy
|
||||
type MarketDeployData struct {
|
||||
Admin interop.Hash160
|
||||
Token interop.Hash160
|
||||
Nft interop.Hash160
|
||||
}
|
||||
|
||||
func _deploy(data any, isUpdate bool) {
|
||||
if isUpdate {
|
||||
return
|
||||
}
|
||||
|
||||
initData := data.(MarketDeployData)
|
||||
admin := initData.Admin
|
||||
if len(admin) != 20 {
|
||||
panic("invalid admin")
|
||||
}
|
||||
|
||||
ctx := storage.GetContext()
|
||||
storage.Put(ctx, []byte(ownerKey), admin)
|
||||
storage.Put(ctx, []byte(tokenKey), initData.Token)
|
||||
storage.Put(ctx, []byte(nftKey), initData.Nft)
|
||||
}
|
||||
|
||||
// OnNEP11Payment вызывается при получении NFT
|
||||
func OnNEP11Payment(from interop.Hash160, amount int, tokenId []byte, data any) {
|
||||
if amount != 1 {
|
||||
panic("Must be non-fungible (amount=1)")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
callingHash := runtime.GetCallingScriptHash()
|
||||
nftHash := storage.Get(ctx, []byte(nftKey)).(interop.Hash160)
|
||||
if !std.Equals(callingHash, nftHash) {
|
||||
panic("NFT not recognized")
|
||||
}
|
||||
|
||||
// Выставляем NFT на продажу, если владелец хочет
|
||||
// Цена берется из свойств NFT (fields: price)
|
||||
saleKey := append([]byte(sellPrefix), tokenId...)
|
||||
storage.Put(ctx, saleKey, []byte{1})
|
||||
}
|
||||
|
||||
// OnNEP17Payment вызывается при получении токенов (оплата за NFT)
|
||||
func OnNEP17Payment(from interop.Hash160, amount int, data any) {
|
||||
ctx := storage.GetContext()
|
||||
callingHash := runtime.GetCallingScriptHash()
|
||||
myToken := storage.Get(ctx, []byte(tokenKey)).(interop.Hash160)
|
||||
if !std.Equals(callingHash, myToken) {
|
||||
panic("invalid token")
|
||||
}
|
||||
|
||||
tokenIdBytes, ok := data.([]byte)
|
||||
if !ok {
|
||||
panic("no tokenId in data")
|
||||
}
|
||||
tokenId := string(tokenIdBytes)
|
||||
|
||||
saleKey := append([]byte(sellPrefix), tokenIdBytes...)
|
||||
if storage.Get(ctx, saleKey) == nil {
|
||||
panic("NFT not on sale")
|
||||
}
|
||||
|
||||
// Узнаем price из NFT
|
||||
nftHash := storage.Get(ctx, []byte(nftKey)).(interop.Hash160)
|
||||
props := contract.Call(nftHash, "properties", contract.ReadStates, tokenId).(map[string]any)
|
||||
// price хранится как string => нужно конвертировать
|
||||
price := std.Atoi(props["price"].(string))
|
||||
if amount < price {
|
||||
panic("insufficient payment")
|
||||
}
|
||||
|
||||
// снимаем с продажи
|
||||
storage.Delete(ctx, saleKey)
|
||||
// передаем NFT покупателю
|
||||
contract.Call(nftHash, "transfer", contract.All, from, tokenId, nil)
|
||||
}
|
||||
|
||||
// List вернет массив token_id, которые сейчас на продаже
|
||||
func List() []string {
|
||||
ctx := storage.GetContext()
|
||||
iter := storage.Find(ctx, []byte(sellPrefix), storage.KeysOnly)
|
||||
var result []string
|
||||
for iterator.Next(iter) {
|
||||
key := iterator.Key(iter).([]byte)
|
||||
tokenId := key[len(sellPrefix):]
|
||||
result = append(result, string(tokenId))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// TransferTokens позволяет владельцу контракта забрать токены с контракта.
|
||||
func TransferTokens(to interop.Hash160, amount int) {
|
||||
ctx := storage.GetContext()
|
||||
owner := storage.Get(ctx, []byte(ownerKey)).(interop.Hash160)
|
||||
if !runtime.CheckWitness(owner) {
|
||||
panic("not an owner")
|
||||
}
|
||||
myToken := storage.Get(ctx, []byte(tokenKey)).(interop.Hash160)
|
||||
contract.Call(myToken, "transfer", contract.All, runtime.GetExecutingScriptHash(), to, amount, nil)
|
||||
}
|
135
Web3-_--main/contracts/nep11/OnlyFansNFT.py
Normal file
135
Web3-_--main/contracts/nep11/OnlyFansNFT.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
from typing import Any, Dict, List
|
||||
from boa3.builtin import public
|
||||
from boa3.builtin.contract import abort
|
||||
from boa3.builtin.interop.runtime import check_witness
|
||||
from boa3.builtin.interop.storage import get, put, delete, find, FindOptions
|
||||
from boa3.builtin.type import UInt160
|
||||
|
||||
TOKEN_PREFIX = b'token_'
|
||||
OWNER_PREFIX = b'owner_'
|
||||
SUPPLY_KEY = b'totalSupply'
|
||||
ADMIN_KEY = b'admin'
|
||||
|
||||
@public
|
||||
def symbol() -> str:
|
||||
return "OFNFT" # OnlyFans NFT
|
||||
|
||||
@public
|
||||
def name() -> str:
|
||||
return "Web3OnlyFansNFT"
|
||||
|
||||
@public
|
||||
def totalSupply() -> int:
|
||||
return get(SUPPLY_KEY).to_int()
|
||||
|
||||
@public
|
||||
def balanceOf(owner: UInt160) -> int:
|
||||
return len(_getTokensOf(owner))
|
||||
|
||||
@public
|
||||
def ownerOf(token_id: str) -> UInt160:
|
||||
token_data = get(TOKEN_PREFIX + token_id.encode())
|
||||
if not token_data:
|
||||
abort()
|
||||
info = _deserialize_token(token_data)
|
||||
return info['owner']
|
||||
|
||||
@public
|
||||
def tokensOf(owner: UInt160) -> List[str]:
|
||||
return _getTokensOf(owner)
|
||||
|
||||
@public
|
||||
def properties(token_id: str) -> Dict[str, Any]:
|
||||
token_data = get(TOKEN_PREFIX + token_id.encode())
|
||||
if not token_data:
|
||||
abort()
|
||||
return _deserialize_token(token_data)
|
||||
|
||||
@public
|
||||
def transfer(to: UInt160, token_id: str, data: Any) -> bool:
|
||||
if len(to) != 20:
|
||||
abort()
|
||||
|
||||
token_info = _readToken(token_id)
|
||||
from_addr = token_info['owner']
|
||||
|
||||
if not check_witness(from_addr):
|
||||
abort()
|
||||
|
||||
if from_addr == to:
|
||||
return True
|
||||
|
||||
_removeTokenOf(from_addr, token_id)
|
||||
_addTokenOf(to, token_id)
|
||||
|
||||
token_info['owner'] = to
|
||||
put(TOKEN_PREFIX + token_id.encode(), _serialize_token(token_info))
|
||||
return True
|
||||
|
||||
@public
|
||||
def mint(token_id: str, name: str, blurred_ref: str, full_ref: str, price: int) -> bool:
|
||||
admin = get(ADMIN_KEY)
|
||||
if not admin or len(admin) != 20:
|
||||
abort()
|
||||
|
||||
if not check_witness(admin.to_uint160()):
|
||||
abort()
|
||||
|
||||
existing = get(TOKEN_PREFIX + token_id.encode())
|
||||
if existing:
|
||||
abort() # уже есть
|
||||
|
||||
owner = admin.to_uint160()
|
||||
|
||||
info = {
|
||||
"owner": owner,
|
||||
"name": name,
|
||||
"blurred_ref": blurred_ref,
|
||||
"full_ref": full_ref,
|
||||
"price": price
|
||||
}
|
||||
put(TOKEN_PREFIX + token_id.encode(), _serialize_token(info))
|
||||
|
||||
current_supply = totalSupply()
|
||||
put(SUPPLY_KEY, current_supply + 1)
|
||||
_addTokenOf(owner, token_id)
|
||||
|
||||
return True
|
||||
|
||||
@public
|
||||
def deploy(admin: UInt160):
|
||||
if totalSupply() != 0:
|
||||
return
|
||||
put(ADMIN_KEY, admin)
|
||||
|
||||
def _readToken(token_id: str) -> Dict[str, Any]:
|
||||
token_data = get(TOKEN_PREFIX + token_id.encode())
|
||||
if not token_data:
|
||||
abort()
|
||||
return _deserialize_token(token_data)
|
||||
|
||||
def _getTokensOf(owner: UInt160) -> List[str]:
|
||||
prefix = OWNER_PREFIX + owner
|
||||
result: List[str] = []
|
||||
iterator = find(prefix, options=FindOptions.KeysOnly)
|
||||
while iterator.next():
|
||||
key = iterator.value
|
||||
token_id = key[len(prefix):].decode()
|
||||
result.append(token_id)
|
||||
return result
|
||||
|
||||
def _addTokenOf(owner: UInt160, token_id: str):
|
||||
owner_key = OWNER_PREFIX + owner + token_id.encode()
|
||||
put(owner_key, b'1')
|
||||
|
||||
def _removeTokenOf(owner: UInt160, token_id: str):
|
||||
owner_key = OWNER_PREFIX + owner + token_id.encode()
|
||||
delete(owner_key)
|
||||
|
||||
def _serialize_token(token_info: Dict[str, Any]) -> bytes:
|
||||
from boa3.builtin import serialize
|
||||
return serialize(token_info)
|
||||
|
||||
def _deserialize_token(data: bytes) -> Dict[str, Any]:
|
||||
from boa3.builtin import deserialize
|
||||
return deserialize(data)
|
77
Web3-_--main/contracts/nep17/MyNeoToken.py
Normal file
77
Web3-_--main/contracts/nep17/MyNeoToken.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from typing import Any
|
||||
from boa3.builtin import public
|
||||
from boa3.builtin.contract import Nep17TransferEvent, abort
|
||||
from boa3.builtin.interop.contract import call_contract
|
||||
from boa3.builtin.interop.runtime import calling_script_hash, check_witness, executing_script_hash
|
||||
from boa3.builtin.interop.storage import get, put
|
||||
from boa3.builtin.type import UInt160
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Конфигурация токена
|
||||
# ------------------------------------------------------
|
||||
TOKEN_SYMBOL = 'MYNEO'
|
||||
TOKEN_DECIMALS = 0
|
||||
|
||||
TOTAL_SUPPLY_KEY = b'totalSupply'
|
||||
BALANCE_PREFIX = b'balance_'
|
||||
ADMIN_KEY = b'admin' # владелец смарт-контракта
|
||||
|
||||
on_transfer = Nep17TransferEvent
|
||||
|
||||
@public
|
||||
def symbol() -> str:
|
||||
return TOKEN_SYMBOL
|
||||
|
||||
@public
|
||||
def decimals() -> int:
|
||||
return TOKEN_DECIMALS
|
||||
|
||||
@public
|
||||
def totalSupply() -> int:
|
||||
return get(TOTAL_SUPPLY_KEY).to_int()
|
||||
|
||||
@public
|
||||
def balanceOf(account: UInt160) -> int:
|
||||
return get(BALANCE_PREFIX + account).to_int()
|
||||
|
||||
@public
|
||||
def transfer(from_addr: UInt160, to_addr: UInt160, amount: int, data: Any) -> bool:
|
||||
if amount < 0:
|
||||
abort()
|
||||
|
||||
if not check_witness(from_addr):
|
||||
abort()
|
||||
|
||||
if len(to_addr) != 20:
|
||||
abort()
|
||||
|
||||
from_balance = balanceOf(from_addr)
|
||||
if from_balance < amount:
|
||||
abort()
|
||||
|
||||
if from_addr != to_addr and amount != 0:
|
||||
put(BALANCE_PREFIX + from_addr, from_balance - amount)
|
||||
to_balance = balanceOf(to_addr)
|
||||
put(BALANCE_PREFIX + to_addr, to_balance + amount)
|
||||
|
||||
on_transfer(from_addr, to_addr, amount)
|
||||
|
||||
if data is not None:
|
||||
# Вызываем onNEP17Payment, если у целевого контракта есть такой метод
|
||||
call_contract(to_addr, 'onNEP17Payment', [from_addr, amount, data])
|
||||
|
||||
return True
|
||||
|
||||
@public
|
||||
def deploy(admin: UInt160):
|
||||
"""
|
||||
При первом деплое: устанавливаем админа и минтим 100_000_000 токенов.
|
||||
"""
|
||||
if totalSupply() != 0:
|
||||
return # уже инициализировано
|
||||
|
||||
put(ADMIN_KEY, admin)
|
||||
minted = 100_000_000
|
||||
put(TOTAL_SUPPLY_KEY, minted)
|
||||
put(BALANCE_PREFIX + admin, minted)
|
||||
on_transfer(None, admin, minted)
|
18
Web3-_--main/services/api/Dockerfile
Normal file
18
Web3-_--main/services/api/Dockerfile
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Dockerfile для API-сервиса
|
||||
FROM golang:1.20 as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/api main.go
|
||||
|
||||
# Финальный образ
|
||||
FROM alpine:3.17
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/bin/api /app/api
|
||||
COPY config.yaml /app/config.yaml
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/app/api"]
|
10
Web3-_--main/services/api/config.yaml
Normal file
10
Web3-_--main/services/api/config.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
listenAddr: ":8080"
|
||||
logLevel: "debug"
|
||||
|
||||
neoRPC: "http://localhost:20332"
|
||||
walletPath: "/app/wallets/dev.wallet.json"
|
||||
walletPass: "dev-pass"
|
||||
|
||||
nftContractHash: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
marketContractHash: "cccccccccccccccccccccccccccccccccccccccc"
|
||||
tokenContractHash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
9
Web3-_--main/services/api/go.mod
Normal file
9
Web3-_--main/services/api/go.mod
Normal file
|
@ -0,0 +1,9 @@
|
|||
module web3-onlyfans/services/api
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/nspcc-dev/neo-go v0.99.0
|
||||
# ...
|
||||
)
|
|
@ -0,0 +1,81 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"web3-onlyfans/services/api/internal/neo"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"web3-onlyfans/services/api/internal/utils"
|
||||
)
|
||||
|
||||
func MarketListHandler(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient, logger utils.Logger, cfg *utils.Config) {
|
||||
marketHash, err := util.Uint160DecodeStringLE(cfg.MarketContractHash)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid market hash", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
invRes, err := neoCli.Actor.Reader().InvokeCall(marketHash, "List", nil, nil)
|
||||
if err != nil {
|
||||
logger.Errorf("invoke List error: %v", err)
|
||||
http.Error(w, "market list error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tokenList, err := utils.DecodeStringArray(invRes.Stack[0])
|
||||
if err != nil {
|
||||
logger.Errorf("decode array error: %v", err)
|
||||
http.Error(w, "decode array error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"tokens": tokenList,
|
||||
})
|
||||
}
|
||||
|
||||
type MarketBuyRequest struct {
|
||||
TokenID string `json:"token_id"`
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
|
||||
func MarketBuyHandler(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient, logger utils.Logger, cfg *utils.Config) {
|
||||
var req MarketBuyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tokenHash, err := util.Uint160DecodeStringLE(cfg.TokenContractHash)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid token contract hash", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Вызываем transfer NEP-17: (from=Actor, to=market, amount=req.Amount, data=req.TokenID)
|
||||
dataBytes := []byte(req.TokenID)
|
||||
amt := big.NewInt(req.Amount)
|
||||
txHash, err := neoCli.Actor.Call(tokenHash, "transfer", []any{
|
||||
neoCli.Actor.Sender(), // from
|
||||
cfg.MarketContractHash, // to
|
||||
amt, // amount
|
||||
dataBytes, // data
|
||||
}, nil)
|
||||
if err != nil {
|
||||
logger.Errorf("market buy transfer error: %v", err)
|
||||
http.Error(w, "market buy error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"txHash": txHash.StringBE(),
|
||||
"tokenID": req.TokenID,
|
||||
"amount": req.Amount,
|
||||
})
|
||||
}
|
86
Web3-_--main/services/api/internal/handlers/nft_handler.go
Normal file
86
Web3-_--main/services/api/internal/handlers/nft_handler.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"net/http"
|
||||
"web3-onlyfans/services/api/internal/neo"
|
||||
"web3-onlyfans/services/api/internal/utils"
|
||||
)
|
||||
|
||||
type MintNFTRequest struct {
|
||||
TokenID string `json:"token_id"`
|
||||
Name string `json:"name"`
|
||||
BlurredRef string `json:"blurred_ref"`
|
||||
FullRef string `json:"full_ref"`
|
||||
Price int `json:"price"`
|
||||
}
|
||||
|
||||
func MintNFTHandler(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient, logger utils.Logger, cfg *utils.Config) {
|
||||
var req MintNFTRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
nftHash, err := util.Uint160DecodeStringLE(cfg.NftContractHash)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid NFT hash", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
txHash, err := neoCli.Actor.Call(nftHash, "mint", []any{
|
||||
req.TokenID,
|
||||
req.Name,
|
||||
req.BlurredRef,
|
||||
req.FullRef,
|
||||
req.Price,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
logger.Errorf("mint call error: %v", err)
|
||||
http.Error(w, "mint call error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"txHash": txHash.StringBE(),
|
||||
"tokenID": req.TokenID,
|
||||
})
|
||||
}
|
||||
|
||||
func NFTPropertiesHandler(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient, logger utils.Logger, cfg *utils.Config) {
|
||||
tokenID := r.URL.Query().Get("token_id")
|
||||
if tokenID == "" {
|
||||
http.Error(w, "missing token_id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
nftHash, err := util.Uint160DecodeStringLE(cfg.NftContractHash)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid NFT hash", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
invRes, err := neoCli.Actor.Reader().InvokeCall(nftHash, "properties", []any{tokenID}, nil)
|
||||
if err != nil {
|
||||
logger.Errorf("invoke call error: %v", err)
|
||||
http.Error(w, "invoke call error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Разбираем stackitem.Map
|
||||
propsMap, err := utils.DecodeStringMap(invRes.Stack[0])
|
||||
if err != nil {
|
||||
logger.Errorf("decode error: %v", err)
|
||||
http.Error(w, "decode error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"props": propsMap,
|
||||
})
|
||||
}
|
49
Web3-_--main/services/api/internal/handlers/token_handler.go
Normal file
49
Web3-_--main/services/api/internal/handlers/token_handler.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"web3-onlyfans/services/api/internal/neo"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"web3-onlyfans/services/api/internal/utils"
|
||||
)
|
||||
|
||||
func TokenBalanceHandler(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient, logger utils.Logger, cfg *utils.Config) {
|
||||
accountParam := r.URL.Query().Get("account")
|
||||
if accountParam == "" {
|
||||
accountParam = neoCli.Actor.Sender().StringLE()
|
||||
}
|
||||
|
||||
tokenHash, err := util.Uint160DecodeStringLE(cfg.TokenContractHash)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid token hash", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
accHash, err := util.Uint160DecodeStringLE(accountParam)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid account param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
invRes, err := neoCli.Actor.Reader().InvokeCall(tokenHash, "balanceOf", []any{accHash}, nil)
|
||||
if err != nil {
|
||||
logger.Errorf("balanceOf error: %v", err)
|
||||
http.Error(w, "balanceOf error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
balanceI, err := utils.DecodeBigInteger(invRes.Stack[0])
|
||||
if err != nil {
|
||||
http.Error(w, "decode balance error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"account": accountParam,
|
||||
"balance": balanceI.String(),
|
||||
})
|
||||
}
|
6
Web3-_--main/services/api/internal/models/market.go
Normal file
6
Web3-_--main/services/api/internal/models/market.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package models
|
||||
|
||||
type MarketItem struct {
|
||||
TokenID string `json:"token_id"`
|
||||
OnSale bool `json:"on_sale"`
|
||||
}
|
9
Web3-_--main/services/api/internal/models/nft.go
Normal file
9
Web3-_--main/services/api/internal/models/nft.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type NFTProperties struct {
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
BlurredRef string `json:"blurred_ref"`
|
||||
FullRef string `json:"full_ref"`
|
||||
Price int `json:"price"`
|
||||
}
|
44
Web3-_--main/services/api/internal/neo/neo_client.go
Normal file
44
Web3-_--main/services/api/internal/neo/neo_client.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package neo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
)
|
||||
|
||||
type NeoClient struct {
|
||||
Actor *actor.Actor
|
||||
RPC *rpcclient.Client
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
func NewNeoClient(rpcEndpoint, walletPath, walletPass string) (*NeoClient, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
rpcCli, err := rpcclient.New(ctx, rpcEndpoint, rpcclient.Options{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rpcclient new: %w", err)
|
||||
}
|
||||
w, err := wallet.NewWalletFromFile(walletPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wallet load: %w", err)
|
||||
}
|
||||
acc := w.GetAccount(w.GetChangeAddress())
|
||||
err = acc.Decrypt(walletPass, w.Scrypt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wallet decrypt: %w", err)
|
||||
}
|
||||
act, err := actor.NewSimple(rpcCli, acc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("actor new: %w", err)
|
||||
}
|
||||
|
||||
return &NeoClient{
|
||||
Actor: act,
|
||||
RPC: rpcCli,
|
||||
Context: ctx,
|
||||
}, nil
|
||||
}
|
36
Web3-_--main/services/api/internal/utils/config.go
Normal file
36
Web3-_--main/services/api/internal/utils/config.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ListenAddr string `yaml:"listenAddr"`
|
||||
LogLevel string `yaml:"logLevel"`
|
||||
NeoRPC string `yaml:"neoRPC"`
|
||||
WalletPath string `yaml:"walletPath"`
|
||||
WalletPass string `yaml:"walletPass"`
|
||||
NftContractHash string `yaml:"nftContractHash"`
|
||||
MarketContractHash string `yaml:"marketContractHash"`
|
||||
TokenContractHash string `yaml:"tokenContractHash"`
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(content, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Можно переопределить через ENV
|
||||
if envListen := os.Getenv("API_LISTEN_ADDR"); envListen != "" {
|
||||
cfg.ListenAddr = envListen
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
38
Web3-_--main/services/api/internal/utils/logger.go
Normal file
38
Web3-_--main/services/api/internal/utils/logger.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Debugf(format string, v ...interface{})
|
||||
Infof(format string, v ...interface{})
|
||||
Errorf(format string, v ...interface{})
|
||||
Fatalf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
type SimpleLogger struct {
|
||||
level string
|
||||
}
|
||||
|
||||
func NewLogger(level string) Logger {
|
||||
return &SimpleLogger{level: strings.ToLower(level)}
|
||||
}
|
||||
|
||||
func (l *SimpleLogger) Debugf(format string, v ...interface{}) {
|
||||
if l.level == "debug" {
|
||||
log.Printf("[DEBUG] "+format, v...)
|
||||
}
|
||||
}
|
||||
func (l *SimpleLogger) Infof(format string, v ...interface{}) {
|
||||
if l.level == "debug" || l.level == "info" {
|
||||
log.Printf("[INFO] "+format, v...)
|
||||
}
|
||||
}
|
||||
func (l *SimpleLogger) Errorf(format string, v ...interface{}) {
|
||||
log.Printf("[ERROR] "+format, v...)
|
||||
}
|
||||
func (l *SimpleLogger) Fatalf(format string, v ...interface{}) {
|
||||
log.Fatalf("[FATAL] "+format, v...)
|
||||
}
|
15
Web3-_--main/services/api/internal/wallet/wallet.go
Normal file
15
Web3-_--main/services/api/internal/wallet/wallet.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package wallet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
)
|
||||
|
||||
func CreateNewWallet(path, password string) error {
|
||||
w, err := wallet.NewWallet(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wallet new: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
66
Web3-_--main/services/api/main.go
Normal file
66
Web3-_--main/services/api/main.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"web3-onlyfans/services/api/internal/handlers"
|
||||
"web3-onlyfans/services/api/internal/neo"
|
||||
"web3-onlyfans/services/api/internal/utils"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Загружаем конфиг
|
||||
configPath := os.Getenv("API_CONFIG_PATH")
|
||||
if configPath == "" {
|
||||
configPath = "./config.yaml"
|
||||
}
|
||||
cfg, err := utils.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Инициализируем логгер
|
||||
logger := utils.NewLogger(cfg.LogLevel)
|
||||
|
||||
// Инициализируем NeoClient (RPC + кошелёк)
|
||||
neoCli, err := neo.NewNeoClient(cfg.NeoRPC, cfg.WalletPath, cfg.WalletPass)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init neo client: %v", err)
|
||||
}
|
||||
|
||||
// Роуты
|
||||
r := mux.NewRouter()
|
||||
// NFT endpoints
|
||||
r.HandleFunc("/nft/mint", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers.MintNFTHandler(w, r, neoCli, logger, cfg)
|
||||
}).Methods("POST")
|
||||
|
||||
r.HandleFunc("/nft/properties", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers.NFTPropertiesHandler(w, r, neoCli, logger, cfg)
|
||||
}).Methods("GET")
|
||||
|
||||
// Market endpoints
|
||||
r.HandleFunc("/market/list", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers.MarketListHandler(w, r, neoCli, logger, cfg)
|
||||
}).Methods("GET")
|
||||
|
||||
r.HandleFunc("/market/buy", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers.MarketBuyHandler(w, r, neoCli, logger, cfg)
|
||||
}).Methods("POST")
|
||||
|
||||
// Token endpoints (например, посмотреть баланс)
|
||||
r.HandleFunc("/token/balance", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers.TokenBalanceHandler(w, r, neoCli, logger, cfg)
|
||||
}).Methods("GET")
|
||||
|
||||
// Запуск сервера
|
||||
addr := cfg.ListenAddr
|
||||
logger.Infof("API starting on %s", addr)
|
||||
if err := http.ListenAndServe(addr, r); err != nil {
|
||||
logger.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
5
Web3-_--main/services/api/scripts/local_run.sh
Normal file
5
Web3-_--main/services/api/scripts/local_run.sh
Normal file
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# Локальный запуск API
|
||||
|
||||
export API_CONFIG_PATH=./config.yaml
|
||||
go run main.go
|
14
Web3-_--main/services/frostfs-uploader/Dockerfile
Normal file
14
Web3-_--main/services/frostfs-uploader/Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
FROM golang:1.20 as builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/uploader main.go
|
||||
|
||||
FROM alpine:3.17
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/bin/uploader /app/uploader
|
||||
COPY config.yaml /app/config.yaml
|
||||
EXPOSE 8081
|
||||
ENTRYPOINT ["/app/uploader"]
|
5
Web3-_--main/services/frostfs-uploader/config.yaml
Normal file
5
Web3-_--main/services/frostfs-uploader/config.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
listenAddr: ":8081"
|
||||
logLevel: "info"
|
||||
frostfsEndpoint: "grpcs://localhost:8080"
|
||||
frostfsPrivKey: "0000000000000000000000000000000..."
|
||||
frostfsContainer: "containerID-here"
|
9
Web3-_--main/services/frostfs-uploader/go.mod
Normal file
9
Web3-_--main/services/frostfs-uploader/go.mod
Normal file
|
@ -0,0 +1,9 @@
|
|||
module web3-onlyfans/services/frostfs-uploader
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/nspcc-dev/neofs-sdk-go v1.2.1
|
||||
# ...
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
package frostuploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/nspcc-dev/neofs-sdk-go/client"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/client/object"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||
)
|
||||
|
||||
func UploadFile(ctx context.Context, cli *client.Client, cntr container.ID, data []byte) (string, error) {
|
||||
obj := object.New()
|
||||
obj.SetPayload(data)
|
||||
|
||||
writer, err := cli.ObjectPutInit(ctx, cntr, obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := writer.Write(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
oid, err := writer.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return oid.String(), nil
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/nspcc-dev/neofs-sdk-go/client"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||
"web3-onlyfans/services/frostfs-uploader/internal/frostuploader"
|
||||
"web3-onlyfans/services/frostfs-uploader/internal/utils"
|
||||
)
|
||||
|
||||
func UploadHandler(w http.ResponseWriter, r *http.Request, cfg *utils.Config, logger utils.Logger) {
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "no file found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, "read file error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Подключаемся к FrostFS
|
||||
cli, err := client.New(client.WithDefaultPrivateKeyStr(cfg.FrostfsPrivKey))
|
||||
if err != nil {
|
||||
logger.Errorf("client init: %v", err)
|
||||
http.Error(w, "FrostFS init error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
if err = cli.Dial(cfg.FrostfsEndpoint); err != nil {
|
||||
logger.Errorf("dial error: %v", err)
|
||||
http.Error(w, "FrostFS dial error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cntr, err := container.IDFromString(cfg.FrostfsContainer)
|
||||
if err != nil {
|
||||
logger.Errorf("invalid container: %v", err)
|
||||
http.Error(w, "invalid container", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
oid, err := frostuploader.UploadFile(context.Background(), cli, cntr, data)
|
||||
if err != nil {
|
||||
logger.Errorf("upload error: %v", err)
|
||||
http.Error(w, "upload error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"status": "ok",
|
||||
"object_id": oid,
|
||||
"filename": header.Filename,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
32
Web3-_--main/services/frostfs-uploader/main.go
Normal file
32
Web3-_--main/services/frostfs-uploader/main.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"web3-onlyfans/services/frostfs-uploader/internal/utils"
|
||||
"web3-onlyfans/services/frostfs-uploader/internal/handlers"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfgPath := os.Getenv("FROSTFS_UPLOADER_CONFIG")
|
||||
if cfgPath == "" {
|
||||
cfgPath = "./config.yaml"
|
||||
}
|
||||
cfg, err := utils.LoadConfig(cfgPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
logger := utils.NewLogger(cfg.LogLevel)
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers.UploadHandler(w, r, cfg, logger)
|
||||
}).Methods("POST")
|
||||
|
||||
addr := cfg.ListenAddr
|
||||
logger.Infof("frostfs-uploader on %s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, r))
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
export FROSTFS_UPLOADER_CONFIG=./config.yaml
|
||||
go run main.go
|
14
Web3-_--main/services/indexer/Dockerfile
Normal file
14
Web3-_--main/services/indexer/Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
FROM golang:1.20 as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/indexer main.go
|
||||
|
||||
FROM alpine:3.17
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/bin/indexer /app/indexer
|
||||
COPY config.yaml /app/config.yaml
|
||||
ENTRYPOINT ["/app/indexer"]
|
4
Web3-_--main/services/indexer/config.yaml
Normal file
4
Web3-_--main/services/indexer/config.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
neoRPC: "http://localhost:20332"
|
||||
logLevel: "info"
|
||||
dbDsn: "postgres://indexer:password@postgres:5432/indexerdb?sslmode=disable"
|
||||
pollIntervalSeconds: 3
|
1
Web3-_--main/services/indexer/go.sum.go
Normal file
1
Web3-_--main/services/indexer/go.sum.go
Normal file
|
@ -0,0 +1 @@
|
|||
package indexer
|
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
height BIGINT PRIMARY KEY,
|
||||
time TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nft_transfers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
block_height BIGINT NOT NULL,
|
||||
tx_hash VARCHAR(66) NOT NULL,
|
||||
token_id TEXT NOT NULL,
|
||||
from_addr VARCHAR(42),
|
||||
to_addr VARCHAR(42),
|
||||
timestamp TIMESTAMP NOT NULL
|
||||
);
|
43
Web3-_--main/services/indexer/internal/db/repository.go
Normal file
43
Web3-_--main/services/indexer/internal/db/repository.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewRepository(dsn string) (*Repository, error) {
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Repository{db: db}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetLastIndexedBlock() (int64, error) {
|
||||
var height int64
|
||||
err := r.db.QueryRow(`SELECT height FROM blocks ORDER BY height DESC LIMIT 1`).Scan(&height)
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, nil
|
||||
}
|
||||
return height, err
|
||||
}
|
||||
|
||||
func (r *Repository) SaveBlock(height int64) error {
|
||||
_, err := r.db.Exec(`INSERT INTO blocks(height,time) VALUES($1,NOW())`, height)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) SaveNFTTransfer(txHash string, blockHeight int64, tokenId, from, to string) error {
|
||||
_, err := r.db.Exec(`INSERT INTO nft_transfers(block_height, tx_hash, token_id, from_addr, to_addr, timestamp)
|
||||
VALUES($1, $2, $3, $4, $5, NOW())`,
|
||||
blockHeight, txHash, tokenId, from, to)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package models
|
||||
|
||||
type BlockRecord struct {
|
||||
Height int64 `json:"height"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
type NftTransferRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
BlockHeight int64 `json:"block_height"`
|
||||
TxHash string `json:"tx_hash"`
|
||||
TokenId string `json:"token_id"`
|
||||
FromAddr string `json:"from_addr"`
|
||||
ToAddr string `json:"to_addr"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
51
Web3-_--main/services/indexer/internal/subscriber/events.go
Normal file
51
Web3-_--main/services/indexer/internal/subscriber/events.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package subscriber
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"web3-onlyfans/services/indexer/internal/utils"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||
)
|
||||
|
||||
type BlockSubscriber struct {
|
||||
cfg *utils.Config
|
||||
logger utils.Logger
|
||||
rpc *rpcclient.Client
|
||||
}
|
||||
|
||||
func NewBlockSubscriber(cfg *utils.Config, logger utils.Logger) (*BlockSubscriber, error) {
|
||||
cli, err := rpcclient.New(context.Background(), cfg.NeoRPC, rpcclient.Options{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BlockSubscriber{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
rpc: cli,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BlockSubscriber) Start() {
|
||||
pollInterval := time.Duration(s.cfg.PollIntervalSeconds) * time.Second
|
||||
for {
|
||||
err := s.pollOnce()
|
||||
if err != nil {
|
||||
s.logger.Errorf("poll error: %v", err)
|
||||
}
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BlockSubscriber) pollOnce() error {
|
||||
// Здесь логика: узнаём текущий блок, сверяемся с локальным бд,
|
||||
// проходимся по новым блокам, анализируем транзакции/нотификации.
|
||||
|
||||
height, err := s.rpc.GetBlockCount()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Debugf("current block count: %d", height)
|
||||
// ... далее - обработка новых блоков (Tx, Notifications, Transfers).
|
||||
return nil
|
||||
}
|
36
Web3-_--main/services/indexer/internal/utils/config.go
Normal file
36
Web3-_--main/services/indexer/internal/utils/config.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
NeoRPC string `yaml:"neoRPC"`
|
||||
LogLevel string `yaml:"logLevel"`
|
||||
DBDsn string `yaml:"dbDsn"`
|
||||
PollIntervalSeconds int `yaml:"pollIntervalSeconds"`
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c Config
|
||||
if err := yaml.Unmarshal(data, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Переопределения через ENV
|
||||
if e := os.Getenv("INDEXER_POLL_INTERVAL"); e != "" {
|
||||
if val, err := strconv.Atoi(e); err == nil {
|
||||
c.PollIntervalSeconds = val
|
||||
}
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
23
Web3-_--main/services/indexer/internal/utils/logger.go
Normal file
23
Web3-_--main/services/indexer/internal/utils/logger.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Debugf(format string, v ...interface{})
|
||||
Infof(format string, v ...interface{})
|
||||
Errorf(format string, v ...interface{})
|
||||
Fatalf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
type SimpleLogger struct {
|
||||
level string
|
||||
}
|
||||
|
||||
func NewLogger(level string) Logger {
|
||||
return &SimpleLogger{level: strings.ToLower(level)}
|
||||
}
|
||||
|
||||
// ... тот же код, что в API
|
23
Web3-_--main/services/indexer/main.go
Normal file
23
Web3-_--main/services/indexer/main.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"web3-onlyfans/services/indexer/internal/subscriber"
|
||||
"web3-onlyfans/services/indexer/internal/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := utils.LoadConfig("./config.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
logger := utils.NewLogger(cfg.LogLevel)
|
||||
|
||||
sub, err := subscriber.NewBlockSubscriber(cfg, logger)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to create subscriber: %v", err)
|
||||
}
|
||||
|
||||
logger.Infof("Indexer started, polling blocks from %s", cfg.NeoRPC)
|
||||
sub.Start()
|
||||
}
|
4
Web3-_--main/services/indexer/scripts/local_run.sh
Normal file
4
Web3-_--main/services/indexer/scripts/local_run.sh
Normal file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
export INDEXER_CONFIG_PATH=./config.yaml
|
||||
go run main.go
|
Loading…
Add table
Reference in a new issue