From f668685d596a8dfdabd590b436cb43b823ec1573 Mon Sep 17 00:00:00 2001 From: Olga Konstantinova Date: Wed, 20 Dec 2023 21:23:31 +0300 Subject: [PATCH] Initial implementation of smart-contract --- contract.go | 219 +++++++++++++++++++++++++++++++++++++++++++++++++++ contract.yml | 5 ++ data.json | 16 ++++ go.mod | 8 ++ go.sum | 9 +++ 5 files changed, 257 insertions(+) create mode 100644 contract.go create mode 100644 contract.yml create mode 100644 data.json create mode 100644 go.mod create mode 100644 go.sum diff --git a/contract.go b/contract.go new file mode 100644 index 0000000..a6f2c78 --- /dev/null +++ b/contract.go @@ -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 +} diff --git a/contract.yml b/contract.yml new file mode 100644 index 0000000..b2a7327 --- /dev/null +++ b/contract.yml @@ -0,0 +1,5 @@ +name: "Bet contract" +supported standards: [] +events: [] +permissions: + - methods: ["request", "transfer"] \ No newline at end of file diff --git a/data.json b/data.json new file mode 100644 index 0000000..9c8d3eb --- /dev/null +++ b/data.json @@ -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 + } + ] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0f333d4 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..388fb72 --- /dev/null +++ b/go.sum @@ -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=