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