Compare commits

..

2 commits

Author SHA1 Message Date
74db02ef42 Implement in-game currency as nep17
Buy/sell items using nep17 tokens instead of in-game balance.

Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-26 23:16:14 +04:00
dd0add8943 Add initial version of l6 materials
The code of the contracts is identical to what it was in l4.

Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-22 20:51:02 +04:00
16 changed files with 1190 additions and 0 deletions

3
l6/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.nef
config.json
bin

172
l6/docs/README.md Normal file
View file

@ -0,0 +1,172 @@
## Prerequisites
To run this example we will need:
* Player wallet with sufficient GAS on it. In our example we will be using wallet with address `NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW`.
## Running the example
In this section we will assume that:
* Wallet of player is located next to `web3-course` directory.
* Repository `frostfs-aio` is located next to `web3-course` directory.
* We run all commands in directory `l6`.
* Game-related contracts are managed by wallet `wallets/game-wallet.json`. Address of this wallet is `NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X`. Password of this wallet is `one`.
### 1. Compile contracts
To compile the contracts we run the following commands:
```sh
$ neo-go contract compile -in forint --config forint/config.yml --out forint/forint.nef --manifest forint/config.json
$ neo-go contract compile -in player/player_contract.go --config player/config.yml --out player/player.nef --manifest player/config.json
$ neo-go contract compile -in merchant/merchant_contract.go --config merchant/config.yml --out merchant/merchant.nef --manifest merchant/config.json
```
### 2. Assign game contracts to the group
We need to assign game contracts (forint and player) to a single group. The group will be owned by `wallets/game-wallet.json`.
We assign group to contracts' manifests with the following commands:
```sh
$ neo-go contract manifest add-group -n forint/forint.nef -m forint/config.json --sender NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --wallet ./wallets/game-wallet.json --address NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X
$ neo-go contract manifest add-group -n player/player.nef -m player/config.json --sender NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --wallet ./wallets/game-wallet.json --address NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X
```
Now our contracts are ready to be deployed.
### 3. Deploy contracts
First, we need to transfer some gas to game wallet, as we are deploying game contracts on it's behalf:
```sh
$ neo-go wallet nep17 transfer -r http://localhost:30333 -w ../../frostfs-aio/morph/node-wallet.json --from Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn --to NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --token GAS --amount 1000
```
Now we deploy forint contract:
```sh
$ neo-go contract deploy -r http://localhost:30333 -w ./wallets/game-wallet.json --in forint/forint.nef --manifest forint/config.json
```
Copy contract hash from the command output and convert it to LE address, for example:
```sh
...
Contract: aaf122a18d90dbb31a6e9903bddd1db3841ab261
$ neo-go util convert 'aaf122a18d90dbb31a6e9903bddd1db3841ab261'
BE ScriptHash to Address NbVpvjsPYAznMgnPBWDu6y6vPtXiHG3sBs
LE ScriptHash to Address NUpY6DaaYVMuReDXu4JLNGxqjTfAiYQFio <-- NOTE: take this value
...
```
Then we can deploy player contract, it takes hash of forint contract as a parameter to its' _deploy method:
```sh
$ neo-go contract deploy -r http://localhost:30333 -w ./wallets/game-wallet.json --in player/player.nef --manifest player/config.json [ hash160:NUpY6DaaYVMuReDXu4JLNGxqjTfAiYQFio ]
```
Copy contract hash from the command output and convert it to LE address, for example:
```sh
...
--- Contract: 45d782ab0e18011030eeab7b3b7ee992e9373644
$ neo-go util convert '45d782ab0e18011030eeab7b3b7ee992e9373644'
BE ScriptHash to Address NSHFyvFWHSf2x8cGt8u9dxyRThCDP8udKR
LE ScriptHash to Address NS8e5xiXL5tTqAJ5ghq2j1mJbzhfcJ8th5 <-- NOTE: take this value
...
```
Finally we can deploy merchant contract. As merchant contract is an external contract and not part of the game, we will deploy it on behalf of node's wallet. It does not make any practical difference, but will be an indication of different parties in the blockchain. Merchant contract takes hash of player contract as a parameter to its' _deploy method, so the command is:
```sh
$ neo-go contract deploy -r http://localhost:30333 -w ../../frostfs-aio/morph/node-wallet.json --in merchant/merchant.nef --manifest merchant/config.json [ hash160:NS8e5xiXL5tTqAJ5ghq2j1mJbzhfcJ8th5 ]
```
Copy contract hash from the command output and convert it to LE address, for example:
```sh
...
# Contract: 443686682d4a3a59408a6b44b766baed894ad679 <-- NOTE: take this value
$ neo-go util convert '443686682d4a3a59408a6b44b766baed894ad679'
BE ScriptHash to Address NS8eTGN8cNabMe6gG9zAZhHyMueuLfM94j
LE ScriptHash to Address NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ <-- take this value
```
### 4. Initialize game funds
We mint game currency (forints) and put it on account of the player contract, because this contract will manage game funds:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ./wallets/game-wallet.json aaf122a18d90dbb31a6e9903bddd1db3841ab261 mint hash160:NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X -- 'NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X:CalledByEntry'
$ neo-go wallet nep17 transfer -r http://localhost:30333 -w ./wallets/game-wallet.json --from NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --to NS8e5xiXL5tTqAJ5ghq2j1mJbzhfcJ8th5 --token aaf122a18d90dbb31a6e9903bddd1db3841ab261 --amount 1000000
```
### 5. Create a new player and trade for in-game currency
To create a new player, invoke `newPlayer` function on the player contract and specify name of the player. In example below we create a player with the name `demo`:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 newPlayer string:demo
```
Check balance of the in-game currency on player's wallet:
```sh
$ neo-go wallet nep17 balance -r http://localhost:30333 --wallet ../../wallet.json --token aaf122a18d90dbb31a6e9903bddd1db3841ab261
Account NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW
FAF: Fairy Forint (aaf122a18d90dbb31a6e9903bddd1db3841ab261)
Amount : 3000
```
Try to buy sword with scope None, it should fail:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:None'
```
Try to do the same with any of the following scopes, it should work:
```sh
neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:Global'
neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry,CustomContracts:aaf122a18d90dbb31a6e9903bddd1db3841ab261'
neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CustomGroups:029dd9b3639a23ea7ad4356fe7abb4f5a01eaea9f2e5c7138f3e174d21b03f682f'
```
Try to sell item:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 sellItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry'
```
### 5. Trade for gas
Transfer gas to account of merchant contract (you will need LE address of merchant contract that we've captured during deployment):
```sh
$ neo-go wallet nep17 transfer -r http://localhost:30333 -w ../../frostfs-aio/morph/node-wallet.json --from Nhfg3TbpwogLvDGVvAvqyThbsHgoSU
Kwtn --to NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ --token GAS --amount 1000
```
Sell Sword to merchant for 20 GAS:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 sellItemForGas string:demo string:Sword int:20 hash160:NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry'
```
Buy back sword from merchant for 23 GAS:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItemForGas str
ing:demo int:1 int:23 hash160:NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CustomContracts:d2a4cff31913016155e38e474a2c06d08be276cf:25b7b493c37fc740bac6603281de01f451847805'
```

Binary file not shown.

BIN
l6/docs/slides.pdf Normal file

Binary file not shown.

14
l6/forint/config.yml Normal file
View file

@ -0,0 +1,14 @@
name: "Fairy Forint"
supportedstandards: ["NEP-17"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply"]
events:
- name: Transfer
parameters:
- name: from
type: Hash160
- name: to
type: Hash160
- name: amount
type: Integer
permissions:
- methods: ["onNEP17Payment"]

View file

@ -0,0 +1,58 @@
package forint
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/lib/address"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
func getToken() Token {
// Owner of the wallet is wallets/game-wallet.json
owner := address.ToHash160("NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X")
token := Token{
Name: "Fairy Forint",
Symbol: "FAF",
Decimals: 0,
Owner: owner,
TotalSupply: 1000000,
CirculationKey: "TokenCirculation",
}
return token
}
// Symbol returns the token symbol.
func Symbol() string {
return getToken().Symbol
}
// Decimals returns the number of digits after decimal point.
func Decimals() int {
return getToken().Decimals
}
// TotalSupply returns the total amount of tokens.
func TotalSupply() int {
ctx := storage.GetReadOnlyContext()
return getToken().GetSupply(ctx)
}
// BalanceOf returns the amount of tokens owned by the specified address.
func BalanceOf(holder interop.Hash160) int {
ctx := storage.GetReadOnlyContext()
return getToken().BalanceOf(ctx, holder)
}
// Transfer moves token from one address to another.
func Transfer(from interop.Hash160, to interop.Hash160, amount int, data any) bool {
ctx := storage.GetContext()
return getToken().Transfer(ctx, from, to, amount, data)
}
// Mint generates initial supply of tokens.
func Mint(to interop.Hash160) {
ctx := storage.GetContext()
minted := getToken().Mint(ctx, to)
if !minted {
panic("failed to mint initial supply")
}
}

160
l6/forint/token.go Normal file
View file

@ -0,0 +1,160 @@
/*
The implementation of the token logic has been copied "as is" from neo-go:
https://github.com/nspcc-dev/neo-go/blob/7a1bf77585a37302ddfae78b2c88ed472f66cbcc/examples/token/nep17/nep17.go
The code is subject to the following license:
MIT License
Copyright (c) 2018-2023 NeoSPCC (@nspcc-dev), Anthony De Meulemeester (@anthdm), City of Zion community (@CityOfZion)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package forint
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/native/management"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)
// Token holds all token info
type Token struct {
// Token name
Name string
// Ticker symbol
Symbol string
// Amount of decimals
Decimals int
// Token owner address
Owner []byte
// Total tokens * multiplier
TotalSupply int
// Storage key for circulation value
CirculationKey string
}
// getIntFromDB is a helper that checks for nil result of storage.Get and returns
// zero as the default value.
func getIntFromDB(ctx storage.Context, key []byte) int {
var res int
val := storage.Get(ctx, key)
if val != nil {
res = val.(int)
}
return res
}
// GetSupply gets the token totalSupply value from VM storage
func (t Token) GetSupply(ctx storage.Context) int {
return getIntFromDB(ctx, []byte(t.CirculationKey))
}
// BalanceOf gets the token balance of a specific address
func (t Token) BalanceOf(ctx storage.Context, holder []byte) int {
return getIntFromDB(ctx, holder)
}
// Transfer token from one user to another
func (t Token) Transfer(ctx storage.Context, from, to interop.Hash160, amount int, data any) bool {
amountFrom := t.CanTransfer(ctx, from, to, amount)
if amountFrom == -1 {
return false
}
if amountFrom == 0 {
storage.Delete(ctx, from)
}
if amountFrom > 0 {
diff := amountFrom - amount
storage.Put(ctx, from, diff)
}
amountTo := getIntFromDB(ctx, to)
totalAmountTo := amountTo + amount
if totalAmountTo != 0 {
storage.Put(ctx, to, totalAmountTo)
}
runtime.Notify("Transfer", from, to, amount)
if to != nil && management.GetContract(to) != nil {
contract.Call(to, "onNEP17Payment", contract.All, from, amount, data)
}
return true
}
// CanTransfer returns the amount it can transfer
func (t Token) CanTransfer(ctx storage.Context, from []byte, to []byte, amount int) int {
if len(to) != 20 || !IsUsableAddress(from) {
return -1
}
amountFrom := getIntFromDB(ctx, from)
if amountFrom < amount {
return -1
}
// Tell Transfer the result is equal - special case since it uses Delete
if amountFrom == amount {
return 0
}
// return amountFrom value back to Transfer, reduces extra Get
return amountFrom
}
// IsUsableAddress checks if the sender is either the correct Neo address or SC address
func IsUsableAddress(addr []byte) bool {
if len(addr) == 20 {
if runtime.CheckWitness(addr) {
return true
}
// Check if a smart contract is calling scripthash
callingScriptHash := runtime.GetCallingScriptHash()
if callingScriptHash.Equals(addr) {
return true
}
}
return false
}
// Mint initial supply of tokens
func (t Token) Mint(ctx storage.Context, to interop.Hash160) bool {
if !IsUsableAddress(t.Owner) {
return false
}
minted := storage.Get(ctx, []byte("minted"))
if minted != nil && minted.(bool) == true {
return false
}
storage.Put(ctx, to, t.TotalSupply)
storage.Put(ctx, []byte("minted"), true)
storage.Put(ctx, []byte(t.CirculationKey), t.TotalSupply)
var from interop.Hash160
runtime.Notify("Transfer", from, to, t.TotalSupply)
return true
}

159
l6/gameclient/main.go Normal file
View file

@ -0,0 +1,159 @@
package main
import (
"context"
"fmt"
"math/big"
"os"
"strconv"
"time"
"l6/player/client"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
type args struct {
walletPath string
walletPassword string
playerHash util.Uint160
lotID int
lotPrice int
merchantHash util.Uint160
playerName string
endpoint string
}
func main() {
args, err := parseArgs()
if err != nil {
fmt.Printf("Invalid arguments: %v\n", err)
os.Exit(-1)
}
ctx := context.Background()
client, txWaiter, err := initPlayerClient(ctx, args.endpoint, args.walletPath, args.walletPassword, args.playerHash)
if err != nil {
fmt.Printf("Cannot connect to blockchain: %v\n", err)
os.Exit(-1)
}
err = buyItem(client, txWaiter, args.playerName, args.lotID, args.lotPrice, args.merchantHash)
if err != nil {
fmt.Printf("Transaction error: %v\n", err)
os.Exit(-1)
}
fmt.Printf("Done\n")
}
func parseArgs() (*args, error) {
walletPath := os.Args[1]
walletPassword := os.Args[2]
playerHash, err := util.Uint160DecodeStringLE(os.Args[3])
if err != nil {
return nil, fmt.Errorf("failed to parse playerHash: %w", err)
}
merchantHash, err := util.Uint160DecodeStringLE(os.Args[4])
if err != nil {
return nil, fmt.Errorf("failed to parse merchantHash: %w", err)
}
endpoint := os.Args[5]
playerName := os.Args[6]
lotID, err := strconv.Atoi(os.Args[7])
if err != nil {
return nil, fmt.Errorf("failed to parse lotID: %w", err)
}
lotPrice, err := strconv.Atoi(os.Args[8])
if err != nil {
return nil, fmt.Errorf("failed to parse lotPrice: %w", err)
}
return &args{
walletPath: walletPath,
walletPassword: walletPassword,
playerHash: playerHash,
merchantHash: merchantHash,
endpoint: endpoint,
playerName: playerName,
lotID: lotID,
lotPrice: lotPrice,
}, nil
}
func initPlayerClient(ctx context.Context, rpcEndpoint, walletPath, walletPassword string, playerHash util.Uint160) (*client.Contract, actor.Waiter, error) {
rpcClient, err := rpcclient.New(ctx, rpcEndpoint, rpcclient.Options{
DialTimeout: time.Second * 5,
RequestTimeout: time.Second * 5,
})
if err != nil {
return nil, nil, err
}
wal, err := wallet.NewWalletFromFile(walletPath)
if err != nil {
return nil, nil, fmt.Errorf("cannot open wallet: %w", err)
}
if len(wal.Accounts) == 0 {
return nil, nil, fmt.Errorf("no accounts in wallet")
}
for _, account := range wal.Accounts {
if err := account.Decrypt(walletPassword, keys.NEP2ScryptParams()); err != nil {
return nil, nil, fmt.Errorf("cannot unlock wallet: %w", err)
}
}
gasRule := transaction.ConditionAnd([]transaction.WitnessCondition{
toPtr(transaction.ConditionCalledByContract(playerHash)),
toPtr(transaction.ConditionScriptHash(util.Uint160([]byte(gas.Hash)))),
})
acc := wal.Accounts[0]
act, err := actor.New(rpcClient, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry | transaction.Rules,
Rules: []transaction.WitnessRule{
{
Action: transaction.WitnessAllow,
Condition: &gasRule,
},
},
},
Account: acc,
}})
if err != nil {
return nil, nil, fmt.Errorf("cannot init actor: %w", err)
}
return client.New(act, playerHash), act.Waiter, nil
}
func toPtr[T any](value T) *T {
return &value
}
func buyItem(
playerClient *client.Contract,
txWaiter actor.Waiter,
playerName string,
lotID int,
lotPrice int,
merchantHash util.Uint160,
) error {
tx, vub, err := playerClient.BuyItemForGas(
playerName,
big.NewInt(int64(lotID)),
big.NewInt(int64(lotPrice)),
merchantHash,
)
_, err = txWaiter.Wait(tx, vub, err)
return err
}

22
l6/go.mod Normal file
View file

@ -0,0 +1,22 @@
module l6
go 1.20
require (
github.com/nspcc-dev/neo-go v0.103.1
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231027092558-8ed6d97085d3
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

39
l6/go.sum Normal file
View file

@ -0,0 +1,39 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg=
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U=
github.com/nspcc-dev/neo-go v0.103.1 h1:BfRBceHUu8jSc1KQy7CzmQ/pa+xzAmgcyteGf0/IGgM=
github.com/nspcc-dev/neo-go v0.103.1/go.mod h1:MD7MPiyshUwrE5n1/LzxeandbItaa/iLW/bJb6gNs/U=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231027092558-8ed6d97085d3 h1:ybQcK5pTNAR+wQU3k4cGeOZN6OCiVcQkbgR3Zl6NFPU=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231027092558-8ed6d97085d3/go.mod h1:J/Mk6+nKeKSW4wygkZQFLQ6SkLOSGX5Ga0RuuuktEag=
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

6
l6/merchant/config.yml Normal file
View file

@ -0,0 +1,6 @@
name: "Merchant"
supportedstandards: []
events:
safemethods: ["getItemName", "getLotsForSale"]
permissions:
- methods: ["onNEP17Payment", "transfer"]

View file

@ -0,0 +1,146 @@
package player
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/native/gas"
"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"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
type Lot struct {
lotID int
itemName string
price int
gasPrice int
}
const (
lotsPrefix = "lots"
lastIDKey = "lastID"
playerHashKey = "playerHash"
maxPrice = 100
gasDecimals = 1_0000_0000
)
func _deploy(data interface{}, isUpdate bool) {
if isUpdate {
return
}
// Parse player contract hash from incoming data
args := data.(struct {
playerHash interop.Hash160
})
if len(args.playerHash) != interop.Hash160Len {
panic("invalid hash of player contract")
}
ctx := storage.GetContext()
storage.Put(ctx, lastIDKey, 0)
storage.Put(ctx, playerHashKey, args.playerHash)
}
func GetItemName(lotID int) string {
return getLot(storage.GetReadOnlyContext(), lotID).itemName
}
func GetLotsForSale() []Lot {
lots := make([]Lot, 0)
ctx := storage.GetReadOnlyContext()
it := storage.Find(ctx, lotsPrefix, storage.ValuesOnly|storage.DeserializeValues)
for iterator.Next(it) {
lot := iterator.Value(it).(Lot)
lots = append(lots, lot)
}
return lots
}
func Sell(itemName string, price int) {
ctx := storage.GetContext()
playerHash := storage.Get(ctx, playerHashKey).(interop.Hash160)
if !runtime.GetCallingScriptHash().Equals(playerHash) {
panic("can be called from player hash only")
}
if price > maxPrice {
panic("unacceptable price")
}
// Pay for the item
if price > 0 {
gasAmount := price * gasDecimals
success := gas.Transfer(runtime.GetExecutingScriptHash(), runtime.GetScriptContainer().Sender, gasAmount, nil)
if !success {
panic("failed to transfer gas to player")
}
}
// Place lot in the sale list
createLot(ctx, itemName, price)
}
func OnNEP17Payment(from interop.Hash160, amount int, data any) {
defer func() {
if r := recover(); r != nil {
runtime.Log(r.(string))
util.Abort()
}
}()
callingHash := runtime.GetCallingScriptHash()
if !callingHash.Equals(gas.Hash) {
panic("only GAS is accepted")
}
if data == nil {
runtime.Log("received operational GAS")
return
}
ctx := storage.GetContext()
lotID := data.(int)
lot := getLot(ctx, lotID)
if amount < lot.gasPrice {
panic("gas amount does not cover lot price")
}
// That's what evil merchant might attempt to do
// gas.Transfer(from, runtime.GetExecutingScriptHash(), 10*gasDecimals, nil)
// Lot must be removed (we can't list it for sale anymore)
deleteLot(ctx, lotID)
}
func getLot(ctx storage.Context, lotID int) Lot {
itemData := storage.Get(ctx, lotsPrefix+std.Itoa10(lotID)).([]byte)
if itemData == nil {
panic("item not found")
}
return std.Deserialize(itemData).(Lot)
}
func createLot(ctx storage.Context, itemName string, price int) {
lastID := storage.Get(ctx, lastIDKey).(int)
nextID := lastID + 1
storage.Put(ctx, lastIDKey, nextID)
resellPrice := price + 1 + price/10
newItem := Lot{
itemName: itemName,
price: resellPrice,
gasPrice: resellPrice * gasDecimals,
lotID: nextID,
}
storage.Put(ctx, lotsPrefix+std.Itoa10(nextID), std.Serialize(newItem))
}
func deleteLot(ctx storage.Context, lotID int) {
storage.Delete(ctx, lotsPrefix+std.Itoa10(lotID))
}

183
l6/player/client/client.go Normal file
View file

@ -0,0 +1,183 @@
package client
import (
"math/big"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// Actor is used by Contract to call state-changing methods.
type Actor interface {
MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error)
MakeRun(script []byte) (*transaction.Transaction, error)
MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error)
MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error)
SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error)
SendRun(script []byte) (util.Uint256, uint32, error)
}
// Contract implements all contract methods.
type Contract struct {
actor Actor
hash util.Uint160
}
// New creates an instance of Contract using Hash and the given Actor.
func New(actor Actor, hash util.Uint160) *Contract {
return &Contract{actor, hash}
}
// Balance creates a transaction invoking `balance` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) Balance(playerName string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "balance", playerName)
}
// BalanceTransaction creates a transaction invoking `balance` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) BalanceTransaction(playerName string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "balance", playerName)
}
// BalanceUnsigned creates a transaction invoking `balance` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) BalanceUnsigned(playerName string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "balance", nil, playerName)
}
// BuyItem creates a transaction invoking `buyItem` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) BuyItem(playerName string, itemName string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "buyItem", playerName, itemName)
}
// BuyItemTransaction creates a transaction invoking `buyItem` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) BuyItemTransaction(playerName string, itemName string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "buyItem", playerName, itemName)
}
// BuyItemUnsigned creates a transaction invoking `buyItem` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) BuyItemUnsigned(playerName string, itemName string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "buyItem", nil, playerName, itemName)
}
// BuyItemForGas creates a transaction invoking `buyItemForGas` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) BuyItemForGas(playerName string, lotID *big.Int, itemPrice *big.Int, merchantHash util.Uint160) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "buyItemForGas", playerName, lotID, itemPrice, merchantHash)
}
// BuyItemForGasTransaction creates a transaction invoking `buyItemForGas` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) BuyItemForGasTransaction(playerName string, lotID *big.Int, itemPrice *big.Int, merchantHash util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "buyItemForGas", playerName, lotID, itemPrice, merchantHash)
}
// BuyItemForGasUnsigned creates a transaction invoking `buyItemForGas` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) BuyItemForGasUnsigned(playerName string, lotID *big.Int, itemPrice *big.Int, merchantHash util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "buyItemForGas", nil, playerName, lotID, itemPrice, merchantHash)
}
// Items creates a transaction invoking `items` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) Items(playerName string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "items", playerName)
}
// ItemsTransaction creates a transaction invoking `items` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) ItemsTransaction(playerName string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "items", playerName)
}
// ItemsUnsigned creates a transaction invoking `items` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) ItemsUnsigned(playerName string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "items", nil, playerName)
}
// NewPlayer creates a transaction invoking `newPlayer` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) NewPlayer(playerName string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "newPlayer", playerName)
}
// NewPlayerTransaction creates a transaction invoking `newPlayer` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) NewPlayerTransaction(playerName string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "newPlayer", playerName)
}
// NewPlayerUnsigned creates a transaction invoking `newPlayer` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) NewPlayerUnsigned(playerName string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "newPlayer", nil, playerName)
}
// SellItem creates a transaction invoking `sellItem` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) SellItem(playerName string, itemName string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "sellItem", playerName, itemName)
}
// SellItemTransaction creates a transaction invoking `sellItem` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) SellItemTransaction(playerName string, itemName string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "sellItem", playerName, itemName)
}
// SellItemUnsigned creates a transaction invoking `sellItem` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) SellItemUnsigned(playerName string, itemName string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "sellItem", nil, playerName, itemName)
}
// SellItemForGas creates a transaction invoking `sellItemForGas` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) SellItemForGas(playerName string, itemName string, itemPrice *big.Int, merchantHash util.Uint160) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "sellItemForGas", playerName, itemName, itemPrice, merchantHash)
}
// SellItemForGasTransaction creates a transaction invoking `sellItemForGas` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) SellItemForGasTransaction(playerName string, itemName string, itemPrice *big.Int, merchantHash util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "sellItemForGas", playerName, itemName, itemPrice, merchantHash)
}
// SellItemForGasUnsigned creates a transaction invoking `sellItemForGas` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) SellItemForGasUnsigned(playerName string, itemName string, itemPrice *big.Int, merchantHash util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "sellItemForGas", nil, playerName, itemName, itemPrice, merchantHash)
}

5
l6/player/config.yml Normal file
View file

@ -0,0 +1,5 @@
name: "Player"
supportedstandards: []
events:
permissions:
- methods: "*"

View file

@ -0,0 +1,222 @@
package player
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/native/gas"
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
"github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
const (
gasDecimals = 1_0000_0000
initialBalance = 3000
forintHashKey = "forintHash"
)
var itemPrices = map[string]int{
"Sword": 200,
"Shortbow": 100,
"Longbow": 500,
}
type Player struct {
owner interop.Hash160
itemCount map[string]int
}
func _deploy(data interface{}, isUpdate bool) {
if isUpdate {
return
}
// Parse hash of forint contract from incoming data
args := data.(struct {
forintHash interop.Hash160
})
if len(args.forintHash) != interop.Hash160Len {
panic("invalid hash of forint contract")
}
ctx := storage.GetContext()
storage.Put(ctx, forintHashKey, args.forintHash)
}
func NewPlayer(playerName string) {
ctx := storage.GetContext()
existingPlayer := storage.Get(ctx, playerName)
if existingPlayer != nil {
panic("player already exists")
}
// Transfer initial funds to the player owner
playerOwner := runtime.GetScriptContainer().Sender
changePlayerBalance(ctx, playerOwner, initialBalance)
player := Player{
owner: playerOwner,
itemCount: make(map[string]int),
}
savePlayer(ctx, playerName, player)
}
func Items(playerName string) []string {
p := getPlayer(storage.GetReadOnlyContext(), playerName)
items := make([]string, len(p.itemCount))
for itemName, count := range p.itemCount {
items = append(items, itemName+" "+std.Itoa10(count))
}
return items
}
func BuyItem(playerName string, itemName string) {
ctx := storage.GetContext()
player := getPlayer(ctx, playerName)
if !runtime.CheckWitness(player.owner) {
panic("player not witnessed")
}
itemPrice := itemPrices[itemName]
changePlayerBalance(ctx, player.owner, -itemPrice)
addItemToInventory(player, itemName)
savePlayer(ctx, playerName, player)
}
func SellItem(playerName string, itemName string) {
ctx := storage.GetContext()
player := getPlayer(ctx, playerName)
if !runtime.CheckWitness(player.owner) {
panic("player not witnessed")
}
removeItemFromInventory(player, itemName)
itemPrice := itemPrices[itemName]
changePlayerBalance(ctx, player.owner, itemPrice)
savePlayer(ctx, playerName, player)
}
func BuyItemForGas(playerName string, lotID int, itemPrice int, merchantHash interop.Hash160) {
ctx := storage.GetContext()
player := getPlayer(ctx, playerName)
if !runtime.CheckWitness(player.owner) {
panic("player not witnessed")
}
itemName := contract.Call(merchantHash, "getItemName", contract.ReadOnly, lotID).(string)
gasPrice := itemPrice * gasDecimals
success := gas.Transfer(runtime.GetScriptContainer().Sender, merchantHash, gasPrice, lotID)
// If we were to use custom token instead of GAS, we would call it like a regular contract:
// success := contract.Call(<tokenContractHash>, "transfer", contract.All, runtime.GetScriptContainer().Sender, merchantHash, gasPrice, lotID).(bool)
if !success {
panic("failed to transfer gas to merchant")
}
addItemToInventory(player, itemName)
savePlayer(ctx, playerName, player)
}
func SellItemForGas(playerName string, itemName string, itemPrice int, merchantHash interop.Hash160) {
ctx := storage.GetContext()
player := getPlayer(ctx, playerName)
if !runtime.CheckWitness(player.owner) {
panic("player not witnessed")
}
removeItemFromInventory(player, itemName)
balanceBefore := gas.BalanceOf(runtime.GetScriptContainer().Sender)
contract.Call(merchantHash, "sell", contract.All, itemName, itemPrice)
balanceAfter := gas.BalanceOf(runtime.GetScriptContainer().Sender)
gasProfit := (balanceAfter - balanceBefore)
if gasProfit < itemPrice*gasDecimals {
runtime.Log("merchant payment does not cover item price: " + std.Itoa10(gasProfit) + " < " + std.Itoa10(itemPrice*gasDecimals))
util.Abort()
}
savePlayer(ctx, playerName, player)
}
func OnNEP17Payment(from interop.Hash160, amount int, data any) {
ctx := storage.GetContext()
forintHash := storage.Get(ctx, forintHashKey).(interop.Hash160)
callingHash := runtime.GetCallingScriptHash()
if !callingHash.Equals(forintHash) {
panic("only FAF is accepted")
}
}
func getPlayer(ctx storage.Context, playerName string) Player {
data := storage.Get(ctx, playerName)
if data == nil {
panic("player not found")
}
return std.Deserialize(data.([]byte)).(Player)
}
func savePlayer(ctx storage.Context, playerName string, player Player) {
storage.Put(ctx, playerName, std.Serialize(player))
}
func hasItemInInventory(player Player, itemName string) bool {
// Standard syntax for go map does not work: _, exists := player.itemCount[itemName]
// So, we use HASKEY command to do this:
hasKey := neogointernal.Opcode2("HASKEY", player.itemCount, itemName).(bool)
if !hasKey {
return false
}
return player.itemCount[itemName] > 0
}
func addItemToInventory(player Player, itemName string) {
if !hasItemInInventory(player, itemName) {
player.itemCount[itemName] = 0
}
player.itemCount[itemName] += 1
}
func removeItemFromInventory(player Player, itemName string) {
if !hasItemInInventory(player, itemName) {
panic("player has no specified item")
}
player.itemCount[itemName] -= 1
if player.itemCount[itemName] == 0 {
delete(player.itemCount, itemName)
}
}
func changePlayerBalance(ctx storage.Context, playerOwner interop.Hash160, balanceChange int) {
forintHash := storage.Get(ctx, forintHashKey).(interop.Hash160)
playerContract := runtime.GetExecutingScriptHash()
var from, to interop.Hash160
var transferAmount int
if balanceChange > 0 {
// Transfer funds from contract to player owner
from = playerContract
to = playerOwner
transferAmount = balanceChange
} else {
// Transfer funds from player owner to contract
from = playerOwner
to = playerContract
transferAmount = -balanceChange // We flip sender/receiver, but keep amount positive
}
transferred := contract.Call(forintHash, "transfer", contract.All, from, to, transferAmount, nil).(bool)
if !transferred {
panic("failed to transfer forints")
}
}

View file

@ -0,0 +1 @@
{"version":"1.0","accounts":[{"address":"NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X","key":"6PYQRSvVEjU8PvLRqygDuzXyFsCQY5de6tzwVk63GuBwq2dUNCGtzyEQvD","label":"l6-game","contract":{"script":"DCECndmzY5oj6nrUNW/nq7T1oB6uqfLlxxOPPhdNIbA/aC9BVuezJw==","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isDefault":false}],"scrypt":{"n":16384,"r":8,"p":8},"extra":{"Tokens":null}}