Add nyan nft contract and client/server

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2024-12-26 16:26:11 +03:00
parent 190d272daa
commit f6d52cd8e3
11 changed files with 1775 additions and 0 deletions

112
app/README.md Normal file
View file

@ -0,0 +1,112 @@
# Nyan cat example
## Prerequisite
* frostfs-aio https://git.frostfs.info/TrueCloudLab/frostfs-aio
## Set Up
1. Run frostfs-aio https://git.frostfs.info/TrueCloudLab/frostfs-aio/src/branch/nightly-v1.7 (branch `nightly-v1.7`)
using `make up`
2. Run `make refill` to get gas to `/path/to/frostfs-aio/wallets/wallet1.json` wallet (this wallet be used as backend
wallet)
3. Compile and deploy `NYAN` NFT token contract:
```shell
$ neo-go contract compile -i nyan/contract.go -o nyan/contract.nef -m nyan/contract.manifest.json -c nyan/contract.yml
$ neo-go contract deploy -i nyan/contract.nef -m nyan/contract.manifest.json -r http://localhost:30333 -w /path/to/frostfs-aio/wallets/wallet1.json [ NhCHDEtGgSph1v6PmjFC1gtzJWNKtNSadk ]
...
Contract: f167e80e620a70b47cd6f8c8e37229107dfab340
```
3. Create public-read-write container:
```shell
$ make cred
# create user-cli-cfg.yaml file with the following content
$ cat user-cli-cfg.yaml
wallet: /config/user-wallet.json
password: ""
rpc-endpoint: localhost:8080
$ docker cp user-cli-cfg.yaml aio:/config/user-cli-cfg.yaml
$ docker exec aio frostfs-cli container create -c /config/user-cli-cfg.yaml --policy 'REP 1' --await
CID: 2aK1oHPovFPoqQwf6A3DPjwPBYUKmEqhWE81omGauNTH
awaiting...
container has been persisted on sidechain
$ docker exec aio frostfs-cli -c /config/user-cli-cfg.yaml ape-manager add --chain-id nyan --rule 'Allow Object.* *' --target-type container --target-name 2aK1oHPovFPoqQwf6A3DPjwPBYUKmEqhWE81omGauNTH
```
## Architecture Overview
![HighLevelArchitecture](seq.svg)
More about notary request: https://github.com/nspcc-dev/neo-go/blob/master/docs/notary.md
## Run server
Update `backend/config.yml` with contract hash and container id from previous steps
```yaml
wallet: "/path/to/frostfs-aio/wallets/wallet1.json"
password: ""
rpc_endpoint: "http://localhost:30333"
rpc_endpoint_ws: "ws://localhost:30333/ws"
nyan_contract: "f167e80e620a70b47cd6f8c8e37229107dfab340"
storage_node: "localhost:8080"
storage_container: "2aK1oHPovFPoqQwf6A3DPjwPBYUKmEqhWE81omGauNTH"
listen_address: ":5555"
```
Run server:
```shell
$ go run backend/main.go backend/config.yml
2024-12-26T17:37:53.272+0300 INFO backend/main.go:315 start listening
```
## Run client
Create brand-new wallet:
```shell
$ neo-go wallet init -a -w wallet.json
```
Update `client/config.yml`:
```yaml
wallet: "wallet.json"
password: ""
rpc_endpoint: "http://localhost:30333"
backend_key: "03b09baabff3f6107c7e9acb8721a6fc5618d45b50247a314d82e548702cce8cd5"
nyan_contract: "f167e80e620a70b47cd6f8c8e37229107dfab340"
backend_url: "http://localhost:5555"
```
Run client:
```shell
$ go run client/main.go client/config.yml
Notarize sending: mainHash - a5ebe7957e54a18fa433fe497b50e11b656a1bdd4595965ba21af49027d2ce0e, fallbackHash - 151be0a2fc3bd39819a2fa95f105532a93723c82112bd188ed735edc5cc3cf9a, vub - 1818
new token id 9db58027be52d270e7ee57385c8731d8eebc6c6deb71e2c8ea11ddc5bd588273
```
Now you can get nft properties:
```shell
$ curl http://localhost:5555/properties/9db58027be52d270e7ee57385c8731d8eebc6c6deb71e2c8ea11ddc5bd588273 | jq
{
"address": "2aK1oHPovFPoqQwf6A3DPjwPBYUKmEqhWE81omGauNTH/DdVQ1rhjeHfLqFdNbhW56RGkzJQnDUPsdZ9jVpMjCcCk",
"id": "9db58027be52d270e7ee57385c8731d8eebc6c6deb71e2c8ea11ddc5bd588273",
"name": "404.gif",
"owner": "NL29YAWjsuj3JUFGgsekhyRTKUYTDqXg2G"
}
```
You also can get gif using frostfs-http-gw, just use the url with address from previous
step `http://localhost:8081/get/<address>`:
http://localhost:8081/get/2aK1oHPovFPoqQwf6A3DPjwPBYUKmEqhWE81omGauNTH/DdVQ1rhjeHfLqFdNbhW56RGkzJQnDUPsdZ9jVpMjCcCk

8
app/backend/config.yml Normal file
View file

@ -0,0 +1,8 @@
wallet: "/backend/wallet.json"
password: ""
rpc_endpoint: "http://localhost:30333"
rpc_endpoint_ws: "ws://localhost:30333/ws"
nyan_contract: "f167e80e620a70b47cd6f8c8e37229107dfab340"
storage_node: "localhost:8080"
storage_container: "2aK1oHPovFPoqQwf6A3DPjwPBYUKmEqhWE81omGauNTH"
listen_address: ":5555"

670
app/backend/main.go Normal file
View file

@ -0,0 +1,670 @@
package main
import (
"bytes"
"context"
"crypto/elliptic"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"net/http"
"os"
"os/signal"
"runtime/debug"
"strconv"
"syscall"
"time"
morphclient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/subscriber"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/mempoolevent"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"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/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/notary"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/spf13/viper"
"go.uber.org/zap"
)
const (
cfgRPCEndpoint = "rpc_endpoint"
cfgRPCEndpointWS = "rpc_endpoint_ws"
cfgWallet = "wallet"
cfgPassword = "password"
cfgNyanContract = "nyan_contract"
cfgStorageNode = "storage_node"
cfgStorageContainer = "storage_container"
cfgListenAddress = "listen_address"
)
func main() {
ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
if len(os.Args) != 2 {
die(fmt.Errorf("invalid args: %v", os.Args))
}
viper.GetViper().SetConfigType("yml")
f, err := os.Open(os.Args[1])
die(err)
die(viper.GetViper().ReadConfig(f))
die(f.Close())
s, err := NewServer(ctx)
die(err)
die(s.Listen(ctx))
}
type Server struct {
p *pool.Pool
acc *wallet.Account
act *actor.Actor
gasAct *nep17.Token
nyanHash util.Uint160
cnrID cid.ID
log *zap.Logger
rpcCli *rpcclient.Client
sub subscriber.Subscriber
}
func NewServer(ctx context.Context) (*Server, error) {
rpcCli, err := rpcclient.New(ctx, viper.GetString(cfgRPCEndpoint), rpcclient.Options{})
if err != nil {
return nil, err
}
w, err := wallet.NewWalletFromFile(viper.GetString(cfgWallet))
if err != nil {
return nil, err
}
acc := w.GetAccount(w.GetChangeAddress())
if err = acc.Decrypt(viper.GetString(cfgPassword), w.Scrypt); err != nil {
return nil, err
}
act, err := actor.NewSimple(rpcCli, acc)
if err != nil {
return nil, err
}
p, err := createPool(ctx, acc, viper.GetString(cfgStorageNode))
if err != nil {
return nil, err
}
contractNyanHash, err := util.Uint160DecodeStringLE(viper.GetString(cfgNyanContract))
if err != nil {
return nil, err
}
var cnrID cid.ID
if err = cnrID.DecodeString(viper.GetString(cfgStorageContainer)); err != nil {
return nil, err
}
neoClient, err := morphclient.New(ctx, acc.PrivateKey(),
morphclient.WithEndpoints(morphclient.Endpoint{Address: viper.GetString(cfgRPCEndpointWS), Priority: 1}),
)
if err != nil {
return nil, fmt.Errorf("new morph client: %w", err)
}
if err = neoClient.EnableNotarySupport(); err != nil {
return nil, err
}
params := new(subscriber.Params)
params.Client = neoClient
l, err := logger.NewLogger(nil)
if err != nil {
return nil, err
}
params.Log = l
sub, err := subscriber.New(ctx, params)
if err != nil {
return nil, err
}
if err = sub.SubscribeForNotaryRequests(acc.ScriptHash()); err != nil {
return nil, err
}
log, err := zap.NewDevelopment()
if err != nil {
return nil, err
}
return &Server{
p: p,
acc: acc,
act: act,
rpcCli: rpcCli,
nyanHash: contractNyanHash,
gasAct: nep17.New(act, gas.Hash),
cnrID: cnrID,
log: log,
sub: sub,
}, nil
}
func (s *Server) Listen(ctx context.Context) error {
if err := s.notaryDeposit(s.acc.ScriptHash()); err != nil {
return fmt.Errorf("notary backend deposit: %w", err)
}
go s.runNotaryValidator(ctx)
http.DefaultServeMux.HandleFunc("/balance", func(w http.ResponseWriter, r *http.Request) {
s.log.Info("balance request")
res, err := s.gasAct.BalanceOf(s.acc.ScriptHash())
if err != nil {
s.log.Error("balance error", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
if _, err = w.Write([]byte(strconv.FormatInt(res.Int64(), 10))); err != nil {
s.log.Error("write response error", zap.Error(err))
}
})
http.DefaultServeMux.HandleFunc("/properties/{tokenID}", func(w http.ResponseWriter, r *http.Request) {
s.log.Info("properties request")
tokenIDStr := r.PathValue("tokenID")
tokenID, err := hex.DecodeString(tokenIDStr)
if err != nil {
s.log.Error("invalid token ID", zap.String("tokenID", tokenIDStr), zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
m, err := unwrap.Map(s.act.Call(s.nyanHash, "properties", tokenID))
if err != nil {
s.log.Error("call properties", zap.String("tokenID", tokenIDStr), zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
props, err := parseMap(m)
if err != nil {
s.log.Error("parse properties", zap.String("tokenID", tokenIDStr), zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
data, err := json.Marshal(props)
if err != nil {
s.log.Error("parse properties", zap.String("tokenID", tokenIDStr), zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
if _, err = w.Write(data); err != nil {
s.log.Error("write response error", zap.Error(err))
}
})
http.DefaultServeMux.HandleFunc("/notary-deposit/{userAddress}", func(w http.ResponseWriter, r *http.Request) {
s.log.Info("notary-deposit request", zap.String("url", r.URL.String()))
var userID user.ID
err := userID.DecodeString(r.PathValue("userAddress"))
if err != nil {
s.log.Error("invalid user address", zap.String("address", r.PathValue("userAddress")), zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
sh, err := userID.ScriptHash()
if err != nil {
s.log.Error("invalid user script hash", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
if err = s.notaryDeposit(sh); err != nil {
s.log.Error("failed to notary deposit", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
return http.ListenAndServe(viper.GetString(cfgListenAddress), nil)
}
func parseMap(m *stackitem.Map) (map[string]string, error) {
items := m.Value().([]stackitem.MapElement)
res := make(map[string]string)
for _, item := range items {
k, err := item.Key.TryBytes()
if err != nil {
return nil, err
}
v, err := item.Value.TryBytes()
if err != nil {
return nil, err
}
kStr := string(k)
switch kStr {
case "id":
res[kStr] = hex.EncodeToString(v)
default:
res[kStr] = string(v)
}
}
return res, nil
}
func createPool(ctx context.Context, acc *wallet.Account, addr string) (*pool.Pool, error) {
var prm pool.InitParameters
prm.SetKey(&acc.PrivateKey().PrivateKey)
prm.AddNode(pool.NewNodeParam(1, addr, 1))
prm.SetNodeDialTimeout(5 * time.Second)
p, err := pool.NewPool(prm)
if err != nil {
return nil, fmt.Errorf("new Pool: %w", err)
}
err = p.Dial(ctx)
if err != nil {
return nil, fmt.Errorf("dial: %w", err)
}
return p, nil
}
func (s *Server) runNotaryValidator(ctx context.Context) {
s.log.Info("start listening")
for {
select {
case <-ctx.Done():
die(ctx.Err())
case notaryEvent, ok := <-s.sub.NotificationChannels().NotaryRequestsCh:
if !ok {
return
}
s.log.Info("notary request", zap.String("hash", notaryEvent.NotaryRequest.Hash().String()),
zap.String("main", notaryEvent.NotaryRequest.MainTransaction.Hash().String()),
zap.String("fb", notaryEvent.NotaryRequest.FallbackTransaction.Hash().String()))
switch notaryEvent.Type {
case mempoolevent.TransactionAdded:
tokenName, err := s.parseNotaryEvent(notaryEvent)
if err != nil {
s.log.Error("parse notary event", zap.Error(err))
continue
}
nAct := s.notaryActor(notaryEvent.NotaryRequest.MainTransaction.Scripts[1])
isMain, err := s.checkNotaryRequest(nAct, tokenName)
if err != nil {
s.log.Error("check notary request", zap.Error(err))
continue
}
if isMain {
err = s.proceedMainTx(ctx, nAct, notaryEvent, tokenName)
} else {
err = s.proceedFbTx(ctx, nAct, notaryEvent, tokenName)
}
if err != nil {
s.log.Error("proceed notary tx", zap.Bool("main", isMain), zap.String("token", tokenName), zap.Error(err))
} else {
s.log.Info("proceed notary tx", zap.Bool("main", isMain), zap.String("token", tokenName))
}
}
}
}
}
func (s *Server) parseNotaryEvent(notaryEvent *result.NotaryRequestEvent) (string, error) {
if len(notaryEvent.NotaryRequest.MainTransaction.Signers) != 3 {
return "", errors.New("error not enough signers")
}
if notaryEvent.NotaryRequest.Witness.ScriptHash().Equals(s.acc.ScriptHash()) {
return "", fmt.Errorf("ignore owned notary request: %s", notaryEvent.NotaryRequest.Hash().String())
}
_, tokenName, err := validateNotaryRequest(notaryEvent.NotaryRequest)
if err != nil {
return "", err
}
return tokenName, err
}
func (s *Server) notaryActor(userWitness transaction.Witness) *notary.Actor {
pubBytes, ok := vm.ParseSignatureContract(userWitness.VerificationScript)
if !ok {
die(errors.New("invalid verification script"))
}
pub, err := keys.NewPublicKeyFromBytes(pubBytes, elliptic.P256())
die(err)
userAcc := notary.FakeSimpleAccount(pub)
coSigners := []actor.SignerAccount{
{
Signer: transaction.Signer{
Account: s.acc.ScriptHash(),
Scopes: transaction.None,
},
Account: s.acc,
},
{
Signer: transaction.Signer{
Account: userAcc.ScriptHash(),
Scopes: transaction.Global,
},
Account: userAcc,
},
}
nAct, err := notary.NewActor(s.rpcCli, coSigners, s.acc)
die(err)
return nAct
}
func (s *Server) notaryDeposit(to util.Uint160) error {
data := []any{to, int64(math.MaxUint32)}
_, err := s.act.Wait(s.gasAct.Transfer(s.act.Sender(), notary.Hash, big.NewInt(1*native.GASFactor), data))
return err
}
func (s *Server) checkNotaryRequest(nAct *notary.Actor, tokenName string) (bool, error) {
return true, nil
}
func (s *Server) proceedMainTx(ctx context.Context, nAct *notary.Actor, notaryEvent *result.NotaryRequestEvent, tokenName string) error {
err := nAct.Sign(notaryEvent.NotaryRequest.MainTransaction)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
mainHash, fallbackHash, vub, err := nAct.Notarize(notaryEvent.NotaryRequest.MainTransaction, nil)
s.log.Info("notarize sending",
zap.String("hash", notaryEvent.NotaryRequest.Hash().String()),
zap.String("main", mainHash.String()), zap.String("fb", fallbackHash.String()),
zap.Uint32("vub", vub))
_, err = nAct.Wait(mainHash, fallbackHash, vub, err)
if err != nil {
return fmt.Errorf("wait: %w", err)
}
url := "https://www.nyan.cat/cats/" + tokenName
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("get url '%s' : %w", url, err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
s.log.Error("close response bode", zap.Error(err))
}
}()
var ownerID user.ID
user.IDFromKey(&ownerID, s.acc.PrivateKey().PrivateKey.PublicKey)
obj := object.New()
obj.SetContainerID(s.cnrID)
obj.SetOwnerID(ownerID)
var prm pool.PrmObjectPut
prm.SetPayload(resp.Body)
prm.SetHeader(*obj)
objID, err := s.p.PutObject(ctx, prm)
if err != nil {
return fmt.Errorf("put object '%s': %w", url, err)
}
addr := s.cnrID.EncodeToString() + "/" + objID.ObjectID.EncodeToString()
s.log.Info("put object", zap.String("url", url), zap.String("address", addr))
_, err = s.act.Wait(s.act.SendCall(s.nyanHash, "setAddress", tokenName, addr))
if err != nil {
return fmt.Errorf("wait setAddress: %w", err)
}
return nil
}
func (s *Server) proceedFbTx(ctx context.Context, nAct *notary.Actor, notaryEvent *result.NotaryRequestEvent, tokenName string) error {
err := nAct.Sign(notaryEvent.NotaryRequest.FallbackTransaction)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
_, err = nAct.Wait(nAct.Notarize(notaryEvent.NotaryRequest.FallbackTransaction, nil))
if err != nil {
return fmt.Errorf("wait: %w", err)
}
return nil
}
// Op is wrapper over Neo VM's opcode
// and its parameter.
type Op struct {
code opcode.Opcode
param []byte
}
// Code returns Neo VM opcode.
func (o Op) Code() opcode.Opcode {
return o.code
}
// Param returns parameter of wrapped
// Neo VM opcode.
func (o Op) Param() []byte {
return o.param
}
func validateNotaryRequest(req *payload.P2PNotaryRequest) (util.Uint160, string, error) {
var (
opCode opcode.Opcode
param []byte
)
ctx := vm.NewContext(req.MainTransaction.Script)
ops := make([]Op, 0, 10) // 10 is maximum num of opcodes for calling contracts with 4 args(no arrays of arrays)
var err error
for {
opCode, param, err = ctx.Next()
if err != nil {
return util.Uint160{}, "", fmt.Errorf("could not get next opcode in script: %w", err)
}
if opCode == opcode.RET {
break
}
ops = append(ops, Op{code: opCode, param: param})
}
opsLen := len(ops)
contractSysCall := make([]byte, 4)
binary.LittleEndian.PutUint32(contractSysCall, interopnames.ToID([]byte(interopnames.SystemContractCall)))
// check if it is tx with contract call
if !bytes.Equal(ops[opsLen-1].param, contractSysCall) {
return util.Uint160{}, "", errors.New("not contract syscall")
}
// retrieve contract's script hash
contractHash, err := util.Uint160DecodeBytesBE(ops[opsLen-2].param)
if err != nil {
return util.Uint160{}, "", err
}
contractHashExpected, err := util.Uint160DecodeStringLE("f167e80e620a70b47cd6f8c8e37229107dfab340")
if err != nil {
return util.Uint160{}, "", err
}
if !contractHash.Equals(contractHashExpected) {
return util.Uint160{}, "", fmt.Errorf("unexpected contract hash: %s", contractHash)
}
// retrieve contract's method
contractMethod := string(ops[opsLen-3].param)
if contractMethod != "mint" {
return util.Uint160{}, "", fmt.Errorf("unexpecred contract method: %s", contractMethod)
}
// check if there is a call flag(must be in range [0:15))
callFlag := callflag.CallFlag(ops[opsLen-4].code - opcode.PUSH0)
if callFlag > callflag.All {
return util.Uint160{}, "", fmt.Errorf("incorrect call flag: %s", callFlag)
}
args := ops[:opsLen-4]
if len(args) != 0 {
err = validateParameterOpcodes(args)
if err != nil {
return util.Uint160{}, "", fmt.Errorf("could not validate arguments: %w", err)
}
// without args packing opcodes
args = args[:len(args)-2]
}
if len(args) != 2 {
return util.Uint160{}, "", fmt.Errorf("invalid param length: %d", len(args))
}
sh, err := util.Uint160DecodeBytesBE(args[1].Param())
return sh, string(args[0].Param()), err
}
// IntFromOpcode tries to retrieve int from Op.
func IntFromOpcode(op Op) (int64, error) {
switch code := op.Code(); {
case code == opcode.PUSHM1:
return -1, nil
case code >= opcode.PUSH0 && code <= opcode.PUSH16:
return int64(code - opcode.PUSH0), nil
case code <= opcode.PUSHINT256:
return bigint.FromBytes(op.Param()).Int64(), nil
default:
return 0, fmt.Errorf("unexpected INT opcode %s", code)
}
}
func validateParameterOpcodes(ops []Op) error {
l := len(ops)
if ops[l-1].code != opcode.PACK {
return fmt.Errorf("unexpected packing opcode: %s", ops[l-1].code)
}
argsLen, err := IntFromOpcode(ops[l-2])
if err != nil {
return fmt.Errorf("could not parse argument len: %w", err)
}
err = validateNestedArgs(argsLen, ops[:l-2])
return err
}
func validateNestedArgs(expArgLen int64, ops []Op) error {
var (
currentCode opcode.Opcode
opsLenGot = len(ops)
)
for i := opsLenGot - 1; i >= 0; i-- {
// only PUSH(also, PACK for arrays and CONVERT for booleans)
// codes are allowed; number of params and their content must
// be checked in a notary parser and a notary handler of a
// particular contract
switch currentCode = ops[i].code; {
case currentCode <= opcode.PUSH16:
case currentCode == opcode.CONVERT:
if i == 0 || ops[i-1].code != opcode.PUSHT && ops[i-1].code != opcode.PUSHF {
return errors.New("errUnexpectedCONVERT")
}
expArgLen++
case currentCode == opcode.PACK:
if i == 0 {
return errors.New("errIncorrectArgPacking")
}
argsLen, err := IntFromOpcode(ops[i-1])
if err != nil {
return fmt.Errorf("could not parse argument len: %w", err)
}
expArgLen += argsLen + 1
i--
default:
return fmt.Errorf("received main tx has unexpected(not PUSH) NeoVM opcode: %s", currentCode)
}
}
if int64(opsLenGot) != expArgLen {
return errors.New("errIncorrectArgPacking")
}
return nil
}
func die(err error) {
if err == nil {
return
}
debug.PrintStack()
_, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

6
app/client/config.yml Normal file
View file

@ -0,0 +1,6 @@
wallet: "/user/wallet.json"
password: ""
rpc_endpoint: "http://localhost:30333"
backend_key: "03b09baabff3f6107c7e9acb8721a6fc5618d45b50247a314d82e548702cce8cd5"
nyan_contract: "f167e80e620a70b47cd6f8c8e37229107dfab340"
backend_url: "http://localhost:5555"

251
app/client/main.go Normal file
View file

@ -0,0 +1,251 @@
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"runtime/debug"
"syscall"
"git.frostfs.info/TrueCloudLab/hrw"
"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/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/notary"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/spf13/viper"
)
const (
cfgRPCEndpoint = "rpc_endpoint"
cfgBackendKey = "backend_key"
cfgWallet = "wallet"
cfgPassword = "password"
cfgNyanContract = "nyan_contract"
cfgBackendURL = "backend_url"
)
func main() {
ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
if len(os.Args) != 2 {
die(fmt.Errorf("invalid args: %v", os.Args))
}
viper.GetViper().SetConfigType("yml")
f, err := os.Open(os.Args[1])
die(err)
die(viper.GetViper().ReadConfig(f))
die(f.Close())
rpcCli, err := rpcclient.New(ctx, viper.GetString(cfgRPCEndpoint), rpcclient.Options{})
die(err)
backendKey, err := keys.NewPublicKeyFromString(viper.GetString(cfgBackendKey))
die(err)
w, err := wallet.NewWalletFromFile(viper.GetString(cfgWallet))
die(err)
acc := w.GetAccount(w.GetChangeAddress())
err = acc.Decrypt(viper.GetString(cfgPassword), w.Scrypt)
die(err)
contractHash, err := util.Uint160DecodeStringLE(viper.GetString(cfgNyanContract))
die(err)
die(claimNotaryDeposit(acc))
die(makeNotaryRequest(backendKey, acc, rpcCli, contractHash))
}
func claimNotaryDeposit(acc *wallet.Account) error {
resp, err := http.Get(viper.GetString(cfgBackendURL) + "/notary-deposit/" + acc.Address)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("notary deposit failed: %d, %s", resp.StatusCode, resp.Status)
}
return nil
}
func makeNotaryRequest(backendKey *keys.PublicKey, acc *wallet.Account, rpcCli *rpcclient.Client, contractHash util.Uint160) error {
coSigners := []actor.SignerAccount{
{
Signer: transaction.Signer{
Account: backendKey.GetScriptHash(),
Scopes: transaction.None,
},
Account: notary.FakeSimpleAccount(backendKey),
},
{
Signer: transaction.Signer{
Account: acc.ScriptHash(),
Scopes: transaction.Global,
},
Account: acc,
},
}
nyanCat, err := getFreeNyanCat(rpcCli, acc, contractHash)
if err != nil {
return fmt.Errorf("get free cat: %w", err)
}
nAct, err := notary.NewActor(rpcCli, coSigners, acc)
if err != nil {
return err
}
tx, err := nAct.MakeTunedCall(contractHash, "mint", nil, nil, acc.ScriptHash(), nyanCat)
if err != nil {
return err
}
mainHash, fallbackHash, vub, err := nAct.Notarize(tx, err)
if err != nil {
return err
}
fmt.Printf("Notarize sending: mainHash - %v, fallbackHash - %v, vub - %d\n", mainHash, fallbackHash, vub)
res, err := nAct.Wait(mainHash, fallbackHash, vub, err)
if err != nil {
return err
}
if res.VMState != vmstate.Halt {
return fmt.Errorf("invalid vm state: %s", res.VMState)
}
if len(res.Stack) != 1 {
return fmt.Errorf("invalid stack size: %d", len(res.Stack))
}
tokenID, err := res.Stack[0].TryBytes()
if err != nil {
return err
}
fmt.Println("new token id", hex.EncodeToString(tokenID))
return nil
}
var listOfCats = []string{
"404.gif",
"america.gif",
"balloon.gif",
"bday.gif",
"bloon.gif",
"breakfast.gif",
"daft.gif",
"dub.gif",
"easter.gif",
"elevator.gif",
"fat.gif",
"fiesta.gif",
"floppy.gif",
"ganja.gif",
"gb.gif",
"grumpy.gif",
"j5.gif",
"jacksnyan.gif",
"jamaicnyan.gif",
"jazz.gif",
"jazzcat.gif",
"manyan.gif",
"melon.gif",
"mexinyan.gif",
"mummy.gif",
"newyear.gif",
"nyanamerica.gif",
"nyancat.gif",
"nyancoin.gif",
"nyandoge.gif",
"nyaninja.gif",
"nyanvirus.gif",
"oldnewyear.gif",
"oldnyan.gif",
"original.gif",
"paddy.gif",
"pikanyan.gif",
"pirate.gif",
"pumpkin.gif",
"rasta.gif",
"retro.gif",
"sad.gif",
"sadnyan.gif",
"skrillex.gif",
"slomo.gif",
"slomocat.gif",
"smooth.gif",
"smurfcat.gif",
"star.gif",
"starsheep.gif",
"tacnayn.gif",
"tacodog.gif",
"technyancolor.gif",
"toaster.gif",
"vday.gif",
"watermelon.gif",
"wtf.gif",
"xmas.gif",
"xmasold.gif",
"zombie.gif",
}
func getFreeNyanCat(cli *rpcclient.Client, acc *wallet.Account, contractHash util.Uint160) (string, error) {
indexes := make([]uint64, len(listOfCats))
for i := range indexes {
indexes[i] = uint64(i)
}
act, err := actor.NewSimple(cli, acc)
if err != nil {
return "", err
}
h := hrw.Hash(acc.ScriptHash().BytesBE())
hrw.Sort(indexes, h)
var cat string
for _, index := range indexes {
cat = listOfCats[index]
hash := sha256.New()
hash.Write([]byte(cat))
tokenID := hash.Sum(nil)
if _, err := unwrap.Uint160(act.Call(contractHash, "ownerOf", tokenID)); err != nil {
break
}
}
if cat == "" {
return "", errors.New("all cats are taken")
}
return cat, nil
}
func die(err error) {
if err == nil {
return
}
debug.PrintStack()
_, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

80
app/go.mod Normal file
View file

@ -0,0 +1,80 @@
module contract
go 1.22
require (
git.frostfs.info/TrueCloudLab/frostfs-node v0.44.4
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20241206094944-81c423e7094d
github.com/nspcc-dev/neo-go v0.106.3
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec
github.com/spf13/viper v1.19.0
go.uber.org/zap v1.27.0
)
replace github.com/nspcc-dev/neo-go => git.frostfs.info/TrueCloudLab/neoneo-go v0.106.1-0.20241015133823-8aee80dbdc07
require (
git.frostfs.info/TrueCloudLab/frostfs-contract v0.21.0-rc.4 // indirect
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20241112082307-f17779933e88 // indirect
git.frostfs.info/TrueCloudLab/hrw v1.2.1 // indirect
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 // indirect
git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02 // indirect
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.14.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.14.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b // indirect
github.com/nspcc-dev/rfc6979 v0.2.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/prometheus/client_golang v1.20.2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ssgreg/journald v1.0.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.66.2 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

292
app/go.sum Normal file
View file

@ -0,0 +1,292 @@
git.frostfs.info/TrueCloudLab/frostfs-contract v0.21.0-rc.4 h1:o3iqVmbvFsfe8kpB2Hvuix6Q/tAhbiPLP91xK4lmoBQ=
git.frostfs.info/TrueCloudLab/frostfs-contract v0.21.0-rc.4/go.mod h1:5fSm/l5xSjGWqsPUffSdboiGFUHa7y/1S0fvxzQowN8=
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 h1:FxqFDhQYYgpe41qsIHVOcdzSVCB8JNSfPG7Uk4r2oSk=
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0/go.mod h1:RUIKZATQLJ+TaYQa60X2fTDwfuhMfm8Ar60bQ5fr+vU=
git.frostfs.info/TrueCloudLab/frostfs-node v0.44.4 h1:ul6/DMWRbd55dfoyMXUBhx0SS1d5mhJbnUTMVL4Uc08=
git.frostfs.info/TrueCloudLab/frostfs-node v0.44.4/go.mod h1:LNEeZQ7Nc8r5wImauf/R9/bxPaDAuGnOk9kspUz4o0I=
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20241112082307-f17779933e88 h1:9bvBDLApbbO5sXBKdODpE9tzy3HV99nXxkDWNn22rdI=
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20241112082307-f17779933e88/go.mod h1:kbwB4v2o6RyOfCo9kEFeUDZIX3LKhmS0yXPrtvzkQ1g=
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20241206094944-81c423e7094d h1:FpXI+mOrmJk3t2MKQFZuhLjCHDyDeo5rtP1WXl7gUWc=
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20241206094944-81c423e7094d/go.mod h1:eoK7+KZQ9GJxbzIs6vTnoUJqFDppavInLRHaN4MYgZg=
git.frostfs.info/TrueCloudLab/hrw v1.2.1 h1:ccBRK21rFvY5R1WotI6LNoPlizk7qSvdfD8lNIRudVc=
git.frostfs.info/TrueCloudLab/hrw v1.2.1/go.mod h1:C1Ygde2n843yTZEQ0FP69jYiuaYV0kriLvP4zm8JuvM=
git.frostfs.info/TrueCloudLab/neoneo-go v0.106.1-0.20241015133823-8aee80dbdc07 h1:gPaqGsk6gSWQyNVjaStydfUz6Z/loHc9XyvGrJ5qSPY=
git.frostfs.info/TrueCloudLab/neoneo-go v0.106.1-0.20241015133823-8aee80dbdc07/go.mod h1:bZyJexBlrja4ngxiBgo8by5pVHuAbhg9l09/8yVGDyg=
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 h1:M2KR3iBj7WpY3hP10IevfIB9MURr4O9mwVfJ+SjT3HA=
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc=
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 h1:UFMnUIk0Zh17m8rjGHJMqku2hCgaXDqjqZzS4gsb4UA=
git.frostfs.info/TrueCloudLab/tzhash v1.8.0/go.mod h1:dhY+oy274hV8wGvGL4MwwMpdL3GYvaX1a8GQZQHvlF8=
git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02 h1:HeY8n27VyPRQe49l/fzyVMkWEB2fsLJYKp64pwA7tz4=
git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02/go.mod h1:rQFJJdEOV7KbbMtQYR2lNfiZk+ONRDJSbMCTWxKt8Fw=
github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V2KhTBPf+Sc=
github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.14.2 h1:YXVoyPndbdvcEVcseEovVfp0qjJp7S+i5+xgp/Nfbdc=
github.com/bits-and-blooms/bitset v1.14.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E=
github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b h1:DRG4cRqIOmI/nUPggMgR92Jxt63Lxsuz40m5QpdvYXI=
github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b/go.mod h1:d3cUseu4Asxfo9/QA/w4TtGjM0AbC9ynyab+PfH+Bso=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec h1:vDrbVXF2+2uP0RlkZmem3QYATcXCu9BzzGGCNsNcK7Q=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec/go.mod h1:/vrbWSHc7YS1KSYhVOyyeucXW/e+1DkVBOgnBEXUCeY=
github.com/nspcc-dev/rfc6979 v0.2.3 h1:QNVykGZ3XjFwM/88rGfV3oj4rKNBy+nYI6jM7q19hDI=
github.com/nspcc-dev/rfc6979 v0.2.3/go.mod h1:q3sCL1Ed7homjqYK8KmFSzEmm+7Ngyo7PePbZanhaDE=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/ssgreg/journald v1.0.0 h1:0YmTDPJXxcWDPba12qNMdO6TxvfkFSYpFIJ31CwmLcU=
github.com/ssgreg/journald v1.0.0/go.mod h1:RUckwmTM8ghGWPslq2+ZBZzbb9/2KgjzYZ4JEP+oRt0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

292
app/nyan/contract.go Normal file
View file

@ -0,0 +1,292 @@
package contract
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/iterator"
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"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"
)
// Prefixes used for contract data storage.
const (
balancePrefix = "b"
accountPrefix = "a"
tokenPrefix = "t"
ownerKey = 'o'
totalSupplyKey = 's'
)
type NFTItem struct {
ID []byte
Name string
Owner interop.Hash160
Address string
}
func _deploy(data interface{}, isUpdate bool) {
if isUpdate {
return
}
args := data.(struct {
Admin interop.Hash160
})
if args.Admin == nil {
panic("invalid admin")
}
if len(args.Admin) != 20 {
panic("invalid admin hash length")
}
ctx := storage.GetContext()
storage.Put(ctx, ownerKey, args.Admin)
storage.Put(ctx, totalSupplyKey, 0)
}
// Symbol returns token symbol, it's NYAN.
func Symbol() string {
return "NYAN"
}
// Decimals returns token decimals, this NFT is non-divisible, so it's 0.
func Decimals() int {
return 0
}
// TotalSupply is a contract method that returns the number of tokens minted.
func TotalSupply() int {
return storage.Get(storage.GetReadOnlyContext(), totalSupplyKey).(int)
}
// BalanceOf returns the number of tokens owned by the specified address.
func BalanceOf(holder interop.Hash160) int {
if len(holder) != 20 {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
return getBalanceOf(ctx, mkBalanceKey(holder))
}
// OwnerOf returns the owner of the specified token.
func OwnerOf(token []byte) interop.Hash160 {
ctx := storage.GetReadOnlyContext()
return getNFT(ctx, token).Owner
}
// Properties returns properties of the given NFT.
func Properties(token []byte) map[string]string {
ctx := storage.GetReadOnlyContext()
nft := getNFT(ctx, token)
result := map[string]string{
"id": string(nft.ID),
"owner": ownerAddress(nft.Owner),
"name": nft.Name,
"address": nft.Address,
}
return result
}
// Tokens returns an iterator that contains all the tokens minted by the contract.
func Tokens() iterator.Iterator {
ctx := storage.GetReadOnlyContext()
key := []byte(tokenPrefix)
iter := storage.Find(ctx, key, storage.RemovePrefix|storage.KeysOnly)
return iter
}
func TokensList() []string {
ctx := storage.GetReadOnlyContext()
key := []byte(tokenPrefix)
iter := storage.Find(ctx, key, storage.RemovePrefix|storage.KeysOnly)
keys := []string{}
for iterator.Next(iter) {
k := iterator.Value(iter)
keys = append(keys, k.(string))
}
return keys
}
// TokensOf returns an iterator with all tokens held by the specified address.
func TokensOf(holder interop.Hash160) iterator.Iterator {
if len(holder) != 20 {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
key := mkAccountPrefix(holder)
iter := storage.Find(ctx, key, storage.ValuesOnly)
return iter
}
func TokensOfList(holder interop.Hash160) [][]byte {
if len(holder) != 20 {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
key := mkAccountPrefix(holder)
res := [][]byte{}
iter := storage.Find(ctx, key, storage.ValuesOnly)
for iterator.Next(iter) {
res = append(res, iterator.Value(iter).([]byte))
}
return res
}
// Transfer token from its owner to another user, notice that it only has three
// parameters because token owner can be deduced from token ID itself.
func Transfer(to interop.Hash160, token []byte, data any) bool {
if len(to) != 20 {
panic("invalid 'to' address")
}
ctx := storage.GetContext()
nft := getNFT(ctx, token)
from := nft.Owner
if !runtime.CheckWitness(from) {
return false
}
if !from.Equals(to) {
nft.Owner = to
setNFT(ctx, token, nft)
addToBalance(ctx, from, -1)
removeToken(ctx, from, token)
addToBalance(ctx, to, 1)
addToken(ctx, to, token)
}
postTransfer(from, to, token, data)
return true
}
func getNFT(ctx storage.Context, token []byte) NFTItem {
key := mkTokenKey(token)
val := storage.Get(ctx, key)
if val == nil {
panic("no token found")
}
serializedNFT := val.([]byte)
deserializedNFT := std.Deserialize(serializedNFT)
return deserializedNFT.(NFTItem)
}
func nftExists(ctx storage.Context, token []byte) bool {
key := mkTokenKey(token)
return storage.Get(ctx, key) != nil
}
func setNFT(ctx storage.Context, token []byte, item NFTItem) {
key := mkTokenKey(token)
val := std.Serialize(item)
storage.Put(ctx, key, val)
}
// postTransfer emits Transfer event and calls onNEP11Payment if needed.
func postTransfer(from interop.Hash160, to interop.Hash160, token []byte, data any) {
runtime.Notify("Transfer", from, to, 1, token)
if management.GetContract(to) != nil {
contract.Call(to, "onNEP11Payment", contract.All, from, 1, token, data)
}
}
func Mint(user interop.Hash160, name string) []byte {
ctx := storage.GetContext()
tokenID := crypto.Sha256([]byte(name))
if nftExists(ctx, tokenID) {
panic("token already exists")
}
nft := NFTItem{
ID: tokenID,
Owner: user,
Name: name,
}
setNFT(ctx, tokenID, nft)
addToBalance(ctx, user, 1)
addToken(ctx, user, tokenID)
total := storage.Get(ctx, totalSupplyKey).(int) + 1
storage.Put(ctx, totalSupplyKey, total)
return tokenID
}
func SetAddress(name string, address string) {
ctx := storage.GetContext()
if !runtime.CheckWitness(storage.Get(ctx, ownerKey).(interop.Hash160)) {
panic("not witnessed")
}
tokenID := crypto.Sha256([]byte(name))
nft := getNFT(ctx, tokenID)
nft.Address = address
setNFT(ctx, tokenID, nft)
}
// mkAccountPrefix creates DB key-prefix for the account tokens specified
// by concatenating accountPrefix and account address.
func mkAccountPrefix(holder interop.Hash160) []byte {
res := []byte(accountPrefix)
return append(res, holder...)
}
// mkBalanceKey creates DB key for the account specified by concatenating balancePrefix
// and account address.
func mkBalanceKey(holder interop.Hash160) []byte {
res := []byte(balancePrefix)
return append(res, holder...)
}
// mkTokenKey creates DB key for the token specified by concatenating tokenPrefix
// and token ID.
func mkTokenKey(tokenID []byte) []byte {
res := []byte(tokenPrefix)
return append(res, tokenID...)
}
// getBalanceOf returns the balance of an account using database key.
func getBalanceOf(ctx storage.Context, balanceKey []byte) int {
val := storage.Get(ctx, balanceKey)
if val != nil {
return val.(int)
}
return 0
}
// addToBalance adds an amount to the account balance. Amount can be negative.
func addToBalance(ctx storage.Context, holder interop.Hash160, amount int) {
key := mkBalanceKey(holder)
old := getBalanceOf(ctx, key)
old += amount
if old > 0 {
storage.Put(ctx, key, old)
} else {
storage.Delete(ctx, key)
}
}
// addToken adds a token to the account.
func addToken(ctx storage.Context, holder interop.Hash160, token []byte) {
key := mkAccountPrefix(holder)
storage.Put(ctx, append(key, token...), token)
}
// removeToken removes the token from the account.
func removeToken(ctx storage.Context, holder interop.Hash160, token []byte) {
key := mkAccountPrefix(holder)
storage.Delete(ctx, append(key, token...))
}
func ownerAddress(owner interop.Hash160) string {
b := append([]byte{0x35}, owner...)
return std.Base58CheckEncode(b)
}

16
app/nyan/contract.yml Normal file
View file

@ -0,0 +1,16 @@
name: "NYAN NFT"
supportedstandards: ["NEP-11"]
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "tokens", "properties"]
events:
- name: Transfer
parameters:
- name: from
type: Hash160
- name: to
type: Hash160
- name: amount
type: Integer
- name: tokenId
type: ByteArray
permissions:
- methods: ["onNEP11Payment"]

19
app/seq.puml Normal file
View file

@ -0,0 +1,19 @@
@startuml
actor user
user -> client: "Register" in system
client -> server: Claim notary deposit
server -> blockhain: Add notary client deposit
server -> blockhain: Add notary server deposit
client -> blockhain: Create notary request
server <- blockhain: Listen notification
server -> server: Validate and sign notary transaction request
server -> blockhain: Send notary request
blockhain -> blockhain: Check all signatures and accept transaction
user -> blockhain: Manage NFT
@enduml

29
app/seq.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8 KiB