WinPredictor/contract.go

219 lines
6.3 KiB
Go

package smart_contract_bets
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/interop/lib/address"
"github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
"github.com/nspcc-dev/neo-go/pkg/interop/native/oracle"
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
// url для оракула:
const dataURLKey = "dataURL"
// контракт, принимает ставки на одно событие:
const eventIndexValue = 1
// значения, получаемые оракулом: 0 (событие не сыграно), 1 (победа первого игрока), 2 (победа второго игрока):
const winnerKey = "winner"
// префикс имён ключей для сохранения в storage ставок игроков
const playerKeyPrefix = "bet_"
type Bet struct {
// величина ставки
amount int
// сторона, на которую сделана ставка (в событии участвует 2 стороны)
winner int
// флаг, обозначающий что выигрыш получен
payed bool
}
func _deploy(data interface{}, isUpdate bool) {
if isUpdate {
return
}
ctx := storage.GetContext()
dataURL := "https://my-json-server.typicode.com/okonstantinova/smart-contract-bets/events/"
storage.Put(ctx, dataURLKey, dataURL)
}
func PlaceBet(amount int, winner int, player interop.Hash160) {
if !runtime.CheckWitness(player) {
runtime.Log("Player is not the same as the caller")
return
}
if amount <= 0 {
runtime.Log("Invalid amount value")
return
}
if winner != 1 && winner != 2 {
runtime.Log("Invalid winner value")
return
}
balance := gas.BalanceOf(player)
if balance < amount {
runtime.Log("Not enough gas on balance")
return
}
ctx := storage.GetContext()
if storage.Get(ctx, winnerKey) != nil {
runtime.Log("The event has played, cannot place bet")
return
}
if storage.Get(ctx, player) != nil {
runtime.Log("Player has already placed bet")
return
}
contractHash := runtime.GetExecutingScriptHash()
result := gas.Transfer(player, contractHash, amount, nil)
if !result {
runtime.Log("Transfer operation failed")
return
}
bet := Bet{
amount: amount,
winner: winner,
payed: false,
}
playerKey := playerKeyPrefix + address.FromHash160(player)
storage.Put(ctx, playerKey, std.Serialize(bet))
runtime.Log("Player " + address.FromHash160(player) + " placed " + std.Itoa10(amount) +
" gas bet on side " + std.Itoa10(winner))
}
// OnNEP17Payment вызывается как callback при успешном переводе газа на счёт контракта
func OnNEP17Payment(from interop.Hash160, amount int, data any) {
contractHash := runtime.GetExecutingScriptHash()
balance := gas.BalanceOf(contractHash)
runtime.Log("Contract's balance changed and has " + std.Itoa10(balance) + " gas")
}
func CheckEventStatus() {
ctx := storage.GetContext()
if storage.Get(ctx, winnerKey) != nil {
runtime.Log("The event has played, no action required anymore")
return
}
dataURL := storage.Get(ctx, dataURLKey).(string) + std.Itoa10(eventIndexValue)
oracle.Request(dataURL, nil, "oracleCallback", nil, 2*oracle.MinimumResponseGas)
runtime.Log("Oracle requested to check " + dataURL)
}
func OracleCallback(url string, userData any, code int, result []byte) {
runtime.Log("Result " + string(result))
callingHash := runtime.GetCallingScriptHash()
if !callingHash.Equals(oracle.Hash) {
panic("Not called from the oracle contract")
}
if code != oracle.Success {
panic("Request failed for " + url + " with code " + std.Itoa10(code))
}
ctx := storage.GetContext()
if storage.Get(ctx, winnerKey) != nil {
panic("Event status change is already processed")
}
data := std.JSONDeserialize(result).(map[string]any)
winner := data["winner"].(int)
if winner == 0 {
runtime.Log("Oracle reports that the event has not played yet")
return
}
if winner != 1 && winner != 2 {
panic("Invalid winner value")
}
storage.Put(ctx, winnerKey, winner)
runtime.Log("The event winner is set to be " + std.Itoa10(winner))
}
func ClaimWin(player interop.Hash160) bool {
if !runtime.CheckWitness(player) {
panic("Player is not the same as the caller")
}
ctx := storage.GetContext()
winnerValue := storage.Get(ctx, winnerKey)
if winnerValue == nil {
panic("The event has not played yet")
}
winner := winnerValue.(int)
playerKey := playerKeyPrefix + address.FromHash160(player)
data := storage.Get(ctx, playerKey)
if data == nil {
panic("Player did not place bet")
}
bet := std.Deserialize(data.([]byte)).(Bet)
if bet.payed {
runtime.Log("Player has already claimed win")
return false
}
if bet.winner != winner {
runtime.Log("The bet has not won")
return false
}
win := CalculateWin(player, bet)
contractHash := runtime.GetExecutingScriptHash()
result := gas.Transfer(contractHash, player, win, nil)
if !result {
panic("Transfer operation failed")
}
bet.payed = true
storage.Put(ctx, playerKey, std.Serialize(bet))
runtime.Log("Player " + address.FromHash160(player) + " claimed their win and received " +
std.Itoa10(win) + " gas")
return true
}
// CalculateWin рассчитывает выигрыш для игрока победителя
func CalculateWin(player interop.Hash160, playerBet Bet) int {
ctx := storage.GetContext()
totalWin := 0 // сумма ставок всех победителей
totalLost := 0 // сумма ставок всех проигравших
it := storage.Find(ctx, playerKeyPrefix, storage.ValuesOnly)
for iterator.Next(it) {
data := iterator.Value(it)
bet := std.Deserialize(data.([]byte)).(Bet)
if bet.winner == playerBet.winner {
totalWin += bet.amount
} else {
totalLost += bet.amount
}
}
// выигрыш состоит из:
// 1) изначальной ставки данного победителя
// 2) доли победителя от суммы ставок всех проигравших пропорционально его ставке к сумме ставок всех победителей,
// поделенной пополам с владельцем контракта
return playerBet.amount + int(float64(totalLost)*float64(playerBet.amount)/float64(totalWin))/2
}