Initial implementation of smart-contract

This commit is contained in:
Olga Konstantinova 2023-12-20 21:23:31 +03:00
commit f668685d59
5 changed files with 257 additions and 0 deletions

219
contract.go Normal file
View file

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

5
contract.yml Normal file
View file

@ -0,0 +1,5 @@
name: "Bet contract"
supported standards: []
events: []
permissions:
- methods: ["request", "transfer"]

16
data.json Normal file
View file

@ -0,0 +1,16 @@
{
"events": [
{
"id": 0,
"date": "2023-12-12",
"description": "Football: Chelsy & Arsenal",
"winner": 1
},
{
"id": 1,
"date": "2024-01-07",
"description": "Hockey: Avangard Omsk & Ak Bars Kazan",
"winner": 0
}
]
}

8
go.mod Normal file
View file

@ -0,0 +1,8 @@
module contract
go 1.18
require (
github.com/nspcc-dev/neo-go v0.104.0
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231121104256-0493ddbd70b2
)

9
go.sum Normal file
View file

@ -0,0 +1,9 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/nspcc-dev/neo-go v0.104.0 h1:FGj3Z46yABcFIAI1SCLd1jQSoh+B00h/2VAgEgY1JKQ=
github.com/nspcc-dev/neo-go v0.104.0/go.mod h1:omsUK5PAtG2/nQ3/evs95QEg3wtkj3LH53e0NKtXVwQ=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231121104256-0493ddbd70b2 h1:hPVF8iMmsQ15GSemj1ma6C9BkwfAugEXsUAVTEniK5M=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231121104256-0493ddbd70b2/go.mod h1:J/Mk6+nKeKSW4wygkZQFLQ6SkLOSGX5Ga0RuuuktEag=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=