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>
This commit is contained in:
parent
2ebf2c5c51
commit
dd0add8943
13 changed files with 926 additions and 0 deletions
3
l6/.gitignore
vendored
Normal file
3
l6/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
*.nef
|
||||
config.json
|
||||
bin
|
42
l6/data.json
Normal file
42
l6/data.json
Normal 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
149
l6/docs/README.md
Normal 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'
|
||||
```
|
BIN
l6/docs/blog - thou shalt check their witnesses.pdf
Normal file
BIN
l6/docs/blog - thou shalt check their witnesses.pdf
Normal file
Binary file not shown.
BIN
l6/docs/slides.pdf
Normal file
BIN
l6/docs/slides.pdf
Normal file
Binary file not shown.
159
l6/gameclient/main.go
Normal file
159
l6/gameclient/main.go
Normal 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
22
l6/go.mod
Normal 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
39
l6/go.sum
Normal 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
6
l6/merchant/config.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
name: "Merchant"
|
||||
supportedstandards: []
|
||||
events:
|
||||
safemethods: ["getItemName", "getLotsForSale"]
|
||||
permissions:
|
||||
- methods: ["onNEP17Payment", "transfer"]
|
146
l6/merchant/merchant_contract.go
Normal file
146
l6/merchant/merchant_contract.go
Normal 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
183
l6/player/client/client.go
Normal 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
5
l6/player/config.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
name: "Player"
|
||||
supportedstandards: []
|
||||
events:
|
||||
permissions:
|
||||
- methods: "*"
|
172
l6/player/player_contract.go
Normal file
172
l6/player/player_contract.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
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 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 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue