Compare commits
2 commits
2ebf2c5c51
...
74db02ef42
Author | SHA1 | Date | |
---|---|---|---|
74db02ef42 | |||
dd0add8943 |
16 changed files with 1190 additions and 0 deletions
3
l6/.gitignore
vendored
Normal file
3
l6/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
*.nef
|
||||
config.json
|
||||
bin
|
172
l6/docs/README.md
Normal file
172
l6/docs/README.md
Normal file
|
@ -0,0 +1,172 @@
|
|||
## Prerequisites
|
||||
|
||||
To run this example we will need:
|
||||
|
||||
* Player wallet with sufficient GAS on it. In our example we will be using wallet with address `NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW`.
|
||||
|
||||
## Running the example
|
||||
|
||||
In this section we will assume that:
|
||||
|
||||
* Wallet of player is located next to `web3-course` directory.
|
||||
* Repository `frostfs-aio` is located next to `web3-course` directory.
|
||||
* We run all commands in directory `l6`.
|
||||
* Game-related contracts are managed by wallet `wallets/game-wallet.json`. Address of this wallet is `NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X`. Password of this wallet is `one`.
|
||||
|
||||
### 1. Compile contracts
|
||||
|
||||
To compile the contracts we run the following commands:
|
||||
|
||||
```sh
|
||||
$ neo-go contract compile -in forint --config forint/config.yml --out forint/forint.nef --manifest forint/config.json
|
||||
|
||||
$ neo-go contract compile -in player/player_contract.go --config player/config.yml --out player/player.nef --manifest player/config.json
|
||||
|
||||
$ neo-go contract compile -in merchant/merchant_contract.go --config merchant/config.yml --out merchant/merchant.nef --manifest merchant/config.json
|
||||
```
|
||||
|
||||
### 2. Assign game contracts to the group
|
||||
|
||||
We need to assign game contracts (forint and player) to a single group. The group will be owned by `wallets/game-wallet.json`.
|
||||
|
||||
We assign group to contracts' manifests with the following commands:
|
||||
|
||||
```sh
|
||||
$ neo-go contract manifest add-group -n forint/forint.nef -m forint/config.json --sender NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --wallet ./wallets/game-wallet.json --address NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X
|
||||
|
||||
$ neo-go contract manifest add-group -n player/player.nef -m player/config.json --sender NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --wallet ./wallets/game-wallet.json --address NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X
|
||||
```
|
||||
|
||||
Now our contracts are ready to be deployed.
|
||||
|
||||
### 3. Deploy contracts
|
||||
|
||||
First, we need to transfer some gas to game wallet, as we are deploying game contracts on it's behalf:
|
||||
|
||||
```sh
|
||||
$ neo-go wallet nep17 transfer -r http://localhost:30333 -w ../../frostfs-aio/morph/node-wallet.json --from Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn --to NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --token GAS --amount 1000
|
||||
```
|
||||
|
||||
Now we deploy forint contract:
|
||||
|
||||
```sh
|
||||
$ neo-go contract deploy -r http://localhost:30333 -w ./wallets/game-wallet.json --in forint/forint.nef --manifest forint/config.json
|
||||
```
|
||||
|
||||
Copy contract hash from the command output and convert it to LE address, for example:
|
||||
|
||||
```sh
|
||||
...
|
||||
Contract: aaf122a18d90dbb31a6e9903bddd1db3841ab261
|
||||
|
||||
$ neo-go util convert 'aaf122a18d90dbb31a6e9903bddd1db3841ab261'
|
||||
BE ScriptHash to Address NbVpvjsPYAznMgnPBWDu6y6vPtXiHG3sBs
|
||||
LE ScriptHash to Address NUpY6DaaYVMuReDXu4JLNGxqjTfAiYQFio <-- NOTE: take this value
|
||||
...
|
||||
```
|
||||
|
||||
Then we can deploy player contract, it takes hash of forint contract as a parameter to its' _deploy method:
|
||||
|
||||
```sh
|
||||
$ neo-go contract deploy -r http://localhost:30333 -w ./wallets/game-wallet.json --in player/player.nef --manifest player/config.json [ hash160:NUpY6DaaYVMuReDXu4JLNGxqjTfAiYQFio ]
|
||||
```
|
||||
|
||||
Copy contract hash from the command output and convert it to LE address, for example:
|
||||
|
||||
```sh
|
||||
...
|
||||
--- Contract: 45d782ab0e18011030eeab7b3b7ee992e9373644
|
||||
|
||||
$ neo-go util convert '45d782ab0e18011030eeab7b3b7ee992e9373644'
|
||||
BE ScriptHash to Address NSHFyvFWHSf2x8cGt8u9dxyRThCDP8udKR
|
||||
LE ScriptHash to Address NS8e5xiXL5tTqAJ5ghq2j1mJbzhfcJ8th5 <-- NOTE: take this value
|
||||
...
|
||||
```
|
||||
|
||||
Finally we can deploy merchant contract. As merchant contract is an external contract and not part of the game, we will deploy it on behalf of node's wallet. It does not make any practical difference, but will be an indication of different parties in the blockchain. Merchant contract takes hash of player contract as a parameter to its' _deploy method, so the command is:
|
||||
|
||||
```sh
|
||||
$ neo-go contract deploy -r http://localhost:30333 -w ../../frostfs-aio/morph/node-wallet.json --in merchant/merchant.nef --manifest merchant/config.json [ hash160:NS8e5xiXL5tTqAJ5ghq2j1mJbzhfcJ8th5 ]
|
||||
```
|
||||
|
||||
Copy contract hash from the command output and convert it to LE address, for example:
|
||||
|
||||
```sh
|
||||
...
|
||||
# Contract: 443686682d4a3a59408a6b44b766baed894ad679 <-- NOTE: take this value
|
||||
|
||||
$ neo-go util convert '443686682d4a3a59408a6b44b766baed894ad679'
|
||||
BE ScriptHash to Address NS8eTGN8cNabMe6gG9zAZhHyMueuLfM94j
|
||||
LE ScriptHash to Address NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ <-- take this value
|
||||
```
|
||||
|
||||
### 4. Initialize game funds
|
||||
|
||||
We mint game currency (forints) and put it on account of the player contract, because this contract will manage game funds:
|
||||
|
||||
```sh
|
||||
$ neo-go contract invokefunction -r http://localhost:30333 -w ./wallets/game-wallet.json aaf122a18d90dbb31a6e9903bddd1db3841ab261 mint hash160:NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X -- 'NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X:CalledByEntry'
|
||||
|
||||
$ neo-go wallet nep17 transfer -r http://localhost:30333 -w ./wallets/game-wallet.json --from NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --to NS8e5xiXL5tTqAJ5ghq2j1mJbzhfcJ8th5 --token aaf122a18d90dbb31a6e9903bddd1db3841ab261 --amount 1000000
|
||||
```
|
||||
|
||||
### 5. Create a new player and trade for in-game currency
|
||||
|
||||
To create a new player, invoke `newPlayer` function on the player contract and specify name of the player. In example below we create a player with the name `demo`:
|
||||
|
||||
```sh
|
||||
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 newPlayer string:demo
|
||||
```
|
||||
|
||||
Check balance of the in-game currency on player's wallet:
|
||||
|
||||
```sh
|
||||
$ neo-go wallet nep17 balance -r http://localhost:30333 --wallet ../../wallet.json --token aaf122a18d90dbb31a6e9903bddd1db3841ab261
|
||||
Account NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW
|
||||
FAF: Fairy Forint (aaf122a18d90dbb31a6e9903bddd1db3841ab261)
|
||||
Amount : 3000
|
||||
```
|
||||
|
||||
Try to buy sword with scope None, it should fail:
|
||||
|
||||
```sh
|
||||
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:None'
|
||||
```
|
||||
|
||||
Try to do the same with any of the following scopes, it should work:
|
||||
|
||||
```sh
|
||||
neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:Global'
|
||||
|
||||
neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry,CustomContracts:aaf122a18d90dbb31a6e9903bddd1db3841ab261'
|
||||
|
||||
neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CustomGroups:029dd9b3639a23ea7ad4356fe7abb4f5a01eaea9f2e5c7138f3e174d21b03f682f'
|
||||
```
|
||||
|
||||
Try to sell item:
|
||||
|
||||
```sh
|
||||
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 sellItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry'
|
||||
```
|
||||
|
||||
### 5. Trade for gas
|
||||
|
||||
Transfer gas to account of merchant contract (you will need LE address of merchant contract that we've captured during deployment):
|
||||
|
||||
```sh
|
||||
$ neo-go wallet nep17 transfer -r http://localhost:30333 -w ../../frostfs-aio/morph/node-wallet.json --from Nhfg3TbpwogLvDGVvAvqyThbsHgoSU
|
||||
Kwtn --to NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ --token GAS --amount 1000
|
||||
```
|
||||
|
||||
Sell Sword to merchant for 20 GAS:
|
||||
|
||||
```sh
|
||||
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 sellItemForGas string:demo string:Sword int:20 hash160:NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry'
|
||||
```
|
||||
|
||||
Buy back sword from merchant for 23 GAS:
|
||||
|
||||
```sh
|
||||
$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 buyItemForGas str
|
||||
ing:demo int:1 int:23 hash160:NX2BfBKZaq2eS28Bn4oDA5uuKFKvB2UpxJ -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CustomContracts:d2a4cff31913016155e38e474a2c06d08be276cf:25b7b493c37fc740bac6603281de01f451847805'
|
||||
```
|
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.
14
l6/forint/config.yml
Normal file
14
l6/forint/config.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: "Fairy Forint"
|
||||
supportedstandards: ["NEP-17"]
|
||||
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply"]
|
||||
events:
|
||||
- name: Transfer
|
||||
parameters:
|
||||
- name: from
|
||||
type: Hash160
|
||||
- name: to
|
||||
type: Hash160
|
||||
- name: amount
|
||||
type: Integer
|
||||
permissions:
|
||||
- methods: ["onNEP17Payment"]
|
58
l6/forint/forint_contract.go
Normal file
58
l6/forint/forint_contract.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package forint
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/lib/address"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
)
|
||||
|
||||
func getToken() Token {
|
||||
// Owner of the wallet is wallets/game-wallet.json
|
||||
owner := address.ToHash160("NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X")
|
||||
token := Token{
|
||||
Name: "Fairy Forint",
|
||||
Symbol: "FAF",
|
||||
Decimals: 0,
|
||||
Owner: owner,
|
||||
TotalSupply: 1000000,
|
||||
CirculationKey: "TokenCirculation",
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// Symbol returns the token symbol.
|
||||
func Symbol() string {
|
||||
return getToken().Symbol
|
||||
}
|
||||
|
||||
// Decimals returns the number of digits after decimal point.
|
||||
func Decimals() int {
|
||||
return getToken().Decimals
|
||||
}
|
||||
|
||||
// TotalSupply returns the total amount of tokens.
|
||||
func TotalSupply() int {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return getToken().GetSupply(ctx)
|
||||
}
|
||||
|
||||
// BalanceOf returns the amount of tokens owned by the specified address.
|
||||
func BalanceOf(holder interop.Hash160) int {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return getToken().BalanceOf(ctx, holder)
|
||||
}
|
||||
|
||||
// Transfer moves token from one address to another.
|
||||
func Transfer(from interop.Hash160, to interop.Hash160, amount int, data any) bool {
|
||||
ctx := storage.GetContext()
|
||||
return getToken().Transfer(ctx, from, to, amount, data)
|
||||
}
|
||||
|
||||
// Mint generates initial supply of tokens.
|
||||
func Mint(to interop.Hash160) {
|
||||
ctx := storage.GetContext()
|
||||
minted := getToken().Mint(ctx, to)
|
||||
if !minted {
|
||||
panic("failed to mint initial supply")
|
||||
}
|
||||
}
|
160
l6/forint/token.go
Normal file
160
l6/forint/token.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
The implementation of the token logic has been copied "as is" from neo-go:
|
||||
https://github.com/nspcc-dev/neo-go/blob/7a1bf77585a37302ddfae78b2c88ed472f66cbcc/examples/token/nep17/nep17.go
|
||||
|
||||
The code is subject to the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2023 NeoSPCC (@nspcc-dev), Anthony De Meulemeester (@anthdm), City of Zion community (@CityOfZion)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package forint
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
)
|
||||
|
||||
// Token holds all token info
|
||||
type Token struct {
|
||||
// Token name
|
||||
Name string
|
||||
// Ticker symbol
|
||||
Symbol string
|
||||
// Amount of decimals
|
||||
Decimals int
|
||||
// Token owner address
|
||||
Owner []byte
|
||||
// Total tokens * multiplier
|
||||
TotalSupply int
|
||||
// Storage key for circulation value
|
||||
CirculationKey string
|
||||
}
|
||||
|
||||
// getIntFromDB is a helper that checks for nil result of storage.Get and returns
|
||||
// zero as the default value.
|
||||
func getIntFromDB(ctx storage.Context, key []byte) int {
|
||||
var res int
|
||||
val := storage.Get(ctx, key)
|
||||
if val != nil {
|
||||
res = val.(int)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// GetSupply gets the token totalSupply value from VM storage
|
||||
func (t Token) GetSupply(ctx storage.Context) int {
|
||||
return getIntFromDB(ctx, []byte(t.CirculationKey))
|
||||
}
|
||||
|
||||
// BalanceOf gets the token balance of a specific address
|
||||
func (t Token) BalanceOf(ctx storage.Context, holder []byte) int {
|
||||
return getIntFromDB(ctx, holder)
|
||||
}
|
||||
|
||||
// Transfer token from one user to another
|
||||
func (t Token) Transfer(ctx storage.Context, from, to interop.Hash160, amount int, data any) bool {
|
||||
amountFrom := t.CanTransfer(ctx, from, to, amount)
|
||||
if amountFrom == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if amountFrom == 0 {
|
||||
storage.Delete(ctx, from)
|
||||
}
|
||||
|
||||
if amountFrom > 0 {
|
||||
diff := amountFrom - amount
|
||||
storage.Put(ctx, from, diff)
|
||||
}
|
||||
|
||||
amountTo := getIntFromDB(ctx, to)
|
||||
totalAmountTo := amountTo + amount
|
||||
if totalAmountTo != 0 {
|
||||
storage.Put(ctx, to, totalAmountTo)
|
||||
}
|
||||
|
||||
runtime.Notify("Transfer", from, to, amount)
|
||||
if to != nil && management.GetContract(to) != nil {
|
||||
contract.Call(to, "onNEP17Payment", contract.All, from, amount, data)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CanTransfer returns the amount it can transfer
|
||||
func (t Token) CanTransfer(ctx storage.Context, from []byte, to []byte, amount int) int {
|
||||
if len(to) != 20 || !IsUsableAddress(from) {
|
||||
return -1
|
||||
}
|
||||
|
||||
amountFrom := getIntFromDB(ctx, from)
|
||||
if amountFrom < amount {
|
||||
return -1
|
||||
}
|
||||
|
||||
// Tell Transfer the result is equal - special case since it uses Delete
|
||||
if amountFrom == amount {
|
||||
return 0
|
||||
}
|
||||
|
||||
// return amountFrom value back to Transfer, reduces extra Get
|
||||
return amountFrom
|
||||
}
|
||||
|
||||
// IsUsableAddress checks if the sender is either the correct Neo address or SC address
|
||||
func IsUsableAddress(addr []byte) bool {
|
||||
if len(addr) == 20 {
|
||||
|
||||
if runtime.CheckWitness(addr) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if a smart contract is calling scripthash
|
||||
callingScriptHash := runtime.GetCallingScriptHash()
|
||||
if callingScriptHash.Equals(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Mint initial supply of tokens
|
||||
func (t Token) Mint(ctx storage.Context, to interop.Hash160) bool {
|
||||
if !IsUsableAddress(t.Owner) {
|
||||
return false
|
||||
}
|
||||
minted := storage.Get(ctx, []byte("minted"))
|
||||
if minted != nil && minted.(bool) == true {
|
||||
return false
|
||||
}
|
||||
|
||||
storage.Put(ctx, to, t.TotalSupply)
|
||||
storage.Put(ctx, []byte("minted"), true)
|
||||
storage.Put(ctx, []byte(t.CirculationKey), t.TotalSupply)
|
||||
var from interop.Hash160
|
||||
runtime.Notify("Transfer", from, to, t.TotalSupply)
|
||||
return true
|
||||
}
|
159
l6/gameclient/main.go
Normal file
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: "*"
|
222
l6/player/player_contract.go
Normal file
222
l6/player/player_contract.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
package player
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
||||
)
|
||||
|
||||
const (
|
||||
gasDecimals = 1_0000_0000
|
||||
initialBalance = 3000
|
||||
|
||||
forintHashKey = "forintHash"
|
||||
)
|
||||
|
||||
var itemPrices = map[string]int{
|
||||
"Sword": 200,
|
||||
"Shortbow": 100,
|
||||
"Longbow": 500,
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
owner interop.Hash160
|
||||
itemCount map[string]int
|
||||
}
|
||||
|
||||
func _deploy(data interface{}, isUpdate bool) {
|
||||
if isUpdate {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse hash of forint contract from incoming data
|
||||
args := data.(struct {
|
||||
forintHash interop.Hash160
|
||||
})
|
||||
if len(args.forintHash) != interop.Hash160Len {
|
||||
panic("invalid hash of forint contract")
|
||||
}
|
||||
|
||||
ctx := storage.GetContext()
|
||||
storage.Put(ctx, forintHashKey, args.forintHash)
|
||||
}
|
||||
|
||||
func NewPlayer(playerName string) {
|
||||
ctx := storage.GetContext()
|
||||
|
||||
existingPlayer := storage.Get(ctx, playerName)
|
||||
if existingPlayer != nil {
|
||||
panic("player already exists")
|
||||
}
|
||||
|
||||
// Transfer initial funds to the player owner
|
||||
playerOwner := runtime.GetScriptContainer().Sender
|
||||
changePlayerBalance(ctx, playerOwner, initialBalance)
|
||||
|
||||
player := Player{
|
||||
owner: playerOwner,
|
||||
itemCount: make(map[string]int),
|
||||
}
|
||||
savePlayer(ctx, playerName, player)
|
||||
}
|
||||
|
||||
func Items(playerName string) []string {
|
||||
p := getPlayer(storage.GetReadOnlyContext(), playerName)
|
||||
|
||||
items := make([]string, len(p.itemCount))
|
||||
for itemName, count := range p.itemCount {
|
||||
items = append(items, itemName+" "+std.Itoa10(count))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func BuyItem(playerName string, itemName string) {
|
||||
ctx := storage.GetContext()
|
||||
player := getPlayer(ctx, playerName)
|
||||
if !runtime.CheckWitness(player.owner) {
|
||||
panic("player not witnessed")
|
||||
}
|
||||
|
||||
itemPrice := itemPrices[itemName]
|
||||
changePlayerBalance(ctx, player.owner, -itemPrice)
|
||||
addItemToInventory(player, itemName)
|
||||
|
||||
savePlayer(ctx, playerName, player)
|
||||
}
|
||||
|
||||
func SellItem(playerName string, itemName string) {
|
||||
ctx := storage.GetContext()
|
||||
player := getPlayer(ctx, playerName)
|
||||
if !runtime.CheckWitness(player.owner) {
|
||||
panic("player not witnessed")
|
||||
}
|
||||
|
||||
removeItemFromInventory(player, itemName)
|
||||
itemPrice := itemPrices[itemName]
|
||||
changePlayerBalance(ctx, player.owner, itemPrice)
|
||||
|
||||
savePlayer(ctx, playerName, player)
|
||||
}
|
||||
|
||||
func BuyItemForGas(playerName string, lotID int, itemPrice int, merchantHash interop.Hash160) {
|
||||
ctx := storage.GetContext()
|
||||
player := getPlayer(ctx, playerName)
|
||||
if !runtime.CheckWitness(player.owner) {
|
||||
panic("player not witnessed")
|
||||
}
|
||||
|
||||
itemName := contract.Call(merchantHash, "getItemName", contract.ReadOnly, lotID).(string)
|
||||
|
||||
gasPrice := itemPrice * gasDecimals
|
||||
success := gas.Transfer(runtime.GetScriptContainer().Sender, merchantHash, gasPrice, lotID)
|
||||
// If we were to use custom token instead of GAS, we would call it like a regular contract:
|
||||
// success := contract.Call(<tokenContractHash>, "transfer", contract.All, runtime.GetScriptContainer().Sender, merchantHash, gasPrice, lotID).(bool)
|
||||
|
||||
if !success {
|
||||
panic("failed to transfer gas to merchant")
|
||||
}
|
||||
|
||||
addItemToInventory(player, itemName)
|
||||
savePlayer(ctx, playerName, player)
|
||||
}
|
||||
|
||||
func SellItemForGas(playerName string, itemName string, itemPrice int, merchantHash interop.Hash160) {
|
||||
ctx := storage.GetContext()
|
||||
player := getPlayer(ctx, playerName)
|
||||
if !runtime.CheckWitness(player.owner) {
|
||||
panic("player not witnessed")
|
||||
}
|
||||
|
||||
removeItemFromInventory(player, itemName)
|
||||
|
||||
balanceBefore := gas.BalanceOf(runtime.GetScriptContainer().Sender)
|
||||
contract.Call(merchantHash, "sell", contract.All, itemName, itemPrice)
|
||||
balanceAfter := gas.BalanceOf(runtime.GetScriptContainer().Sender)
|
||||
|
||||
gasProfit := (balanceAfter - balanceBefore)
|
||||
if gasProfit < itemPrice*gasDecimals {
|
||||
runtime.Log("merchant payment does not cover item price: " + std.Itoa10(gasProfit) + " < " + std.Itoa10(itemPrice*gasDecimals))
|
||||
util.Abort()
|
||||
}
|
||||
|
||||
savePlayer(ctx, playerName, player)
|
||||
}
|
||||
|
||||
func OnNEP17Payment(from interop.Hash160, amount int, data any) {
|
||||
ctx := storage.GetContext()
|
||||
forintHash := storage.Get(ctx, forintHashKey).(interop.Hash160)
|
||||
|
||||
callingHash := runtime.GetCallingScriptHash()
|
||||
if !callingHash.Equals(forintHash) {
|
||||
panic("only FAF is accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func getPlayer(ctx storage.Context, playerName string) Player {
|
||||
data := storage.Get(ctx, playerName)
|
||||
if data == nil {
|
||||
panic("player not found")
|
||||
}
|
||||
|
||||
return std.Deserialize(data.([]byte)).(Player)
|
||||
}
|
||||
|
||||
func savePlayer(ctx storage.Context, playerName string, player Player) {
|
||||
storage.Put(ctx, playerName, std.Serialize(player))
|
||||
}
|
||||
|
||||
func hasItemInInventory(player Player, itemName string) bool {
|
||||
// Standard syntax for go map does not work: _, exists := player.itemCount[itemName]
|
||||
// So, we use HASKEY command to do this:
|
||||
hasKey := neogointernal.Opcode2("HASKEY", player.itemCount, itemName).(bool)
|
||||
if !hasKey {
|
||||
return false
|
||||
}
|
||||
return player.itemCount[itemName] > 0
|
||||
}
|
||||
|
||||
func addItemToInventory(player Player, itemName string) {
|
||||
if !hasItemInInventory(player, itemName) {
|
||||
player.itemCount[itemName] = 0
|
||||
}
|
||||
player.itemCount[itemName] += 1
|
||||
}
|
||||
|
||||
func removeItemFromInventory(player Player, itemName string) {
|
||||
if !hasItemInInventory(player, itemName) {
|
||||
panic("player has no specified item")
|
||||
}
|
||||
player.itemCount[itemName] -= 1
|
||||
if player.itemCount[itemName] == 0 {
|
||||
delete(player.itemCount, itemName)
|
||||
}
|
||||
}
|
||||
|
||||
func changePlayerBalance(ctx storage.Context, playerOwner interop.Hash160, balanceChange int) {
|
||||
forintHash := storage.Get(ctx, forintHashKey).(interop.Hash160)
|
||||
playerContract := runtime.GetExecutingScriptHash()
|
||||
|
||||
var from, to interop.Hash160
|
||||
var transferAmount int
|
||||
if balanceChange > 0 {
|
||||
// Transfer funds from contract to player owner
|
||||
from = playerContract
|
||||
to = playerOwner
|
||||
transferAmount = balanceChange
|
||||
} else {
|
||||
// Transfer funds from player owner to contract
|
||||
from = playerOwner
|
||||
to = playerContract
|
||||
transferAmount = -balanceChange // We flip sender/receiver, but keep amount positive
|
||||
}
|
||||
|
||||
transferred := contract.Call(forintHash, "transfer", contract.All, from, to, transferAmount, nil).(bool)
|
||||
if !transferred {
|
||||
panic("failed to transfer forints")
|
||||
}
|
||||
}
|
1
l6/wallets/game-wallet.json
Normal file
1
l6/wallets/game-wallet.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":"1.0","accounts":[{"address":"NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X","key":"6PYQRSvVEjU8PvLRqygDuzXyFsCQY5de6tzwVk63GuBwq2dUNCGtzyEQvD","label":"l6-game","contract":{"script":"DCECndmzY5oj6nrUNW/nq7T1oB6uqfLlxxOPPhdNIbA/aC9BVuezJw==","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isDefault":false}],"scrypt":{"n":16384,"r":8,"p":8},"extra":{"Tokens":null}}
|
Loading…
Reference in a new issue