From 74db02ef42509eef7cc1d485b1b4a387138833a1 Mon Sep 17 00:00:00 2001 From: Vladimir Domnich Date: Sun, 26 Nov 2023 23:16:14 +0400 Subject: [PATCH] Implement in-game currency as nep17 Buy/sell items using nep17 tokens instead of in-game balance. Signed-off-by: Vladimir Domnich --- l6/data.json | 42 --------- l6/docs/README.md | 129 ++++++++++++++++------------ l6/forint/config.yml | 14 +++ l6/forint/forint_contract.go | 58 +++++++++++++ l6/forint/token.go | 160 +++++++++++++++++++++++++++++++++++ l6/player/player_contract.go | 82 ++++++++++++++---- l6/wallets/game-wallet.json | 1 + 7 files changed, 375 insertions(+), 111 deletions(-) delete mode 100644 l6/data.json create mode 100644 l6/forint/config.yml create mode 100644 l6/forint/forint_contract.go create mode 100644 l6/forint/token.go create mode 100644 l6/wallets/game-wallet.json diff --git a/l6/data.json b/l6/data.json deleted file mode 100644 index a8d4f08..0000000 --- a/l6/data.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "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 - } - ] - } -} \ No newline at end of file diff --git a/l6/docs/README.md b/l6/docs/README.md index e9ab4e9..2e69cd3 100644 --- a/l6/docs/README.md +++ b/l6/docs/README.md @@ -2,128 +2,151 @@ To run this example we will need: - * Wallet with sufficient GAS on it. In our example we will be using wallet with address `NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW`. + * 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: - * Our wallet is located next to `web3-course` directory. + * 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/confi -g.json +$ neo-go contract compile -in merchant/merchant_contract.go --config merchant/config.yml --out merchant/merchant.nef --manifest merchant/config.json ``` -### 2. Assign player contract to the group +### 2. Assign game contracts 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. +We need to assign game contracts (forint and player) to a single group. The group will be owned by `wallets/game-wallet.json`. -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: +We assign group to contracts' manifests with the following commands: ```sh -$ neo-go wallet init -w ../../group-wallet.json -a -Enter the name of the account > l6-game -Enter new password > -Confirm password > -``` +$ neo-go contract manifest add-group -n forint/forint.nef -m forint/config.json --sender NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X --wallet ./wallets/game-wallet.json --address NYThT8RYFhkBzJfzMwCe67A63p2WoDev9X -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 +$ 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 -Now we can deploy player contract: +First, we need to transfer some gas to game wallet, as we are deploying game contracts on it's behalf: ```sh -$ $ neo-go contract deploy -r http://localhost:30333 -w ../../wallet.json --in player/player.nef --manifest player/config.json +$ 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: 25b7b493c37fc740bac6603281de01f451847805 +Contract: aaf122a18d90dbb31a6e9903bddd1db3841ab261 -$ neo-go util convert '25b7b493c37fc740bac6603281de01f451847805' -BE ScriptHash to Address NPMQFvjSdBWotKA1PrsrkAtiSzyRXPhDF9 -LE ScriptHash to Address NLQtwNfn4LW3As2r2YfkaRZTpoiTuRfEc5 <-- NOTE: take this value +$ neo-go util convert 'aaf122a18d90dbb31a6e9903bddd1db3841ab261' +BE ScriptHash to Address NbVpvjsPYAznMgnPBWDu6y6vPtXiHG3sBs +LE ScriptHash to Address NUpY6DaaYVMuReDXu4JLNGxqjTfAiYQFio <-- NOTE: take this value ... ``` -Now we can deploy merchant contract, it takes hash of player contract as a parameter to its' _deploy method: +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 ../../wallet.json --in merchant/merchant.nef --manifest merchant/config.json [ hash160:N -LQtwNfn4LW3As2r2YfkaRZTpoiTuRfEc5 ] +$ 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: 443686682d4a3a59408a6b44b766baed894ad679 <-- NOTE: take this value +--- 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. Create a new player and trade for in-game currency +### 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 25b7b493c37fc740bac6603281de01f451847805 newPlayer string:demo +$ 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 25b7b493c37fc740bac6603281de01f451847805 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:None' +$ 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 scope CalledByEntry, it should work: +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 25b7b493c37fc740bac6603281de01f451847805 buyItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry' +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 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' +$ neo-go contract invokefunction -r http://localhost:30333 -w ../../wallet.json 45d782ab0e18011030eeab7b3b7ee992e9373644 sellItem string:demo string:Sword -- 'NP8wjGz3Wvxe4gUAkTbK2McR95Y4LM2jMW:CalledByEntry' ``` ### 5. Trade for gas @@ -138,12 +161,12 @@ 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' +$ 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 25b7b493c37fc740bac6603281de01f451847805 buyItemForGas str +$ 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' ``` diff --git a/l6/forint/config.yml b/l6/forint/config.yml new file mode 100644 index 0000000..5094f41 --- /dev/null +++ b/l6/forint/config.yml @@ -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"] diff --git a/l6/forint/forint_contract.go b/l6/forint/forint_contract.go new file mode 100644 index 0000000..13b92ab --- /dev/null +++ b/l6/forint/forint_contract.go @@ -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") + } +} diff --git a/l6/forint/token.go b/l6/forint/token.go new file mode 100644 index 0000000..01eab14 --- /dev/null +++ b/l6/forint/token.go @@ -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 +} diff --git a/l6/player/player_contract.go b/l6/player/player_contract.go index 46e31a4..35c820a 100644 --- a/l6/player/player_contract.go +++ b/l6/player/player_contract.go @@ -11,20 +11,41 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop/util" ) -const gasDecimals = 1_0000_0000 +const ( + gasDecimals = 1_0000_0000 + initialBalance = 3000 + + forintHashKey = "forintHash" +) var itemPrices = map[string]int{ "Sword": 200, "Shortbow": 100, - "Longbow": 300, + "Longbow": 500, } type Player struct { - balance int 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() @@ -33,19 +54,17 @@ func NewPlayer(playerName string) { panic("player already exists") } + // Transfer initial funds to the player owner + playerOwner := runtime.GetScriptContainer().Sender + changePlayerBalance(ctx, playerOwner, initialBalance) + player := Player{ - balance: 3000, - owner: runtime.GetScriptContainer().Sender, + owner: playerOwner, 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) @@ -64,12 +83,8 @@ func BuyItem(playerName string, itemName string) { } itemPrice := itemPrices[itemName] - if player.balance < itemPrice { - panic("insufficient balance") - } - + changePlayerBalance(ctx, player.owner, -itemPrice) addItemToInventory(player, itemName) - player.balance -= itemPrice savePlayer(ctx, playerName, player) } @@ -82,7 +97,8 @@ func SellItem(playerName string, itemName string) { } removeItemFromInventory(player, itemName) - player.balance += itemPrices[itemName] + itemPrice := itemPrices[itemName] + changePlayerBalance(ctx, player.owner, itemPrice) savePlayer(ctx, playerName, player) } @@ -131,6 +147,16 @@ func SellItemForGas(playerName string, itemName string, itemPrice int, merchantH 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 { @@ -170,3 +196,27 @@ func removeItemFromInventory(player Player, itemName string) { 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") + } +} diff --git a/l6/wallets/game-wallet.json b/l6/wallets/game-wallet.json new file mode 100644 index 0000000..00887ed --- /dev/null +++ b/l6/wallets/game-wallet.json @@ -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}} \ No newline at end of file