219 lines
6.3 KiB
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
|
|
}
|