Compare commits

...

10 commits

Author SHA1 Message Date
a743337632 Add slides and reference article
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-22 20:45:03 +04:00
8bd839047e Update README
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-22 20:42:46 +04:00
1eb1468068 Add evil merchant and client with protective rules
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-21 15:01:42 +04:00
9185c63d17 Add simple game client
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-21 12:34:49 +04:00
1fa8f126da Refactor player inventory management
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-21 10:58:08 +04:00
fdb6e9fa8b Fix buying items from merchants
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-20 16:05:27 +04:00
9de7e04e0f Fixed various bugs in contracts
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-20 00:22:02 +04:00
8c031ee31b Fix the way items are stored in player
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-19 23:21:16 +04:00
e338acbd5a Simple version of l6 materials
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
2023-11-19 23:12:42 +04:00
c776a7e4e9 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-19 11:31:41 +04:00
13 changed files with 923 additions and 0 deletions

3
l6/.gitignore vendored Normal file
View file

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

42
l6/data.json Normal file
View file

@ -0,0 +1,42 @@
{
"store": {
"item": [
{
"id": 0,
"category": "Headgear",
"title": "Bucket Hat",
"price": 400
},
{
"id": 1,
"category": "Headgear",
"title": "Fugu Bell Hat",
"price": 1700
},
{
"id": 2,
"category": "Clothing",
"title": "Lumberjack Shirt",
"price": 800
},
{
"id": 3,
"category": "Clothing",
"title": "Pink Hoodie",
"price": 3400
},
{
"id": 4,
"category": "Shoes",
"title": "Cyan Trainers",
"price": 700
},
{
"id": 5,
"category": "Shoes",
"title": "Red Hi-Tops",
"price": 1800
}
]
}
}

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

@ -0,0 +1,149 @@
## Prerequisites
To run this example we will need:
* 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:
* Our wallet is located next to `web3-course` directory.
* We run all commands in directory `l6`.
### 1. Compile contracts
To compile the contracts we run the following commands:
```sh
$ 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/confi
g.json
```
### 2. Assign player contract to the group
Now we will create a wallet for a group that will contain both contracts. It is possible to skip this step and use wallet `frostfs-aio/morph/node-wallet.json`, but for the sake of purity it is recommended to use a separate wallet.
To create a wallet, we run the following command. When prompted, enter any name for the account, we will use `l6-game` as group name for this example:
```sh
$ neo-go wallet init -w ../../group-wallet.json -a
Enter the name of the account > l6-game
Enter new password >
Confirm password >
```
Copy account address from the command's output, we will need it in a moment:
```json
{
"version": "1.0",
"accounts": [
{
"address": "NgdcCGcR7QveKn9yjQgpemtgtn5zLn33b6", <-- NOTE: take this value
...
```
Now we need to assign group to contracts' manifest with the following command (pay attention that sender is address of our wallet with GAS, but --wallet and --address are referring to the group wallet that we've just created):
```sh
$ neo-go contract manifest add-group -n player/player.nef -m player/config.json --sender NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW --wallet ../../group-wallet.json --address NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X
```
Now our contracts are ready to be deployed.
### 3. Deploy contracts
Now we can deploy player contract:
```sh
$ $ neo-go contract deploy -r http://localhost:30333 -w ../../wallet.json --in player/player.nef --manifest player/config.json
```
Copy contract hash from the command output and convert it to LE address, for example:
```sh
...
Contract: 25b7b493c37fc740bac6603281de01f451847805
$ neo-go util convert '25b7b493c37fc740bac6603281de01f451847805'
BE ScriptHash to Address NPMQFvjSdBWotKA1PrsrkAtiSzyRXPhDF9
LE ScriptHash to Address NLQtwNfn4LW3As2r2YfkaRZTpoiTuRfEc5 <-- NOTE: take this value
...
```
Now we can deploy merchant contract, it takes hash of player contract as a parameter to its' _deploy method:
```sh
$ neo-go contract deploy -r http://localhost:30333 -w ../../wallet.json --in merchant/merchant.nef --manifest merchant/config.json [ hash160:N
LQtwNfn4LW3As2r2YfkaRZTpoiTuRfEc5 ]
```
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. 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 25b7b493c37fc740bac6603281de01f451847805 newPlayer string:demo
```
Try to buy sword with scope None, it should fail:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 25b7b493c37fc740bac6603281de01f451847805 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:None'
```
Try to do the same with scope CalledByEntry, it should work:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 25b7b493c37fc740bac6603281de01f451847805 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry'
```
Try to sell item:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 25b7b493c37fc740bac6603281de01f451847805 sellItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry'
```
Also you can try scopes `CustomContracts` and `CustomGroups`, they should both work too:
```sh
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 25b7b493c37fc740bac6603281de01f451847805 buyItemForGas string:demo int:1 int:23 hash160:NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CustomContracts:25b7b493c37fc740bac6603281de01f451847805'
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 25b7b493c37fc740bac6603281de01f451847805 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CustomGroups:029dd9b3639a23ea7ad4356fe7abb4f5a01eaea9f2e5c7138f3e174d21b03f682f'
```
### 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 25b7b493c37fc740bac6603281de01f451847805 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 25b7b493c37fc740bac6603281de01f451847805 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.

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,169 @@
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
var itemPrices = map[string]int{
"Sword": 200,
"Shortbow": 100,
"Longbow": 300,
}
type Player struct {
balance int
owner interop.Hash160
itemCount map[string]int
}
func NewPlayer(playerName string) {
ctx := storage.GetContext()
existingPlayer := storage.Get(ctx, playerName)
if existingPlayer != nil {
panic("player already exists")
}
player := Player{
balance: 3000,
owner: runtime.GetScriptContainer().Sender,
itemCount: make(map[string]int),
}
savePlayer(ctx, playerName, player)
}
func Balance(playerName string) int {
p := getPlayer(storage.GetReadOnlyContext(), playerName)
return p.balance
}
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]
if player.balance < itemPrice {
panic("insufficient balance")
}
addItemToInventory(player, itemName)
player.balance -= itemPrice
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)
player.balance += itemPrices[itemName]
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 !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 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)
}
}