Add files via upload

This commit is contained in:
P4vlushaaa 2025-01-23 13:06:36 +03:00 committed by GitHub
parent cc5617974d
commit 4fad092e18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 684 additions and 0 deletions

118
contracts/market/Market.go Normal file
View 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)
}

View 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)

View 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)

75
docker-compose.yml Normal file
View file

@ -0,0 +1,75 @@
version: "3.8"
services:
# NEO приватная нода для локальной разработки
neo-node:
image: nspccdev/neo-go
container_name: neo-node
command: [
"node",
"--network=priv",
"--rpc",
"--rpc-port=20332",
"--wallet", "/neo-go/configs/wallet.json",
"--unlock", "address_in_wallet",
"--wallet-password", "pass"
]
ports:
- "20332:20332"
volumes:
- ./neo-config:/neo-go/configs
# FrostFS (локальный dev-кластер или удаленный endpoint)
# Для примера укажем некий самопальный образ,
# в реальности нужна полноценная конфигурация.
frostfs:
image: yourorg/frostfs-dev
container_name: frostfs
ports:
- "8080:8080"
environment:
- SOME_ENV=...
# ...
# Сервис для загрузки в FrostFS
frostfs-uploader:
build:
context: ./services/frostfs-uploader
container_name: frostfs-uploader
environment:
- FROSTFS_ENDPOINT=grpcs://frostfs:8080
- FROSTFS_PRIVKEY=... # dev key
- FROSTFS_CONTAINER_ID=... # dev container ID
ports:
- "8081:8081"
depends_on:
- frostfs
# API-сервис
api:
build:
context: ./services/api
container_name: onlyfans-api
environment:
- NEO_RPC_ENDPOINT=http://neo-node:20332
- NEO_WALLET_PATH=/app/wallets/dev.wallet.json
- NEO_WALLET_PASS=somepass
- NFT_CONTRACT_HASH=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
- MARKET_CONTRACT_HASH=cccccccccccccccccccccccccccccccccccccccc
volumes:
- ./services/api/wallets:/app/wallets
ports:
- "8080:8080"
depends_on:
- neo-node
# Фронтенд
frontend:
build:
context: ./frontend
container_name: onlyfans-frontend
ports:
- "3000:3000"
depends_on:
- api
- frostfs-uploader

View file

@ -0,0 +1,95 @@
package handlers
import (
"encoding/json"
"math/big"
"net/http"
"os"
"myproject/services/api/internal/neo"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// Пример: /nft/mint?token_id=myPost1&blurred=OID1&full=OID2&price=100
func HandleMintNFT(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient) {
// Получаем параметры
tokenId := r.URL.Query().Get("token_id")
blurred := r.URL.Query().Get("blurred")
full := r.URL.Query().Get("full")
priceStr := r.URL.Query().Get("price")
if tokenId == "" || blurred == "" || full == "" {
http.Error(w, "missing params", http.StatusBadRequest)
return
}
// Конвертируем price
// (в примере NEP-17 без decimals, так что не умножаем на 10^дробность)
price := big.NewInt(0)
price.SetString(priceStr, 10)
// Хэш NFT-контракта из ENV
nftHashStr := os.Getenv("NFT_CONTRACT_HASH")
nftHash, err := util.Uint160DecodeStringLE(nftHashStr)
if err != nil {
http.Error(w, "invalid NFT hash", http.StatusInternalServerError)
return
}
// Вызываем метод "mint"
txHash, err := neoCli.Actor.Call(nftHash, "mint", []any{
tokenId, // str
"My NFT", // name (можно отдельно передавать)
blurred, // blurred_ref
full, // full_ref
price.Int64(),
}, nil)
if err != nil {
http.Error(w, "mint call error: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"tx_hash": txHash.StringBE(),
})
}
func HandleListMarket(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient) {
marketHashStr := os.Getenv("MARKET_CONTRACT_HASH")
marketHash, err := util.Uint160DecodeStringLE(marketHashStr)
if err != nil {
http.Error(w, "invalid MARKET hash", http.StatusInternalServerError)
return
}
// Invoke без отправки транзакции (чтение из стейта)
res, err := neoCli.Actor.Reader().InvokeCall(marketHash, "List", []any{}, nil)
if err != nil {
http.Error(w, "List call error: "+err.Error(), http.StatusInternalServerError)
return
}
// res.Value — stackitem
// Для простоты декодируем
tokenIDs, err := res.Stack[0].TryArray()
if err != nil {
http.Error(w, "decoding array error: "+err.Error(), http.StatusInternalServerError)
return
}
list := []string{}
for _, t := range tokenIDs {
bs, err := t.TryBytes()
if err == nil {
list = append(list, string(bs))
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"onSale": list,
})
}

View file

@ -0,0 +1,47 @@
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
}

38
services/api/main.go Normal file
View file

@ -0,0 +1,38 @@
package main
import (
"log"
"net/http"
"os"
"myproject/services/api/internal/handlers"
"myproject/services/api/internal/neo"
"github.com/gorilla/mux"
)
func main() {
rpcEndpoint := os.Getenv("NEO_RPC_ENDPOINT") // "http://localhost:20332"
walletPath := os.Getenv("NEO_WALLET_PATH")
walletPass := os.Getenv("NEO_WALLET_PASS")
// Инициализируем клиент NEO
neoCli, err := neo.NewNeoClient(rpcEndpoint, walletPath, walletPass)
if err != nil {
log.Fatalf("failed to init neo client: %v", err)
}
r := mux.NewRouter()
r.HandleFunc("/nft/mint", func(w http.ResponseWriter, r *http.Request) {
handlers.HandleMintNFT(w, r, neoCli)
}).Methods("POST")
r.HandleFunc("/nft/market-list", func(w http.ResponseWriter, r *http.Request) {
handlers.HandleListMarket(w, r, neoCli)
}).Methods("GET")
// и т.д.
log.Println("Starting API on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}

View file

@ -0,0 +1,99 @@
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
"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"
// ...
)
var (
frostfsEndpoint = os.Getenv("FROSTFS_ENDPOINT") // типа "grpcs://localhost:8080"
frostfsPrivKey = os.Getenv("FROSTFS_PRIVKEY")
frostfsContainer = os.Getenv("FROSTFS_CONTAINER_ID")
)
func main() {
// Инициализация
r := mux.NewRouter()
r.HandleFunc("/upload", handleUpload).Methods("POST")
// HTTP-сервер
addr := ":8081"
log.Println("Starting FrostFS uploader on", addr)
log.Fatal(http.ListenAndServe(addr, r))
}
func handleUpload(w http.ResponseWriter, r *http.Request) {
// Пример: берем файл из multipart/form-data
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "file error: "+err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
// Читаем в []byte (для простоты, но лучше стримить)
data, err := io.ReadAll(file)
if err != nil {
http.Error(w, "read error: "+err.Error(), http.StatusInternalServerError)
return
}
// Подключаемся к FrostFS
cli, err := client.New(client.WithDefaultPrivateKeyStr(frostfsPrivKey))
if err != nil {
http.Error(w, "client init error: "+err.Error(), http.StatusInternalServerError)
return
}
defer cli.Close()
err = cli.Dial(frostfsEndpoint)
if err != nil {
http.Error(w, "dial error: "+err.Error(), http.StatusInternalServerError)
return
}
cntr, err := container.IDFromString(frostfsContainer)
if err != nil {
http.Error(w, "invalid container: "+err.Error(), http.StatusInternalServerError)
return
}
oid, err := uploadFileToFrostFS(r.Context(), cli, cntr, data)
if err != nil {
http.Error(w, "frostfs upload error: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"ok","object_id":"%s","filename":"%s"}`, oid, header.Filename)
}
func uploadFileToFrostFS(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
}
err = writer.Write(data)
if err != nil {
return "", err
}
oid, err := writer.Close()
if err != nil {
return "", err
}
return oid.String(), nil
}