diff --git a/test.go b/test.go new file mode 100644 index 000000000..3f7d7cc51 --- /dev/null +++ b/test.go @@ -0,0 +1,407 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "strings" + "time" + + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/block" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "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/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/policy" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/pkg/errors" +) + +const ( + rpcEndpoint = "http://localhost:30333" + wsEndpoint = "ws://localhost:30333/ws" + walletPath = "./w.json" + accPass = "" +) + +var ( + isNeoGoServer bool + + transferTxH, _ = util.Uint256DecodeStringLE("f359d99a16a93f4c5d838ae9669bc0b6406f783cd71e917c65af6774e9035fd9") +) + +func main() { + // Simple RPC client: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/rpcclient + c, err := rpcclient.New(context.Background(), rpcEndpoint, rpcclient.Options{ + Cert: "", + Key: "", + CACert: "", + DialTimeout: 0, + RequestTimeout: 0, + MaxConnsPerHost: 0, + }) + check(err, "create RPC client") + defer c.Close() + + err = c.Init() + check(err, "init RPC client") + fmt.Printf("Work with simple RPC client:\n\n") + + // Simple RPC methods: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/rpcclient#hdr-Client + v, err := c.GetVersion() + check(err, "retrieve version") + fmt.Printf("RPC node version: %s\n", v.UserAgent) + isNeoGoServer = strings.Contains(v.UserAgent, config.UserAgentPrefix) // NeoGo node offers several useful extensions. + + p, err := v.Protocol.MarshalJSON() + check(err, "marshal protocol config") + fmt.Printf("RPC node protocol config: %s\n", p) + + bCount, err := c.GetBlockCount() + check(err, "retrieve block height") + fmt.Printf("Block count: %d\n", bCount) + + currentB, err := c.GetBlockByIndex(bCount - 1) // Be careful with block count/block index conversions. + check(err, "retrieve currentB block") + + transferTx, err := c.GetRawTransactionVerbose(transferTxH) + check(err, "retrieve transfer tx") + fmt.Printf("Transfer tx script: %s\n", base64.StdEncoding.EncodeToString(transferTx.Script)) + + transferB, err := c.GetBlockByHash(transferTx.TransactionMetadata.Blockhash) + check(err, "retrieve transferB block") + + tr := trigger.Application + applog, err := c.GetApplicationLog(transferTxH, &tr) // Various triggers are supported. + check(err, "retrieve applog") + if len(applog.Executions) != 1 { + panic("unexpected executions number") + } + stack, err := json.MarshalIndent(applog.Executions[0].Stack, "", "\t") + check(err, "marshal stack") + fmt.Printf("Transfer applog invocation state: %s\nTransfer applog stack: %s\n", applog.Executions[0].VMState, stack) + + // ... + + // Work with wallets: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/wallet + w, err := wallet.NewWalletFromFile(walletPath) + check(err, "open wallet") + + acc := w.Accounts[0] + err = acc.Decrypt(accPass, w.Scrypt) + check(err, "decrypt account") + + // Some of the extensions offered by NeoGo RPC server: https://github.com/nspcc-dev/neo-go/blob/master/docs/rpc.md#extensions + if isNeoGoServer { + // NEP17/NEP11 transfers paging: https://github.com/nspcc-dev/neo-go/blob/master/docs/rpc.md#limits-and-paging-for-getnep11transfers-and-getnep17transfers + var ( + start = uint64(0) + stop = currentB.Timestamp + limit = 10 + page = 0 + ) + nep17T, err := c.GetNEP17Transfers(acc.ScriptHash(), &start, &stop, &limit, &page) + check(err, "retrieve NEP17 transfers") + tBytes, err := json.MarshalIndent(nep17T, "", "\t") + check(err, "marshal NEP17 transfers") + fmt.Printf("NEP17 transfers of %s:\n%s\n", acc.Address, tBytes) + + // `getblocksysfee` RPC call: https://github.com/nspcc-dev/neo-go/blob/master/docs/rpc.md#getblocksysfee-call + sysfee, err := c.GetBlockSysFee(nep17T.Received[0].Index) + check(err, "getblocksysfee") + fmt.Printf("Block #%d system fee: %d\n", nep17T.Received[0].Index, sysfee) + + // ... + } + + // Web-socket client and server: https://github.com/nspcc-dev/neo-go/blob/master/docs/rpc.md#websocket-server + ctx, cancel := context.WithCancel(context.Background()) + wsC, err := rpcclient.NewWS(ctx, wsEndpoint, rpcclient.WSOptions{ + Options: rpcclient.Options{}, + CloseNotificationChannelIfFull: false, + }) + // Do not use `defer wsC.Close()`, we'll close the client manually later. + + err = wsC.Init() + check(err, "init WS RPC client") + fmt.Printf("\nWork with WS RPC client:\n\n") + + // Same simple RPC methods over web-socket: + gasState, err := wsC.GetContractStateByAddressOrName(nativenames.Gas) + check(err, "getcontractstate") + fmt.Printf("GAS contract ID: %d\n", gasState.ID) + + // Unwrap helper package: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap + dec, err := unwrap.BigInt(wsC.InvokeFunction(gasState.Hash, "decimals", []smartcontract.Parameter{}, nil)) + check(err, "invokefunction") + fmt.Printf("GAS contract decimals: %s\n", dec) + + // ... + + // Notifications subsystem: https://github.com/nspcc-dev/neo-go/blob/master/docs/notifications.md + bCh := make(chan *block.Block, 5) // Add some buffer to prevent WSC from blocking even regular requests. + ntfCh := make(chan *state.ContainedNotificationEvent, 5) + aerCh := make(chan *state.AppExecResult, 5) + dispatcherToMainCh := make(chan struct{}) + go func(ctx context.Context, bCh chan *block.Block, ntfCh chan *state.ContainedNotificationEvent, exCh chan *state.AppExecResult, exitCh chan struct{}) { // TODO: move to a separate function + dispatcherLoop: + for { + select { + case b := <-bCh: + fmt.Printf("Block was received:\n\tIndex: %d\n\tPrimary: %d\n\tTransactions: %d\n", + b.Index, b.PrimaryIndex, len(b.Transactions)) + case ntf := <-ntfCh: + params, _ := ntf.Item.MarshalJSON() + fmt.Printf("Notification from execution was received:\n\tContainer: %s\n\tContract: %s\n\tName: %s\n\tParams: %s\n", + ntf.Container.StringLE(), ntf.ScriptHash.StringLE(), ntf.Name, params) + case aer := <-aerCh: + st, _ := json.Marshal(aer.Stack) + fmt.Printf("Application execution result was received:\n\tContainer: %s\n\tTrigger: %s\n\tVM state: %s\n\tStack:%s\n\tFault exception: %s\n", aer.Container.StringLE(), aer.Trigger, aer.VMState, st, aer.FaultException) + case <-ctx.Done(): + break dispatcherLoop + } + } + drainLoop: + for { + select { + case <-bCh: + case <-ntfCh: + case <-aerCh: + default: + break drainLoop + } + } + close(bCh) + close(ntfCh) + close(aerCh) + + // Send notification to the main routine. + close(dispatcherToMainCh) + }(ctx, bCh, ntfCh, aerCh, dispatcherToMainCh) + + primary := 0 + till := bCount + 5 + fltB := &neorpc.BlockFilter{ + Primary: &primary, + Since: nil, + Till: &till, + } + bSubID, err := wsC.ReceiveBlocks(fltB, bCh) + check(err, "subscribe for blocks notifications") + + ctr := gasState.Hash + name := "Transfer" + fltNtf := &neorpc.NotificationFilter{ + Contract: &ctr, + Name: &name, + } + ntfSubID, err := wsC.ReceiveExecutionNotifications(fltNtf, ntfCh) + check(err, "subscribe for execution notifications") + + st := vmstate.Halt.String() + fltEx := &neorpc.ExecutionFilter{ + State: &st, + } + aerSubId, err := wsC.ReceiveExecutions(fltEx, aerCh) + check(err, "subscribe for AppExecResults") + + // Pretend our app is running and performing some useful work. + t := time.NewTimer(10 * time.Second) + var exitErr error +mainLoop: + for { + select { + case <-t.C: + break mainLoop + case <-ctx.Done(): + exitErr = wsC.GetError() + break mainLoop + } + } + + // End of the app work. + if exitErr != nil { + fmt.Printf("WS RPC client closing error: %s\n", exitErr) + } else { + // We can use wsC.UnsubscribeAll(), but let us show the ID-based unsubscriptions: + err = wsC.Unsubscribe(bSubID) + check(err, "unsubscribe from blocks") + err = wsC.Unsubscribe(ntfSubID) + check(err, "unsubscribe from execution notifications") + wsC.Unsubscribe(aerSubId) + check(err, "unsubscribe from application execution results") + } + cancel() + <-dispatcherToMainCh // Wait for the dispatcher routine to properly finish. + + wsC.Close() + err = wsC.GetError() // Any error if the closing process wasn't initiated by user via wsC.Close(). + if err != nil { + fmt.Printf("WS RPC client closing error: %s\n", err) + } + + // Invoker functionality: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker + ctx, cancel = context.WithCancel(context.Background()) + wsC, err = rpcclient.NewWS(ctx, wsEndpoint, rpcclient.WSOptions{}) + check(err, "create WS client") + + signers := []transaction.Signer{ + { + Account: acc.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + } + inv := invoker.New(wsC, signers) + + // Test invocation of method "transfer" of the GAS contract. + var ( + from = acc.ScriptHash() + to = random.Uint160() + amount = 5 + data = smartcontract.Parameter{Type: smartcontract.AnyType} + ) + res, err := inv.Call(gasState.Hash, "transfer", from, to, amount, data) + ok, err := unwrap.Bool(res, err) + check(err, "perform `transfer` testinvoke") + fmt.Printf("`transfer` result: %t\n", ok) + + // Historic invocation functionality: https://github.com/nspcc-dev/neo-go/blob/master/docs/rpc.md#invokecontractverifyhistoric-invokefunctionhistoric-and-invokescripthistoric-calls + invH := invoker.NewHistoricAtHeight(transferB.Index-1, wsC, signers) + resH, err := invH.Call(gasState.Hash, "transfer", from, to, amount, data) + ok, err = unwrap.Bool(resH, err) + check(err, "invoke historic `transfer`") + fmt.Printf("`transfer` historic result: %t\n", ok) + + // Painful manual events unwrapping... + var tEvent nep17.TransferEvent + arr := res.Notifications[0].Item.Value().([]stackitem.Item) + fromB, _ := arr[0].TryBytes() + tEvent.From, _ = util.Uint160DecodeBytesBE(fromB) + toB, _ := arr[1].TryBytes() + tEvent.To, _ = util.Uint160DecodeBytesBE(toB) + tEvent.Amount, _ = arr[2].TryInteger() + + // A set of other invoker methods: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker + + // Actor functionality: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/rpcclient/actor + actSigners := []actor.SignerAccount{ + { + Signer: signers[0], + Account: acc, // Provide the decrypted account if possible. It can be signture/multisignature/contract/dummy. + }, + } + act, err := actor.New(wsC, actSigners) + check(err, "create actor") + + aer, err := act.Wait(act.SendCall(gasState.Hash, "transfer", from, to, amount, data)) + check(err, "send `transfer` call via actor") + if aer.VMState != vmstate.Halt { + panic("unexpected `transfer` result") + } + + aer, err = act.Wait(act.SendTunedCall(gasState.Hash, "transfer", []transaction.Attribute{}, func(r *result.Invoke, t *transaction.Transaction) error { + err := actor.DefaultCheckerModifier(r, t) + if err != nil { + return err + } + + // Perform some additional checks... + if len(r.Stack) != 1 { + return fmt.Errorf("unexpected result stack len: %d", len(r.Stack)) + } + ok, err := r.Stack[0].TryBool() + if err != nil { + return fmt.Errorf("unexpected result stack content: %s", r.Stack[0].Type()) + } + if !ok { + return errors.New("false `transfer` result") + } + + // Change the transaction... + t.NetworkFee = r.GasConsumed + 1 + + return nil + }, from, to, amount, data)) + check(err, "send `transfer` tuned call via actor") + if aer.VMState != vmstate.Halt { + panic("unexpected `transfer` tuned result") + } + + // A set of other Actor methods: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/rpcclient/actor + + // Customizable Actor. + + // NEP17 package: https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17 + nep17Act := nep17.New(act, gasState.Hash) + + d, err := nep17Act.Decimals() + check(err, "retrieve GAS decimals via NEP17 actor") + fmt.Printf("GAS decimals: %d\n", d) + + aer, err = act.Wait(nep17Act.MultiTransfer([]nep17.TransferParameters{ + { + From: from, + To: from, + Amount: big.NewInt(5), + Data: nil, + }, + { + From: from, + To: to, + Amount: big.NewInt(5), + Data: nil, + }, + })) + check(err, "GAS multitransfer") + applogTr, err := wsC.GetApplicationLog(aer.Container, nil) + check(err, "retrieve applog for multitransfer") + transferEvents, err := nep17.TransferEventsFromApplicationLog(applogTr) + check(err, "retrieve events from multitransfer applog") + transferEventsBytes, _ := json.MarshalIndent(transferEvents, "", "\t") + fmt.Printf("GAS multitransfer events:\n%s\n", transferEventsBytes) + + // Native-specific actor packages: + gasAct := gas.New(act) + fromBalance, err := gasAct.BalanceOf(from) + check(err, "retrieve `acc` balance") + fmt.Printf("`acc` balance: %d\n", fromBalance) + + policyAct := policy.New(act) + feePerByte, err := policyAct.GetFeePerByte() + check(err, "retrieve FeePerByte") + fmt.Printf("FeePerByte value: %d", feePerByte) + + // ... + + // Contract deployment example: + + // TODO: deploy storage contract with iterator inside, put some values, show how iterator sessions work. + + // Iterator sessions: + + // Usage of RPC autogenerated wrappers: + + // TODO: generate wrappers for the same storage contract. +} + +func check(err error, msg string) { + if err != nil { + panic(fmt.Errorf("failed to %s: %w", msg, err)) + } +}