408 lines
13 KiB
Go
408 lines
13 KiB
Go
|
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))
|
||
|
}
|
||
|
}
|