Merge pull request #1884 from nspcc-dev/oracle-example-and-docs

Oracle example and docs
This commit is contained in:
Roman Khimov 2021-04-07 10:50:57 +03:00 committed by GitHub
commit 99ca0b2578
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 233 additions and 5 deletions

4
.gitignore vendored
View file

@ -39,3 +39,7 @@ chain/
# Coverage
coverage.txt
coverage.html
# Compiler output
examples/*/*.nef
examples/*/*.json

View file

@ -25,6 +25,7 @@ See the table below for the detailed examples description.
| [engine](engine) | This contract demonstrates how to use `runtime` interop package which implements an API for `System.Runtime.*` NEO system calls. Please, refer to the `runtime` [package documentation](../pkg/interop/doc.go) for details. |
| [events](events) | The contract shows how execution notifications with the different arguments types can be sent with the help of `runtime.Notify` function of the `runtime` interop package. Please, refer to the `runtime.Notify` [function documentation](../pkg/interop/runtime/runtime.go) for details. |
| [iterator](iterator) | This example describes a way to work with NEO iterators. Please, refer to the `iterator` [package documentation](../pkg/interop/iterator/iterator.go) for details. |
| [oracle](oracle) | Oracle demo contract exposing two methods that you can use to process URLs. It uses oracle native contract, see [interop package documentation](../pkg/interop/native/oracle/oracle.go) also. |
| [runtime](runtime) | This contract demonstrates how to use special `_initialize` and `_deploy` methods. See the [compiler documentation](../docs/compiler.md#vm-api-interop-layer ) for methods details. It also shows the pattern for checking owner witness inside the contract with the help of `runtime.CheckWitness` interop [function](../pkg/interop/runtime/runtime.go). |
| [storage](storage) | The contract implements API for basic operations with a contract storage. It shows hos to use `storage` interop package. See the `storage` [package documentation](../pkg/interop/storage/storage.go). |
| [timer](timer) | The idea of the contract is to count `tick` method invocations and destroy itself after the third invocation. It shows how to use `contract.Call` interop function to call, update (migrate) and destroy the contract. Please, refer to the `contract.Call` [function documentation](../pkg/interop/contract/contract.go) |

36
examples/oracle/oracle.go Normal file
View file

@ -0,0 +1,36 @@
package oraclecontract
import (
"github.com/nspcc-dev/neo-go/pkg/interop/native/oracle"
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
)
// Request does an oracle request for the URL specified. It adds minimum
// response fee which should suffice for small requests. The data from this
// URL is subsequently processed by OracleCallback function. This request
// has no JSONPath filters or user data.
func Request(url string) {
oracle.Request(url, nil, "oracleCallback", nil, oracle.MinimumResponseGas)
}
// FilteredRequest is similar to Request but allows you to specify JSONPath filter
// to run against data got from the url specified.
func FilteredRequest(url string, filter []byte) {
oracle.Request(url, filter, "oracleCallback", nil, oracle.MinimumResponseGas)
}
// OracleCallback is called by Oracle native contract when request is finished.
// It either throws an error (if the result is not successful) or logs the data
// got as a result.
func OracleCallback(url string, data interface{}, code int, res []byte) {
// This function shouldn't be called directly, we only expect oracle native
// contract to be calling it.
if string(runtime.GetCallingScriptHash()) != oracle.Hash {
panic("not called from oracle contract")
}
if code != oracle.Success {
panic("request failed for " + url + " with code " + std.Itoa(code, 10))
}
runtime.Log("result for " + url + ": " + string(res))
}

View file

@ -0,0 +1,3 @@
name: "Oracle example"
supportedstandards: []
events:

View file

@ -11,6 +11,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
"github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
"github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
@ -89,6 +90,20 @@ func TestCryptoLibNamedCurve(t *testing.T) {
require.EqualValues(t, native.Secp256r1, crypto.Secp256r1)
}
func TestOracleContractValues(t *testing.T) {
require.EqualValues(t, oracle.Success, transaction.Success)
require.EqualValues(t, oracle.ProtocolNotSupported, transaction.ProtocolNotSupported)
require.EqualValues(t, oracle.ConsensusUnreachable, transaction.ConsensusUnreachable)
require.EqualValues(t, oracle.NotFound, transaction.NotFound)
require.EqualValues(t, oracle.Timeout, transaction.Timeout)
require.EqualValues(t, oracle.Forbidden, transaction.Forbidden)
require.EqualValues(t, oracle.ResponseTooLarge, transaction.ResponseTooLarge)
require.EqualValues(t, oracle.InsufficientFunds, transaction.InsufficientFunds)
require.EqualValues(t, oracle.Error, transaction.Error)
require.EqualValues(t, oracle.MinimumResponseGas, native.MinimumResponseGas)
}
type nativeTestCase struct {
method string
params []string

View file

@ -61,6 +61,9 @@ const (
// DefaultOracleRequestPrice is default amount GAS needed for oracle request.
DefaultOracleRequestPrice = 5000_0000
// MinimumResponseGas is the minimum response fee permitted for request.
MinimumResponseGas = 10_000_000
)
var (
@ -74,6 +77,7 @@ var (
var (
ErrBigArgument = errors.New("some of the arguments are invalid")
ErrInvalidWitness = errors.New("witness check failed")
ErrLowResponseGas = errors.New("not enough gas for response")
ErrNotEnoughGas = errors.New("gas limit exceeded")
ErrRequestNotFound = errors.New("oracle request not found")
ErrResponseNotFound = errors.New("oracle response not found")
@ -323,9 +327,12 @@ func (o *Oracle) request(ic *interop.Context, args []stackitem.Item) stackitem.I
// RequestInternal processes oracle request.
func (o *Oracle) RequestInternal(ic *interop.Context, url string, filter *string, cb string, userData stackitem.Item, gas *big.Int) error {
if len(url) > maxURLLength || (filter != nil && len(*filter) > maxFilterLength) || len(cb) > maxCallbackLength || gas.Uint64() < 1000_0000 {
if len(url) > maxURLLength || (filter != nil && len(*filter) > maxFilterLength) || len(cb) > maxCallbackLength {
return ErrBigArgument
}
if gas.Uint64() < MinimumResponseGas {
return ErrLowResponseGas
}
if strings.HasPrefix(cb, "_") {
return errors.New("disallowed callback method (starts with '_')")
}

View file

@ -29,6 +29,18 @@ func TestAttribute_EncodeBinary(t *testing.T) {
},
}
testserdes.EncodeDecodeBinary(t, attr, new(Attribute))
for _, code := range []OracleResponseCode{ProtocolNotSupported, ConsensusUnreachable,
NotFound, Timeout, Forbidden, ResponseTooLarge, InsufficientFunds, Error} {
attr = &Attribute{
Type: OracleResponseT,
Value: &OracleResponse{
ID: 42,
Code: code,
Result: []byte{},
},
}
testserdes.EncodeDecodeBinary(t, attr, new(Attribute))
}
})
t.Run("NotValidBefore", func(t *testing.T) {
t.Run("positive", func(t *testing.T) {
@ -144,7 +156,7 @@ func TestAttribute_MarshalJSON(t *testing.T) {
require.JSONEq(t, `{
"type":"OracleResponse",
"id": 123,
"code": 0,
"code": "Success",
"result": "`+base64.StdEncoding.EncodeToString(res)+`"}`, string(data))
actual := new(Attribute)

View file

@ -1,12 +1,16 @@
package transaction
import (
"encoding/json"
"errors"
"math"
"strings"
"github.com/nspcc-dev/neo-go/pkg/io"
)
//go:generate stringer -type=OracleResponseCode
// OracleResponseCode represents result code of oracle response.
type OracleResponseCode byte
@ -46,6 +50,43 @@ func (c OracleResponseCode) IsValid() bool {
c == InsufficientFunds || c == Error
}
// MarshalJSON implements json.Marshaler interface.
func (c OracleResponseCode) MarshalJSON() ([]byte, error) {
return []byte(`"` + c.String() + `"`), nil
}
// UnmarshalJSON implements json.Unmarshaler interface.
func (c *OracleResponseCode) UnmarshalJSON(data []byte) error {
var js string
if err := json.Unmarshal(data, &js); err != nil {
return err
}
js = strings.ToLower(js)
switch js {
case "success":
*c = Success
case "protocolnotsupported":
*c = ProtocolNotSupported
case "consensusunreachable":
*c = ConsensusUnreachable
case "notfound":
*c = NotFound
case "timeout":
*c = Timeout
case "forbidden":
*c = Forbidden
case "responsetoolarge":
*c = ResponseTooLarge
case "insufficientfunds":
*c = InsufficientFunds
case "error":
*c = Error
default:
return errors.New("invalid oracle response code")
}
return nil
}
// DecodeBinary implements io.Serializable interface.
func (r *OracleResponse) DecodeBinary(br *io.BinReader) {
r.ID = br.ReadU64LE()

View file

@ -0,0 +1,57 @@
// Code generated by "stringer -type=OracleResponseCode"; DO NOT EDIT.
package transaction
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Success-0]
_ = x[ProtocolNotSupported-16]
_ = x[ConsensusUnreachable-18]
_ = x[NotFound-20]
_ = x[Timeout-22]
_ = x[Forbidden-24]
_ = x[ResponseTooLarge-26]
_ = x[InsufficientFunds-28]
_ = x[Error-255]
}
const (
_OracleResponseCode_name_0 = "Success"
_OracleResponseCode_name_1 = "ProtocolNotSupported"
_OracleResponseCode_name_2 = "ConsensusUnreachable"
_OracleResponseCode_name_3 = "NotFound"
_OracleResponseCode_name_4 = "Timeout"
_OracleResponseCode_name_5 = "Forbidden"
_OracleResponseCode_name_6 = "ResponseTooLarge"
_OracleResponseCode_name_7 = "InsufficientFunds"
_OracleResponseCode_name_8 = "Error"
)
func (i OracleResponseCode) String() string {
switch {
case i == 0:
return _OracleResponseCode_name_0
case i == 16:
return _OracleResponseCode_name_1
case i == 18:
return _OracleResponseCode_name_2
case i == 20:
return _OracleResponseCode_name_3
case i == 22:
return _OracleResponseCode_name_4
case i == 24:
return _OracleResponseCode_name_5
case i == 26:
return _OracleResponseCode_name_6
case i == 28:
return _OracleResponseCode_name_7
case i == 255:
return _OracleResponseCode_name_8
default:
return "OracleResponseCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

View file

@ -10,22 +10,74 @@ import (
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
)
// These are potential response codes you get in your callback completing
// oracle request. Resulting data is only passed with Success code, it's
// nil otherwise.
const (
Success = 0x00
ProtocolNotSupported = 0x10
ConsensusUnreachable = 0x12
NotFound = 0x14
Timeout = 0x16
Forbidden = 0x18
ResponseTooLarge = 0x1a
InsufficientFunds = 0x1c
Error = 0xff
)
// Hash represents Oracle contract hash.
const Hash = "\x58\x87\x17\x11\x7e\x0a\xa8\x10\x72\xaf\xab\x71\xd2\xdd\x89\xfe\x7c\x4b\x92\xfe"
// Request represents `request` method of Oracle native contract.
// MinimumResponseGas is the minimum response fee permitted for request (that is
// you can't attach less than that to your request). It's 0.1 GAS at the moment.
const MinimumResponseGas = 10_000_000
// Request makes an oracle request. It can only be successfully invoked by
// deployed contract and it takes the following parameters:
//
// url
// URL to fetch, only https and neofs URLs are supported like
// https://example.com/some.json or
// neofs:6pJtLUnGqDxE2EitZYLsDzsfTDVegD6BrRUn8QAFZWyt/5Cyxb3wrHDw5pqY63hb5otCSsJ24ZfYmsA8NAjtho2gr
//
// filter
// JSONPath filter to process the result, if specified it will be
// applied to the data returned from HTTP/NeoFS and you'll only get
// filtered data in your callback method.
//
// cb
// name of the method that will process oracle data, it must be a method
// of the same contract that invokes Request and it must have the following
// signature for correct invocation:
//
// Method(url string, userData interface{}, code int, result []byte)
//
// where url is the same url specified for Request, userData is anything
// passed in the next parameter, code is the status of the reply and
// result is data returned from request if any.
//
// userData
// data to pass to the callback function.
//
// gasForResponse
// GAS attached to this request for reply callback processing,
// note that it's different from the oracle request price, this
// GAS is used for oracle transaction's network and system fees,
// so it should be enough to pay for reply data as well as
// its processing.
func Request(url string, filter []byte, cb string, userData interface{}, gasForResponse int) {
contract.Call(interop.Hash160(Hash), "request",
contract.States|contract.AllowNotify,
url, filter, cb, userData, gasForResponse)
}
// GetPrice represents `getPrice` method of Oracle native contract.
// GetPrice returns current oracle request price.
func GetPrice() int {
return contract.Call(interop.Hash160(Hash), "getPrice", contract.ReadStates).(int)
}
// SetPrice represents `setPrice` method of Oracle native contract.
// SetPrice allows to set oracle request price. This method can only be
// successfully invoked by the committee.
func SetPrice(amount int) {
contract.Call(interop.Hash160(Hash), "setPrice", contract.States, amount)
}