diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..f45c4d6 --- /dev/null +++ b/app/README.md @@ -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/
`: +http://localhost:8081/get/2aK1oHPovFPoqQwf6A3DPjwPBYUKmEqhWE81omGauNTH/DdVQ1rhjeHfLqFdNbhW56RGkzJQnDUPsdZ9jVpMjCcCk + diff --git a/app/backend/config.yml b/app/backend/config.yml new file mode 100644 index 0000000..2e24268 --- /dev/null +++ b/app/backend/config.yml @@ -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" \ No newline at end of file diff --git a/app/backend/main.go b/app/backend/main.go new file mode 100644 index 0000000..340cc98 --- /dev/null +++ b/app/backend/main.go @@ -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) +} diff --git a/app/client/config.yml b/app/client/config.yml new file mode 100644 index 0000000..27e7dfc --- /dev/null +++ b/app/client/config.yml @@ -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" diff --git a/app/client/main.go b/app/client/main.go new file mode 100644 index 0000000..893945e --- /dev/null +++ b/app/client/main.go @@ -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) +} diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..e75d9a5 --- /dev/null +++ b/app/go.mod @@ -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 +) diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 0000000..71aa242 --- /dev/null +++ b/app/go.sum @@ -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= diff --git a/app/nyan/contract.go b/app/nyan/contract.go new file mode 100644 index 0000000..6dec7b7 --- /dev/null +++ b/app/nyan/contract.go @@ -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) +} diff --git a/app/nyan/contract.yml b/app/nyan/contract.yml new file mode 100644 index 0000000..22e1817 --- /dev/null +++ b/app/nyan/contract.yml @@ -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"] diff --git a/app/seq.puml b/app/seq.puml new file mode 100644 index 0000000..2e2008b --- /dev/null +++ b/app/seq.puml @@ -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 diff --git a/app/seq.svg b/app/seq.svg new file mode 100644 index 0000000..6b880fe --- /dev/null +++ b/app/seq.svg @@ -0,0 +1,29 @@ +useruserclientclientserverserverblockhainblockhain"Register" in systemClaim notary depositAdd notary client depositAdd notary server depositCreate notary requestListen notificationValidate and sign notary transaction requestSend notary requestCheck all signatures and accept transactionManage NFT \ No newline at end of file