Merge pull request #1884 from nspcc-dev/oracle-example-and-docs
Oracle example and docs
This commit is contained in:
commit
99ca0b2578
10 changed files with 233 additions and 5 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -39,3 +39,7 @@ chain/
|
|||
# Coverage
|
||||
coverage.txt
|
||||
coverage.html
|
||||
|
||||
# Compiler output
|
||||
examples/*/*.nef
|
||||
examples/*/*.json
|
||||
|
|
|
@ -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
36
examples/oracle/oracle.go
Normal 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))
|
||||
}
|
3
examples/oracle/oracle.yml
Normal file
3
examples/oracle/oracle.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
name: "Oracle example"
|
||||
supportedstandards: []
|
||||
events:
|
|
@ -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
|
||||
|
|
|
@ -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 '_')")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
57
pkg/core/transaction/oracleresponsecode_string.go
Normal file
57
pkg/core/transaction/oracleresponsecode_string.go
Normal 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) + ")"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue