diff --git a/.circleci/config.yml b/.circleci/config.yml index 345a8c51745..24899be65f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -141,6 +141,8 @@ jobs: - run: ./tools/ci/ethereum_test - store_artifacts: path: ./integration/cypress/screenshots + - store_artifacts: + path: ./integration/logs parity-postgres: resource_class: xlarge docker: @@ -177,7 +179,10 @@ jobs: - run: ./tools/ci/ethereum_test parity - store_artifacts: path: ./integration/cypress/screenshots + - store_artifacts: + path: ./integration/logs truffle: + resource_class: large docker: - image: smartcontract/builder:1.0.25 steps: @@ -194,6 +199,26 @@ jobs: - /usr/local/share/.cache/yarn - run: pip3 install -r requirements.txt - run: ./tools/ci/truffle_test + - store_artifacts: + path: ./integration/logs + json-api-client: + docker: + - image: smartcontract/builder:1.0.25 + steps: + - checkout + - run: echo $CACHE_VERSION > cache.version + - restore_cache: + name: Restore Yarn Package Cache + key: v{{ checksum "cache.version" }}-yarn-vendor-{{ checksum "yarn.lock" }} + - run: yarn install + - save_cache: + name: Save Yarn Package Cache + key: v{{ checksum "cache.version" }}-yarn-vendor-{{ checksum "yarn.lock" }} + paths: + - /usr/local/share/.cache/yarn + - run: yarn workspace @chainlink/json-api-client + - store_artifacts: + path: ./integration/logs operator-ui: docker: - image: smartcontract/builder:1.0.25 @@ -211,6 +236,8 @@ jobs: - /usr/local/share/.cache/yarn - run: ./tools/ci/init_gcloud - run: ./tools/ci/operator_ui_test + - store_artifacts: + path: ./integration/logs explorer: working_directory: ~/chainlink docker: @@ -247,9 +274,8 @@ jobs: - run: name: Run Client Tests command: yarn workspace @chainlink/explorer-client run test-ci:silent - - run: - name: Run E2E Tests - command: yarn workspace @chainlink/explorer-client run build && yarn workspace @chainlink/explorer run test-ci:e2e:silent + - store_artifacts: + path: ./integration/logs forks: machine: image: ubuntu-1604:201903-01 @@ -260,8 +286,12 @@ jobs: - run: name: Install Yarn command: npm install -g yarn + - run: + name: Install New Packages + command: yarn install - run: ./tools/ci/forks_test - + - store_artifacts: + path: ./integration/forks/logs build-publish-explorer: machine: true steps: @@ -277,6 +307,17 @@ jobs: name: Docker push, if applicable command: | tools/ci/push_explorer "${CIRCLE_BRANCH}" "${CIRCLE_TAG}" + build-explorer: + machine: true + steps: + - checkout + - run: + name: Docker login + command: | + echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin + - run: + name: Docker build + command: docker build -f explorer/Dockerfile -t smartcontract/explorer:circleci . build-publish-chainlink: machine: true steps: @@ -293,6 +334,18 @@ jobs: name: Docker push, if applicable command: | tools/ci/push_chainlink "${CIRCLE_BRANCH}" "${CIRCLE_TAG}" + build-chainlink: + machine: true + steps: + - checkout + - run: + name: Docker login + command: | + echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin + - run: + name: Docker build + command: | + DOCKER_TAG=circleci make docker reportcoverage: docker: - image: smartcontract/builder:1.0.25 @@ -300,6 +353,23 @@ jobs: - checkout - run: ./tools/ci/init_gcloud - run: ./tools/ci/report_coverage + prepublish_npm: + resource_class: large + docker: + - image: smartcontract/builder:1.0.25 + steps: + - checkout + - run: echo $CACHE_VERSION > cache.version + - restore_cache: + name: Restore Yarn Package Cache + key: v{{ checksum "cache.version" }}-yarn-vendor-{{ checksum "yarn.lock" }} + - run: yarn install + - save_cache: + name: Save Yarn Package Cache + key: v{{ checksum "cache.version" }}-yarn-vendor-{{ checksum "yarn.lock" }} + paths: + - /usr/local/share/.cache/yarn + - run: ./tools/ci/prepublish_npm_test workflows: version: 2 @@ -329,6 +399,10 @@ workflows: filters: tags: only: /^v.*/ + - json-api-client: + filters: + tags: + only: /^v.*/ - operator-ui: filters: tags: @@ -345,6 +419,18 @@ workflows: filters: tags: only: /^v.*/ + - build-chainlink: + filters: + tags: + only: /^v.*/ + - build-explorer: + filters: + tags: + only: /^v.*/ + - prepublish_npm: + filters: + tags: + only: /^v.*/ - build-publish-explorer: requires: - explorer @@ -362,6 +448,7 @@ workflows: - truffle - geth-postgres - parity-postgres + - json-api-client - operator-ui - rust filters: diff --git a/.solhintignore b/.solhintignore index ddb6e7610a1..14325ce6534 100644 --- a/.solhintignore +++ b/.solhintignore @@ -1,7 +1,4 @@ node_modules -examples/echo_server/node_modules -examples/uptime_sla/node_modules -examples/twilio_sms/node_modules examples/testnet/node_modules examples/testnet/contracts/TestnetConsumer.sol examples/testnet/contracts/Oracle.sol @@ -9,4 +6,6 @@ examples/testnet/contracts/Aggregator.sol examples/testnet/contracts/AggregatorProxy.sol evm/contracts/lib evm/box/node_modules -evm/node_modules \ No newline at end of file +evm/node_modules +evm/contracts/vendor +evm/v0.5/contracts/vendor diff --git a/Dockerfile b/Dockerfile index b9d2f0a9420..a251980af48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,8 @@ COPY operator_ui/package.json ./operator_ui/ COPY styleguide/package.json ./styleguide/ COPY tools/prettier-config/package.json ./tools/prettier-config/ COPY tools/eslint-config/package.json ./tools/eslint-config/ +COPY tools/json-api-client/package.json ./tools/json-api-client/ +COPY tools/local-storage/package.json ./tools/local-storage/ RUN make yarndep # Install chainlink diff --git a/README.md b/README.md index c53069602ab..c7a03969e81 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ and will go on to form the basis for Chainlink's [decentralized oracle network]( Further development of the Chainlink Node and Chainlink Network will happen here, if you are interested in contributing please see our [contribution guidelines](./docs/CONTRIBUTING.md). The current node supports: + - easy connectivity of on-chain contracts to any off-chain computation or API - multiple methods for scheduling both on-chain and off-chain computation for a user's smart contract - automatic gas price bumping to prevent stuck transactions, assuring your data is delivered in a timely manner @@ -46,16 +47,21 @@ Ethereum node versions currently tested and supported: **NOTE**: By default, chainlink will run in TLS mode. For local development you can either disable this by setting CHAINLINK_DEV to true, or generate self signed certificates using `tools/bin/self-signed-certs` or [manually](https://github.com/smartcontractkit/chainlink/wiki/Creating-Self-Signed-Certificates). To start your Chainlink node, simply run: + ```bash $ chainlink local node ``` + By default this will start on port 6688, where it exposes a [REST API](https://github.com/smartcontractkit/chainlink/wiki/REST-API). Once your node has started, you can view your current jobs with: + ```bash $ chainlink jobspecs -```` +``` + View details of a specific job with: + ```bash $ chainlink show $JOB_ID ``` @@ -72,20 +78,16 @@ You can configure your node's behavior by setting environment variables which ca This project contains several sub-projects, some with their own documentation. - - [evm](/evm) - smart contract-related resources - - [box](/evm/box) - [Chainlink Truffle box](https://www.trufflesuite.com/blog/using-truffle-to-interact-with-chainlink-smart-contracts) - - [v0.5](/evm/v0.5) - Chainlink using Solidity v0.5.0 - - [examples](/examples) - collection of example Chainlink integrations - - [echo_server](/examples/echo_server) - demonstrates using Chainlink to monitor incoming Ethereum logs and report them as JSON - - [testnet](/examples/testnet) - guide to creating, deploying and using Chainlinked smart contracts - - [twilio_sms](/examples/twilio_sms) - send a text message when a smart contract receives payment - - [uptime_sla](/examples/uptime_sla) - example SLA that uses ChainLink to determine the release of payment - - [explorer](/explorer) - [Chainlink Explorer](https://explorer.chain.link/) - - [integration/forks](/integration/forks) - integration test for [ommers](https://ethereum.stackexchange.com/a/46/19503) and [re-orgs](https://en.bitcoin.it/wiki/Chain_Reorganization) - - [sgx](/sgx) - experimental, optional module that can be loaded into Chainlink to do processing within an [SGX](https://software.intel.com/en-us/sgx) enclave - - [styleguide](/styleguide) - Chainlink style guide - - [tools](/tools) - Chainlink tools - +- [evm](/evm) - smart contract-related resources + - [box](/evm/box) - [Chainlink Truffle box](https://www.trufflesuite.com/blog/using-truffle-to-interact-with-chainlink-smart-contracts) + - [v0.5](/evm/v0.5) - Chainlink using Solidity v0.5.0 +- [examples](/examples) - collection of example Chainlink integrations + - [testnet](/examples/testnet) - guide to creating, deploying and using Chainlinked smart contracts +- [explorer](/explorer) - [Chainlink Explorer](https://explorer.chain.link/) +- [integration/forks](/integration/forks) - integration test for [ommers](https://ethereum.stackexchange.com/a/46/19503) and [re-orgs](https://en.bitcoin.it/wiki/Chain_Reorganization) +- [sgx](/sgx) - experimental, optional module that can be loaded into Chainlink to do processing within an [SGX](https://software.intel.com/en-us/sgx) enclave +- [styleguide](/styleguide) - Chainlink style guide +- [tools](/tools) - Chainlink tools ## External Adapters @@ -94,7 +96,6 @@ A Chainlink node communicates with external adapters via a simple REST API. For more information on creating and using external adapters, please see our [external adapters page](https://github.com/smartcontractkit/chainlink/wiki/External-Adapters). - ## Development Setup For the latest information on setting up a development environment, see the [guide here](https://github.com/smartcontractkit/chainlink/wiki/Development-Setup-Guide). @@ -106,6 +107,7 @@ $ go build -o chainlink ./core/ ``` - Run the binary: + ```bash $ ./chainlink ``` @@ -120,14 +122,18 @@ $ go test ./... 1. [Install Yarn](https://yarnpkg.com/lang/en/docs/install) 2. Install the dependencies: + ```bash $ cd evm $ yarn install ``` + 3. Run tests: + ```bash $ yarn run test-sol ``` + ### Development Tips For more tips on how to build and test Chainlink, see our [development tips page](https://github.com/smartcontractkit/chainlink/wiki/Development-Tips). diff --git a/VERSION b/VERSION index 04e84f897ba..faef31a4357 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.10 +0.7.0 diff --git a/core/adapters/adapter.go b/core/adapters/adapter.go index 0f7a063ebe8..4886acd5e37 100644 --- a/core/adapters/adapter.go +++ b/core/adapters/adapter.go @@ -4,9 +4,10 @@ import ( "encoding/json" "fmt" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/orm" ) var ( @@ -49,7 +50,7 @@ var ( // BaseAdapter is the minimum interface required to create an adapter. Only core // adapters have this minimum requirement. type BaseAdapter interface { - Perform(models.RunResult, *store.Store) models.RunResult + Perform(models.RunInput, *store.Store) models.RunOutput } // PipelineAdapter wraps a BaseAdapter with requirements for execution in the pipeline. @@ -70,10 +71,10 @@ func (p PipelineAdapter) MinContractPayment() *assets.Link { } // For determines the adapter type to use for a given task. -func For(task models.TaskSpec, store *store.Store) (*PipelineAdapter, error) { +func For(task models.TaskSpec, config orm.ConfigReader, orm *orm.ORM) (*PipelineAdapter, error) { var ba BaseAdapter var err error - mic := store.Config.MinIncomingConfirmations() + mic := config.MinIncomingConfirmations() mcp := assets.NewLink(0) switch task.Type { @@ -94,11 +95,11 @@ func For(task models.TaskSpec, store *store.Store) (*PipelineAdapter, error) { err = unmarshalParams(task.Params, ba) case TaskTypeEthTx: ba = &EthTx{} - mcp = store.Config.MinimumContractPayment() + mcp = config.MinimumContractPayment() err = unmarshalParams(task.Params, ba) case TaskTypeEthTxABIEncode: ba = &EthTxABIEncode{} - mcp = store.Config.MinimumContractPayment() + mcp = config.MinimumContractPayment() err = unmarshalParams(task.Params, ba) case TaskTypeHTTPGet: ba = &HTTPGet{} @@ -131,11 +132,11 @@ func For(task models.TaskSpec, store *store.Store) (*PipelineAdapter, error) { ba = &Compare{} err = unmarshalParams(task.Params, ba) default: - bt, err := store.FindBridge(task.Type) + bt, err := orm.FindBridge(task.Type) if err != nil { return nil, fmt.Errorf("%s is not a supported adapter type", task.Type) } - b := Bridge{BridgeType: &bt, Params: &task.Params} + b := Bridge{BridgeType: bt, Params: task.Params} ba = &b mic = b.Confirmations mcp = bt.MinimumContractPayment diff --git a/core/adapters/adapter_test.go b/core/adapters/adapter_test.go index 13606a2605f..5a54a929bad 100644 --- a/core/adapters/adapter_test.go +++ b/core/adapters/adapter_test.go @@ -4,10 +4,11 @@ import ( "reflect" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" ) @@ -17,8 +18,8 @@ func TestCreatingAdapterWithConfig(t *testing.T) { defer cleanup() task := models.TaskSpec{Type: adapters.TaskTypeNoOp} - adapter, err := adapters.For(task, store) - adapter.Perform(models.RunResult{}, nil) + adapter, err := adapters.For(task, store.Config, store.ORM) + adapter.Perform(models.RunInput{}, nil) assert.NoError(t, err) } @@ -48,7 +49,7 @@ func TestAdapterFor(t *testing.T) { for _, test := range cases { t.Run(test.wantType, func(t *testing.T) { task := models.TaskSpec{Type: models.MustNewTaskType(test.bridgeName)} - adapter, err := adapters.For(task, store) + adapter, err := adapters.For(task, store.Config, store.ORM) if test.wantErrored { assert.Error(t, err) } else { diff --git a/core/adapters/bridge.go b/core/adapters/bridge.go index f8eaefd8c41..eb54cba93e5 100644 --- a/core/adapters/bridge.go +++ b/core/adapters/bridge.go @@ -8,87 +8,93 @@ import ( "net/http" "net/url" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store" + "chainlink/core/store/models" + + "github.com/pkg/errors" ) // Bridge adapter is responsible for connecting the task pipeline to external // adapters, allowing for custom computations to be executed and included in runs. type Bridge struct { - *models.BridgeType - Params *models.JSON + models.BridgeType + Params models.JSON } -// Perform sends a POST request containing the JSON of the input RunResult to -// the external adapter specified in the BridgeType. +// Perform sends a POST request containing the JSON of the input to the +// external adapter specified in the BridgeType. +// // It records the RunResult returned to it, and optionally marks the RunResult pending. // // If the Perform is resumed with a pending RunResult, the RunResult is marked // not pending and the RunResult is returned. -func (ba *Bridge) Perform(input models.RunResult, store *store.Store) models.RunResult { - if input.Status.Finished() { - return input - } else if input.Status.PendingBridge() { - return resumeBridge(input) +func (ba *Bridge) Perform(input models.RunInput, store *store.Store) models.RunOutput { + if input.Status().Completed() { + return models.NewRunOutputComplete(input.Data()) + } else if input.Status().PendingBridge() { + return models.NewRunOutputInProgress(input.Data()) } return ba.handleNewRun(input, store.Config.BridgeResponseURL()) } -func resumeBridge(input models.RunResult) models.RunResult { - input.Status = models.RunStatusInProgress - return input -} - -func (ba *Bridge) handleNewRun(input models.RunResult, bridgeResponseURL *url.URL) models.RunResult { - if ba.Params == nil { - ba.Params = new(models.JSON) - } - var err error - if input.Data, err = input.Data.Merge(*ba.Params); err != nil { - return models.RunResultError(baRunResultError("handling data param", err)) +func (ba *Bridge) handleNewRun(input models.RunInput, bridgeResponseURL *url.URL) models.RunOutput { + data, err := models.Merge(input.Data(), ba.Params) + if err != nil { + return models.NewRunOutputError(baRunResultError("handling data param", err)) } responseURL := bridgeResponseURL if *responseURL != *zeroURL { - responseURL.Path += fmt.Sprintf("/v2/runs/%s", input.CachedJobRunID) + responseURL.Path += fmt.Sprintf("/v2/runs/%s", input.JobRunID().String()) } body, err := ba.postToExternalAdapter(input, responseURL) if err != nil { - return models.RunResultError(baRunResultError("post to external adapter", err)) + return models.NewRunOutputError(baRunResultError("post to external adapter", err)) } - return responseToRunResult(body, input) + input = *models.NewRunInput(input.JobRunID(), data, input.Status()) + return ba.responseToRunResult(body, input) } -func responseToRunResult(body []byte, input models.RunResult) models.RunResult { - var output models.RunResult - +func (ba *Bridge) responseToRunResult(body []byte, input models.RunInput) models.RunOutput { var brr models.BridgeRunResult err := json.Unmarshal(body, &brr) if err != nil { - return models.RunResultError(baRunResultError("unmarshaling JSON", err)) - } else if brr.HasError() { - return brr.RunResult + return models.NewRunOutputError(baRunResultError("unmarshaling JSON", err)) } - if brr.RunResult.Data.Exists() && !brr.RunResult.Data.IsObject() { - output.CompleteWithResult(brr.RunResult.Data.String()) + if brr.HasError() { + return models.NewRunOutputError(brr.GetError()) } - err = output.Merge(brr.RunResult) - if err != nil { - output.SetError(err) + if brr.ExternalPending { + return models.NewRunOutputPendingBridge() + } + + if brr.Data.IsObject() { + data, err := models.Merge(ba.Params, brr.Data) + if err != nil { + return models.NewRunOutputError(baRunResultError("handling data param", err)) + } + + return models.NewRunOutputComplete(data) } - return output + return models.NewRunOutputCompleteWithResult(brr.Data.String()) } -func (ba *Bridge) postToExternalAdapter(input models.RunResult, bridgeResponseURL *url.URL) ([]byte, error) { - in, err := json.Marshal(&bridgeOutgoing{ - RunResult: input, - ResponseURL: bridgeResponseURL, - }) +func (ba *Bridge) postToExternalAdapter(input models.RunInput, bridgeResponseURL *url.URL) ([]byte, error) { + data, err := models.Merge(input.Data(), ba.Params) + if err != nil { + return nil, errors.Wrap(err, "error merging bridge params with input params") + } + + outgoing := bridgeOutgoing{JobRunID: input.JobRunID().String(), Data: data} + if bridgeResponseURL != nil { + outgoing.ResponseURL = bridgeResponseURL.String() + } + in, err := json.Marshal(&outgoing) if err != nil { return nil, fmt.Errorf("marshaling request body: %v", err) } @@ -121,21 +127,9 @@ func baRunResultError(str string, err error) error { } type bridgeOutgoing struct { - models.RunResult - ResponseURL *url.URL -} - -func (bp bridgeOutgoing) MarshalJSON() ([]byte, error) { - anon := struct { - JobRunID *models.ID `json:"id"` - Data models.JSON `json:"data"` - ResponseURL string `json:"responseURL,omitempty"` - }{ - JobRunID: bp.CachedJobRunID, - Data: bp.Data, - ResponseURL: bp.ResponseURL.String(), - } - return json.Marshal(anon) + JobRunID string `json:"id"` + Data models.JSON `json:"data"` + ResponseURL string `json:"responseURL,omitempty"` } var zeroURL = new(url.URL) diff --git a/core/adapters/bridge_test.go b/core/adapters/bridge_test.go index 7c70b37cdc1..da00e076276 100644 --- a/core/adapters/bridge_test.go +++ b/core/adapters/bridge_test.go @@ -5,10 +5,12 @@ import ( "net/http" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBridge_PerformEmbedsParamsInData(t *testing.T) { @@ -29,14 +31,11 @@ func TestBridge_PerformEmbedsParamsInData(t *testing.T) { _, bt := cltest.NewBridgeType(t, "auctionBidding", mock.URL) params := cltest.JSONFromString(t, `{"bodyParam": true}`) - ba := &adapters.Bridge{BridgeType: bt, Params: ¶ms} - - input := models.RunResult{ - Data: cltest.JSONFromString(t, `{"result":"100"}`), - Status: models.RunStatusUnstarted, - } - ba.Perform(input, store) + ba := &adapters.Bridge{BridgeType: *bt, Params: params} + input := cltest.NewRunInputWithResult("100") + result := ba.Perform(input, store) + require.NoError(t, result.Error()) assert.Equal(t, `{"bodyParam":true,"result":"100"}`, data) assert.Equal(t, "Bearer "+bt.OutgoingToken, token) } @@ -45,23 +44,21 @@ func TestBridge_PerformAcceptsNonJsonObjectResponses(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() store.Config.Set("BRIDGE_RESPONSE_URL", cltest.WebURL(t, "")) + jobRunID := models.NewID() - mock, cleanup := cltest.NewHTTPMockServer(t, http.StatusOK, "POST", fmt.Sprintf(`{"jobRunID": "%s", "data": 251990120, "statusCode": 200}`, models.NewID()), + mock, cleanup := cltest.NewHTTPMockServer(t, http.StatusOK, "POST", fmt.Sprintf(`{"jobRunID": "%s", "data": 251990120, "statusCode": 200}`, jobRunID.String()), func(h http.Header, b string) {}, ) defer cleanup() _, bt := cltest.NewBridgeType(t, "auctionBidding", mock.URL) params := cltest.JSONFromString(t, `{"bodyParam": true}`) - ba := &adapters.Bridge{BridgeType: bt, Params: ¶ms} + ba := &adapters.Bridge{BridgeType: *bt, Params: params} - input := models.RunResult{ - Data: cltest.JSONFromString(t, `{"jobRunID": "jobID", "data": 251990120, "statusCode": 200}`), - Status: models.RunStatusUnstarted, - } + input := *models.NewRunInput(jobRunID, cltest.JSONFromString(t, `{"jobRunID": "jobID", "data": 251990120, "statusCode": 200}`), models.RunStatusUnstarted) result := ba.Perform(input, store) - assert.NoError(t, result.GetError()) - assert.Equal(t, "251990120", result.Data.Get("result").String()) + require.NoError(t, result.Error()) + assert.Equal(t, "251990120", result.Result().String()) } func TestBridge_Perform_transitionsTo(t *testing.T) { @@ -73,8 +70,7 @@ func TestBridge_Perform_transitionsTo(t *testing.T) { result string }{ {"from pending bridge", models.RunStatusPendingBridge, models.RunStatusInProgress, `{"result":"100"}`}, - {"from errored", models.RunStatusErrored, models.RunStatusErrored, `{"result":"100"}`}, - {"from in progress", models.RunStatusInProgress, models.RunStatusPendingBridge, `{}`}, + {"from in progress", models.RunStatusInProgress, models.RunStatusPendingBridge, ""}, {"from completed", models.RunStatusCompleted, models.RunStatusCompleted, `{"result":"100"}`}, } @@ -86,20 +82,13 @@ func TestBridge_Perform_transitionsTo(t *testing.T) { t.Run(test.name, func(t *testing.T) { mock, _ := cltest.NewHTTPMockServer(t, http.StatusOK, "POST", `{"pending": true}`) _, bt := cltest.NewBridgeType(t, "auctionBidding", mock.URL) - ba := &adapters.Bridge{BridgeType: bt} - - input := models.RunResult{ - Data: cltest.JSONFromString(t, `{"result":"100"}`), - Status: test.status, - } + ba := &adapters.Bridge{BridgeType: *bt} + input := *models.NewRunInputWithResult(models.NewID(), "100", test.status) result := ba.Perform(input, store) - assert.Equal(t, test.result, result.Data.String()) - assert.Equal(t, test.wantStatus, result.Status) - if test.wantStatus.Errored() || test.wantStatus.Completed() { - assert.Equal(t, input, result) - } + assert.Equal(t, test.result, result.Data().String()) + assert.Equal(t, test.wantStatus, result.Status()) }) } } @@ -138,22 +127,20 @@ func TestBridge_Perform_startANewRun(t *testing.T) { defer ensureCalled() _, bt := cltest.NewBridgeType(t, "auctionBidding", mock.URL) - eb := &adapters.Bridge{BridgeType: bt} - input := cltest.RunResultWithResult("lot 49") - input.CachedJobRunID = runID + eb := &adapters.Bridge{BridgeType: *bt} + input := *models.NewRunInput(runID, cltest.JSONFromString(t, `{"result": "lot 49"}`), models.RunStatusUnstarted) result := eb.Perform(input, store) val := result.Result() assert.Equal(t, test.want, val.String()) assert.Equal(t, test.wantErrored, result.HasError()) - assert.Equal(t, test.wantPending, result.Status.PendingBridge()) + assert.Equal(t, test.wantPending, result.Status().PendingBridge()) }) } } func TestBridge_Perform_responseURL(t *testing.T) { - input := cltest.RunResultWithResult("lot 49") - input.CachedJobRunID = models.NewID() + input := cltest.NewRunInputWithResult("lot 49") t.Parallel() cases := []struct { @@ -164,12 +151,12 @@ func TestBridge_Perform_responseURL(t *testing.T) { { name: "basic URL", configuredURL: cltest.WebURL(t, "https://chain.link"), - want: fmt.Sprintf(`{"id":"%s","data":{"result":"lot 49"},"responseURL":"https://chain.link/v2/runs/%s"}`, input.CachedJobRunID, input.CachedJobRunID), + want: fmt.Sprintf(`{"id":"%s","data":{"result":"lot 49"},"responseURL":"https://chain.link/v2/runs/%s"}`, input.JobRunID().String(), input.JobRunID().String()), }, { name: "blank URL", configuredURL: cltest.WebURL(t, ""), - want: fmt.Sprintf(`{"id":"%s","data":{"result":"lot 49"}}`, input.CachedJobRunID), + want: fmt.Sprintf(`{"id":"%s","data":{"result":"lot 49"}}`, input.JobRunID().String()), }, } @@ -186,7 +173,7 @@ func TestBridge_Perform_responseURL(t *testing.T) { defer ensureCalled() _, bt := cltest.NewBridgeType(t, "auctionBidding", mock.URL) - eb := &adapters.Bridge{BridgeType: bt} + eb := &adapters.Bridge{BridgeType: *bt} eb.Perform(input, store) }) } diff --git a/core/adapters/compare.go b/core/adapters/compare.go index 3b4b4d8f350..e18925bba50 100644 --- a/core/adapters/compare.go +++ b/core/adapters/compare.go @@ -4,9 +4,8 @@ import ( "errors" "strconv" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/tidwall/gjson" + "chainlink/core/store" + "chainlink/core/store/models" ) // Compare adapter type takes an Operator and a Value field to @@ -25,56 +24,49 @@ var ( // Perform uses the Operator to check the run's result against the // specified Value. -func (c *Compare) Perform(input models.RunResult, _ *store.Store) models.RunResult { - prevResult := input.Result() +func (c *Compare) Perform(input models.RunInput, _ *store.Store) models.RunOutput { + prevResult := input.Result().String() if c.Value == "" { - input.SetError(ErrValueNotSpecified) - return input + return models.NewRunOutputError(ErrValueNotSpecified) } switch c.Operator { case "eq": - input.CompleteWithResult(c.Value == prevResult.String()) + return models.NewRunOutputCompleteWithResult(c.Value == prevResult) case "neq": - input.CompleteWithResult(c.Value != prevResult.String()) + return models.NewRunOutputCompleteWithResult(c.Value != prevResult) case "gt": value, desired, err := getValues(prevResult, c.Value) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } - input.CompleteWithResult(desired < value) + return models.NewRunOutputCompleteWithResult(desired < value) case "gte": value, desired, err := getValues(prevResult, c.Value) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } - input.CompleteWithResult(desired <= value) + return models.NewRunOutputCompleteWithResult(desired <= value) case "lt": value, desired, err := getValues(prevResult, c.Value) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } - input.CompleteWithResult(desired > value) + return models.NewRunOutputCompleteWithResult(desired > value) case "lte": value, desired, err := getValues(prevResult, c.Value) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } - input.CompleteWithResult(desired >= value) + return models.NewRunOutputCompleteWithResult(desired >= value) default: - input.SetError(ErrOperatorNotSpecified) + return models.NewRunOutputError(ErrOperatorNotSpecified) } - - return input } -func getValues(result gjson.Result, d string) (float64, float64, error) { - value, err := strconv.ParseFloat(result.String(), 64) +func getValues(result string, d string) (float64, float64, error) { + value, err := strconv.ParseFloat(result, 64) if err != nil { return 0, 0, ErrResultNotNumber } diff --git a/core/adapters/compare_test.go b/core/adapters/compare_test.go index d6eab147f52..2e8886970fc 100644 --- a/core/adapters/compare_test.go +++ b/core/adapters/compare_test.go @@ -3,8 +3,9 @@ package adapters_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "github.com/stretchr/testify/assert" ) @@ -803,11 +804,11 @@ func TestCompare_Perform(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() - input := cltest.RunResultWithResult(test.input) + input := cltest.NewRunInputWithResult(test.input) adapter := test.adapter result := adapter.Perform(input, nil) val := result.Result() - assert.NoError(t, result.GetError()) + assert.NoError(t, result.Error()) assert.Equal(t, test.wantResult, val.Bool()) }) } @@ -879,12 +880,11 @@ func TestCompareError_Perform(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() - input := cltest.RunResultWithResult(test.input) + + input := cltest.NewRunInputWithResult(test.input) adapter := test.adapter result := adapter.Perform(input, nil) - _, err := result.ResultString() - assert.NoError(t, err) - assert.Equal(t, test.expected, result.GetError()) + assert.Equal(t, test.expected, result.Error()) }) } } diff --git a/core/adapters/copy.go b/core/adapters/copy.go index 78e3e818f59..99de8ea0e3a 100644 --- a/core/adapters/copy.go +++ b/core/adapters/copy.go @@ -1,8 +1,8 @@ package adapters import ( - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store" + "chainlink/core/store/models" ) // Copy obj keys refers to which value to copy inside `data`, @@ -12,24 +12,13 @@ type Copy struct { } // Perform returns the copied values from the desired mapping within the `data` JSON object -func (c *Copy) Perform(input models.RunResult, store *store.Store) models.RunResult { - jp := JSONParse{Path: c.CopyPath} - - data, err := input.Data.Add("result", input.Data.String()) - if err != nil { - return models.RunResultError(err) - } - input.Data = data - - rr := jp.Perform(input, store) - if rr.HasError() { - return rr - } - - rr.Data, err = input.Data.Merge(rr.Data) +func (c *Copy) Perform(input models.RunInput, store *store.Store) models.RunOutput { + data, err := models.JSON{}.Add("result", input.Data().String()) if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } - return rr + jp := JSONParse{Path: c.CopyPath} + input = *models.NewRunInput(input.JobRunID(), data, input.Status()) + return jp.Perform(input, store) } diff --git a/core/adapters/copy_test.go b/core/adapters/copy_test.go index 215f30a1f71..9e1826fabef 100644 --- a/core/adapters/copy_test.go +++ b/core/adapters/copy_test.go @@ -2,42 +2,80 @@ package adapters_test import ( "encoding/json" + "errors" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" ) func TestCopy_Perform(t *testing.T) { tests := []struct { name string - result string + input string copyPath []string wantData string wantStatus models.RunStatus - wantResultError bool + wantResultError error }{ - {"existing path", `{"high":"11850.00","last":"11779.99"}`, []string{"last"}, - `{"high":"11850.00","last":"11779.99","result":"11779.99"}`, models.RunStatusCompleted, false}, - {"nonexistent path", `{"high":"11850.00","last":"11779.99"}`, []string{"doesnotexist"}, - `{"high":"11850.00","last":"11779.99","result":null}`, models.RunStatusCompleted, false}, - {"double nonexistent path", `{"high":"11850.00","last":"11779.99"}`, []string{"no", "really"}, - ``, models.RunStatusErrored, true}, - {"array index path", `{"data":[{"availability":"0.99991"}]}`, []string{"data", "0", "availability"}, - `{"data":[{"availability":"0.99991"}],"result":"0.99991"}`, models.RunStatusCompleted, false}, - {"float result", `{"availability":0.99991}`, []string{"availability"}, - `{"availability":0.99991,"result":0.99991}`, models.RunStatusCompleted, false}, - {"result with quotes", `{"availability":"\""}`, []string{`"`}, - `{"availability":"\"","result":null}`, models.RunStatusCompleted, false}, + { + "existing path", + `{"high":"11850.00","last":"11779.99"}`, + []string{"last"}, + `{"result":"11779.99"}`, + models.RunStatusCompleted, + nil, + }, + { + "nonexistent path", + `{"high":"11850.00","last":"11779.99"}`, + []string{"doesnotexist"}, + `{"result":null}`, + models.RunStatusCompleted, + nil, + }, + { + "array index path", + `{"data":[{"availability":"0.99991"}]}`, + []string{"data", "0", "availability"}, + `{"result":"0.99991"}`, + models.RunStatusCompleted, + nil, + }, + { + "float result", + `{"availability":0.99991}`, + []string{"availability"}, + `{"result":0.99991}`, + models.RunStatusCompleted, + nil, + }, + { + "result with quotes", + `{"availability":"\""}`, + []string{`"`}, + `{"result":null}`, + models.RunStatusCompleted, + nil, + }, { "index array of array", `{"data":[[0,1]]}`, []string{"data", "0", "0"}, - `{"data":[[0,1]],"result":0}`, + `{"result":0}`, models.RunStatusCompleted, - false, + nil, + }, + { + "double nonexistent path", + `{"high":"11850.00","last":"11779.99"}`, + []string{"no", "really"}, + ``, + models.RunStatusErrored, + errors.New("No value could be found for the key 'no'"), }, } @@ -45,17 +83,13 @@ func TestCopy_Perform(t *testing.T) { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - input := cltest.RunResultWithData(test.result) + input := cltest.NewRunInputWithString(t, test.input) adapter := adapters.Copy{CopyPath: test.copyPath} result := adapter.Perform(input, nil) - assert.Equal(t, test.wantData, result.Data.String()) - assert.Equal(t, test.wantStatus, result.Status) + assert.Equal(t, test.wantData, result.Data().String()) + assert.Equal(t, test.wantStatus, result.Status()) - if test.wantResultError { - assert.NotNil(t, result.GetError()) - } else { - assert.Nil(t, result.GetError()) - } + assert.Equal(t, test.wantResultError, result.Error()) }) } } diff --git a/core/adapters/eth_bool.go b/core/adapters/eth_bool.go index 30133e4b445..fc337b39c73 100644 --- a/core/adapters/eth_bool.go +++ b/core/adapters/eth_bool.go @@ -1,8 +1,9 @@ package adapters import ( - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store" + "chainlink/core/store/models" + "github.com/tidwall/gjson" ) @@ -17,12 +18,12 @@ type EthBool struct{} // For example, after converting the result false to hex encoded Ethereum // ABI, it would be: // "0x0000000000000000000000000000000000000000000000000000000000000000" -func (*EthBool) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (*EthBool) Perform(input models.RunInput, _ *store.Store) models.RunOutput { if boolean(input.Result().Type) { - return models.RunResultComplete(evmTrue) - } else { - return models.RunResultComplete(evmFalse) + return models.NewRunOutputCompleteWithResult(evmTrue) } + + return models.NewRunOutputCompleteWithResult(evmFalse) } func boolean(t gjson.Type) bool { diff --git a/core/adapters/eth_bool_test.go b/core/adapters/eth_bool_test.go index 248edb5605d..b8ad7846880 100644 --- a/core/adapters/eth_bool_test.go +++ b/core/adapters/eth_bool_test.go @@ -3,9 +3,9 @@ package adapters_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "github.com/stretchr/testify/assert" ) @@ -29,16 +29,12 @@ func TestEthBool_Perform(t *testing.T) { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - past := models.RunResult{ - Data: cltest.JSONFromString(t, test.json), - } + past := cltest.NewRunInputWithString(t, test.json) adapter := adapters.EthBool{} result := adapter.Perform(past, nil) - val, err := result.ResultString() - assert.Equal(t, test.expected, val) - assert.NoError(t, err) - assert.NoError(t, result.GetError()) + assert.NoError(t, result.Error()) + assert.Equal(t, test.expected, result.Result().String()) }) } } diff --git a/core/adapters/eth_format.go b/core/adapters/eth_format.go index a412fd31874..22a107b9636 100644 --- a/core/adapters/eth_format.go +++ b/core/adapters/eth_format.go @@ -1,11 +1,12 @@ package adapters import ( + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" ) // EthBytes32 holds no fields. @@ -17,7 +18,7 @@ type EthBytes32 struct{} // For example, after converting the string "16800.01" to hex encoded Ethereum // ABI, it would be: // "0x31363830302e3031000000000000000000000000000000000000000000000000" -func (*EthBytes32) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (*EthBytes32) Perform(input models.RunInput, _ *store.Store) models.RunOutput { result := input.Result() value := common.RightPadBytes([]byte(result.String()), utils.EVMWordByteLen) hex := utils.RemoveHexPrefix(hexutil.Encode(value)) @@ -26,7 +27,7 @@ func (*EthBytes32) Perform(input models.RunResult, _ *store.Store) models.RunRes hex = hex[:utils.EVMWordHexLen] } - return models.RunResultComplete(utils.AddHexPrefix(hex)) + return models.NewRunOutputCompleteWithResult(utils.AddHexPrefix(hex)) } // EthInt256 holds no fields @@ -38,14 +39,13 @@ type EthInt256 struct{} // For example, after converting the string "-123.99" to hex encoded Ethereum // ABI, it would be: // "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff85" -func (*EthInt256) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (*EthInt256) Perform(input models.RunInput, _ *store.Store) models.RunOutput { value, err := utils.EVMTranscodeInt256(input.Result()) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } - return models.RunResultComplete(hexutil.Encode(value)) + return models.NewRunOutputCompleteWithResult(hexutil.Encode(value)) } // EthUint256 holds no fields. @@ -57,12 +57,11 @@ type EthUint256 struct{} // For example, after converting the string "123.99" to hex encoded Ethereum // ABI, it would be: // "0x000000000000000000000000000000000000000000000000000000000000007b" -func (*EthUint256) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (*EthUint256) Perform(input models.RunInput, _ *store.Store) models.RunOutput { value, err := utils.EVMTranscodeUint256(input.Result()) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } - return models.RunResultComplete(hexutil.Encode(value)) + return models.NewRunOutputCompleteWithResult(hexutil.Encode(value)) } diff --git a/core/adapters/eth_format_test.go b/core/adapters/eth_format_test.go index e713512cd74..b65308a95c2 100644 --- a/core/adapters/eth_format_test.go +++ b/core/adapters/eth_format_test.go @@ -3,10 +3,11 @@ package adapters_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestEthBytes32_Perform(t *testing.T) { @@ -34,16 +35,13 @@ func TestEthBytes32_Perform(t *testing.T) { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - past := models.RunResult{ - Data: cltest.JSONFromString(t, test.json), - } + + past := cltest.NewRunInputWithString(t, test.json) adapter := adapters.EthBytes32{} result := adapter.Perform(past, nil) - val, err := result.ResultString() - assert.NoError(t, err) - assert.NoError(t, result.GetError()) - assert.Equal(t, test.expected, val) + require.NoError(t, result.Error()) + assert.Equal(t, test.expected, result.Result().String()) }) } } @@ -78,18 +76,14 @@ func TestEthInt256_Perform(t *testing.T) { adapter := adapters.EthInt256{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { - input := models.RunResult{ - Data: cltest.JSONFromString(t, test.json), - } + input := cltest.NewRunInputWithString(t, test.json) result := adapter.Perform(input, nil) if test.errored { - assert.Error(t, result.GetError()) + assert.Error(t, result.Error()) } else { - val, err := result.ResultString() - assert.NoError(t, result.GetError()) - assert.NoError(t, err) - assert.Equal(t, test.want, val) + require.NoError(t, result.Error()) + assert.Equal(t, test.want, result.Result().String()) } }) } @@ -124,18 +118,14 @@ func TestEthUint256_Perform(t *testing.T) { adapter := adapters.EthUint256{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { - input := models.RunResult{ - Data: cltest.JSONFromString(t, test.json), - } + input := cltest.NewRunInput(cltest.JSONFromString(t, test.json)) result := adapter.Perform(input, nil) if test.errored { - assert.Error(t, result.GetError()) + require.Error(t, result.Error()) } else { - val, err := result.ResultString() - assert.NoError(t, result.GetError()) - assert.NoError(t, err) - assert.Equal(t, test.want, val) + require.NoError(t, result.Error()) + assert.Equal(t, test.want, result.Result().String()) } }) } diff --git a/core/adapters/eth_tx.go b/core/adapters/eth_tx.go index 98ba71993c7..964d8abf42c 100644 --- a/core/adapters/eth_tx.go +++ b/core/adapters/eth_tx.go @@ -6,14 +6,15 @@ import ( "net" "regexp" + "chainlink/core/logger" + "chainlink/core/store" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" "gopkg.in/guregu/null.v3" ) @@ -37,29 +38,28 @@ type EthTx struct { // Perform creates the run result for the transaction if the existing run result // is not currently pending. Then it confirms the transaction was confirmed on // the blockchain. -func (etx *EthTx) Perform(input models.RunResult, store *strpkg.Store) models.RunResult { +func (etx *EthTx) Perform(input models.RunInput, store *strpkg.Store) models.RunOutput { if !store.TxManager.Connected() { - var output models.RunResult - // output.Data = input.Data - output.MarkPendingConnection() - return output + return models.NewRunOutputPendingConnection() } - if !input.Status.PendingConfirmations() { - value, err := getTxData(etx, input) - if err != nil { - input.SetError(errors.Wrap(err, "while constructing EthTx data")) - return input - } - data := utils.ConcatBytes(etx.FunctionSelector.Bytes(), etx.DataPrefix, value) - return createTxRunResult(etx.Address, etx.GasPrice, etx.GasLimit, data, input, store) + if input.Status().PendingConfirmations() { + return ensureTxRunResult(input, store) + } + + value, err := getTxData(etx, input) + if err != nil { + err = errors.Wrap(err, "while constructing EthTx data") + return models.NewRunOutputError(err) } - return ensureTxRunResult(input, store) + + data := utils.ConcatBytes(etx.FunctionSelector.Bytes(), etx.DataPrefix, value) + return createTxRunResult(etx.Address, etx.GasPrice, etx.GasLimit, data, input, store) } // getTxData returns the data to save against the callback encoded according to // the dataFormat parameter in the job spec -func getTxData(e *EthTx, input models.RunResult) ([]byte, error) { +func getTxData(e *EthTx, input models.RunInput) ([]byte, error) { result := input.Result() if e.DataFormat == "" { return common.HexToHash(result.Str).Bytes(), nil @@ -81,43 +81,35 @@ func createTxRunResult( gasPrice *models.Big, gasLimit uint64, data []byte, - input models.RunResult, + input models.RunInput, store *strpkg.Store, -) models.RunResult { - jobRunID := null.String{} - if input.CachedJobRunID != nil { - jobRunID = null.StringFrom(input.CachedJobRunID.String()) - } - +) models.RunOutput { tx, err := store.TxManager.CreateTxWithGas( - jobRunID, + null.StringFrom(input.JobRunID().String()), address, data, gasPrice.ToInt(), gasLimit, ) if IsClientRetriable(err) { - var output models.RunResult - output.MarkPendingConnection() - return output + return models.NewRunOutputPendingConnection() } else if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } - var output models.RunResult - output.ApplyResult(tx.Hash.String()) + output, err := models.JSON{}.Add("result", tx.Hash.String()) + if err != nil { + return models.NewRunOutputError(err) + } txAttempt := tx.Attempts[0] - receipt, state, err := store.TxManager.CheckAttempt(txAttempt, tx.SentAt) if IsClientRetriable(err) { - output.MarkPendingConnection() - return output + return models.NewRunOutputPendingConnectionWithData(output) } else if IsClientEmptyError(err) { - output.MarkPendingConfirmations() - return output + return models.NewRunOutputPendingConfirmationsWithData(output) } else if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } logger.Debugw( @@ -129,36 +121,26 @@ func createTxRunResult( "receiptHash", receipt.Hash.Hex(), ) - if state != strpkg.Safe { - output.MarkPendingConfirmations() - return output + if state == strpkg.Safe { + return addReceiptToResult(receipt, input, output) } - if receipt != nil { - addReceiptToResult(receipt, input, &output) - } - return output + return models.NewRunOutputPendingConfirmationsWithData(output) } -func ensureTxRunResult(input models.RunResult, str *strpkg.Store) models.RunResult { +func ensureTxRunResult(input models.RunInput, str *strpkg.Store) models.RunOutput { val, err := input.ResultString() if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } hash := common.HexToHash(val) - if err != nil { - return models.RunResultError(err) - } - receipt, state, err := str.TxManager.BumpGasUntilSafe(hash) if err != nil { if IsClientEmptyError(err) { - var output models.RunResult - output.MarkPendingConfirmations() - return output + return models.NewRunOutputPendingConfirmations() } else if state == strpkg.Unknown { - return models.RunResultError(err) + return models.NewRunOutputError(err) } // We failed to get one of the TxAttempt receipts, so we won't mark this @@ -166,45 +148,59 @@ func ensureTxRunResult(input models.RunResult, str *strpkg.Store) models.RunResu logger.Warn("EthTx Adapter Perform Resuming: ", err) } - var output models.RunResult + var output models.JSON if receipt != nil && !receipt.Unconfirmed() { - // If the tx has been confirmed, add its hash to the RunResult. + // If the tx has been confirmed, record the hash in the output hex := receipt.Hash.String() - output.ApplyResult(hex) - output.Add("latestOutgoingTxHash", hex) + output, err = output.Add("result", hex) + if err != nil { + return models.NewRunOutputError(err) + } + output, err = output.Add("latestOutgoingTxHash", hex) + if err != nil { + return models.NewRunOutputError(err) + } } else { // If the tx is still unconfirmed, just copy over the original tx hash. - output.ApplyResult(hash) + output, err = output.Add("result", hash) + if err != nil { + return models.NewRunOutputError(err) + } } - if state != strpkg.Safe { - output.MarkPendingConfirmations() - return output + if state == strpkg.Safe { + return addReceiptToResult(receipt, input, output) } - if receipt != nil { - addReceiptToResult(receipt, input, &output) - } - return output + return models.NewRunOutputPendingConfirmationsWithData(output) } -var zero = common.Hash{} - -func addReceiptToResult(receipt *models.TxReceipt, input models.RunResult, output *models.RunResult) { +func addReceiptToResult( + receipt *models.TxReceipt, + input models.RunInput, + data models.JSON, +) models.RunOutput { receipts := []models.TxReceipt{} - if !output.Get("ethereumReceipts").IsArray() { - output.Add("ethereumReceipts", receipts) - } - - if err := json.Unmarshal([]byte(input.Get("ethereumReceipts").String()), &receipts); err != nil { - logger.Error(fmt.Errorf("EthTx Adapter unmarshaling ethereum Receipts: %v", err)) + ethereumReceipts := input.Data().Get("ethereumReceipts").String() + if ethereumReceipts != "" { + if err := json.Unmarshal([]byte(ethereumReceipts), &receipts); err != nil { + logger.Errorw("Error unmarshaling ethereum Receipts", "error", err) + } } receipts = append(receipts, *receipt) - output.Add("ethereumReceipts", receipts) - output.CompleteWithResult(receipt.Hash.String()) + var err error + data, err = data.Add("ethereumReceipts", receipts) + if err != nil { + return models.NewRunOutputError(err) + } + data, err = data.Add("result", receipt.Hash.String()) + if err != nil { + return models.NewRunOutputError(err) + } + return models.NewRunOutputComplete(data) } // IsClientRetriable does its best effort to see if an error indicates one that diff --git a/core/adapters/eth_tx_abi_encode.go b/core/adapters/eth_tx_abi_encode.go index b09fe91c584..7f9a8dae193 100644 --- a/core/adapters/eth_tx_abi_encode.go +++ b/core/adapters/eth_tx_abi_encode.go @@ -13,9 +13,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" ) const evmWordSize = 32 @@ -63,17 +63,15 @@ func (etx *EthTxABIEncode) UnmarshalJSON(data []byte) error { // Perform creates the run result for the transaction if the existing run result // is not currently pending. Then it confirms the transaction was confirmed on // the blockchain. -func (etx *EthTxABIEncode) Perform( - input models.RunResult, store *strpkg.Store) models.RunResult { +func (etx *EthTxABIEncode) Perform(input models.RunInput, store *strpkg.Store) models.RunOutput { if !store.TxManager.Connected() { - input.MarkPendingConnection() - return input + return models.NewRunOutputPendingConnection() } - if !input.Status.PendingConfirmations() { + if !input.Status().PendingConfirmations() { data, err := etx.abiEncode(&input) if err != nil { - input.SetError(errors.Wrap(err, "while constructing EthTxABIEncode data")) - return input + err = errors.Wrap(err, "while constructing EthTxABIEncode data") + return models.NewRunOutputError(err) } return createTxRunResult(etx.Address, etx.GasPrice, etx.GasLimit, data, input, store) } @@ -82,8 +80,8 @@ func (etx *EthTxABIEncode) Perform( // abiEncode ABI-encodes the arguments passed in a RunResult's result field // according to etx.FunctionABI -func (etx *EthTxABIEncode) abiEncode(runResult *models.RunResult) ([]byte, error) { - args, ok := runResult.Get("result").Value().(map[string]interface{}) +func (etx *EthTxABIEncode) abiEncode(input *models.RunInput) ([]byte, error) { + args, ok := input.Data().Get("result").Value().(map[string]interface{}) if !ok { return nil, errors.Errorf("json result is not an object") } diff --git a/core/adapters/eth_tx_abi_encode_test.go b/core/adapters/eth_tx_abi_encode_test.go index b16762333b4..ab0f7c68333 100644 --- a/core/adapters/eth_tx_abi_encode_test.go +++ b/core/adapters/eth_tx_abi_encode_test.go @@ -15,10 +15,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -148,7 +148,7 @@ func TestEthTxABIEncodeAdapter_Perform_ConfirmedWithJSON(t *testing.T) { }) receipt := models.TxReceipt{Hash: hash, BlockNumber: cltest.Int(confirmed)} ethMock.Register("eth_getTransactionReceipt", receipt) - input := cltest.RunResultWithData(rawInput) + input := cltest.NewRunInputWithString(t, rawInput) responseData := adapterUnderTest.Perform(input, store) assert.False(t, responseData.HasError()) from := cltest.GetAccountAddress(t, store) diff --git a/core/adapters/eth_tx_test.go b/core/adapters/eth_tx_test.go index 7bb31e20c0b..9d2ed8cc97a 100644 --- a/core/adapters/eth_tx_test.go +++ b/core/adapters/eth_tx_test.go @@ -7,17 +7,18 @@ import ( "syscall" "testing" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/golang/mock/gomock" "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/internal/mocks" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -32,7 +33,6 @@ func TestEthTxAdapter_Perform_Confirmed(t *testing.T) { fHash := models.HexToFunctionSelector("b3f98adc") dataPrefix := hexutil.Bytes( hexutil.MustDecode("0x0000000000000000000000000000000000000000000000000045746736453745")) - inputValue := "0x9786856756" ethMock, err := app.MockStartAndConnect() require.NoError(t, err) @@ -56,15 +56,14 @@ func TestEthTxAdapter_Perform_Confirmed(t *testing.T) { receipt := models.TxReceipt{Hash: hash, BlockNumber: cltest.Int(confirmed)} ethMock.Register("eth_getTransactionReceipt", receipt) + input := *models.NewRunInputWithResult(models.NewID(), "0x9786856756", models.RunStatusUnstarted) adapter := adapters.EthTx{ Address: address, DataPrefix: dataPrefix, FunctionSelector: fHash, } - input := cltest.RunResultWithResult(inputValue) - data := adapter.Perform(input, store) - - assert.NoError(t, data.GetError()) + result := adapter.Perform(input, store) + require.NoError(t, result.Error()) from := cltest.GetAccountAddress(t, store) txs, err := store.TxFrom(from) @@ -86,7 +85,6 @@ func TestEthTxAdapter_Perform_ConfirmedWithBytes(t *testing.T) { fHash := models.HexToFunctionSelector("b3f98adc") dataPrefix := hexutil.Bytes( hexutil.MustDecode("0x0000000000000000000000000000000000000000000000000045746736453745")) - inputValue := "cönfirmed" // contains diacritic acute to check bytes counted for length not chars ethMock, err := app.MockStartAndConnect() require.NoError(t, err) @@ -118,10 +116,11 @@ func TestEthTxAdapter_Perform_ConfirmedWithBytes(t *testing.T) { FunctionSelector: fHash, DataFormat: adapters.DataFormatBytes, } - input := cltest.RunResultWithResult(inputValue) - data := adapter.Perform(input, store) - assert.NoError(t, data.GetError()) + inputValue := "cönfirmed" // contains diacritic acute to check bytes counted for length not chars + input := *models.NewRunInputWithResult(models.NewID(), inputValue, models.RunStatusUnstarted) + result := adapter.Perform(input, store) + assert.NoError(t, result.Error()) from := cltest.GetAccountAddress(t, store) txs, err := store.TxFrom(from) @@ -141,8 +140,6 @@ func TestEthTxAdapter_Perform_SafeWithBytesAndNoDataPrefix(t *testing.T) { address := cltest.NewAddress() fHash := models.HexToFunctionSelector("b3f98adc") - // contains diacritic acute to check bytes counted for length not chars - inputValue := "cönfirmed" currentHeight := uint64(23456) require.NoError(t, app.Store.ORM.CreateHead(cltest.Head(currentHeight))) @@ -173,11 +170,13 @@ func TestEthTxAdapter_Perform_SafeWithBytesAndNoDataPrefix(t *testing.T) { FunctionSelector: fHash, DataFormat: adapters.DataFormatBytes, } - input := cltest.RunResultWithResult(inputValue) - data := adapter.Perform(input, store) - assert.NoError(t, data.GetError()) - assert.Equal(t, string(models.RunStatusCompleted), string(data.Status)) + // contains diacritic acute to check bytes counted for length not chars + inputValue := "cönfirmed" + input := *models.NewRunInputWithResult(models.NewID(), inputValue, models.RunStatusUnstarted) + result := adapter.Perform(input, store) + require.NoError(t, result.Error()) + assert.Equal(t, string(models.RunStatusCompleted), string(result.Status())) from := cltest.GetAccountAddress(t, store) var txs []models.Tx @@ -212,13 +211,14 @@ func TestEthTxAdapter_Perform_FromPendingConfirmations_StillPending(t *testing.T tx := cltest.CreateTx(t, store, from, sentAt) a := tx.Attempts[0] adapter := adapters.EthTx{} - sentResult := cltest.RunResultWithResult(a.Hash.String()) - sentResult.MarkPendingConfirmations() + sentResult := *models.NewRunInputWithResult( + models.NewID(), a.Hash.String(), models.RunStatusPendingConfirmations, + ) output := adapter.Perform(sentResult, store) - assert.False(t, output.HasError()) - assert.True(t, output.Status.PendingConfirmations()) + require.NoError(t, output.Error()) + assert.True(t, output.Status().PendingConfirmations()) tx, err := store.FindTx(tx.ID) require.NoError(t, err) assert.Len(t, tx.Attempts, 1) @@ -249,12 +249,13 @@ func TestEthTxAdapter_Perform_FromPendingConfirmations_BumpGas(t *testing.T) { a := tx.Attempts[0] adapter := adapters.EthTx{} - sentResult := cltest.RunResultWithResult(a.Hash.String()) - sentResult.MarkPendingConfirmations() + sentResult := *models.NewRunInputWithResult( + models.NewID(), a.Hash.String(), models.RunStatusPendingConfirmations, + ) output := adapter.Perform(sentResult, store) - assert.False(t, output.HasError()) - assert.True(t, output.Status.PendingConfirmations()) + require.NoError(t, output.Error()) + assert.True(t, output.Status().PendingConfirmations()) tx, err = store.FindTx(tx.ID) require.NoError(t, err) assert.Len(t, tx.Attempts, 2) @@ -293,20 +294,19 @@ func TestEthTxAdapter_Perform_FromPendingConfirmations_ConfirmCompletes(t *testi store.AddTxAttempt(tx, tx.EthTx(big.NewInt(2)), sentAt+1) a3, _ := store.AddTxAttempt(tx, tx.EthTx(big.NewInt(3)), sentAt+2) adapter := adapters.EthTx{} - sentResult := cltest.RunResultWithResult(a3.Hash.String()) - sentResult.MarkPendingConfirmations() + sentResult := *models.NewRunInputWithResult( + models.NewID(), a3.Hash.String(), models.RunStatusPendingConfirmations, + ) assert.False(t, tx.Confirmed) output := adapter.Perform(sentResult, store) - assert.True(t, output.Status.Completed()) - assert.False(t, output.HasError()) - value, err := output.ResultString() - assert.Nil(t, err) - assert.Equal(t, confirmedHash.String(), value) + require.NoError(t, output.Error()) + assert.True(t, output.Status().Completed()) + assert.Equal(t, confirmedHash.String(), output.Result().String()) - tx, err = store.FindTx(tx.ID) + tx, err := store.FindTx(tx.ID) require.NoError(t, err) assert.True(t, tx.Confirmed) require.Len(t, tx.Attempts, 2) @@ -315,8 +315,8 @@ func TestEthTxAdapter_Perform_FromPendingConfirmations_ConfirmCompletes(t *testi receiptsJSON := output.Get("ethereumReceipts").String() var receipts []models.TxReceipt - assert.NoError(t, json.Unmarshal([]byte(receiptsJSON), &receipts)) - assert.Equal(t, 1, len(receipts)) + require.NoError(t, json.Unmarshal([]byte(receiptsJSON), &receipts)) + require.Len(t, receipts, 1) assert.Equal(t, receipt, receipts[0]) confirmedTxHex := output.Get("latestOutgoingTxHash").String() @@ -341,6 +341,9 @@ func TestEthTxAdapter_Perform_AppendingTransactionReceipts(t *testing.T) { confirmedAt := sentAt + config.MinOutgoingConfirmations() - 1 // confirmations are 0-based idx require.NoError(t, app.Store.ORM.CreateHead(cltest.Head(confirmedAt))) ethMock.Register("eth_chainId", config.ChainID()) + ethMock.Register("eth_getTransactionCount", `0x100`) + ethMock.Register("eth_getBalance", "0x100") + ethMock.Register("eth_call", "0x1") require.NoError(t, app.StartAndConnect()) @@ -350,18 +353,22 @@ func TestEthTxAdapter_Perform_AppendingTransactionReceipts(t *testing.T) { a, err := store.AddTxAttempt(tx, tx.EthTx(big.NewInt(1)), sentAt) assert.NoError(t, err) adapter := adapters.EthTx{} - sentResult := cltest.RunResultWithResult(a.Hash.String()) - input := sentResult - input.MarkPendingConfirmations() previousReceipt := models.TxReceipt{Hash: cltest.NewHash(), BlockNumber: cltest.Int(sentAt - 10)} - input.Add("ethereumReceipts", []models.TxReceipt{previousReceipt}) + data, err := models.JSON{}.Add("result", a.Hash.String()) + require.NoError(t, err) + data, err = data.Add("ethereumReceipts", []models.TxReceipt{previousReceipt}) + require.NoError(t, err) + input := *models.NewRunInput(models.NewID(), data, models.RunStatusPendingConfirmations) output := adapter.Perform(input, store) - assert.True(t, output.Status.Completed()) + require.NoError(t, output.Error()) + assert.True(t, output.Status().Completed()) + receiptsJSON := output.Get("ethereumReceipts").String() var receipts []models.TxReceipt - assert.NoError(t, json.Unmarshal([]byte(receiptsJSON), &receipts)) + require.NotEqual(t, "", receiptsJSON) + require.NoError(t, json.Unmarshal([]byte(receiptsJSON), &receipts)) assert.Equal(t, []models.TxReceipt{previousReceipt, receipt}, receipts) ethMock.EventuallyAllCalled(t) @@ -381,12 +388,12 @@ func TestEthTxAdapter_Perform_WithError(t *testing.T) { Address: cltest.NewAddress(), FunctionSelector: models.HexToFunctionSelector("0xb3f98adc"), } - input := cltest.RunResultWithResult("0x9786856756") + input := *models.NewRunInputWithResult(models.NewID(), "0x9786856756", models.RunStatusUnstarted) ethMock.RegisterError("eth_sendRawTransaction", "Cannot connect to nodes") output := adapter.Perform(input, store) - assert.True(t, output.HasError()) - assert.Contains(t, output.Error(), "Cannot connect to nodes") + require.Error(t, output.Error()) + assert.Contains(t, output.Error().Error(), "Cannot connect to nodes") } func TestEthTxAdapter_Perform_WithErrorInvalidInput(t *testing.T) { @@ -405,12 +412,12 @@ func TestEthTxAdapter_Perform_WithErrorInvalidInput(t *testing.T) { Address: cltest.NewAddress(), FunctionSelector: models.HexToFunctionSelector("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1"), } - input := cltest.RunResultWithResult("0x9786856756") + input := *models.NewRunInputWithResult(models.NewID(), "0x9786856756", models.RunStatusUnstarted) ethMock.RegisterError("eth_sendRawTransaction", "Cannot connect to nodes") output := adapter.Perform(input, store) - assert.True(t, output.HasError()) - assert.Contains(t, output.Error(), "Cannot connect to nodes") + require.Error(t, output.Error()) + assert.Contains(t, output.Error().Error(), "Cannot connect to nodes") } func TestEthTxAdapter_Perform_PendingConfirmations_WithFatalErrorInTxManager(t *testing.T) { @@ -431,13 +438,14 @@ func TestEthTxAdapter_Perform_PendingConfirmations_WithFatalErrorInTxManager(t * Address: cltest.NewAddress(), FunctionSelector: models.HexToFunctionSelector("0xb3f98adc"), } - input := cltest.RunResultWithResult(cltest.NewHash().String()) - input.Status = models.RunStatusPendingConfirmations + input := *models.NewRunInputWithResult( + models.NewID(), cltest.NewHash().String(), models.RunStatusPendingConfirmations, + ) output := adapter.Perform(input, store) ethMock.AssertAllCalled() - assert.Equal(t, models.RunStatusErrored, output.Status) + assert.Equal(t, models.RunStatusErrored, output.Status()) assert.NotNil(t, output.Error()) } @@ -455,9 +463,9 @@ func TestEthTxAdapter_Perform_PendingConfirmations_WithRecoverableErrorInTxManag from := cltest.GetAccountAddress(t, store) tx := cltest.CreateTx(t, store, from, uint64(14372)) - input := cltest.RunResultWithResult(tx.Attempts[0].Hash.String()) - input.Status = models.RunStatusPendingConfirmations - + input := *models.NewRunInputWithResult( + models.NewID(), tx.Attempts[0].Hash.String(), models.RunStatusPendingConfirmations, + ) ethMock.RegisterError("eth_getTransactionReceipt", "Connection reset by peer") require.NoError(t, app.WaitForConnection()) @@ -470,48 +478,43 @@ func TestEthTxAdapter_Perform_PendingConfirmations_WithRecoverableErrorInTxManag ethMock.AssertAllCalled() - assert.Equal(t, models.RunStatusPendingConfirmations, output.Status) - assert.NoError(t, output.GetError()) + require.NoError(t, output.Error()) + assert.Equal(t, models.RunStatusPendingConfirmations, output.Status()) } func TestEthTxAdapter_DeserializationBytesFormat(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - txmMock := mocks.NewMockTxManager(ctrl) - store.TxManager = txmMock + txManager := new(mocks.TxManager) txAttempt := &models.TxAttempt{} tx := &models.Tx{Attempts: []*models.TxAttempt{txAttempt}} - txmMock.EXPECT().Connected().Return(true).AnyTimes() - txmMock.EXPECT().CreateTxWithGas(gomock.Any(), gomock.Any(), hexutil.MustDecode( + txManager.On("Connected").Maybe().Return(true) + txManager.On("CreateTxWithGas", mock.Anything, mock.Anything, hexutil.MustDecode( "0x00000000"+ "0000000000000000000000000000000000000000000000000000000000000020"+ "000000000000000000000000000000000000000000000000000000000000000b"+ "68656c6c6f20776f726c64000000000000000000000000000000000000000000"), - gomock.Any(), gomock.Any()).Return(tx, nil) - txmMock.EXPECT().CheckAttempt(txAttempt, uint64(0)).Return(&models.TxReceipt{}, strpkg.Unconfirmed, nil) + mock.Anything, mock.Anything).Return(tx, nil) + txManager.On("CheckAttempt", txAttempt, uint64(0)).Return(&models.TxReceipt{}, strpkg.Unconfirmed, nil) + store.TxManager = txManager task := models.TaskSpec{} err := json.Unmarshal([]byte(`{"type": "EthTx", "params": {"format": "bytes"}}`), &task) assert.NoError(t, err) assert.Equal(t, task.Type, adapters.TaskTypeEthTx) - adapter, err := adapters.For(task, store) + adapter, err := adapters.For(task, store.Config, store.ORM) assert.NoError(t, err) ethtx, ok := adapter.BaseAdapter.(*adapters.EthTx) assert.True(t, ok) assert.Equal(t, ethtx.DataFormat, adapters.DataFormatBytes) - input := models.RunResult{ - Data: cltest.JSONFromString(t, `{"result": "hello world"}`), - Status: models.RunStatusInProgress, - } + input := *models.NewRunInputWithResult(models.NewID(), "hello world", models.RunStatusInProgress) result := adapter.Perform(input, store) - assert.False(t, result.HasError()) - assert.Equal(t, result.Error(), "") + assert.NoError(t, result.Error()) + + txManager.AssertExpectations(t) } func TestEthTxAdapter_Perform_CustomGas(t *testing.T) { @@ -523,22 +526,19 @@ func TestEthTxAdapter_Perform_CustomGas(t *testing.T) { gasPrice := big.NewInt(187) gasLimit := uint64(911) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - txmMock := mocks.NewMockTxManager(ctrl) - store.TxManager = txmMock - txmMock.EXPECT().Connected().Return(true) - txmMock.EXPECT().CreateTxWithGas( - gomock.Any(), - gomock.Any(), - gomock.Any(), + txManager := new(mocks.TxManager) + txManager.On("Connected").Return(true) + txManager.On("CreateTxWithGas", + mock.Anything, + mock.Anything, + mock.Anything, gasPrice, gasLimit, ).Return(&models.Tx{ Attempts: []*models.TxAttempt{&models.TxAttempt{}}, }, nil) - txmMock.EXPECT().CheckAttempt(gomock.Any(), gomock.Any()).Return(&models.TxReceipt{}, strpkg.Unconfirmed, nil) + txManager.On("CheckAttempt", mock.Anything, mock.Anything).Return(&models.TxReceipt{}, strpkg.Unconfirmed, nil) + store.TxManager = txManager adapter := adapters.EthTx{ Address: cltest.NewAddress(), @@ -547,135 +547,119 @@ func TestEthTxAdapter_Perform_CustomGas(t *testing.T) { GasLimit: gasLimit, } - input := models.RunResult{ - Data: cltest.JSONFromString(t, `{"result": "hello world"}`), - Status: models.RunStatusInProgress, - } - + input := *models.NewRunInputWithResult(models.NewID(), "hello world", models.RunStatusInProgress) result := adapter.Perform(input, store) - assert.False(t, result.HasError()) + assert.NoError(t, result.Error()) + + txManager.AssertExpectations(t) } func TestEthTxAdapter_Perform_NotConnected(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - store := app.Store adapter := adapters.EthTx{} - input := models.RunResult{} - data := adapter.Perform(input, store) + data := adapter.Perform(models.RunInput{}, store) - assert.NoError(t, data.GetError()) - assert.Equal(t, models.RunStatusPendingConnection, data.Status) + require.NoError(t, data.Error()) + assert.Equal(t, models.RunStatusPendingConnection, data.Status()) } func TestEthTxAdapter_Perform_CreateTxWithGasErrorTreatsAsNotConnected(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - store := app.Store - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - txmMock := mocks.NewMockTxManager(ctrl) - store.TxManager = txmMock - txmMock.EXPECT().Connected().Return(true) - txmMock.EXPECT().CreateTxWithGas( - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), + txManager := new(mocks.TxManager) + txManager.On("Connected").Return(true) + txManager.On("CreateTxWithGas", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, ).Return(nil, syscall.ETIMEDOUT) + store.TxManager = txManager adapter := adapters.EthTx{} - input := models.RunResult{} - data := adapter.Perform(input, store) + data := adapter.Perform(models.RunInput{}, store) - assert.NoError(t, data.GetError()) - assert.Equal(t, models.RunStatusPendingConnection, data.Status) + require.NoError(t, data.Error()) + assert.Equal(t, models.RunStatusPendingConnection, data.Status()) + + txManager.AssertExpectations(t) } func TestEthTxAdapter_Perform_CheckAttemptErrorTreatsAsNotConnected(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - store := app.Store - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - txmMock := mocks.NewMockTxManager(ctrl) - store.TxManager = txmMock - txmMock.EXPECT().Connected().Return(true) - txmMock.EXPECT().CreateTxWithGas( - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), + txManager := new(mocks.TxManager) + txManager.On("Connected").Return(true) + txManager.On("CreateTxWithGas", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, ).Return(&models.Tx{ Attempts: []*models.TxAttempt{&models.TxAttempt{}}, }, nil) - txmMock.EXPECT().CheckAttempt(gomock.Any(), gomock.Any()).Return(nil, strpkg.Unknown, syscall.EWOULDBLOCK) + txManager.On("CheckAttempt", mock.Anything, mock.Anything).Return(nil, strpkg.Unknown, syscall.EWOULDBLOCK) + store.TxManager = txManager adapter := adapters.EthTx{} - input := models.RunResult{} - data := adapter.Perform(input, store) + data := adapter.Perform(models.RunInput{}, store) + + require.NoError(t, data.Error()) + assert.Equal(t, models.RunStatusPendingConnection, data.Status()) - assert.NoError(t, data.GetError()) - assert.Equal(t, models.RunStatusPendingConnection, data.Status) + txManager.AssertExpectations(t) } func TestEthTxAdapter_Perform_CreateTxWithEmptyResponseErrorTreatsAsPendingConfirmations(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - store := app.Store - from := cltest.GetAccountAddress(t, store) + from := cltest.NewAddress() tx := cltest.CreateTx(t, store, from, 1) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - txmMock := mocks.NewMockTxManager(ctrl) - store.TxManager = txmMock - txmMock.EXPECT().Connected().Return(true) - txmMock.EXPECT().CreateTxWithGas( - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), + badResponseErr := errors.New("Bad response on request: [ TransactionIndex ]. Error cause was EmptyResponse, (majority count: 94 / total: 94)") + txManager := new(mocks.TxManager) + txManager.On("Connected").Return(true) + txManager.On("CreateTxWithGas", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, ).Return(tx, nil) - txmMock.EXPECT().CheckAttempt( - gomock.Any(), - gomock.Any(), - ).Return(nil, strpkg.Unknown, errors.New("Bad response on request: [ TransactionIndex ]. Error cause was EmptyResponse, (majority count: 94 / total: 94)")) + txManager.On("CheckAttempt", mock.Anything, mock.Anything).Return(nil, strpkg.Unknown, badResponseErr) + store.TxManager = txManager adapter := adapters.EthTx{} - input := models.RunResult{} - input = adapter.Perform(input, store) + output := adapter.Perform(models.RunInput{}, store) - assert.False(t, input.HasError()) - assert.Equal(t, models.RunStatusPendingConfirmations, input.Status) + require.NoError(t, output.Error()) + assert.Equal(t, models.RunStatusPendingConfirmations, output.Status()) // Have a head come through with the same empty response - txmMock.EXPECT().Connected().Return(true) - txmMock.EXPECT().BumpGasUntilSafe( - gomock.Any(), - ).Return(nil, strpkg.Unknown, errors.New("Bad response on request: [ TransactionIndex ]. Error cause was EmptyResponse, (majority count: 94 / total: 94)")) - - input = adapter.Perform(input, store) - assert.False(t, input.HasError()) - assert.Equal(t, models.RunStatusPendingConfirmations, input.Status) + txManager.On("Connected").Return(true) + txManager.On("BumpGasUntilSafe", mock.Anything).Return(nil, strpkg.Unknown, badResponseErr) + + input := *models.NewRunInput(models.NewID(), output.Data(), output.Status()) + output = adapter.Perform(input, store) + require.NoError(t, output.Error()) + assert.Equal(t, models.RunStatusPendingConfirmations, output.Status()) + + txManager.AssertExpectations(t) } func TestEthTxAdapter_Perform_NoDoubleSpendOnSendTransactionFail(t *testing.T) { @@ -713,10 +697,9 @@ func TestEthTxAdapter_Perform_NoDoubleSpendOnSendTransactionFail(t *testing.T) { DataPrefix: dataPrefix, FunctionSelector: fHash, } - input := cltest.RunResultWithResult(inputValue) - input.CachedJobRunID = models.NewID() + input := cltest.NewRunInputWithResult(inputValue) data := adapter.Perform(input, store) - assert.Error(t, data.GetError()) + require.Error(t, data.Error()) // Run the adapter again confirmed := sentAt + 1 @@ -730,7 +713,7 @@ func TestEthTxAdapter_Perform_NoDoubleSpendOnSendTransactionFail(t *testing.T) { ethMock.Register("eth_getTransactionReceipt", receipt) data = adapter.Perform(input, store) - assert.NoError(t, data.GetError()) + require.NoError(t, data.Error()) // The first and second transaction should have the same data assert.Equal(t, firstTxData, secondTxData) @@ -789,10 +772,9 @@ func TestEthTxAdapter_Perform_NoDoubleSpendOnSendTransactionFailAndNonceChange(t DataPrefix: dataPrefix, FunctionSelector: fHash, } - input := cltest.RunResultWithResult(inputValue) - input.CachedJobRunID = models.NewID() + input := cltest.NewRunInputWithResult(inputValue) data := adapter.Perform(input, store) - assert.Error(t, data.GetError()) + require.Error(t, data.Error()) // Run the adapter again var secondTxData []interface{} @@ -803,7 +785,7 @@ func TestEthTxAdapter_Perform_NoDoubleSpendOnSendTransactionFailAndNonceChange(t }) data = adapter.Perform(input, store) - assert.NoError(t, data.GetError()) + require.NoError(t, data.Error()) // Since the nonce (and from address) changed, the data should also change assert.NotEqual(t, firstTxData, secondTxData) diff --git a/core/adapters/http.go b/core/adapters/http.go index 807e2a3adbc..abedf4cbfef 100644 --- a/core/adapters/http.go +++ b/core/adapters/http.go @@ -12,9 +12,9 @@ import ( "path" "strings" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" ) // HTTPGet requires a URL which is used for a GET request when the adapter is called. @@ -28,10 +28,10 @@ type HTTPGet struct { // Perform ensures that the adapter's URL responds to a GET request without // errors and returns the response body as the "value" field of the result. -func (hga *HTTPGet) Perform(input models.RunResult, store *store.Store) models.RunResult { +func (hga *HTTPGet) Perform(input models.RunInput, store *store.Store) models.RunOutput { request, err := hga.GetRequest() if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } return sendRequest(input, request, store.Config.DefaultHTTPLimit()) } @@ -68,10 +68,10 @@ type HTTPPost struct { // Perform ensures that the adapter's URL responds to a POST request without // errors and returns the response body as the "value" field of the result. -func (hpa *HTTPPost) Perform(input models.RunResult, store *store.Store) models.RunResult { - request, err := hpa.GetRequest(input.Data.String()) +func (hpa *HTTPPost) Perform(input models.RunInput, store *store.Store) models.RunOutput { + request, err := hpa.GetRequest(input.Data().String()) if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } return sendRequest(input, request, store.Config.DefaultHTTPLimit()) } @@ -132,14 +132,14 @@ func setHeaders(request *http.Request, headers http.Header, contentType string) } } -func sendRequest(input models.RunResult, request *http.Request, limit int64) models.RunResult { +func sendRequest(input models.RunInput, request *http.Request, limit int64) models.RunOutput { tr := &http.Transport{ DisableCompression: true, } client := &http.Client{Transport: tr} response, err := client.Do(request) if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } defer response.Body.Close() @@ -147,15 +147,15 @@ func sendRequest(input models.RunResult, request *http.Request, limit int64) mod source := newMaxBytesReader(response.Body, limit) bytes, err := ioutil.ReadAll(source) if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } responseBody := string(bytes) if response.StatusCode >= 400 { - return models.RunResultError(errors.New(responseBody)) + return models.NewRunOutputError(errors.New(responseBody)) } - return models.RunResultComplete(responseBody) + return models.NewRunOutputCompleteWithResult(responseBody) } // maxBytesReader is inspired by diff --git a/core/adapters/http_test.go b/core/adapters/http_test.go index f7fae1aa76b..8c74ccefb3f 100644 --- a/core/adapters/http_test.go +++ b/core/adapters/http_test.go @@ -5,12 +5,14 @@ import ( "net/http" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func leanStore() *store.Store { @@ -31,9 +33,9 @@ func TestHttpAdapters_NotAUrlError(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - result := test.adapter.Perform(models.RunResult{}, store) - assert.Equal(t, models.JSON{}, result.Data) + result := test.adapter.Perform(models.RunInput{}, store) assert.True(t, result.HasError()) + assert.Empty(t, result.Data()) }) } } @@ -70,7 +72,7 @@ func TestHTTPGet_Perform(t *testing.T) { for _, test := range cases { t.Run(test.name, func(t *testing.T) { - input := cltest.RunResultWithResult("inputValue") + input := cltest.NewRunInputWithResult("inputValue") mock, cleanup := cltest.NewHTTPMockServer(t, test.status, "GET", test.response, func(header http.Header, body string) { assert.Equal(t, ``, body) @@ -89,13 +91,13 @@ func TestHTTPGet_Perform(t *testing.T) { result := hga.Perform(input, store) - if !test.wantErrored { - val, err := result.ResultString() - assert.NoError(t, err) - assert.Equal(t, test.want, val) + if test.wantErrored { + require.Error(t, result.Error()) + } else { + require.NoError(t, result.Error()) + assert.Equal(t, test.want, result.Result().String()) } - assert.Equal(t, test.wantErrored, result.HasError()) - assert.Equal(t, false, result.Status.PendingBridge()) + assert.Equal(t, false, result.Status().PendingBridge()) }) } } @@ -114,7 +116,7 @@ func TestHTTP_TooLarge(t *testing.T) { } for _, test := range tests { t.Run(test.verb, func(t *testing.T) { - input := cltest.RunResultWithResult("inputValue") + input := cltest.NewRunInputWithResult("inputValue") largePayload := "12" mock, cleanup := cltest.NewHTTPMockServer(t, http.StatusOK, test.verb, largePayload) defer cleanup() @@ -122,8 +124,8 @@ func TestHTTP_TooLarge(t *testing.T) { hga := test.factory(cltest.WebURL(t, mock.URL)) result := hga.Perform(input, store) - assert.Equal(t, true, result.HasError()) - assert.Equal(t, "HTTP request too large, must be less than 1 bytes", result.Error()) + require.Error(t, result.Error()) + assert.Equal(t, "HTTP request too large, must be less than 1 bytes", result.Error().Error()) assert.Equal(t, "", result.Result().String()) }) } @@ -250,7 +252,7 @@ func TestHttpPost_Perform(t *testing.T) { for _, test := range cases { t.Run(test.name, func(t *testing.T) { - input := cltest.RunResultWithResult("inputVal") + input := cltest.NewRunInputWithResult("inputVal") mock, cleanup := cltest.NewHTTPMockServer(t, test.status, "POST", test.response, func(header http.Header, body string) { assert.Equal(t, test.wantBody, body) @@ -273,8 +275,8 @@ func TestHttpPost_Perform(t *testing.T) { val := result.Result() assert.Equal(t, test.want, val.String()) assert.NotEqual(t, test.wantErrored, val.Exists()) - assert.Equal(t, test.wantErrored, result.HasError()) - assert.Equal(t, false, result.Status.PendingBridge()) + require.Equal(t, test.wantErrored, result.HasError()) + assert.Equal(t, false, result.Status().PendingBridge()) }) } } diff --git a/core/adapters/json_parse.go b/core/adapters/json_parse.go index 3fd848fe864..658ada66396 100644 --- a/core/adapters/json_parse.go +++ b/core/adapters/json_parse.go @@ -6,10 +6,11 @@ import ( "strconv" "strings" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" + simplejson "github.com/bitly/go-simplejson" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" ) // JSONParse holds a path to the desired field in a JSON object, @@ -30,15 +31,15 @@ type JSONParse struct { // } // // Then ["0","last"] would be the path, and "111" would be the returned value -func (jpa *JSONParse) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (jpa *JSONParse) Perform(input models.RunInput, _ *store.Store) models.RunOutput { val, err := input.ResultString() if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } js, err := simplejson.NewJson([]byte(val)) if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } last, err := dig(js, jpa.Path) @@ -46,7 +47,7 @@ func (jpa *JSONParse) Perform(input models.RunResult, _ *store.Store) models.Run return moldErrorOutput(js, jpa.Path, input) } - return models.RunResultComplete(last.Interface()) + return models.NewRunOutputCompleteWithResult(last.Interface()) } func dig(js *simplejson.Json, path []string) (*simplejson.Json, error) { @@ -66,11 +67,11 @@ func dig(js *simplejson.Json, path []string) (*simplejson.Json, error) { // only error if any keys prior to the last one in the path are nonexistent. // i.e. Path = ["errorIfNonExistent", "nullIfNonExistent"] -func moldErrorOutput(js *simplejson.Json, path []string, input models.RunResult) models.RunResult { +func moldErrorOutput(js *simplejson.Json, path []string, input models.RunInput) models.RunOutput { if _, err := getEarlyPath(js, path); err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } - return models.RunResultComplete(nil) + return models.NewRunOutputCompleteWithResult(nil) } func getEarlyPath(js *simplejson.Json, path []string) (*simplejson.Json, error) { diff --git a/core/adapters/json_parse_test.go b/core/adapters/json_parse_test.go index 87cfa8dbfda..123f6e25629 100644 --- a/core/adapters/json_parse_test.go +++ b/core/adapters/json_parse_test.go @@ -4,9 +4,10 @@ import ( "encoding/json" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" ) @@ -115,16 +116,16 @@ func TestJsonParse_Perform(t *testing.T) { for _, tt := range tests { test := tt t.Run(test.name, func(t *testing.T) { - input := cltest.RunResultWithResult(test.result) + input := cltest.NewRunInputWithResult(test.result) adapter := adapters.JSONParse{Path: test.path} result := adapter.Perform(input, nil) - assert.Equal(t, test.wantData, result.Data.String()) - assert.Equal(t, test.wantStatus, result.Status) + assert.Equal(t, test.wantData, result.Data().String()) + assert.Equal(t, test.wantStatus, result.Status()) if test.wantResultError { - assert.Error(t, result.GetError()) + assert.Error(t, result.Error()) } else { - assert.NoError(t, result.GetError()) + assert.NoError(t, result.Error()) } }) } diff --git a/core/adapters/multiply.go b/core/adapters/multiply.go index d99392de8d6..5ac0950e377 100644 --- a/core/adapters/multiply.go +++ b/core/adapters/multiply.go @@ -7,9 +7,9 @@ import ( "math/big" "strconv" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" ) // Multiplier represents the number to multiply by in Multiply adapter. @@ -38,15 +38,15 @@ type Multiply struct { // // For example, if input value is "99.994" and the adapter's "times" is // set to "100", the result's value will be "9999.4". -func (ma *Multiply) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (ma *Multiply) Perform(input models.RunInput, _ *store.Store) models.RunOutput { val := input.Result() i, ok := (&big.Float{}).SetString(val.String()) if !ok { - return models.RunResultError(fmt.Errorf("cannot parse into big.Float: %v", val.String())) + return models.NewRunOutputError(fmt.Errorf("cannot parse into big.Float: %v", val.String())) } if ma.Times != nil { i.Mul(i, big.NewFloat(float64(*ma.Times))) } - return models.RunResultComplete(i.String()) + return models.NewRunOutputCompleteWithResult(i.String()) } diff --git a/core/adapters/multiply_sgx.go b/core/adapters/multiply_sgx.go index 02731967f82..a0120543c1e 100644 --- a/core/adapters/multiply_sgx.go +++ b/core/adapters/multiply_sgx.go @@ -15,9 +15,9 @@ import ( "strconv" "unsafe" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" ) // Multiplier represents the number to multiply by in Multiply adapter. @@ -46,16 +46,14 @@ type Multiply struct { // // For example, if input value is "99.994" and the adapter's "times" is // set to "100", the result's value will be "9999.4". -func (ma *Multiply) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (ma *Multiply) Perform(input models.RunInput, _ *store.Store) models.RunOutput { adapterJSON, err := json.Marshal(ma) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } inputJSON, err := json.Marshal(input) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } cAdapter := C.CString(string(adapterJSON)) @@ -70,15 +68,13 @@ func (ma *Multiply) Perform(input models.RunResult, _ *store.Store) models.RunRe outputLenPtr := (*C.int)(unsafe.Pointer(&outputLen)) if _, err = C.multiply(cAdapter, cInput, output, bufferCapacity, outputLenPtr); err != nil { - input.SetError(fmt.Errorf("SGX multiply: %v", err)) - return input + return models.NewRunOutputError(fmt.Errorf("SGX multiply: %v", err)) } sgxResult := C.GoStringN(output, outputLen) var result models.RunResult if err := json.Unmarshal([]byte(sgxResult), &result); err != nil { - input.SetError(fmt.Errorf("unmarshaling SGX result: %v", err)) - return input + return models.NewRunOutputError(fmt.Errorf("unmarshaling SGX result: %v", err)) } return result diff --git a/core/adapters/multiply_test.go b/core/adapters/multiply_test.go index 1d518eefb17..b4e0decb4ae 100644 --- a/core/adapters/multiply_test.go +++ b/core/adapters/multiply_test.go @@ -4,10 +4,11 @@ import ( "encoding/json" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMultiply_Perform(t *testing.T) { @@ -41,9 +42,7 @@ func TestMultiply_Perform(t *testing.T) { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() - input := models.RunResult{ - Data: cltest.JSONFromString(t, test.json), - } + input := cltest.NewRunInputWithString(t, test.json) adapter := adapters.Multiply{} jsonErr := json.Unmarshal([]byte(test.params), &adapter) result := adapter.Perform(input, nil) @@ -51,14 +50,11 @@ func TestMultiply_Perform(t *testing.T) { if test.jsonError { assert.Error(t, jsonErr) } else if test.errored { - assert.Error(t, result.GetError()) + require.Error(t, result.Error()) assert.NoError(t, jsonErr) } else { - val, err := result.ResultString() - assert.NoError(t, err) - assert.Equal(t, test.want, val) - assert.NoError(t, result.GetError()) - assert.NoError(t, jsonErr) + require.NoError(t, result.Error()) + assert.Equal(t, test.want, result.Result().String()) } }) } diff --git a/core/adapters/no_op.go b/core/adapters/no_op.go index 662823c977c..eeb5df37df1 100644 --- a/core/adapters/no_op.go +++ b/core/adapters/no_op.go @@ -1,17 +1,17 @@ package adapters import ( - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store" + "chainlink/core/store/models" ) // NoOp adapter type holds no fields type NoOp struct{} -// Perform returns the empty RunResult -func (noa *NoOp) Perform(input models.RunResult, _ *store.Store) models.RunResult { +// Perform returns the input +func (noa *NoOp) Perform(input models.RunInput, _ *store.Store) models.RunOutput { val := input.Result().Value() - return models.RunResultComplete(val) + return models.NewRunOutputCompleteWithResult(val) } // NoOpPend adapter type holds no fields @@ -19,8 +19,6 @@ type NoOpPend struct{} // Perform on this adapter type returns an empty RunResult with an // added field for the status to indicate the task is Pending. -func (noa *NoOpPend) Perform(input models.RunResult, _ *store.Store) models.RunResult { - var output models.RunResult - output.MarkPendingConfirmations() - return output +func (noa *NoOpPend) Perform(_ models.RunInput, _ *store.Store) models.RunOutput { + return models.NewRunOutputPendingConfirmations() } diff --git a/core/adapters/random.go b/core/adapters/random.go index 748d0fd88c5..a7e5a4168d4 100644 --- a/core/adapters/random.go +++ b/core/adapters/random.go @@ -4,20 +4,20 @@ import ( "crypto/rand" "math/big" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store" + "chainlink/core/store/models" ) // Random adapter type holds no fields type Random struct{} // Perform returns a random uint256 number in 0 | 2**256-1 range -func (ra *Random) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (ra *Random) Perform(input models.RunInput, _ *store.Store) models.RunOutput { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { - return models.RunResultError(err) + return models.NewRunOutputError(err) } ran := new(big.Int).SetBytes(b) - return models.RunResultComplete(ran.String()) + return models.NewRunOutputCompleteWithResult(ran.String()) } diff --git a/core/adapters/random_test.go b/core/adapters/random_test.go index 0e39e7d45a6..cdcc11b4aa0 100644 --- a/core/adapters/random_test.go +++ b/core/adapters/random_test.go @@ -4,19 +4,18 @@ import ( "math/big" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRandom_Perform(t *testing.T) { - input := models.RunResult{} adapter := adapters.Random{} - result := adapter.Perform(input, nil) - val, err := result.ResultString() - assert.NoError(t, err) - assert.NoError(t, result.GetError()) + result := adapter.Perform(models.RunInput{}, nil) + require.NoError(t, result.Error()) res := new(big.Int) - res, ok := res.SetString(val, 10) + res, ok := res.SetString(result.Result().String(), 10) assert.True(t, ok) } diff --git a/core/adapters/sleep.go b/core/adapters/sleep.go index c79fde2ec72..2746c40b220 100644 --- a/core/adapters/sleep.go +++ b/core/adapters/sleep.go @@ -3,9 +3,10 @@ package adapters import ( "time" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/logger" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" ) // Sleep adapter allows a job to do nothing for some amount of wall time. @@ -14,10 +15,14 @@ type Sleep struct { } // Perform returns the input RunResult after waiting for the specified Until parameter. -func (adapter *Sleep) Perform(input models.RunResult, str *store.Store) models.RunResult { - var output models.RunResult - output.Status = models.RunStatusPendingSleep - return output +func (adapter *Sleep) Perform(input models.RunInput, str *store.Store) models.RunOutput { + duration := adapter.Duration() + if duration > 0 { + logger.Debugw("Task sleeping...", "duration", duration) + <-str.Clock.After(duration) + } + + return models.NewRunOutputComplete(models.JSON{}) } // Duration returns the amount of sleeping this task should be paused for. diff --git a/core/adapters/sleep_test.go b/core/adapters/sleep_test.go index a4e791189af..d8a48268309 100644 --- a/core/adapters/sleep_test.go +++ b/core/adapters/sleep_test.go @@ -4,20 +4,52 @@ import ( "encoding/json" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSleep_Perform(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() + clock := cltest.NewTriggerClock(t) + store.Clock = clock adapter := adapters.Sleep{} - err := json.Unmarshal([]byte(`{"until": 1332151919}`), &adapter) + err := json.Unmarshal([]byte(`{"until": 2147483647}`), &adapter) assert.NoError(t, err) - result := adapter.Perform(models.RunResult{}, store) - assert.Equal(t, string(models.RunStatusPendingSleep), string(result.Status)) + doneChan := make(chan struct{}) + go func() { + result := adapter.Perform(models.RunInput{}, store) + assert.Equal(t, string(models.RunStatusCompleted), string(result.Status())) + doneChan <- struct{}{} + }() + + select { + case <-doneChan: + t.Error("Sleep adapter did not sleep") + default: + } + + clock.Trigger() + + _, ok := <-doneChan + assert.True(t, ok) +} + +func TestSleep_Perform_AlreadyElapsed(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + adapter := adapters.Sleep{} + err := json.Unmarshal([]byte(`{"until": 1332151919}`), &adapter) + require.NoError(t, err) + + result := adapter.Perform(models.RunInput{}, store) + require.NoError(t, result.Error()) + assert.Equal(t, string(models.RunStatusCompleted), string(result.Status())) } diff --git a/core/adapters/wasm.go b/core/adapters/wasm.go index 6f611f25f64..bce4e33dc3f 100644 --- a/core/adapters/wasm.go +++ b/core/adapters/wasm.go @@ -5,8 +5,8 @@ package adapters import ( "fmt" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store" + "chainlink/core/store/models" ) // Wasm represents a wasm binary encoded as base64 or wasm encoded as text (a lisp like language). @@ -15,7 +15,7 @@ type Wasm struct { } // Perform ships the wasm representation to the SGX enclave where it is evaluated. -func (wasm *Wasm) Perform(input models.RunResult, _ *store.Store) models.RunResult { - input.SetError(fmt.Errorf("Wasm is not supported without SGX")) - return input +func (wasm *Wasm) Perform(input models.RunInput, _ *store.Store) models.RunOutput { + err := fmt.Errorf("Wasm is not supported without SGX") + return models.NewRunOutputError(err) } diff --git a/core/adapters/wasm_sgx.go b/core/adapters/wasm_sgx.go index 5dfd17b9405..9915826fd3e 100644 --- a/core/adapters/wasm_sgx.go +++ b/core/adapters/wasm_sgx.go @@ -14,8 +14,8 @@ import ( "fmt" "unsafe" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store" + "chainlink/core/store/models" ) // Wasm represents a wasm binary encoded as base64 or wasm encoded as text (a lisp like language). @@ -24,16 +24,14 @@ type Wasm struct { } // Perform ships the wasm representation to the SGX enclave where it is evaluated. -func (wasm *Wasm) Perform(input models.RunResult, _ *store.Store) models.RunResult { +func (wasm *Wasm) Perform(input models.RunInput, _ *store.Store) models.RunOutput { adapterJSON, err := json.Marshal(wasm) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } inputJSON, err := json.Marshal(input) if err != nil { - input.SetError(err) - return input + return models.NewRunOutputError(err) } cAdapter := C.CString(string(adapterJSON)) @@ -49,15 +47,13 @@ func (wasm *Wasm) Perform(input models.RunResult, _ *store.Store) models.RunResu _, err = C.wasm(cAdapter, cInput, output, bufferCapacity, outputLenPtr) if err != nil { - input.SetError(fmt.Errorf("SGX wasm: %v", err)) - return input + return models.NewRunOutputError(fmt.Errorf("SGX wasm: %v", err))) } sgxResult := C.GoStringN(output, outputLen) var result models.RunResult if err := json.Unmarshal([]byte(sgxResult), &result); err != nil { - input.SetError(fmt.Errorf("unmarshaling SGX result: %v", err)) - return input + return models.NewRunOutputError(fmt.Errorf("unmarshaling SGX result: %v", err)) } return result diff --git a/core/adapters/wasm_test.go b/core/adapters/wasm_test.go index 011ebb22240..18be079ff60 100644 --- a/core/adapters/wasm_test.go +++ b/core/adapters/wasm_test.go @@ -7,9 +7,10 @@ import ( "fmt" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" ) @@ -85,14 +86,13 @@ func TestWasm_Perform(t *testing.T) { if test.jsonError { assert.Error(t, jsonErr) } else if test.errored { - assert.Error(t, result.GetError()) assert.NoError(t, jsonErr) + assert.Error(t, result.GetError()) } else { - val, err := result.ResultString() - assert.NoError(t, err) - assert.Equal(t, test.want, val) - assert.NoError(t, result.GetError()) assert.NoError(t, jsonErr) + value := cltest.MustResultString(t, result) + assert.Equal(t, test.want, value) + assert.NoError(t, result.GetError()) } }) } diff --git a/core/cmd/app.go b/core/cmd/app.go index e0f3337b40c..7f1573a686e 100644 --- a/core/cmd/app.go +++ b/core/cmd/app.go @@ -4,7 +4,8 @@ import ( "fmt" "os" - "github.com/smartcontractkit/chainlink/core/store" + "chainlink/core/store" + "github.com/urfave/cli" ) @@ -104,8 +105,8 @@ func NewApp(client *Client) *cli.App { Usage: "Commands for the node's configuration", Subcommands: []cli.Command{ { - Name: "list", - Usage: "Show the node's environment variables", + Name: "list", + Usage: "Show the node's environment variables", Action: client.GetConfiguration, }, { @@ -233,6 +234,11 @@ func NewApp(client *Client) *cli.App { Usage: "Show a Run for a specific ID", Action: client.ShowJobRun, }, + { + Name: "cancel", + Usage: "Cancel a Run with a specified ID", + Action: client.CancelJobRun, + }, }, }, diff --git a/core/cmd/client.go b/core/cmd/client.go index 4ef157dc029..c014836a7e7 100644 --- a/core/cmd/client.go +++ b/core/cmd/client.go @@ -13,13 +13,14 @@ import ( "strings" "time" + "chainlink/core/logger" + "chainlink/core/services" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/web" + "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/web" clipkg "github.com/urfave/cli" "go.uber.org/multierr" "golang.org/x/sync/errgroup" @@ -137,6 +138,7 @@ func createServer(handler *gin.Engine, port uint16) *http.Server { type HTTPClient interface { Get(string, ...map[string]string) (*http.Response, error) Post(string, io.Reader) (*http.Response, error) + Put(string, io.Reader) (*http.Response, error) Patch(string, io.Reader, ...map[string]string) (*http.Response, error) Delete(string) (*http.Response, error) } @@ -167,6 +169,11 @@ func (h *authenticatedHTTPClient) Post(path string, body io.Reader) (*http.Respo return h.doRequest("POST", path, body) } +// Put performs an HTTP Put using the authenticated HTTP client's cookie. +func (h *authenticatedHTTPClient) Put(path string, body io.Reader) (*http.Response, error) { + return h.doRequest("PUT", path, body) +} + // Patch performs an HTTP Patch using the authenticated HTTP client's cookie. func (h *authenticatedHTTPClient) Patch(path string, body io.Reader, headers ...map[string]string) (*http.Response, error) { return h.doRequest("PATCH", path, body, headers...) diff --git a/core/cmd/client_test.go b/core/cmd/client_test.go index 96da6363623..04f2d6ef8ef 100644 --- a/core/cmd/client_test.go +++ b/core/cmd/client_test.go @@ -3,9 +3,11 @@ package cmd_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/cmd" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,12 +25,11 @@ func TestTerminalCookieAuthenticator_AuthenticateWithoutSession(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - app, cleanup := cltest.NewApplication(t) - defer cleanup() + config := orm.NewConfig() sr := models.SessionRequest{Email: test.email, Password: test.pwd} store := &cmd.MemoryCookieStore{} - tca := cmd.NewSessionCookieAuthenticator(app.Config.Config, store) + tca := cmd.NewSessionCookieAuthenticator(config, store) cookie, err := tca.Authenticate(sr) assert.Error(t, err) @@ -45,6 +46,7 @@ func TestTerminalCookieAuthenticator_AuthenticateWithSession(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) app.MustSeedUserSession() tests := []struct { diff --git a/core/cmd/enclave.go b/core/cmd/enclave.go index 1a4f952cdc9..56e27fbd1ac 100644 --- a/core/cmd/enclave.go +++ b/core/cmd/enclave.go @@ -3,7 +3,7 @@ package cmd import ( - "github.com/smartcontractkit/chainlink/core/logger" + "chainlink/core/logger" ) // InitEnclave is a stub in non SGX enabled builds. diff --git a/core/cmd/enclave_sgx.go b/core/cmd/enclave_sgx.go index bc40c277624..0d9b0444b1a 100644 --- a/core/cmd/enclave_sgx.go +++ b/core/cmd/enclave_sgx.go @@ -2,7 +2,7 @@ package cmd -import "github.com/smartcontractkit/chainlink/core/logger" +import "chainlink/core/logger" // InitEnclave initialized the SGX enclave for use by adapters func InitEnclave() error { diff --git a/core/cmd/key_store_authenticator.go b/core/cmd/key_store_authenticator.go index 3de97c32f5c..00b16b67e2d 100644 --- a/core/cmd/key_store_authenticator.go +++ b/core/cmd/key_store_authenticator.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/smartcontractkit/chainlink/core/store" + "chainlink/core/store" ) // KeyStoreAuthenticator implements the Authenticate method for the store and diff --git a/core/cmd/key_store_authenticator_test.go b/core/cmd/key_store_authenticator_test.go index 0009df018ce..f3dfc7b55ed 100644 --- a/core/cmd/key_store_authenticator_test.go +++ b/core/cmd/key_store_authenticator_test.go @@ -3,65 +3,73 @@ package cmd_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/internal/cltest" + "chainlink/core/cmd" + "chainlink/core/internal/cltest" + "github.com/stretchr/testify/assert" ) func TestTerminalKeyStoreAuthenticator_WithNoAcctNoPwdCreatesAccount(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplication(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - prompt := &cltest.MockCountingPrompter{EnteredStrings: []string{ - cltest.Password, "wrongconfirmation", cltest.Password, cltest.Password, - }} + prompt := &cltest.MockCountingPrompter{ + T: t, + EnteredStrings: []string{ + cltest.Password, + "wrongconfirmation", + cltest.Password, + cltest.Password, + }, + } auth := cmd.TerminalKeyStoreAuthenticator{Prompter: prompt} - assert.False(t, app.Store.KeyStore.HasAccounts()) - _, err := auth.Authenticate(app.Store, "") + assert.False(t, store.KeyStore.HasAccounts()) + _, err := auth.Authenticate(store, "") assert.NoError(t, err) assert.Equal(t, 4, prompt.Count) - assert.Equal(t, 1, len(app.Store.KeyStore.Accounts())) + assert.Len(t, store.KeyStore.Accounts(), 1) } func TestTerminalKeyStoreAuthenticator_WithNoAcctWithInitialPwdCreatesAcct(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplication(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - auth := cmd.TerminalKeyStoreAuthenticator{Prompter: &cltest.MockCountingPrompter{}} + auth := cmd.TerminalKeyStoreAuthenticator{Prompter: &cltest.MockCountingPrompter{T: t}} - assert.Equal(t, 0, len(app.Store.KeyStore.Accounts())) - _, err := auth.Authenticate(app.Store, "somepassword") + assert.Len(t, store.KeyStore.Accounts(), 0) + _, err := auth.Authenticate(store, "somepassword") assert.NoError(t, err) - assert.True(t, app.Store.KeyStore.HasAccounts()) - assert.Equal(t, 1, len(app.Store.KeyStore.Accounts())) + assert.True(t, store.KeyStore.HasAccounts()) + assert.Len(t, store.KeyStore.Accounts(), 1) } func TestTerminalKeyStoreAuthenticator_WithAcctNoInitialPwdPromptLoop(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() // prompt loop tries all in array prompt := &cltest.MockCountingPrompter{ - EnteredStrings: []string{"wrongpassword", cltest.Password}, + T: t, + EnteredStrings: []string{"wrongpassword", cltest.Password, cltest.Password, cltest.Password}, } auth := cmd.TerminalKeyStoreAuthenticator{Prompter: prompt} - _, err := auth.Authenticate(app.Store, "") + _, err := auth.Authenticate(store, "") assert.NoError(t, err) - assert.Equal(t, 2, prompt.Count) + assert.Equal(t, 4, prompt.Count) } func TestTerminalKeyStoreAuthenticator_WithAcctAndPwd(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() tests := []struct { @@ -74,8 +82,8 @@ func TestTerminalKeyStoreAuthenticator_WithAcctAndPwd(t *testing.T) { for _, test := range tests { t.Run(test.password, func(t *testing.T) { - auth := cmd.TerminalKeyStoreAuthenticator{Prompter: &cltest.MockCountingPrompter{}} - _, err := auth.Authenticate(app.Store, test.password) + auth := cmd.TerminalKeyStoreAuthenticator{Prompter: &cltest.MockCountingPrompter{T: t}} + _, err := auth.Authenticate(store, test.password) assert.Equal(t, test.wantError, err != nil) }) } diff --git a/core/cmd/local_client.go b/core/cmd/local_client.go index 81a4c30b4b1..7d5e88fd594 100644 --- a/core/cmd/local_client.go +++ b/core/cmd/local_client.go @@ -8,13 +8,14 @@ import ( "os" "strings" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/logger" + "chainlink/core/services" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/store/presenters" + "chainlink/core/utils" + clipkg "github.com/urfave/cli" "go.uber.org/zap/zapcore" ) @@ -117,20 +118,16 @@ func logNodeBalance(store *strpkg.Store) { accounts, err := presenters.ShowEthBalance(store) logger.WarnIf(err) for _, a := range accounts { - logAccountBalance(a) + logger.Infow(a["message"], "address", a["address"], "ethBalance", a["balance"]) } accounts, err = presenters.ShowLinkBalance(store) logger.WarnIf(err) for _, a := range accounts { - logAccountBalance(a) + logger.Infow(a["message"], "address", a["address"], "linkBalance", a["balance"]) } } -func logAccountBalance(kv map[string]interface{}) { - logger.Infow(fmt.Sprint(kv["message"]), "address", kv["address"], "balance", kv["balance"]) -} - func logConfigVariables(store *strpkg.Store) error { wlc, err := presenters.NewConfigWhitelist(store) if err != nil { @@ -158,7 +155,6 @@ func (cli *Client) DeleteUser(c *clipkg.Context) error { func (cli *Client) ImportKey(c *clipkg.Context) error { logger.SetLogger(cli.Config.CreateProductionLogger()) app := cli.AppFactory.NewApplication(cli.Config) - defer app.Stop() if !c.Args().Present() { return cli.errorOut(errors.New("Must pass in filepath to key")) diff --git a/core/cmd/local_client_test.go b/core/cmd/local_client_test.go index 52b09d8f486..71639170637 100644 --- a/core/cmd/local_client_test.go +++ b/core/cmd/local_client_test.go @@ -6,30 +6,40 @@ import ( "path/filepath" "sort" "testing" + "time" + + "chainlink/core/cmd" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/logger" + strpkg "chainlink/core/store" + "chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli" ) func TestClient_RunNodeShowsEnv(t *testing.T) { - config, configCleanup := cltest.NewConfig(t) - defer configCleanup() - config.Set("LINK_CONTRACT_ADDRESS", "0x514910771AF9Ca656af840dff83E8264EcF986CA") - config.Set("CHAINLINK_PORT", 6688) - - app, cleanup := cltest.NewApplicationWithConfigAndKey(t, config) + store, cleanup := cltest.NewStore(t) defer cleanup() + _, err := store.KeyStore.NewAccount(cltest.Password) + require.NoError(t, err) + require.NoError(t, store.KeyStore.Unlock(cltest.Password)) + + store.Config.Set("LINK_CONTRACT_ADDRESS", "0x514910771AF9Ca656af840dff83E8264EcF986CA") + store.Config.Set("CHAINLINK_PORT", 6688) - auth := cltest.CallbackAuthenticator{Callback: func(*store.Store, string) (string, error) { return "", nil }} + app := new(mocks.Application) + app.On("GetStore").Return(store) + app.On("Start").Return(nil) + app.On("Stop").Return(nil) + + auth := cltest.CallbackAuthenticator{Callback: func(*strpkg.Store, string) (string, error) { return "", nil }} runner := cltest.BlockedRunner{Done: make(chan struct{})} client := cmd.Client{ - Config: app.Store.Config, - AppFactory: cltest.InstanceAppFactory{App: app.ChainlinkApplication}, + Config: store.Config, + AppFactory: cltest.InstanceAppFactory{App: app}, KeyStoreAuthenticator: auth, FallbackAPIInitializer: &cltest.MockAPIInitializer{}, Runner: runner, @@ -39,23 +49,20 @@ func TestClient_RunNodeShowsEnv(t *testing.T) { set.Bool("debug", true, "") c := cli.NewContext(nil, set, nil) - eth := app.MockEthCallerSubscriber() - eth.Register("eth_getTransactionCount", `0x1`) - eth.Register("eth_chainId", config.ChainID()) - // Start RunNode in a goroutine, it will block until we resume the runner go func() { assert.NoError(t, client.RunNode(c)) }() - // Wait for RunNode to connect the app - require.NoError(t, app.WaitForConnection()) - // Unlock the runner to the client can begin shutdown - runner.Done <- struct{}{} + select { + case runner.Done <- struct{}{}: + case <-time.After(30 * time.Second): + t.Fatal("Timed out waiting for runner") + } logger.Sync() - logs, err := cltest.ReadLogs(app) + logs, err := cltest.ReadLogs(store.Config) require.NoError(t, err) assert.Contains(t, logs, "LOG_LEVEL: debug\\n") @@ -76,6 +83,8 @@ func TestClient_RunNodeShowsEnv(t *testing.T) { assert.Contains(t, logs, "ORACLE_CONTRACT_ADDRESS: \\n") assert.Contains(t, logs, "ALLOW_ORIGINS: http://localhost:3000,http://localhost:6688\\n") assert.Contains(t, logs, "BRIDGE_RESPONSE_URL: http://localhost:6688\\n") + + app.AssertExpectations(t) } func TestClient_RunNodeWithPasswords(t *testing.T) { @@ -93,13 +102,22 @@ func TestClient_RunNodeWithPasswords(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - app, cleanup := cltest.NewApplication(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - _, err := app.Store.KeyStore.NewAccount("password") // matches correct_password.txt - assert.NoError(t, err) + _, err := store.KeyStore.NewAccount(cltest.Password) + require.NoError(t, err) + require.NoError(t, store.KeyStore.Unlock(cltest.Password)) + + app := new(mocks.Application) + app.On("GetStore").Return(store) + app.On("Start").Maybe().Return(nil) + app.On("Stop").Maybe().Return(nil) + + _, err = store.KeyStore.NewAccount("password") // matches correct_password.txt + require.NoError(t, err) var unlocked bool - callback := func(store *store.Store, phrase string) (string, error) { + callback := func(store *strpkg.Store, phrase string) (string, error) { err := store.KeyStore.Unlock(phrase) unlocked = err == nil return phrase, err @@ -108,7 +126,7 @@ func TestClient_RunNodeWithPasswords(t *testing.T) { auth := cltest.CallbackAuthenticator{Callback: callback} apiPrompt := &cltest.MockAPIInitializer{} client := cmd.Client{ - Config: app.Store.Config, + Config: store.Config, AppFactory: cltest.InstanceAppFactory{App: app}, KeyStoreAuthenticator: auth, FallbackAPIInitializer: apiPrompt, @@ -119,8 +137,6 @@ func TestClient_RunNodeWithPasswords(t *testing.T) { set.String("password", test.pwdfile, "") c := cli.NewContext(nil, set, nil) - eth := app.MockEthCallerSubscriber() - eth.Register("eth_getTransactionCount", `0x1`) if test.wantUnlocked { assert.NoError(t, client.RunNode(c)) assert.True(t, unlocked) @@ -150,13 +166,24 @@ func TestClient_RunNodeWithAPICredentialsFile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - app, cleanup := cltest.NewApplicationWithKey(t) + config := orm.NewConfig() + + store, cleanup := cltest.NewStore(t) defer cleanup() + _, err := store.KeyStore.NewAccount(cltest.Password) + require.NoError(t, err) + require.NoError(t, store.KeyStore.Unlock(cltest.Password)) - noauth := cltest.CallbackAuthenticator{Callback: func(*store.Store, string) (string, error) { return "", nil }} + app := new(mocks.Application) + app.On("GetStore").Return(store) + app.On("Start").Maybe().Return(nil) + app.On("Stop").Maybe().Return(nil) + + callback := func(*strpkg.Store, string) (string, error) { return "", nil } + noauth := cltest.CallbackAuthenticator{Callback: callback} apiPrompt := &cltest.MockAPIInitializer{} client := cmd.Client{ - Config: app.Config.Config, + Config: config, AppFactory: cltest.InstanceAppFactory{App: app}, KeyStoreAuthenticator: noauth, FallbackAPIInitializer: apiPrompt, @@ -167,15 +194,14 @@ func TestClient_RunNodeWithAPICredentialsFile(t *testing.T) { set.String("api", test.apiFile, "") c := cli.NewContext(nil, set, nil) - eth := app.MockEthCallerSubscriber() - eth.Register("eth_getTransactionCount", `0x1`) - if test.wantError { assert.Error(t, client.RunNode(c)) } else { assert.NoError(t, client.RunNode(c)) } assert.Equal(t, test.wantPrompt, apiPrompt.Count > 0) + + app.AssertExpectations(t) }) } } @@ -185,6 +211,8 @@ func TestClient_ImportKey(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client, _ := app.NewClientAndRenderer() set := flag.NewFlagSet("import", 0) diff --git a/core/cmd/prompter.go b/core/cmd/prompter.go index e2d5fefcf9a..defcd04a201 100644 --- a/core/cmd/prompter.go +++ b/core/cmd/prompter.go @@ -8,7 +8,8 @@ import ( "strings" "syscall" - "github.com/smartcontractkit/chainlink/core/logger" + "chainlink/core/logger" + "golang.org/x/crypto/ssh/terminal" ) diff --git a/core/cmd/remote_client.go b/core/cmd/remote_client.go index 5355f342f63..e9efb313436 100644 --- a/core/cmd/remote_client.go +++ b/core/cmd/remote_client.go @@ -11,15 +11,16 @@ import ( "os" "strconv" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "chainlink/core/web" + "github.com/ethereum/go-ethereum/common" "github.com/manyminds/api2go/jsonapi" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" - "github.com/smartcontractkit/chainlink/core/web" "github.com/tidwall/gjson" clipkg "github.com/urfave/cli" "go.uber.org/multierr" @@ -513,17 +514,6 @@ func (cli *Client) deserializeAPIResponse(resp *http.Response, dst interface{}, return nil } -func (cli *Client) deserializeResponse(resp *http.Response, dst interface{}) error { - b, err := cli.parseResponse(resp) - if err != nil { - return err - } - if err = json.Unmarshal(b, &dst); err != nil { - return cli.errorOut(err) - } - return nil -} - func (cli *Client) parseResponse(resp *http.Response) ([]byte, error) { b, err := parseResponse(resp) if err == errUnauthorized { @@ -560,14 +550,6 @@ func (cli *Client) printResponseBody(resp *http.Response) error { return nil } -func (cli *Client) renderResponse(resp *http.Response, dst interface{}) error { - err := cli.deserializeResponse(resp, dst) - if err != nil { - return cli.errorOut(err) - } - return cli.errorOut(cli.Render(dst)) -} - func (cli *Client) renderAPIResponse(resp *http.Response, dst interface{}) error { var links jsonapi.Links if err := cli.deserializeAPIResponse(resp, dst, &links); err != nil { @@ -639,7 +621,6 @@ func (cli *Client) SetMinimumGasPrice(c *clipkg.Context) error { return cli.errorOut(cli.Render(&patchResponse)) } - // GetConfiguration gets the nodes environment variables func (cli *Client) GetConfiguration(c *clipkg.Context) error { resp, err := cli.HTTP.Get("/v2/config") @@ -649,4 +630,21 @@ func (cli *Client) GetConfiguration(c *clipkg.Context) error { defer resp.Body.Close() cwl := presenters.ConfigWhitelist{} return cli.renderAPIResponse(resp, &cwl) -} \ No newline at end of file +} + +// CancelJob cancels a running job +func (cli *Client) CancelJobRun(c *clipkg.Context) error { + if !c.Args().Present() { + return cli.errorOut(errors.New("Must pass the run id to be cancelled")) + } + + response, err := cli.HTTP.Put(fmt.Sprintf("/v2/runs/%s/cancellation", c.Args().First()), nil) + if err != nil { + return cli.errorOut(errors.Wrap(err, "HTTP.Put")) + } + _, err = cli.parseResponse(response) + if err != nil { + return cli.errorOut(errors.Wrap(err, "cli.parseResponse")) + } + return nil +} diff --git a/core/cmd/remote_client_test.go b/core/cmd/remote_client_test.go index f3efebbfce6..1ce19abfd85 100644 --- a/core/cmd/remote_client_test.go +++ b/core/cmd/remote_client_test.go @@ -9,12 +9,13 @@ import ( "testing" "time" + "chainlink/core/cmd" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli" @@ -25,6 +26,7 @@ func TestClient_DisplayAccountBalance(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) ethMock := app.MockEthCallerSubscriber() ethMock.Register("eth_getBalance", "0x0100") @@ -44,6 +46,7 @@ func TestClient_IndexJobSpecs(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) j1 := cltest.NewJob() app.Store.CreateJob(&j1) @@ -63,6 +66,7 @@ func TestClient_ShowJobRun_Exists(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) j := cltest.NewJobWithWebInitiator() assert.NoError(t, app.Store.CreateJob(&j)) @@ -84,6 +88,7 @@ func TestClient_ShowJobRun_NotFound(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client, r := app.NewClientAndRenderer() @@ -99,25 +104,27 @@ func TestClient_IndexJobRuns(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) j := cltest.NewJobWithWebInitiator() assert.NoError(t, app.Store.CreateJob(&j)) - jr0 := cltest.CreateJobRunViaWeb(t, app, j, `{"result":"100"}`) - jr1 := cltest.CreateJobRunViaWeb(t, app, j, `{"result":"105"}`) - jr2 := cltest.CreateJobRunViaWeb(t, app, j, `{"result":"110"}`) + jr0 := j.NewRun(j.Initiators[0]) + jr0.Result.Data = cltest.JSONFromString(t, `{"a":"b"}`) + require.NoError(t, app.Store.CreateJobRun(&jr0)) + jr1 := j.NewRun(j.Initiators[0]) + jr1.Result.Data = cltest.JSONFromString(t, `{"x":"y"}`) + require.NoError(t, app.Store.CreateJobRun(&jr1)) client, r := app.NewClientAndRenderer() require.Nil(t, client.IndexJobRuns(cltest.EmptyCLIContext())) runs := *r.Renders[0].(*[]presenters.JobRun) - require.Equal(t, 3, len(runs)) + require.Len(t, runs, 2) assert.Equal(t, jr0.ID, runs[0].ID) - assert.Equal(t, jr0.Result.ID, runs[0].Result.ID) + assert.JSONEq(t, `{"a":"b"}`, runs[0].Result.Data.String()) assert.Equal(t, jr1.ID, runs[1].ID) - assert.Equal(t, jr1.Result.ID, runs[1].Result.ID) - assert.Equal(t, jr2.ID, runs[2].ID) - assert.Equal(t, jr2.Result.ID, runs[2].Result.ID) + assert.JSONEq(t, `{"x":"y"}`, runs[1].Result.Data.String()) } func TestClient_ShowJobSpec_Exists(t *testing.T) { @@ -125,6 +132,8 @@ func TestClient_ShowJobSpec_Exists(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + job := cltest.NewJob() app.Store.CreateJob(&job) @@ -143,6 +152,7 @@ func TestClient_ShowJobSpec_NotFound(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client, r := app.NewClientAndRenderer() @@ -160,6 +170,8 @@ func TestClient_CreateServiceAgreement(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) + client, _ := app.NewClientAndRenderer() sa := string(cltest.MustReadFile(t, "testdata/hello_world_agreement.json")) @@ -219,6 +231,7 @@ func TestClient_CreateExternalInitiator(t *testing.T) { t.Run(test.name, func(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) client, _ := app.NewClientAndRenderer() @@ -256,6 +269,7 @@ func TestClient_CreateExternalInitiator_Errors(t *testing.T) { t.Run(test.name, func(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) client, _ := app.NewClientAndRenderer() @@ -277,6 +291,8 @@ func TestClient_CreateJobSpec(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client, _ := app.NewClientAndRenderer() tests := []struct { @@ -311,6 +327,7 @@ func TestClient_ArchiveJobSpec(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) job := cltest.NewJob() require.NoError(t, app.Store.CreateJob(&job)) @@ -332,6 +349,8 @@ func TestClient_CreateJobSpec_JSONAPIErrors(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client, _ := app.NewClientAndRenderer() set := flag.NewFlagSet("create", 0) @@ -348,6 +367,8 @@ func TestClient_CreateJobRun(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client, _ := app.NewClientAndRenderer() tests := []struct { @@ -395,6 +416,8 @@ func TestClient_CreateBridge(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client, _ := app.NewClientAndRenderer() tests := []struct { @@ -431,6 +454,8 @@ func TestClient_IndexBridges(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + bt1 := &models.BridgeType{ Name: models.MustNewTaskType("testingbridges1"), URL: cltest.WebURL(t, "https://testing.com/bridges"), @@ -460,6 +485,8 @@ func TestClient_ShowBridge(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + bt := &models.BridgeType{ Name: models.MustNewTaskType("testingbridges1"), URL: cltest.WebURL(t, "https://testing.com/bridges"), @@ -482,6 +509,8 @@ func TestClient_RemoveBridge(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + bt := &models.BridgeType{ Name: models.MustNewTaskType("testingbridges1"), URL: cltest.WebURL(t, "https://testing.com/bridges"), @@ -505,7 +534,7 @@ func TestClient_RemoteLogin(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() - app.Start() + require.NoError(t, app.Start()) tests := []struct { name, file string @@ -543,8 +572,7 @@ func TestClient_WithdrawSuccess(t *testing.T) { app, cleanup, _ := setupWithdrawalsApplication(t) defer cleanup() - - assert.NoError(t, app.StartAndConnect()) + require.NoError(t, app.StartAndConnect()) client, _ := app.NewClientAndRenderer() set := flag.NewFlagSet("admin withdraw", 0) @@ -581,8 +609,7 @@ func TestClient_WithdrawFromSpecifiedContractAddress(t *testing.T) { app, cleanup, ethMockCheck := setupWithdrawalsApplication(t) defer cleanup() - - assert.NoError(t, app.StartAndConnect()) + require.NoError(t, app.StartAndConnect()) client, _ := app.NewClientAndRenderer() cliParserRouter := cmd.NewApp(client) @@ -658,7 +685,7 @@ func TestClient_ChangePassword(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() - app.Start() + require.NoError(t, app.Start()) enteredStrings := []string{cltest.APIEmail, cltest.Password} prompter := &cltest.MockCountingPrompter{EnteredStrings: enteredStrings} @@ -695,6 +722,7 @@ func TestClient_IndexTransactions(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) store := app.GetStore() from := cltest.GetAccountAddress(t, store) @@ -729,6 +757,7 @@ func TestClient_ShowTransaction(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) store := app.GetStore() from := cltest.GetAccountAddress(t, store) @@ -750,6 +779,7 @@ func TestClient_IndexTxAttempts(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) store := app.GetStore() from := cltest.GetAccountAddress(t, store) @@ -784,7 +814,7 @@ func TestClient_CreateExtraKey(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() - app.Start() + require.NoError(t, app.Start()) client, _ := app.NewClientAndRenderer() @@ -804,8 +834,7 @@ func TestClient_SetMinimumGasPrice(t *testing.T) { app, cleanup, _ := setupWithdrawalsApplication(t) defer cleanup() - - assert.NoError(t, app.StartAndConnect()) + require.NoError(t, app.StartAndConnect()) client, _ := app.NewClientAndRenderer() set := flag.NewFlagSet("setgasprice", 0) @@ -832,11 +861,12 @@ func TestClient_GetConfiguration(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) client, r := app.NewClientAndRenderer() assert.NoError(t, client.GetConfiguration(cltest.EmptyCLIContext())) require.Equal(t, 1, len(r.Renders)) - + cwl := *r.Renders[0].(*presenters.ConfigWhitelist) assert.Equal(t, cwl.Whitelist.BridgeResponseURL, app.Config.BridgeResponseURL().String()) assert.Equal(t, cwl.Whitelist.ChainID, app.Config.ChainID()) @@ -850,3 +880,34 @@ func TestClient_GetConfiguration(t *testing.T) { assert.Equal(t, cwl.Whitelist.RootDir, app.Config.RootDir()) assert.Equal(t, cwl.Whitelist.SessionTimeout, app.Config.SessionTimeout()) } + +func TestClient_CancelJobRun(t *testing.T) { + t.Parallel() + + app, cleanup := cltest.NewApplication(t) + defer cleanup() + ethMock := app.MockEthCallerSubscriber() + ethMock.Context("app.Start()", func(ethMock *cltest.EthMock) { + ethMock.Register("eth_getTransactionCount", 0) + ethMock.Register("eth_chainId", app.Config.ChainID()) + }) + require.NoError(t, app.Start()) + + job := cltest.NewJobWithWebInitiator() + require.NoError(t, app.Store.CreateJob(&job)) + run := job.NewRun(job.Initiators[0]) + require.NoError(t, app.Store.CreateJobRun(&run)) + + client, _ := app.NewClientAndRenderer() + + set := flag.NewFlagSet("cancel", 0) + set.Parse([]string{run.ID.String()}) + c := cli.NewContext(nil, set, nil) + + require.NoError(t, client.CancelJobRun(c)) + + runs := cltest.MustAllJobsWithStatus(t, app.Store, models.RunStatusCancelled) + require.Len(t, runs, 1) + assert.Equal(t, models.RunStatusCancelled, runs[0].Status) + assert.NotNil(t, runs[0].FinishedAt) +} diff --git a/core/cmd/renderer.go b/core/cmd/renderer.go index 07c7cb46a13..53a2f331b5c 100644 --- a/core/cmd/renderer.go +++ b/core/cmd/renderer.go @@ -1,18 +1,19 @@ package cmd import ( - "reflect" "fmt" "io" + "reflect" "strconv" + "chainlink/core/logger" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "chainlink/core/web" + "github.com/olekukonko/tablewriter" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" - "github.com/smartcontractkit/chainlink/core/web" ) // Renderer implements the Render method. @@ -93,7 +94,7 @@ func (rt RendererTable) renderJobs(jobs []models.JobSpec) error { func (rt RendererTable) renderConfiguration(cwl presenters.ConfigWhitelist) error { table := rt.newTable([]string{"Key", "Value"}) - + table.Append([]string{ "ACCOUNT_ADDRESS", cwl.AccountAddress, @@ -134,7 +135,6 @@ func (rt RendererTable) renderConfiguration(cwl presenters.ConfigWhitelist) erro return nil } - func render(name string, table *tablewriter.Table) { table.SetRowLine(true) table.SetColumnSeparator("║") @@ -267,7 +267,7 @@ func (rt RendererTable) renderJobRuns(runs []presenters.JobRun) error { utils.ISO8601UTC(jr.CreatedAt), utils.NullISO8601UTC(jr.FinishedAt), jr.Result.Data.String(), - jr.Result.ErrorMessage.String, + jr.ErrorString(), }) } diff --git a/core/cmd/renderer_test.go b/core/cmd/renderer_test.go index de42ee6a1bb..5eacd1e45c3 100644 --- a/core/cmd/renderer_test.go +++ b/core/cmd/renderer_test.go @@ -8,11 +8,12 @@ import ( "regexp" "testing" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/web" + "chainlink/core/cmd" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/web" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,12 +39,13 @@ func TestRendererTable_RenderConfiguration(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() - + resp, cleanup := client.Get("/v2/config") cwl := presenters.ConfigWhitelist{} require.NoError(t, cltest.ParseJSONAPIResponse(t, resp, &cwl)) - + r := cmd.RendererTable{Writer: ioutil.Discard} assert.NoError(t, r.Render(&cwl)) } diff --git a/core/internal/cltest/cltest.go b/core/internal/cltest/cltest.go index 7ef6ad09b4f..8b525ba7f1b 100644 --- a/core/internal/cltest/cltest.go +++ b/core/internal/cltest/cltest.go @@ -17,6 +17,17 @@ import ( "testing" "time" + "chainlink/core/cmd" + "chainlink/core/logger" + "chainlink/core/services" + strpkg "chainlink/core/store" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "chainlink/core/web" + "github.com/ethereum/go-ethereum/common" "github.com/gin-gonic/gin" "github.com/gobuffalo/packr" @@ -25,16 +36,6 @@ import ( "github.com/gorilla/websocket" "github.com/manyminds/api2go/jsonapi" "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" - "github.com/smartcontractkit/chainlink/core/web" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -140,6 +141,7 @@ type TestApplication struct { Server *httptest.Server wsServer *httptest.Server connectedChannel chan struct{} + Started bool } func newWSServer() (*httptest.Server, func()) { @@ -245,6 +247,13 @@ func (ta *TestApplication) NewBox() packr.Box { return packr.NewBox("../fixtures/operator_ui/dist") } +func (ta *TestApplication) Start() error { + ta.t.Helper() + ta.Started = true + + return ta.ChainlinkApplication.Start() +} + func (ta *TestApplication) StartAndConnect() error { ta.t.Helper() @@ -283,6 +292,12 @@ func (ta *TestApplication) MockStartAndConnect() (*EthMock, error) { // Stop will stop the test application and perform cleanup func (ta *TestApplication) Stop() error { + ta.t.Helper() + + if !ta.Started { + ta.t.Fatal("TestApplication Stop() called on an unstarted application") + } + // TODO: Here we double close, which is less than ideal. // We would prefer to invoke a method on an interface that // cleans up only in test. @@ -401,17 +416,6 @@ func cleanUpStore(t testing.TB, store *strpkg.Store) { require.NoError(t, store.Close()) } -// NewJobSubscriber creates a new JobSubscriber -func NewJobSubscriber(t testing.TB) (*strpkg.Store, services.JobSubscriber, func()) { - t.Helper() - - store, cl := NewStore(t) - nl := services.NewJobSubscriber(store) - return store, nl, func() { - cl() - } -} - func ParseJSON(t testing.TB, body io.Reader) models.JSON { t.Helper() @@ -454,6 +458,11 @@ func (r *HTTPClientCleaner) Post(path string, body io.Reader) (*http.Response, f return bodyCleaner(r.t, resp, err) } +func (r *HTTPClientCleaner) Put(path string, body io.Reader) (*http.Response, func()) { + resp, err := r.HTTPClient.Put(path, body) + return bodyCleaner(r.t, resp, err) +} + func (r *HTTPClientCleaner) Patch(path string, body io.Reader, headers ...map[string]string) (*http.Response, func()) { resp, err := r.HTTPClient.Patch(path, body, headers...) return bodyCleaner(r.t, resp, err) @@ -521,8 +530,8 @@ func ParseJSONAPIResponseMetaCount(input []byte) (int, error) { } // ReadLogs returns the contents of the applications log file as a string -func ReadLogs(app *TestApplication) (string, error) { - logFile := fmt.Sprintf("%s/log.jsonl", app.Store.Config.RootDir()) +func ReadLogs(config orm.ConfigReader) (string, error) { + logFile := fmt.Sprintf("%s/log.jsonl", config.RootDir()) b, err := ioutil.ReadFile(logFile) return string(b), err } @@ -626,8 +635,9 @@ func CreateHelloWorldJobViaWeb(t testing.TB, app *TestApplication, url string) m err := json.Unmarshal(buffer, &job) require.NoError(t, err) - job.Tasks[0].Params, err = job.Tasks[0].Params.Merge(JSONFromString(t, `{"get":"%v"}`, url)) - assert.NoError(t, err) + data, err := models.Merge(job.Tasks[0].Params, JSONFromString(t, `{"get":"%v"}`, url)) + require.NoError(t, err) + job.Tasks[0].Params = data return CreateJobSpecViaWeb(t, app, job) } @@ -731,17 +741,6 @@ func WaitForJobRunToPendConfirmations( return WaitForJobRunStatus(t, store, jr, models.RunStatusPendingConfirmations) } -// WaitForJobRunToPendSleep waits for a JobRun to reach PendingBridge Status -func WaitForJobRunToPendSleep( - t testing.TB, - store *strpkg.Store, - jr models.JobRun, -) models.JobRun { - t.Helper() - - return WaitForJobRunStatus(t, store, jr, models.RunStatusPendingSleep) -} - // WaitForJobRunStatus waits for a JobRun to reach given status func WaitForJobRunStatus( t testing.TB, @@ -925,11 +924,6 @@ func Head(val interface{}) *models.Head { } } -// NewBlockHeader return a new BlockHeader with given number -func NewBlockHeader(number int) *models.BlockHeader { - return &models.BlockHeader{Number: BigHexInt(number)} -} - // GetAccountAddress returns Address of the account in the keystore of the passed in store func GetAccountAddress(t testing.TB, store *strpkg.Store) common.Address { t.Helper() @@ -1140,3 +1134,9 @@ func MustParseURL(input string) *url.URL { } return u } + +func MustResultString(t *testing.T, input models.RunResult) string { + result := input.Data.Get("result") + require.Equal(t, gjson.String, result.Type, fmt.Sprintf("result type %s is not string", result.Type)) + return result.String() +} diff --git a/core/internal/cltest/factories.go b/core/internal/cltest/factories.go index 761d60363fb..8f2d49c2561 100644 --- a/core/internal/cltest/factories.go +++ b/core/internal/cltest/factories.go @@ -14,17 +14,17 @@ import ( "testing" "time" + "chainlink/core/adapters" + "chainlink/core/logger" + "chainlink/core/store" + strpkg "chainlink/core/store" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/sjson" @@ -221,30 +221,6 @@ func WebURL(t testing.TB, unparsed string) models.WebURL { return models.WebURL(*parsed) } -// NullString creates null.String from given value -func NullString(val interface{}) null.String { - switch val.(type) { - case string: - return null.StringFrom(val.(string)) - case nil: - return null.NewString("", false) - default: - panic("cannot create a null string of any type other than string or nil") - } -} - -// NullTime creates a null.Time from given value -func NullTime(t testing.TB, val interface{}) null.Time { - switch val.(type) { - case string: - return ParseNullableTime(t, val.(string)) - case nil: - return null.NewTime(time.Unix(0, 0), false) - default: - panic("cannot create a null time of any type other than string or nil") - } -} - // JSONFromString create JSON from given body and arguments func JSONFromString(t testing.TB, body string, args ...interface{}) models.JSON { return JSONFromBytes(t, []byte(fmt.Sprintf(body, args...))) @@ -439,48 +415,13 @@ func Int(val interface{}) *models.Big { } } -// RunResultWithResult creates a RunResult with given result -func RunResultWithResult(val interface{}) models.RunResult { - data := models.JSON{} - data, err := data.Add("result", val) - if err != nil { - return RunResultWithError(err) - } - - return models.RunResult{Data: data} -} - -// RunResultWithData creates a run result with a given data JSON object -func RunResultWithData(val string) models.RunResult { - data, err := models.ParseJSON([]byte(val)) - if err != nil { - return RunResultWithError(err) - } - return models.RunResult{Data: data} -} - -// RunResultWithError creates a runresult with given error -func RunResultWithError(err error) models.RunResult { - return models.RunResult{ - Status: models.RunStatusErrored, - ErrorMessage: null.StringFrom(err.Error()), - } -} - // MarkJobRunPendingBridge marks the jobrun as Pending Bridge Status func MarkJobRunPendingBridge(jr models.JobRun, i int) models.JobRun { jr.Status = models.RunStatusPendingBridge - jr.Result.Status = models.RunStatusPendingBridge jr.TaskRuns[i].Status = models.RunStatusPendingBridge - jr.TaskRuns[i].Result.Status = models.RunStatusPendingBridge return jr } -func NewJobRunner(s *strpkg.Store) (services.JobRunner, func()) { - rm := services.NewJobRunner(s) - return rm, func() { rm.Stop() } -} - type MockSigner struct{} func (s MockSigner) SignHash(common.Hash) (models.Signature, error) { @@ -551,3 +492,19 @@ func CreateServiceAgreementViaWeb( return FindServiceAgreement(t, app.Store, responseSA.ID) } + +func NewRunInput(value models.JSON) models.RunInput { + jobRunID := models.NewID() + return *models.NewRunInput(jobRunID, value, models.RunStatusUnstarted) +} + +func NewRunInputWithString(t testing.TB, value string) models.RunInput { + jobRunID := models.NewID() + data := JSONFromString(t, value) + return *models.NewRunInput(jobRunID, data, models.RunStatusUnstarted) +} + +func NewRunInputWithResult(value interface{}) models.RunInput { + jobRunID := models.NewID() + return *models.NewRunInputWithResult(jobRunID, value, models.RunStatusUnstarted) +} diff --git a/core/internal/cltest/fixtures.go b/core/internal/cltest/fixtures.go index 78bad62bfb2..ad7a1e63212 100644 --- a/core/internal/cltest/fixtures.go +++ b/core/internal/cltest/fixtures.go @@ -4,7 +4,8 @@ import ( "encoding/json" "testing" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store/models" + "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) diff --git a/core/internal/cltest/mocks.go b/core/internal/cltest/mocks.go index 9077cae4650..85d6fdddd2f 100644 --- a/core/internal/cltest/mocks.go +++ b/core/internal/cltest/mocks.go @@ -18,14 +18,15 @@ import ( "testing" "time" + "chainlink/core/cmd" + "chainlink/core/logger" + "chainlink/core/services" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/ethereum/go-ethereum/common" "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -92,7 +93,6 @@ func (mock *EthMock) ShouldCall(setup func(mock *EthMock)) ethMockDuring { type ethMockDuring struct { mock *EthMock - t testing.TB } func (emd ethMockDuring) During(action func()) { @@ -374,47 +374,10 @@ func (ta *TestApplication) InstantClock() InstantClock { return clock } -// UseSettableClock creates a SettableClock on the store -func UseSettableClock(s *store.Store) *SettableClock { - clock := &SettableClock{} - s.Clock = clock - return clock -} - -// SettableClock a settable clock -type SettableClock struct { - mutex sync.Mutex - time time.Time -} - -// Now get the current time -func (clock *SettableClock) Now() time.Time { - clock.mutex.Lock() - defer clock.mutex.Unlock() - if clock.time.IsZero() { - return time.Now() - } - return clock.time -} - -// SetTime set the current time -func (clock *SettableClock) SetTime(t time.Time) { - clock.mutex.Lock() - defer clock.mutex.Unlock() - clock.time = t -} - -// After return channel of time -func (*SettableClock) After(_ time.Duration) <-chan time.Time { - channel := make(chan time.Time, 1) - channel <- time.Now() - return channel -} - // InstantClock an InstantClock type InstantClock struct{} -// Now current local time +// Now returns the current local time func (InstantClock) Now() time.Time { return time.Now() } @@ -451,24 +414,16 @@ func (t *TriggerClock) Trigger() { } } +// Now returns the current local time +func (t TriggerClock) Now() time.Time { + return time.Now() +} + // After waits on a manual trigger. func (t *TriggerClock) After(_ time.Duration) <-chan time.Time { return t.triggers } -// NeverClock a never clock -type NeverClock struct{} - -// After return channel of time -func (NeverClock) After(_ time.Duration) <-chan time.Time { - return make(chan time.Time) -} - -// Now returns current local time -func (NeverClock) Now() time.Time { - return time.Now() -} - // RendererMock a mock renderer type RendererMock struct { Renders []interface{} @@ -537,6 +492,7 @@ func (r EmptyRunner) Run(app services.Application) error { // MockCountingPrompter is a mock counting prompt type MockCountingPrompter struct { + T *testing.T EnteredStrings []string Count int NotTerminal bool @@ -546,6 +502,10 @@ type MockCountingPrompter struct { func (p *MockCountingPrompter) Prompt(string) string { i := p.Count p.Count++ + if len(p.EnteredStrings)-1 < i { + p.T.Errorf("Not enough passwords supplied to MockCountingPrompter, wanted %d", i) + p.T.FailNow() + } return p.EnteredStrings[i] } @@ -553,6 +513,10 @@ func (p *MockCountingPrompter) Prompt(string) string { func (p *MockCountingPrompter) PasswordPrompt(string) string { i := p.Count p.Count++ + if len(p.EnteredStrings)-1 < i { + p.T.Errorf("Not enough passwords supplied to MockCountingPrompter, wanted %d", i) + p.T.FailNow() + } return p.EnteredStrings[i] } @@ -737,28 +701,6 @@ func (m mockSecretGenerator) Generate(orm.Config) ([]byte, error) { return []byte(SessionSecret), nil } -type MockRunChannel struct { - Runs []models.RunResult - neverReturningChan chan store.RunRequest -} - -func NewMockRunChannel() *MockRunChannel { - return &MockRunChannel{ - neverReturningChan: make(chan store.RunRequest, 1), - } -} - -func (m *MockRunChannel) Send(jobRunID *models.ID) error { - m.Runs = append(m.Runs, models.RunResult{}) - return nil -} - -func (m *MockRunChannel) Receive() <-chan store.RunRequest { - return m.neverReturningChan -} - -func (m *MockRunChannel) Close() {} - // extractERC20BalanceTargetAddress returns the address whose balance is being // queried by the message in the given call to an ERC20 contract, which is // interpreted as a callArgs. diff --git a/core/internal/cltest/postgres.go b/core/internal/cltest/postgres.go index 698e2d75069..979bb4821ed 100644 --- a/core/internal/cltest/postgres.go +++ b/core/internal/cltest/postgres.go @@ -6,9 +6,9 @@ import ( "net/url" "testing" - "github.com/smartcontractkit/chainlink/core/store/dbutil" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" + "chainlink/core/store/dbutil" + "chainlink/core/store/models" + "chainlink/core/store/orm" ) // PrepareTestDB prepares the database to run tests, functionality varies diff --git a/core/internal/features_test.go b/core/internal/features_test.go index 047b72104d2..a92cd77ecd5 100644 --- a/core/internal/features_test.go +++ b/core/internal/features_test.go @@ -1,7 +1,6 @@ package internal_test import ( - "bytes" "encoding/json" "fmt" "io" @@ -12,12 +11,12 @@ import ( "testing" "time" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/utils" + "chainlink/core/web" + "github.com/ethereum/go-ethereum/common" - "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" - "github.com/smartcontractkit/chainlink/core/web" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -88,6 +87,9 @@ func TestIntegration_HttpRequestWithHeaders(t *testing.T) { eth.EventuallyAllCalled(t) cltest.WaitForTxAttemptCount(t, app.Store, 1) + jr.ObservedHeight = confirmedReceipt.BlockNumber + require.NoError(t, app.Store.SaveJobRun(&jr)) + eth.Context("ethTx.Perform()#4 at block 23465", func(eth *cltest.EthMock) { eth.Register("eth_getTransactionReceipt", confirmedReceipt) // confirmed for gas bumped txat eth.Register("eth_getBalance", "0x0100") @@ -224,18 +226,15 @@ func TestIntegration_FeeBump(t *testing.T) { eth.EventuallyAllCalled(t) jr = cltest.WaitForJobRunToComplete(t, app.Store, jr) - val, err := jr.TaskRuns[0].Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, tickerResponse, val) - val, err = jr.TaskRuns[1].Result.ResultString() - assert.Equal(t, "10583.75", val) - assert.NoError(t, err) - val, err = jr.TaskRuns[3].Result.ResultString() - assert.Equal(t, attempt1Hash.String(), val) - assert.NoError(t, err) - val, err = jr.Result.ResultString() - assert.Equal(t, attempt1Hash.String(), val) - assert.NoError(t, err) + require.Len(t, jr.TaskRuns, 4) + value := cltest.MustResultString(t, jr.TaskRuns[0].Result) + assert.Equal(t, tickerResponse, value) + value = cltest.MustResultString(t, jr.TaskRuns[1].Result) + assert.Equal(t, "10583.75", value) + value = cltest.MustResultString(t, jr.TaskRuns[3].Result) + assert.Equal(t, attempt1Hash.String(), value) + value = cltest.MustResultString(t, jr.Result) + assert.Equal(t, attempt1Hash.String(), value) } func TestIntegration_RunAt(t *testing.T) { @@ -367,35 +366,6 @@ func TestIntegration_RunLog(t *testing.T) { } } -func TestIntegration_EndAt(t *testing.T) { - t.Parallel() - - app, cleanup := cltest.NewApplication(t) - defer cleanup() - eth := app.MockEthCallerSubscriber(cltest.Strict) - eth.Register("eth_chainId", app.Store.Config.ChainID()) - clock := cltest.UseSettableClock(app.Store) - app.Start() - client := app.NewHTTPClient() - - j := cltest.FixtureCreateJobViaWeb(t, app, "fixtures/web/end_at_job.json") - endAt := cltest.ParseISO8601(t, "3000-01-01T00:00:00.000Z") - assert.Equal(t, endAt, j.EndAt.Time) - - cltest.CreateJobRunViaWeb(t, app, j) - - clock.SetTime(endAt.Add(time.Nanosecond)) - - resp, cleanup := client.Post("/v2/specs/"+j.ID.String()+"/runs", &bytes.Buffer{}) - defer cleanup() - assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - gomega.NewGomegaWithT(t).Consistently(func() []models.JobRun { - jobRuns, err := app.Store.JobRunsFor(j.ID) - assert.NoError(t, err) - return jobRuns - }).Should(gomega.HaveLen(1)) -} - func TestIntegration_StartAt(t *testing.T) { t.Parallel() @@ -403,21 +373,12 @@ func TestIntegration_StartAt(t *testing.T) { defer cleanup() eth := app.MockEthCallerSubscriber(cltest.Strict) eth.Register("eth_chainId", app.Store.Config.ChainID()) - clock := cltest.UseSettableClock(app.Store) app.Start() - client := app.NewHTTPClient() j := cltest.FixtureCreateJobViaWeb(t, app, "fixtures/web/start_at_job.json") - startAt := cltest.ParseISO8601(t, "3000-01-01T00:00:00.000Z") + startAt := cltest.ParseISO8601(t, "1970-01-01T00:00:00.000Z") assert.Equal(t, startAt, j.StartAt.Time) - resp, cleanup := client.Post("/v2/specs/"+j.ID.String()+"/runs", &bytes.Buffer{}) - defer cleanup() - assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - cltest.WaitForRuns(t, j, app.Store, 0) - - clock.SetTime(startAt) - cltest.CreateJobRunViaWeb(t, app, j) } @@ -470,10 +431,9 @@ func TestIntegration_ExternalAdapter_RunLogInitiated(t *testing.T) { tr := jr.TaskRuns[0] assert.Equal(t, "randomnumber", tr.TaskSpec.Type.String()) - val, err := tr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, eaValue, val) - res := tr.Result.Get("extra") + value := cltest.MustResultString(t, tr.Result) + assert.Equal(t, eaValue, value) + res := tr.Result.Data.Get("extra") assert.Equal(t, eaExtra, res.String()) assert.True(t, eth.AllCalled(), eth.Remaining()) @@ -497,20 +457,20 @@ func TestIntegration_ExternalAdapter_Copy(t *testing.T) { eaResponse := fmt.Sprintf(`{"data":{"price": "%v", "quote": "%v"}}`, eaPrice, eaQuote) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "POST", r.Method) - assert.Equal(t, "/", r.URL.Path) + require.Equal(t, "POST", r.Method) + require.Equal(t, "/", r.URL.Path) b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) + require.NoError(t, err) body := cltest.JSONFromBytes(t, b) data := body.Get("data") - assert.True(t, data.Exists()) + require.True(t, data.Exists()) bodyParam := data.Get("bodyParam") - assert.True(t, bodyParam.Exists()) - assert.Equal(t, true, bodyParam.Bool()) + require.True(t, bodyParam.Exists()) + require.Equal(t, true, bodyParam.Bool()) url := body.Get("responseURL") - assert.Contains(t, url.String(), "https://test.chain.link/always/v2/runs") + require.Contains(t, url.String(), "https://test.chain.link/always/v2/runs") w.WriteHeader(http.StatusOK) io.WriteString(w, eaResponse) @@ -526,11 +486,8 @@ func TestIntegration_ExternalAdapter_Copy(t *testing.T) { assert.Equal(t, "assetprice", tr.TaskSpec.Type.String()) tr = jr.TaskRuns[1] assert.Equal(t, "copy", tr.TaskSpec.Type.String()) - val, err := tr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, eaPrice, val) - res := tr.Result.Get("quote") - assert.Equal(t, eaQuote, res.String()) + value := cltest.MustResultString(t, tr.Result) + assert.Equal(t, eaPrice, value) } // This test ensures that an bridge adapter task is resumed from pending after @@ -574,17 +531,15 @@ func TestIntegration_ExternalAdapter_Pending(t *testing.T) { tr := jr.TaskRuns[0] assert.Equal(t, models.RunStatusPendingBridge, tr.Status) - val, err := tr.Result.ResultString() - assert.Error(t, err) - assert.Equal(t, "", val) + assert.Equal(t, gjson.Null, tr.Result.Data.Get("result").Type) jr = cltest.UpdateJobRunViaWeb(t, app, jr, bta, `{"data":{"result":"100"}}`) jr = cltest.WaitForJobRunToComplete(t, app.Store, jr) tr = jr.TaskRuns[0] assert.Equal(t, models.RunStatusCompleted, tr.Status) - val, err = tr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, "100", val) + + value := cltest.MustResultString(t, tr.Result) + assert.Equal(t, "100", value) } func TestIntegration_WeiWatchers(t *testing.T) { @@ -634,9 +589,8 @@ func TestIntegration_MultiplierInt256(t *testing.T) { jr := cltest.CreateJobRunViaWeb(t, app, j, `{"result":"-10221.30"}`) jr = cltest.WaitForJobRunToComplete(t, app.Store, jr) - val, err := jr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0674e", val) + value := cltest.MustResultString(t, jr.Result) + assert.Equal(t, "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0674e", value) } func TestIntegration_MultiplierUint256(t *testing.T) { @@ -650,9 +604,8 @@ func TestIntegration_MultiplierUint256(t *testing.T) { jr := cltest.CreateJobRunViaWeb(t, app, j, `{"result":"10221.30"}`) jr = cltest.WaitForJobRunToComplete(t, app.Store, jr) - val, err := jr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, "0x00000000000000000000000000000000000000000000000000000000000f98b2", val) + value := cltest.MustResultString(t, jr.Result) + assert.Equal(t, "0x00000000000000000000000000000000000000000000000000000000000f98b2", value) } func TestIntegration_NonceManagement_firstRunWithExistingTxs(t *testing.T) { @@ -724,7 +677,7 @@ func TestIntegration_CreateServiceAgreement(t *testing.T) { assert.NotEqual(t, "", sa.ID) j := cltest.FindJob(t, app.Store, sa.JobSpecID) - assert.Equal(t, cltest.NewLink(t, "1000000000000000000"), sa.Encumbrance.Payment) + assert.Equal(t, *cltest.NewLink(t, "1000000000000000000"), sa.Encumbrance.Payment) assert.Equal(t, uint64(300), sa.Encumbrance.Expiration) assert.Equal(t, endAt, sa.Encumbrance.EndAt.Time) @@ -759,10 +712,9 @@ func TestIntegration_SyncJobRuns(t *testing.T) { eth.Register("eth_chainId", config.ChainID()) app.InstantClock() - app.Store.StatsPusher.Period = 300 * time.Millisecond - app.Start() + j := cltest.FixtureCreateJobViaWeb(t, app, "fixtures/web/run_at_job.json") cltest.CallbackOrTimeout(t, "stats pusher connects", func() { @@ -795,8 +747,8 @@ func TestIntegration_SleepAdapter(t *testing.T) { runInput := fmt.Sprintf("{\"until\": \"%s\"}", time.Now().Local().Add(time.Second*time.Duration(sleepSeconds))) jr := cltest.CreateJobRunViaWeb(t, app, j, runInput) - cltest.WaitForJobRunToPendSleep(t, app.Store, jr) - cltest.JobRunStays(t, app.Store, jr, models.RunStatusPendingSleep, time.Second) + cltest.WaitForJobRunStatus(t, app.Store, jr, models.RunStatusInProgress) + cltest.JobRunStays(t, app.Store, jr, models.RunStatusInProgress, time.Second) cltest.WaitForJobRunToComplete(t, app.Store, jr) } diff --git a/core/internal/fixtures/web/start_at_job.json b/core/internal/fixtures/web/start_at_job.json index be92a87267b..85dc13084ce 100644 --- a/core/internal/fixtures/web/start_at_job.json +++ b/core/internal/fixtures/web/start_at_job.json @@ -1,5 +1,5 @@ { "initiators": [ { "type": "web" } ], "tasks": [ { "type": "NoOp" } ], - "startAt": "3000-01-01T00:00:00.000Z" + "startAt": "1970-01-01T00:00:00.000Z" } diff --git a/core/internal/mocks/application.go b/core/internal/mocks/application.go new file mode 100644 index 00000000000..92426d3b255 --- /dev/null +++ b/core/internal/mocks/application.go @@ -0,0 +1,245 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import big "math/big" +import mock "github.com/stretchr/testify/mock" +import models "chainlink/core/store/models" +import packr "github.com/gobuffalo/packr" + +import store "chainlink/core/store" + +// Application is an autogenerated mock type for the Application type +type Application struct { + mock.Mock +} + +// AddJob provides a mock function with given fields: job +func (_m *Application) AddJob(job models.JobSpec) error { + ret := _m.Called(job) + + var r0 error + if rf, ok := ret.Get(0).(func(models.JobSpec) error); ok { + r0 = rf(job) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AddServiceAgreement provides a mock function with given fields: _a0 +func (_m *Application) AddServiceAgreement(_a0 *models.ServiceAgreement) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.ServiceAgreement) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ArchiveJob provides a mock function with given fields: _a0 +func (_m *Application) ArchiveJob(_a0 *models.ID) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.ID) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Cancel provides a mock function with given fields: runID +func (_m *Application) Cancel(runID *models.ID) (*models.JobRun, error) { + ret := _m.Called(runID) + + var r0 *models.JobRun + if rf, ok := ret.Get(0).(func(*models.ID) *models.JobRun); ok { + r0 = rf(runID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.JobRun) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.ID) error); ok { + r1 = rf(runID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Create provides a mock function with given fields: jobSpecID, initiator, data, creationHeight, runRequest +func (_m *Application) Create(jobSpecID *models.ID, initiator *models.Initiator, data *models.JSON, creationHeight *big.Int, runRequest *models.RunRequest) (*models.JobRun, error) { + ret := _m.Called(jobSpecID, initiator, data, creationHeight, runRequest) + + var r0 *models.JobRun + if rf, ok := ret.Get(0).(func(*models.ID, *models.Initiator, *models.JSON, *big.Int, *models.RunRequest) *models.JobRun); ok { + r0 = rf(jobSpecID, initiator, data, creationHeight, runRequest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.JobRun) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.ID, *models.Initiator, *models.JSON, *big.Int, *models.RunRequest) error); ok { + r1 = rf(jobSpecID, initiator, data, creationHeight, runRequest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateErrored provides a mock function with given fields: jobSpecID, initiator, err +func (_m *Application) CreateErrored(jobSpecID *models.ID, initiator models.Initiator, err error) (*models.JobRun, error) { + ret := _m.Called(jobSpecID, initiator, err) + + var r0 *models.JobRun + if rf, ok := ret.Get(0).(func(*models.ID, models.Initiator, error) *models.JobRun); ok { + r0 = rf(jobSpecID, initiator, err) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.JobRun) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.ID, models.Initiator, error) error); ok { + r1 = rf(jobSpecID, initiator, err) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStore provides a mock function with given fields: +func (_m *Application) GetStore() *store.Store { + ret := _m.Called() + + var r0 *store.Store + if rf, ok := ret.Get(0).(func() *store.Store); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.Store) + } + } + + return r0 +} + +// NewBox provides a mock function with given fields: +func (_m *Application) NewBox() packr.Box { + ret := _m.Called() + + var r0 packr.Box + if rf, ok := ret.Get(0).(func() packr.Box); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(packr.Box) + } + + return r0 +} + +// ResumeAllConfirming provides a mock function with given fields: currentBlockHeight +func (_m *Application) ResumeAllConfirming(currentBlockHeight *big.Int) error { + ret := _m.Called(currentBlockHeight) + + var r0 error + if rf, ok := ret.Get(0).(func(*big.Int) error); ok { + r0 = rf(currentBlockHeight) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ResumeAllConnecting provides a mock function with given fields: +func (_m *Application) ResumeAllConnecting() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ResumeAllInProgress provides a mock function with given fields: +func (_m *Application) ResumeAllInProgress() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ResumePending provides a mock function with given fields: runID, input +func (_m *Application) ResumePending(runID *models.ID, input models.BridgeRunResult) error { + ret := _m.Called(runID, input) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.ID, models.BridgeRunResult) error); ok { + r0 = rf(runID, input) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Start provides a mock function with given fields: +func (_m *Application) Start() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Stop provides a mock function with given fields: +func (_m *Application) Stop() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// WakeSessionReaper provides a mock function with given fields: +func (_m *Application) WakeSessionReaper() { + _m.Called() +} diff --git a/core/internal/mocks/eth_client.go b/core/internal/mocks/eth_client.go new file mode 100644 index 00000000000..13f0a94cf4d --- /dev/null +++ b/core/internal/mocks/eth_client.go @@ -0,0 +1,241 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import assets "chainlink/core/store/assets" +import big "math/big" +import common "github.com/ethereum/go-ethereum/common" +import ethereum "github.com/ethereum/go-ethereum" +import mock "github.com/stretchr/testify/mock" +import models "chainlink/core/store/models" + +// EthClient is an autogenerated mock type for the EthClient type +type EthClient struct { + mock.Mock +} + +// GetBlockByNumber provides a mock function with given fields: hex +func (_m *EthClient) GetBlockByNumber(hex string) (models.BlockHeader, error) { + ret := _m.Called(hex) + + var r0 models.BlockHeader + if rf, ok := ret.Get(0).(func(string) models.BlockHeader); ok { + r0 = rf(hex) + } else { + r0 = ret.Get(0).(models.BlockHeader) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(hex) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetChainID provides a mock function with given fields: +func (_m *EthClient) GetChainID() (*big.Int, error) { + ret := _m.Called() + + var r0 *big.Int + if rf, ok := ret.Get(0).(func() *big.Int); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetERC20Balance provides a mock function with given fields: address, contractAddress +func (_m *EthClient) GetERC20Balance(address common.Address, contractAddress common.Address) (*big.Int, error) { + ret := _m.Called(address, contractAddress) + + var r0 *big.Int + if rf, ok := ret.Get(0).(func(common.Address, common.Address) *big.Int); ok { + r0 = rf(address, contractAddress) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Address, common.Address) error); ok { + r1 = rf(address, contractAddress) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetEthBalance provides a mock function with given fields: address +func (_m *EthClient) GetEthBalance(address common.Address) (*assets.Eth, error) { + ret := _m.Called(address) + + var r0 *assets.Eth + if rf, ok := ret.Get(0).(func(common.Address) *assets.Eth); ok { + r0 = rf(address) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*assets.Eth) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Address) error); ok { + r1 = rf(address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLogs provides a mock function with given fields: q +func (_m *EthClient) GetLogs(q ethereum.FilterQuery) ([]models.Log, error) { + ret := _m.Called(q) + + var r0 []models.Log + if rf, ok := ret.Get(0).(func(ethereum.FilterQuery) []models.Log); ok { + r0 = rf(q) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Log) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(ethereum.FilterQuery) error); ok { + r1 = rf(q) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetNonce provides a mock function with given fields: address +func (_m *EthClient) GetNonce(address common.Address) (uint64, error) { + ret := _m.Called(address) + + var r0 uint64 + if rf, ok := ret.Get(0).(func(common.Address) uint64); ok { + r0 = rf(address) + } else { + r0 = ret.Get(0).(uint64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Address) error); ok { + r1 = rf(address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTxReceipt provides a mock function with given fields: hash +func (_m *EthClient) GetTxReceipt(hash common.Hash) (*models.TxReceipt, error) { + ret := _m.Called(hash) + + var r0 *models.TxReceipt + if rf, ok := ret.Get(0).(func(common.Hash) *models.TxReceipt); ok { + r0 = rf(hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.TxReceipt) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Hash) error); ok { + r1 = rf(hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SendRawTx provides a mock function with given fields: hex +func (_m *EthClient) SendRawTx(hex string) (common.Hash, error) { + ret := _m.Called(hex) + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(string) common.Hash); ok { + r0 = rf(hex) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(hex) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubscribeToLogs provides a mock function with given fields: channel, q +func (_m *EthClient) SubscribeToLogs(channel chan<- models.Log, q ethereum.FilterQuery) (models.EthSubscription, error) { + ret := _m.Called(channel, q) + + var r0 models.EthSubscription + if rf, ok := ret.Get(0).(func(chan<- models.Log, ethereum.FilterQuery) models.EthSubscription); ok { + r0 = rf(channel, q) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.EthSubscription) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(chan<- models.Log, ethereum.FilterQuery) error); ok { + r1 = rf(channel, q) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubscribeToNewHeads provides a mock function with given fields: channel +func (_m *EthClient) SubscribeToNewHeads(channel chan<- models.BlockHeader) (models.EthSubscription, error) { + ret := _m.Called(channel) + + var r0 models.EthSubscription + if rf, ok := ret.Get(0).(func(chan<- models.BlockHeader) models.EthSubscription); ok { + r0 = rf(channel) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.EthSubscription) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(chan<- models.BlockHeader) error); ok { + r1 = rf(channel) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/core/internal/mocks/eth_client_mocks.go b/core/internal/mocks/eth_client_mocks.go deleted file mode 100644 index fed8d3d5eca..00000000000 --- a/core/internal/mocks/eth_client_mocks.go +++ /dev/null @@ -1,188 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/smartcontractkit/chainlink/core/store (interfaces: EthClient) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - go_ethereum "github.com/ethereum/go-ethereum" - common "github.com/ethereum/go-ethereum/common" - gomock "github.com/golang/mock/gomock" - assets "github.com/smartcontractkit/chainlink/core/store/assets" - models "github.com/smartcontractkit/chainlink/core/store/models" - big "math/big" - reflect "reflect" -) - -// MockEthClient is a mock of EthClient interface -type MockEthClient struct { - ctrl *gomock.Controller - recorder *MockEthClientMockRecorder -} - -// MockEthClientMockRecorder is the mock recorder for MockEthClient -type MockEthClientMockRecorder struct { - mock *MockEthClient -} - -// NewMockEthClient creates a new mock instance -func NewMockEthClient(ctrl *gomock.Controller) *MockEthClient { - mock := &MockEthClient{ctrl: ctrl} - mock.recorder = &MockEthClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockEthClient) EXPECT() *MockEthClientMockRecorder { - return m.recorder -} - -// GetBlockByNumber mocks base method -func (m *MockEthClient) GetBlockByNumber(arg0 string) (models.BlockHeader, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlockByNumber", arg0) - ret0, _ := ret[0].(models.BlockHeader) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlockByNumber indicates an expected call of GetBlockByNumber -func (mr *MockEthClientMockRecorder) GetBlockByNumber(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByNumber", reflect.TypeOf((*MockEthClient)(nil).GetBlockByNumber), arg0) -} - -// GetChainID mocks base method -func (m *MockEthClient) GetChainID() (*big.Int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChainID") - ret0, _ := ret[0].(*big.Int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChainID indicates an expected call of GetChainID -func (mr *MockEthClientMockRecorder) GetChainID() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChainID", reflect.TypeOf((*MockEthClient)(nil).GetChainID)) -} - -// GetERC20Balance mocks base method -func (m *MockEthClient) GetERC20Balance(arg0, arg1 common.Address) (*big.Int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetERC20Balance", arg0, arg1) - ret0, _ := ret[0].(*big.Int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetERC20Balance indicates an expected call of GetERC20Balance -func (mr *MockEthClientMockRecorder) GetERC20Balance(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetERC20Balance", reflect.TypeOf((*MockEthClient)(nil).GetERC20Balance), arg0, arg1) -} - -// GetEthBalance mocks base method -func (m *MockEthClient) GetEthBalance(arg0 common.Address) (*assets.Eth, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEthBalance", arg0) - ret0, _ := ret[0].(*assets.Eth) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetEthBalance indicates an expected call of GetEthBalance -func (mr *MockEthClientMockRecorder) GetEthBalance(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEthBalance", reflect.TypeOf((*MockEthClient)(nil).GetEthBalance), arg0) -} - -// GetLogs mocks base method -func (m *MockEthClient) GetLogs(arg0 go_ethereum.FilterQuery) ([]models.Log, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLogs", arg0) - ret0, _ := ret[0].([]models.Log) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetLogs indicates an expected call of GetLogs -func (mr *MockEthClientMockRecorder) GetLogs(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockEthClient)(nil).GetLogs), arg0) -} - -// GetNonce mocks base method -func (m *MockEthClient) GetNonce(arg0 common.Address) (uint64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNonce", arg0) - ret0, _ := ret[0].(uint64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetNonce indicates an expected call of GetNonce -func (mr *MockEthClientMockRecorder) GetNonce(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNonce", reflect.TypeOf((*MockEthClient)(nil).GetNonce), arg0) -} - -// GetTxReceipt mocks base method -func (m *MockEthClient) GetTxReceipt(arg0 common.Hash) (*models.TxReceipt, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTxReceipt", arg0) - ret0, _ := ret[0].(*models.TxReceipt) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTxReceipt indicates an expected call of GetTxReceipt -func (mr *MockEthClientMockRecorder) GetTxReceipt(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTxReceipt", reflect.TypeOf((*MockEthClient)(nil).GetTxReceipt), arg0) -} - -// SendRawTx mocks base method -func (m *MockEthClient) SendRawTx(arg0 string) (common.Hash, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendRawTx", arg0) - ret0, _ := ret[0].(common.Hash) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SendRawTx indicates an expected call of SendRawTx -func (mr *MockEthClientMockRecorder) SendRawTx(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRawTx", reflect.TypeOf((*MockEthClient)(nil).SendRawTx), arg0) -} - -// SubscribeToLogs mocks base method -func (m *MockEthClient) SubscribeToLogs(arg0 chan<- models.Log, arg1 go_ethereum.FilterQuery) (models.EthSubscription, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubscribeToLogs", arg0, arg1) - ret0, _ := ret[0].(models.EthSubscription) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SubscribeToLogs indicates an expected call of SubscribeToLogs -func (mr *MockEthClientMockRecorder) SubscribeToLogs(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeToLogs", reflect.TypeOf((*MockEthClient)(nil).SubscribeToLogs), arg0, arg1) -} - -// SubscribeToNewHeads mocks base method -func (m *MockEthClient) SubscribeToNewHeads(arg0 chan<- models.BlockHeader) (models.EthSubscription, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubscribeToNewHeads", arg0) - ret0, _ := ret[0].(models.EthSubscription) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SubscribeToNewHeads indicates an expected call of SubscribeToNewHeads -func (mr *MockEthClientMockRecorder) SubscribeToNewHeads(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeToNewHeads", reflect.TypeOf((*MockEthClient)(nil).SubscribeToNewHeads), arg0) -} diff --git a/core/internal/mocks/job_subscriber.go b/core/internal/mocks/job_subscriber.go new file mode 100644 index 00000000000..4b3819b77f1 --- /dev/null +++ b/core/internal/mocks/job_subscriber.go @@ -0,0 +1,79 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import models "chainlink/core/store/models" + +// JobSubscriber is an autogenerated mock type for the JobSubscriber type +type JobSubscriber struct { + mock.Mock +} + +// AddJob provides a mock function with given fields: job, bn +func (_m *JobSubscriber) AddJob(job models.JobSpec, bn *models.Head) error { + ret := _m.Called(job, bn) + + var r0 error + if rf, ok := ret.Get(0).(func(models.JobSpec, *models.Head) error); ok { + r0 = rf(job, bn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Connect provides a mock function with given fields: _a0 +func (_m *JobSubscriber) Connect(_a0 *models.Head) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.Head) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Disconnect provides a mock function with given fields: +func (_m *JobSubscriber) Disconnect() { + _m.Called() +} + +// Jobs provides a mock function with given fields: +func (_m *JobSubscriber) Jobs() []models.JobSpec { + ret := _m.Called() + + var r0 []models.JobSpec + if rf, ok := ret.Get(0).(func() []models.JobSpec); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.JobSpec) + } + } + + return r0 +} + +// OnNewHead provides a mock function with given fields: _a0 +func (_m *JobSubscriber) OnNewHead(_a0 *models.Head) { + _m.Called(_a0) +} + +// RemoveJob provides a mock function with given fields: ID +func (_m *JobSubscriber) RemoveJob(ID *models.ID) error { + ret := _m.Called(ID) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.ID) error); ok { + r0 = rf(ID) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/core/internal/mocks/run_executor.go b/core/internal/mocks/run_executor.go new file mode 100644 index 00000000000..e4d145ae5fd --- /dev/null +++ b/core/internal/mocks/run_executor.go @@ -0,0 +1,25 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import models "chainlink/core/store/models" + +// RunExecutor is an autogenerated mock type for the RunExecutor type +type RunExecutor struct { + mock.Mock +} + +// Execute provides a mock function with given fields: _a0 +func (_m *RunExecutor) Execute(_a0 *models.ID) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.ID) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/core/internal/mocks/run_manager.go b/core/internal/mocks/run_manager.go new file mode 100644 index 00000000000..f1b29ad559d --- /dev/null +++ b/core/internal/mocks/run_manager.go @@ -0,0 +1,137 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import big "math/big" +import mock "github.com/stretchr/testify/mock" +import models "chainlink/core/store/models" + +// RunManager is an autogenerated mock type for the RunManager type +type RunManager struct { + mock.Mock +} + +// Cancel provides a mock function with given fields: runID +func (_m *RunManager) Cancel(runID *models.ID) (*models.JobRun, error) { + ret := _m.Called(runID) + + var r0 *models.JobRun + if rf, ok := ret.Get(0).(func(*models.ID) *models.JobRun); ok { + r0 = rf(runID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.JobRun) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.ID) error); ok { + r1 = rf(runID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Create provides a mock function with given fields: jobSpecID, initiator, data, creationHeight, runRequest +func (_m *RunManager) Create(jobSpecID *models.ID, initiator *models.Initiator, data *models.JSON, creationHeight *big.Int, runRequest *models.RunRequest) (*models.JobRun, error) { + ret := _m.Called(jobSpecID, initiator, data, creationHeight, runRequest) + + var r0 *models.JobRun + if rf, ok := ret.Get(0).(func(*models.ID, *models.Initiator, *models.JSON, *big.Int, *models.RunRequest) *models.JobRun); ok { + r0 = rf(jobSpecID, initiator, data, creationHeight, runRequest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.JobRun) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.ID, *models.Initiator, *models.JSON, *big.Int, *models.RunRequest) error); ok { + r1 = rf(jobSpecID, initiator, data, creationHeight, runRequest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateErrored provides a mock function with given fields: jobSpecID, initiator, err +func (_m *RunManager) CreateErrored(jobSpecID *models.ID, initiator models.Initiator, err error) (*models.JobRun, error) { + ret := _m.Called(jobSpecID, initiator, err) + + var r0 *models.JobRun + if rf, ok := ret.Get(0).(func(*models.ID, models.Initiator, error) *models.JobRun); ok { + r0 = rf(jobSpecID, initiator, err) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.JobRun) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.ID, models.Initiator, error) error); ok { + r1 = rf(jobSpecID, initiator, err) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ResumeAllConfirming provides a mock function with given fields: currentBlockHeight +func (_m *RunManager) ResumeAllConfirming(currentBlockHeight *big.Int) error { + ret := _m.Called(currentBlockHeight) + + var r0 error + if rf, ok := ret.Get(0).(func(*big.Int) error); ok { + r0 = rf(currentBlockHeight) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ResumeAllConnecting provides a mock function with given fields: +func (_m *RunManager) ResumeAllConnecting() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ResumeAllInProgress provides a mock function with given fields: +func (_m *RunManager) ResumeAllInProgress() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ResumePending provides a mock function with given fields: runID, input +func (_m *RunManager) ResumePending(runID *models.ID, input models.BridgeRunResult) error { + ret := _m.Called(runID, input) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.ID, models.BridgeRunResult) error); ok { + r0 = rf(runID, input) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/core/internal/mocks/run_queue.go b/core/internal/mocks/run_queue.go new file mode 100644 index 00000000000..c03dd79a462 --- /dev/null +++ b/core/internal/mocks/run_queue.go @@ -0,0 +1,49 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import models "chainlink/core/store/models" + +// RunQueue is an autogenerated mock type for the RunQueue type +type RunQueue struct { + mock.Mock +} + +// Run provides a mock function with given fields: _a0 +func (_m *RunQueue) Run(_a0 *models.JobRun) { + _m.Called(_a0) +} + +// Start provides a mock function with given fields: +func (_m *RunQueue) Start() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Stop provides a mock function with given fields: +func (_m *RunQueue) Stop() { + _m.Called() +} + +// WorkerCount provides a mock function with given fields: +func (_m *RunQueue) WorkerCount() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} diff --git a/core/internal/mocks/tx_manager.go b/core/internal/mocks/tx_manager.go new file mode 100644 index 00000000000..026f9855161 --- /dev/null +++ b/core/internal/mocks/tx_manager.go @@ -0,0 +1,432 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import accounts "github.com/ethereum/go-ethereum/accounts" +import assets "chainlink/core/store/assets" +import big "math/big" +import common "github.com/ethereum/go-ethereum/common" +import ethereum "github.com/ethereum/go-ethereum" +import mock "github.com/stretchr/testify/mock" +import models "chainlink/core/store/models" +import null "gopkg.in/guregu/null.v3" +import store "chainlink/core/store" + +// TxManager is an autogenerated mock type for the TxManager type +type TxManager struct { + mock.Mock +} + +// BumpGasUntilSafe provides a mock function with given fields: hash +func (_m *TxManager) BumpGasUntilSafe(hash common.Hash) (*models.TxReceipt, store.AttemptState, error) { + ret := _m.Called(hash) + + var r0 *models.TxReceipt + if rf, ok := ret.Get(0).(func(common.Hash) *models.TxReceipt); ok { + r0 = rf(hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.TxReceipt) + } + } + + var r1 store.AttemptState + if rf, ok := ret.Get(1).(func(common.Hash) store.AttemptState); ok { + r1 = rf(hash) + } else { + r1 = ret.Get(1).(store.AttemptState) + } + + var r2 error + if rf, ok := ret.Get(2).(func(common.Hash) error); ok { + r2 = rf(hash) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// CheckAttempt provides a mock function with given fields: txAttempt, blockHeight +func (_m *TxManager) CheckAttempt(txAttempt *models.TxAttempt, blockHeight uint64) (*models.TxReceipt, store.AttemptState, error) { + ret := _m.Called(txAttempt, blockHeight) + + var r0 *models.TxReceipt + if rf, ok := ret.Get(0).(func(*models.TxAttempt, uint64) *models.TxReceipt); ok { + r0 = rf(txAttempt, blockHeight) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.TxReceipt) + } + } + + var r1 store.AttemptState + if rf, ok := ret.Get(1).(func(*models.TxAttempt, uint64) store.AttemptState); ok { + r1 = rf(txAttempt, blockHeight) + } else { + r1 = ret.Get(1).(store.AttemptState) + } + + var r2 error + if rf, ok := ret.Get(2).(func(*models.TxAttempt, uint64) error); ok { + r2 = rf(txAttempt, blockHeight) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// Connect provides a mock function with given fields: _a0 +func (_m *TxManager) Connect(_a0 *models.Head) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.Head) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Connected provides a mock function with given fields: +func (_m *TxManager) Connected() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ContractLINKBalance provides a mock function with given fields: wr +func (_m *TxManager) ContractLINKBalance(wr models.WithdrawalRequest) (assets.Link, error) { + ret := _m.Called(wr) + + var r0 assets.Link + if rf, ok := ret.Get(0).(func(models.WithdrawalRequest) assets.Link); ok { + r0 = rf(wr) + } else { + r0 = ret.Get(0).(assets.Link) + } + + var r1 error + if rf, ok := ret.Get(1).(func(models.WithdrawalRequest) error); ok { + r1 = rf(wr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateTx provides a mock function with given fields: to, data +func (_m *TxManager) CreateTx(to common.Address, data []byte) (*models.Tx, error) { + ret := _m.Called(to, data) + + var r0 *models.Tx + if rf, ok := ret.Get(0).(func(common.Address, []byte) *models.Tx); ok { + r0 = rf(to, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Tx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Address, []byte) error); ok { + r1 = rf(to, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateTxWithEth provides a mock function with given fields: from, to, value +func (_m *TxManager) CreateTxWithEth(from common.Address, to common.Address, value *assets.Eth) (*models.Tx, error) { + ret := _m.Called(from, to, value) + + var r0 *models.Tx + if rf, ok := ret.Get(0).(func(common.Address, common.Address, *assets.Eth) *models.Tx); ok { + r0 = rf(from, to, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Tx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Address, common.Address, *assets.Eth) error); ok { + r1 = rf(from, to, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateTxWithGas provides a mock function with given fields: surrogateID, to, data, gasPriceWei, gasLimit +func (_m *TxManager) CreateTxWithGas(surrogateID null.String, to common.Address, data []byte, gasPriceWei *big.Int, gasLimit uint64) (*models.Tx, error) { + ret := _m.Called(surrogateID, to, data, gasPriceWei, gasLimit) + + var r0 *models.Tx + if rf, ok := ret.Get(0).(func(null.String, common.Address, []byte, *big.Int, uint64) *models.Tx); ok { + r0 = rf(surrogateID, to, data, gasPriceWei, gasLimit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Tx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(null.String, common.Address, []byte, *big.Int, uint64) error); ok { + r1 = rf(surrogateID, to, data, gasPriceWei, gasLimit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Disconnect provides a mock function with given fields: +func (_m *TxManager) Disconnect() { + _m.Called() +} + +// GetBlockByNumber provides a mock function with given fields: hex +func (_m *TxManager) GetBlockByNumber(hex string) (models.BlockHeader, error) { + ret := _m.Called(hex) + + var r0 models.BlockHeader + if rf, ok := ret.Get(0).(func(string) models.BlockHeader); ok { + r0 = rf(hex) + } else { + r0 = ret.Get(0).(models.BlockHeader) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(hex) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetChainID provides a mock function with given fields: +func (_m *TxManager) GetChainID() (*big.Int, error) { + ret := _m.Called() + + var r0 *big.Int + if rf, ok := ret.Get(0).(func() *big.Int); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetEthBalance provides a mock function with given fields: address +func (_m *TxManager) GetEthBalance(address common.Address) (*assets.Eth, error) { + ret := _m.Called(address) + + var r0 *assets.Eth + if rf, ok := ret.Get(0).(func(common.Address) *assets.Eth); ok { + r0 = rf(address) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*assets.Eth) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Address) error); ok { + r1 = rf(address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLINKBalance provides a mock function with given fields: address +func (_m *TxManager) GetLINKBalance(address common.Address) (*assets.Link, error) { + ret := _m.Called(address) + + var r0 *assets.Link + if rf, ok := ret.Get(0).(func(common.Address) *assets.Link); ok { + r0 = rf(address) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*assets.Link) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Address) error); ok { + r1 = rf(address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLogs provides a mock function with given fields: q +func (_m *TxManager) GetLogs(q ethereum.FilterQuery) ([]models.Log, error) { + ret := _m.Called(q) + + var r0 []models.Log + if rf, ok := ret.Get(0).(func(ethereum.FilterQuery) []models.Log); ok { + r0 = rf(q) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Log) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(ethereum.FilterQuery) error); ok { + r1 = rf(q) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTxReceipt provides a mock function with given fields: _a0 +func (_m *TxManager) GetTxReceipt(_a0 common.Hash) (*models.TxReceipt, error) { + ret := _m.Called(_a0) + + var r0 *models.TxReceipt + if rf, ok := ret.Get(0).(func(common.Hash) *models.TxReceipt); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.TxReceipt) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(common.Hash) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NextActiveAccount provides a mock function with given fields: +func (_m *TxManager) NextActiveAccount() *store.ManagedAccount { + ret := _m.Called() + + var r0 *store.ManagedAccount + if rf, ok := ret.Get(0).(func() *store.ManagedAccount); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*store.ManagedAccount) + } + } + + return r0 +} + +// OnNewHead provides a mock function with given fields: _a0 +func (_m *TxManager) OnNewHead(_a0 *models.Head) { + _m.Called(_a0) +} + +// Register provides a mock function with given fields: _a0 +func (_m *TxManager) Register(_a0 []accounts.Account) { + _m.Called(_a0) +} + +// SubscribeToLogs provides a mock function with given fields: channel, q +func (_m *TxManager) SubscribeToLogs(channel chan<- models.Log, q ethereum.FilterQuery) (models.EthSubscription, error) { + ret := _m.Called(channel, q) + + var r0 models.EthSubscription + if rf, ok := ret.Get(0).(func(chan<- models.Log, ethereum.FilterQuery) models.EthSubscription); ok { + r0 = rf(channel, q) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.EthSubscription) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(chan<- models.Log, ethereum.FilterQuery) error); ok { + r1 = rf(channel, q) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubscribeToNewHeads provides a mock function with given fields: channel +func (_m *TxManager) SubscribeToNewHeads(channel chan<- models.BlockHeader) (models.EthSubscription, error) { + ret := _m.Called(channel) + + var r0 models.EthSubscription + if rf, ok := ret.Get(0).(func(chan<- models.BlockHeader) models.EthSubscription); ok { + r0 = rf(channel) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.EthSubscription) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(chan<- models.BlockHeader) error); ok { + r1 = rf(channel) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// WithdrawLINK provides a mock function with given fields: wr +func (_m *TxManager) WithdrawLINK(wr models.WithdrawalRequest) (common.Hash, error) { + ret := _m.Called(wr) + + var r0 common.Hash + if rf, ok := ret.Get(0).(func(models.WithdrawalRequest) common.Hash); ok { + r0 = rf(wr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(common.Hash) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(models.WithdrawalRequest) error); ok { + r1 = rf(wr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/core/internal/mocks/tx_manager_mocks.go b/core/internal/mocks/tx_manager_mocks.go deleted file mode 100644 index f8f00e07753..00000000000 --- a/core/internal/mocks/tx_manager_mocks.go +++ /dev/null @@ -1,346 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/smartcontractkit/chainlink/core/store (interfaces: TxManager) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - go_ethereum "github.com/ethereum/go-ethereum" - accounts "github.com/ethereum/go-ethereum/accounts" - common "github.com/ethereum/go-ethereum/common" - gomock "github.com/golang/mock/gomock" - store "github.com/smartcontractkit/chainlink/core/store" - assets "github.com/smartcontractkit/chainlink/core/store/assets" - models "github.com/smartcontractkit/chainlink/core/store/models" - null_v3 "gopkg.in/guregu/null.v3" - big "math/big" - reflect "reflect" -) - -// MockTxManager is a mock of TxManager interface -type MockTxManager struct { - ctrl *gomock.Controller - recorder *MockTxManagerMockRecorder -} - -// MockTxManagerMockRecorder is the mock recorder for MockTxManager -type MockTxManagerMockRecorder struct { - mock *MockTxManager -} - -// NewMockTxManager creates a new mock instance -func NewMockTxManager(ctrl *gomock.Controller) *MockTxManager { - mock := &MockTxManager{ctrl: ctrl} - mock.recorder = &MockTxManagerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockTxManager) EXPECT() *MockTxManagerMockRecorder { - return m.recorder -} - -// BumpGasUntilSafe mocks base method -func (m *MockTxManager) BumpGasUntilSafe(arg0 common.Hash) (*models.TxReceipt, store.AttemptState, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BumpGasUntilSafe", arg0) - ret0, _ := ret[0].(*models.TxReceipt) - ret1, _ := ret[1].(store.AttemptState) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// BumpGasUntilSafe indicates an expected call of BumpGasUntilSafe -func (mr *MockTxManagerMockRecorder) BumpGasUntilSafe(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BumpGasUntilSafe", reflect.TypeOf((*MockTxManager)(nil).BumpGasUntilSafe), arg0) -} - -// CheckAttempt mocks base method -func (m *MockTxManager) CheckAttempt(arg0 *models.TxAttempt, arg1 uint64) (*models.TxReceipt, store.AttemptState, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CheckAttempt", arg0, arg1) - ret0, _ := ret[0].(*models.TxReceipt) - ret1, _ := ret[1].(store.AttemptState) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// CheckAttempt indicates an expected call of CheckAttempt -func (mr *MockTxManagerMockRecorder) CheckAttempt(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckAttempt", reflect.TypeOf((*MockTxManager)(nil).CheckAttempt), arg0, arg1) -} - -// Connect mocks base method -func (m *MockTxManager) Connect(arg0 *models.Head) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Connect", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Connect indicates an expected call of Connect -func (mr *MockTxManagerMockRecorder) Connect(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockTxManager)(nil).Connect), arg0) -} - -// Connected mocks base method -func (m *MockTxManager) Connected() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Connected") - ret0, _ := ret[0].(bool) - return ret0 -} - -// Connected indicates an expected call of Connected -func (mr *MockTxManagerMockRecorder) Connected() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connected", reflect.TypeOf((*MockTxManager)(nil).Connected)) -} - -// ContractLINKBalance mocks base method -func (m *MockTxManager) ContractLINKBalance(arg0 models.WithdrawalRequest) (assets.Link, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ContractLINKBalance", arg0) - ret0, _ := ret[0].(assets.Link) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ContractLINKBalance indicates an expected call of ContractLINKBalance -func (mr *MockTxManagerMockRecorder) ContractLINKBalance(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractLINKBalance", reflect.TypeOf((*MockTxManager)(nil).ContractLINKBalance), arg0) -} - -// CreateTx mocks base method -func (m *MockTxManager) CreateTx(arg0 common.Address, arg1 []byte) (*models.Tx, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTx", arg0, arg1) - ret0, _ := ret[0].(*models.Tx) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateTx indicates an expected call of CreateTx -func (mr *MockTxManagerMockRecorder) CreateTx(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTx", reflect.TypeOf((*MockTxManager)(nil).CreateTx), arg0, arg1) -} - -// CreateTxWithEth mocks base method -func (m *MockTxManager) CreateTxWithEth(arg0, arg1 common.Address, arg2 *assets.Eth) (*models.Tx, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTxWithEth", arg0, arg1, arg2) - ret0, _ := ret[0].(*models.Tx) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateTxWithEth indicates an expected call of CreateTxWithEth -func (mr *MockTxManagerMockRecorder) CreateTxWithEth(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTxWithEth", reflect.TypeOf((*MockTxManager)(nil).CreateTxWithEth), arg0, arg1, arg2) -} - -// CreateTxWithGas mocks base method -func (m *MockTxManager) CreateTxWithGas(arg0 null_v3.String, arg1 common.Address, arg2 []byte, arg3 *big.Int, arg4 uint64) (*models.Tx, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTxWithGas", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(*models.Tx) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateTxWithGas indicates an expected call of CreateTxWithGas -func (mr *MockTxManagerMockRecorder) CreateTxWithGas(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTxWithGas", reflect.TypeOf((*MockTxManager)(nil).CreateTxWithGas), arg0, arg1, arg2, arg3, arg4) -} - -// Disconnect mocks base method -func (m *MockTxManager) Disconnect() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Disconnect") -} - -// Disconnect indicates an expected call of Disconnect -func (mr *MockTxManagerMockRecorder) Disconnect() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnect", reflect.TypeOf((*MockTxManager)(nil).Disconnect)) -} - -// GetBlockByNumber mocks base method -func (m *MockTxManager) GetBlockByNumber(arg0 string) (models.BlockHeader, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlockByNumber", arg0) - ret0, _ := ret[0].(models.BlockHeader) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlockByNumber indicates an expected call of GetBlockByNumber -func (mr *MockTxManagerMockRecorder) GetBlockByNumber(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByNumber", reflect.TypeOf((*MockTxManager)(nil).GetBlockByNumber), arg0) -} - -// GetChainID mocks base method -func (m *MockTxManager) GetChainID() (*big.Int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChainID") - ret0, _ := ret[0].(*big.Int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChainID indicates an expected call of GetChainID -func (mr *MockTxManagerMockRecorder) GetChainID() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChainID", reflect.TypeOf((*MockTxManager)(nil).GetChainID)) -} - -// GetEthBalance mocks base method -func (m *MockTxManager) GetEthBalance(arg0 common.Address) (*assets.Eth, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEthBalance", arg0) - ret0, _ := ret[0].(*assets.Eth) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetEthBalance indicates an expected call of GetEthBalance -func (mr *MockTxManagerMockRecorder) GetEthBalance(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEthBalance", reflect.TypeOf((*MockTxManager)(nil).GetEthBalance), arg0) -} - -// GetLINKBalance mocks base method -func (m *MockTxManager) GetLINKBalance(arg0 common.Address) (*assets.Link, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLINKBalance", arg0) - ret0, _ := ret[0].(*assets.Link) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetLINKBalance indicates an expected call of GetLINKBalance -func (mr *MockTxManagerMockRecorder) GetLINKBalance(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLINKBalance", reflect.TypeOf((*MockTxManager)(nil).GetLINKBalance), arg0) -} - -// GetLogs mocks base method -func (m *MockTxManager) GetLogs(arg0 go_ethereum.FilterQuery) ([]models.Log, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLogs", arg0) - ret0, _ := ret[0].([]models.Log) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetLogs indicates an expected call of GetLogs -func (mr *MockTxManagerMockRecorder) GetLogs(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockTxManager)(nil).GetLogs), arg0) -} - -// GetTxReceipt mocks base method -func (m *MockTxManager) GetTxReceipt(arg0 common.Hash) (*models.TxReceipt, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTxReceipt", arg0) - ret0, _ := ret[0].(*models.TxReceipt) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTxReceipt indicates an expected call of GetTxReceipt -func (mr *MockTxManagerMockRecorder) GetTxReceipt(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTxReceipt", reflect.TypeOf((*MockTxManager)(nil).GetTxReceipt), arg0) -} - -// NextActiveAccount mocks base method -func (m *MockTxManager) NextActiveAccount() *store.ManagedAccount { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NextActiveAccount") - ret0, _ := ret[0].(*store.ManagedAccount) - return ret0 -} - -// NextActiveAccount indicates an expected call of NextActiveAccount -func (mr *MockTxManagerMockRecorder) NextActiveAccount() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextActiveAccount", reflect.TypeOf((*MockTxManager)(nil).NextActiveAccount)) -} - -// OnNewHead mocks base method -func (m *MockTxManager) OnNewHead(arg0 *models.Head) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "OnNewHead", arg0) -} - -// OnNewHead indicates an expected call of OnNewHead -func (mr *MockTxManagerMockRecorder) OnNewHead(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnNewHead", reflect.TypeOf((*MockTxManager)(nil).OnNewHead), arg0) -} - -// Register mocks base method -func (m *MockTxManager) Register(arg0 []accounts.Account) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Register", arg0) -} - -// Register indicates an expected call of Register -func (mr *MockTxManagerMockRecorder) Register(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockTxManager)(nil).Register), arg0) -} - -// SubscribeToLogs mocks base method -func (m *MockTxManager) SubscribeToLogs(arg0 chan<- models.Log, arg1 go_ethereum.FilterQuery) (models.EthSubscription, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubscribeToLogs", arg0, arg1) - ret0, _ := ret[0].(models.EthSubscription) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SubscribeToLogs indicates an expected call of SubscribeToLogs -func (mr *MockTxManagerMockRecorder) SubscribeToLogs(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeToLogs", reflect.TypeOf((*MockTxManager)(nil).SubscribeToLogs), arg0, arg1) -} - -// SubscribeToNewHeads mocks base method -func (m *MockTxManager) SubscribeToNewHeads(arg0 chan<- models.BlockHeader) (models.EthSubscription, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubscribeToNewHeads", arg0) - ret0, _ := ret[0].(models.EthSubscription) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SubscribeToNewHeads indicates an expected call of SubscribeToNewHeads -func (mr *MockTxManagerMockRecorder) SubscribeToNewHeads(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeToNewHeads", reflect.TypeOf((*MockTxManager)(nil).SubscribeToNewHeads), arg0) -} - -// WithdrawLINK mocks base method -func (m *MockTxManager) WithdrawLINK(arg0 models.WithdrawalRequest) (common.Hash, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "WithdrawLINK", arg0) - ret0, _ := ret[0].(common.Hash) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// WithdrawLINK indicates an expected call of WithdrawLINK -func (mr *MockTxManagerMockRecorder) WithdrawLINK(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithdrawLINK", reflect.TypeOf((*MockTxManager)(nil).WithdrawLINK), arg0) -} diff --git a/core/logger/prettyconsole.go b/core/logger/prettyconsole.go index ed58f237a75..d6413f4ffd6 100644 --- a/core/logger/prettyconsole.go +++ b/core/logger/prettyconsole.go @@ -7,8 +7,9 @@ import ( "strings" "time" + "chainlink/core/utils" + "github.com/fatih/color" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/tidwall/gjson" "go.uber.org/zap" ) diff --git a/core/main.go b/core/main.go index 730287c7d7a..dfcfa756470 100644 --- a/core/main.go +++ b/core/main.go @@ -4,9 +4,9 @@ import ( "os" "time" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store/orm" + "chainlink/core/cmd" + "chainlink/core/logger" + "chainlink/core/store/orm" ) func init() { diff --git a/core/main_test.go b/core/main_test.go index 270433983b7..d84f1d4afe4 100644 --- a/core/main_test.go +++ b/core/main_test.go @@ -6,8 +6,8 @@ import ( "io/ioutil" "testing" - "github.com/smartcontractkit/chainlink/core/cmd" - "github.com/smartcontractkit/chainlink/core/internal/cltest" + "chainlink/core/cmd" + "chainlink/core/internal/cltest" ) func ExampleRun() { diff --git a/core/services/application.go b/core/services/application.go index 40174d6887d..f58a61cfe90 100644 --- a/core/services/application.go +++ b/core/services/application.go @@ -1,21 +1,23 @@ package services import ( - "errors" "os" "os/signal" "sync" "syscall" + "chainlink/core/logger" + "chainlink/core/store" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/gobuffalo/packr" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" "go.uber.org/multierr" ) +//go:generate mockery -name Application -output ../internal/mocks/ -case=underscore + // Application implements the common functions used in the core node. type Application interface { Start() error @@ -26,15 +28,17 @@ type Application interface { ArchiveJob(*models.ID) error AddServiceAgreement(*models.ServiceAgreement) error NewBox() packr.Box + RunManager } // ChainlinkApplication contains fields for the JobSubscriber, Scheduler, // and Store. The JobSubscriber and Scheduler are also available // in the services package, but the Store has its own package. type ChainlinkApplication struct { - Exiter func(int) - HeadTracker *HeadTracker - JobRunner JobRunner + Exiter func(int) + HeadTracker *HeadTracker + RunManager + RunQueue RunQueue JobSubscriber JobSubscriber Scheduler *Scheduler Store *store.Store @@ -51,16 +55,21 @@ func NewApplication(config *orm.Config, onConnectCallbacks ...func(Application)) store := store.NewStore(config) config.SetRuntimeStore(store.ORM) - jobSubscriber := NewJobSubscriber(store) - pendingConnectionResumer := newPendingConnectionResumer(store) + runExecutor := NewRunExecutor(store) + runQueue := NewRunQueue(runExecutor) + runManager := NewRunManager(runQueue, config, store.ORM, store.TxManager, store.Clock) + jobSubscriber := NewJobSubscriber(store, runManager) + + pendingConnectionResumer := newPendingConnectionResumer(runManager) app := &ChainlinkApplication{ - JobSubscriber: jobSubscriber, - JobRunner: NewJobRunner(store), - Scheduler: NewScheduler(store), - Store: store, - SessionReaper: NewStoreReaper(store), - Exiter: os.Exit, + JobSubscriber: jobSubscriber, + RunManager: runManager, + RunQueue: runQueue, + Scheduler: NewScheduler(store, runManager), + Store: store, + SessionReaper: NewStoreReaper(store), + Exiter: os.Exit, pendingConnectionResumer: pendingConnectionResumer, } @@ -96,15 +105,15 @@ func (app *ChainlinkApplication) Start() error { return multierr.Combine( app.Store.Start(), + app.RunQueue.Start(), + app.RunManager.ResumeAllInProgress(), - // Deliberately started immediately after Store, to start the RunChannel consumer - app.JobRunner.Start(), - app.JobRunner.resumeRunsSinceLastShutdown(), // Started before any other service writes RunStatus to db. - - // HeadTracker deliberately started after JobRunner#resumeRunsSinceLastShutdown - // since it Connects JobSubscriber which leads to writes of JobRuns RunStatus to the db. + // HeadTracker deliberately started after + // RunQueue#resumeRunsSinceLastShutdown since it Connects JobSubscriber + // which leads to writes of JobRuns RunStatus to the db. // https://www.pivotaltracker.com/story/show/162230780 app.HeadTracker.Start(), + app.Scheduler.Start(), app.SessionReaper.Start(), ) @@ -120,7 +129,7 @@ func (app *ChainlinkApplication) Stop() error { app.Scheduler.Stop() merr = multierr.Append(merr, app.HeadTracker.Stop()) - app.JobRunner.Stop() + app.RunQueue.Stop() merr = multierr.Append(merr, app.SessionReaper.Stop()) merr = multierr.Append(merr, app.Store.Close()) }) @@ -175,28 +184,15 @@ func (app *ChainlinkApplication) NewBox() packr.Box { } type pendingConnectionResumer struct { - store *store.Store - resumer func(*models.JobRun, *store.Store) error + runManager RunManager } -func newPendingConnectionResumer(store *store.Store) *pendingConnectionResumer { - return &pendingConnectionResumer{store: store, resumer: ResumeConnectingTask} +func newPendingConnectionResumer(runManager RunManager) *pendingConnectionResumer { + return &pendingConnectionResumer{runManager: runManager} } func (p *pendingConnectionResumer) Connect(head *models.Head) error { - var merr error - err := p.store.UnscopedJobRunsWithStatus(func(run *models.JobRun) { - err := p.resumer(run, p.store.Unscoped()) - if err != nil { - merr = multierr.Append(merr, err) - } - }, models.RunStatusPendingConnection) - - if err != nil { - return multierr.Append(errors.New("error resuming pending connections"), err) - } - - return merr + return p.runManager.ResumeAllConnecting() } func (p *pendingConnectionResumer) Disconnect() {} diff --git a/core/services/application_test.go b/core/services/application_test.go index 5bd7d15a09e..210ff2d04de 100644 --- a/core/services/application_test.go +++ b/core/services/application_test.go @@ -6,15 +6,13 @@ import ( "syscall" "testing" - "github.com/golang/mock/gomock" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/services/mock_services" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/tevino/abool" ) @@ -32,7 +30,7 @@ func TestChainlinkApplication_SignalShutdown(t *testing.T) { completed.Set() } - app.Start() + require.NoError(t, app.Start()) syscall.Kill(syscall.Getpid(), syscall.SIGTERM) gomega.NewGomegaWithT(t).Eventually(func() bool { @@ -43,13 +41,15 @@ func TestChainlinkApplication_SignalShutdown(t *testing.T) { func TestChainlinkApplication_AddJob(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() - ctrl := gomock.NewController(t) - defer ctrl.Finish() + require.NoError(t, app.Start()) + + jobSubscriber := new(mocks.JobSubscriber) + jobSubscriber.On("AddJob", mock.Anything, (*models.Head)(nil)).Return(nil, nil) + app.ChainlinkApplication.JobSubscriber = jobSubscriber - jobSubscriberMock := mock_services.NewMockJobSubscriber(ctrl) - app.ChainlinkApplication.JobSubscriber = jobSubscriberMock - jobSubscriberMock.EXPECT().AddJob(gomock.Any(), nil) // nil to represent "latest" block app.AddJob(cltest.NewJob()) + + jobSubscriber.AssertExpectations(t) } func TestChainlinkApplication_resumesPendingConnection_Happy(t *testing.T) { @@ -81,29 +81,3 @@ func TestChainlinkApplication_resumesPendingConnection_Archived(t *testing.T) { require.NoError(t, utils.JustError(app.MockStartAndConnect())) _ = cltest.WaitForJobRunToComplete(t, store, jr) } - -func TestPendingConnectionResumer(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - resumedRuns := []*models.ID{} - resumer := func(run *models.JobRun, store *strpkg.Store) error { - resumedRuns = append(resumedRuns, run.ID) - return nil - } - pcr := services.ExportedNewPendingConnectionResumer(store, resumer) - - j := cltest.NewJobWithWebInitiator() - require.NoError(t, store.CreateJob(&j)) - - expectedRun := cltest.CreateJobRunWithStatus(t, store, j, models.RunStatusPendingConnection) - _ = cltest.CreateJobRunWithStatus(t, store, j, models.RunStatusPendingConfirmations) - _ = cltest.CreateJobRunWithStatus(t, store, j, models.RunStatusInProgress) - _ = cltest.CreateJobRunWithStatus(t, store, j, models.RunStatusUnstarted) - _ = cltest.CreateJobRunWithStatus(t, store, j, models.RunStatusPendingBridge) - _ = cltest.CreateJobRunWithStatus(t, store, j, models.RunStatusInProgress) - _ = cltest.CreateJobRunWithStatus(t, store, j, models.RunStatusCompleted) - - assert.NoError(t, pcr.Connect(cltest.Head(1))) - assert.Equal(t, []*models.ID{expectedRun.ID}, resumedRuns) -} diff --git a/core/services/export_test.go b/core/services/export_test.go deleted file mode 100644 index 4ab06fafe41..00000000000 --- a/core/services/export_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package services - -import ( - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" -) - -func ExportedExecuteRun( - run *models.JobRun, - store *store.Store, -) error { - return executeRun(run, store) -} - -func ExportedChannelForRun(jr JobRunner, runID *models.ID) chan<- struct{} { - return jr.channelForRun(runID) -} - -func ExportedResumeRunsSinceLastShutdown(jr JobRunner) error { - return jr.resumeRunsSinceLastShutdown() -} - -func ExportedWorkerCount(jr JobRunner) int { - return jr.workerCount() -} - -func ExportedNewPendingConnectionResumer( - store *store.Store, - resumer func(*models.JobRun, *store.Store) error, -) store.HeadTrackable { - return &pendingConnectionResumer{ - store: store, - resumer: resumer, - } -} diff --git a/core/services/head_trackable_callback.go b/core/services/head_trackable_callback.go index d99b4688e8f..4b6d6b379ea 100644 --- a/core/services/head_trackable_callback.go +++ b/core/services/head_trackable_callback.go @@ -1,6 +1,6 @@ package services -import "github.com/smartcontractkit/chainlink/core/store/models" +import "chainlink/core/store/models" // headTrackableCallback is a simple wrapper around an On Connect callback type headTrackableCallback struct { diff --git a/core/services/head_tracker.go b/core/services/head_tracker.go index c22402424c6..78138309474 100644 --- a/core/services/head_tracker.go +++ b/core/services/head_tracker.go @@ -5,12 +5,13 @@ import ( "sync" "time" + "chainlink/core/logger" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" ) // HeadTracker holds and stores the latest block number experienced by this particular node @@ -90,6 +91,7 @@ func (ht *HeadTracker) Stop() error { ht.connected = false ht.disconnect() } + logger.Info(fmt.Sprintf("Head tracker disconnecting from %v", ht.store.Config.EthereumURL())) close(ht.done) close(ht.subscriptionSucceeded) ht.started = false @@ -179,7 +181,7 @@ func (ht *HeadTracker) subscribe() bool { ht.sleeper.Reset() for { ht.unsubscribeFromHead() - logger.Info("Connecting to node ", ht.store.Config.EthereumURL(), " in ", ht.sleeper.Duration()) + logger.Info("Connecting to ethereum node ", ht.store.Config.EthereumURL(), " in ", ht.sleeper.Duration()) select { case <-ht.done: return false @@ -239,7 +241,7 @@ func (ht *HeadTracker) subscribeToHead() error { } if err := verifyEthereumChainID(ht); err != nil { - return errors.Wrap(err, "verifyEthereumChainID") + return errors.Wrap(err, "verifyEthereumChainID failed") } ht.headSubscription = sub diff --git a/core/services/head_tracker_test.go b/core/services/head_tracker_test.go index f0475d09f5d..891596512cb 100644 --- a/core/services/head_tracker_test.go +++ b/core/services/head_tracker_test.go @@ -6,15 +6,16 @@ import ( "sync/atomic" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/services" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/golang/mock/gomock" "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/internal/mocks" - "github.com/smartcontractkit/chainlink/core/services" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -157,21 +158,17 @@ func TestHeadTracker_ReconnectOnError(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - txmMock := mocks.NewMockTxManager(ctrl) - store.TxManager = txmMock + txManager := new(mocks.TxManager) + subscription := cltest.EmptyMockSubscription() + txManager.On("GetChainID").Maybe().Return(store.Config.ChainID(), nil) + txManager.On("SubscribeToNewHeads", mock.Anything).Return(subscription, nil) + txManager.On("SubscribeToNewHeads", mock.Anything).Return(nil, errors.New("cannot reconnect")) + txManager.On("SubscribeToNewHeads", mock.Anything).Return(subscription, nil) + store.TxManager = txManager checker := &cltest.MockHeadTrackable{} ht := services.NewHeadTracker(store, []strpkg.HeadTrackable{checker}, cltest.NeverSleeper{}) - subscription := cltest.EmptyMockSubscription() - txmMock.EXPECT().GetChainID().Return(store.Config.ChainID(), nil).AnyTimes() - txmMock.EXPECT().SubscribeToNewHeads(gomock.Any()).Return(subscription, nil) - txmMock.EXPECT().SubscribeToNewHeads(gomock.Any()).Return(nil, errors.New("cannot reconnect")) - txmMock.EXPECT().SubscribeToNewHeads(gomock.Any()).Return(subscription, nil) - // connect assert.Nil(t, ht.Start()) g.Eventually(func() int32 { return checker.ConnectedCount() }).Should(gomega.Equal(int32(1))) diff --git a/core/services/job_runner.go b/core/services/job_runner.go deleted file mode 100644 index 503b6c7c87a..00000000000 --- a/core/services/job_runner.go +++ /dev/null @@ -1,272 +0,0 @@ -package services - -import ( - "errors" - "fmt" - "sync" - - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "go.uber.org/multierr" -) - -// JobRunner safely handles coordinating job runs. -type JobRunner interface { - Start() error - Stop() - resumeRunsSinceLastShutdown() error - channelForRun(*models.ID) chan<- struct{} - workerCount() int -} - -type jobRunner struct { - started bool - done chan struct{} - bootMutex sync.Mutex - store *store.Store - workerMutex sync.RWMutex - workers map[string]chan struct{} - workersWg sync.WaitGroup - demultiplexStopperWg sync.WaitGroup -} - -// NewJobRunner initializes a JobRunner. -func NewJobRunner(str *store.Store) JobRunner { - return &jobRunner{ - // Unscoped allows the processing of runs that are soft deleted asynchronously - store: str.Unscoped(), - workers: make(map[string]chan struct{}), - } -} - -// Start reinitializes runs and starts the execution of the store's runs. -func (rm *jobRunner) Start() error { - rm.bootMutex.Lock() - defer rm.bootMutex.Unlock() - - if rm.started { - return errors.New("JobRunner already started") - } - rm.done = make(chan struct{}) - rm.started = true - - var starterWg sync.WaitGroup - starterWg.Add(1) - go rm.demultiplexRuns(&starterWg) - starterWg.Wait() - - rm.demultiplexStopperWg.Add(1) - return nil -} - -// Stop closes all open worker channels. -func (rm *jobRunner) Stop() { - rm.bootMutex.Lock() - defer rm.bootMutex.Unlock() - - if !rm.started { - return - } - close(rm.done) - rm.started = false - rm.demultiplexStopperWg.Wait() -} - -// resumeRunsSinceLastShutdown queries the db for job runs that should be resumed -// since a previous node shutdown. -// -// As a result of its reliance on the database, it must run before anything -// persists a job RunStatus to the db to ensure that it only captures pending and in progress -// jobs as a result of the last shutdown, and not as a result of what's happening now. -// -// To recap: This must run before anything else writes job run status to the db, -// ie. tries to run a job. -// https://github.com/smartcontractkit/chainlink/pull/807 -func (rm *jobRunner) resumeRunsSinceLastShutdown() error { - // Do all querying of run statuses since last shutdown before enqueuing - // runs in progress and asleep, to prevent the following race condition: - // 1. resume sleep, 2. awake from sleep, 3. in progress, 4. resume in progress (double enqueued). - var merr error - err := rm.store.UnscopedJobRunsWithStatus(func(run *models.JobRun) { - - if run.Result.Status == models.RunStatusPendingSleep { - if err := QueueSleepingTask(run, rm.store.Unscoped()); err != nil { - logger.Errorw("Error resuming sleeping job", "error", err) - } - } else { - merr = multierr.Append(merr, rm.store.RunChannel.Send(run.ID)) - } - - }, models.RunStatusInProgress, models.RunStatusPendingSleep) - - if err != nil { - return err - } - - return merr -} - -func (rm *jobRunner) demultiplexRuns(starterWg *sync.WaitGroup) { - starterWg.Done() - defer rm.demultiplexStopperWg.Done() - for { - select { - case <-rm.done: - logger.Debug("JobRunner demultiplexing of job runs finished") - rm.workersWg.Wait() - return - case rr, ok := <-rm.store.RunChannel.Receive(): - if !ok { - logger.Panic("RunChannel closed before JobRunner, can no longer demultiplexing job runs") - return - } - rm.channelForRun(rr.ID) <- struct{}{} - } - } -} - -func (rm *jobRunner) channelForRun(runID *models.ID) chan<- struct{} { - rm.workerMutex.Lock() - defer rm.workerMutex.Unlock() - - workerChannel, present := rm.workers[runID.String()] - if !present { - workerChannel = make(chan struct{}, 1) - rm.workers[runID.String()] = workerChannel - rm.workersWg.Add(1) - - go func() { - rm.workerLoop(runID, workerChannel) - - rm.workerMutex.Lock() - delete(rm.workers, runID.String()) - rm.workersWg.Done() - rm.workerMutex.Unlock() - - logger.Debug("Worker finished for ", runID) - }() - } - return workerChannel -} - -func (rm *jobRunner) workerLoop(runID *models.ID, workerChannel chan struct{}) { - for { - select { - case <-workerChannel: - run, err := rm.store.FindJobRun(runID) - if err != nil { - logger.Errorw(fmt.Sprint("Error finding run ", runID), run.ForLogger("error", err)...) - } - - if err := executeRun(&run, rm.store); err != nil { - logger.Errorw(fmt.Sprint("Error executing run ", runID), run.ForLogger("error", err)...) - return - } - - if run.Status.Finished() { - logger.Debugw("All tasks complete for run", "run", run.ID.String()) - return - } - - case <-rm.done: - logger.Debug("JobRunner worker loop for ", runID.String(), " finished") - return - } - } -} - -func (rm *jobRunner) workerCount() int { - rm.workerMutex.RLock() - defer rm.workerMutex.RUnlock() - - return len(rm.workers) -} - -func prepareTaskInput(run *models.JobRun, input models.JSON) (models.JSON, error) { - var err error - if input, err = run.Result.Data.Merge(input); err != nil { - return models.JSON{}, err - } - - if input, err = run.Overrides.Merge(input); err != nil { - return models.JSON{}, err - } - return input, nil -} - -func executeTask(run *models.JobRun, currentTaskRun *models.TaskRun, store *store.Store) models.RunResult { - taskCopy := currentTaskRun.TaskSpec // deliberately copied to keep mutations local - - var err error - if taskCopy.Params, err = taskCopy.Params.Merge(run.Overrides); err != nil { - currentTaskRun.Result.SetError(err) - return currentTaskRun.Result - } - - adapter, err := adapters.For(taskCopy, store) - if err != nil { - currentTaskRun.Result.SetError(err) - return currentTaskRun.Result - } - - logger.Infow(fmt.Sprintf("Processing task %s", taskCopy.Type), []interface{}{"task", currentTaskRun.ID.String()}...) - - data, err := prepareTaskInput(run, currentTaskRun.Result.Data) - if err != nil { - currentTaskRun.Result.SetError(err) - return currentTaskRun.Result - } - - currentTaskRun.Result.CachedJobRunID = run.ID - currentTaskRun.Result.Data = data - result := adapter.Perform(currentTaskRun.Result, store) - result.ID = currentTaskRun.Result.ID - - logger.Infow(fmt.Sprintf("Finished processing task %s", taskCopy.Type), []interface{}{ - "task", currentTaskRun.ID, - "result", result.Status, - "result_data", result.Data, - }...) - - return result -} - -func executeRun(run *models.JobRun, store *store.Store) error { - logger.Infow("Processing run", run.ForLogger()...) - - if !run.Status.Runnable() { - return fmt.Errorf("Run triggered in non runnable state %s", run.Status) - } - - currentTaskRun := run.NextTaskRun() - if currentTaskRun == nil { - return errors.New("Run triggered with no remaining tasks") - } - - result := executeTask(run, currentTaskRun, store) - - currentTaskRun.ApplyResult(result) - run.ApplyResult(result) - - if currentTaskRun.Status.PendingSleep() { - logger.Debugw("Task is sleeping", []interface{}{"run", run.ID.String()}...) - if err := QueueSleepingTask(run, store); err != nil { - return err - } - } else if !currentTaskRun.Status.Runnable() { - logger.Debugw("Task execution blocked", []interface{}{"run", run.ID.String(), "task", currentTaskRun.ID.String(), "state", currentTaskRun.Result.Status}...) - } else if currentTaskRun.Status.Unstarted() { - return fmt.Errorf("run %s task %s cannot return a status of empty string or Unstarted", run.ID.String(), currentTaskRun.TaskSpec.Type) - } else if futureTaskRun := run.NextTaskRun(); futureTaskRun != nil { - validateMinimumConfirmations(run, futureTaskRun, run.ObservedHeight, store) - } - - if err := updateAndTrigger(run, store); err != nil { - return err - } - logger.Infow("Run finished processing", run.ForLogger()...) - - return nil -} diff --git a/core/services/job_runner_test.go b/core/services/job_runner_test.go deleted file mode 100644 index 4fd5ff39f04..00000000000 --- a/core/services/job_runner_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package services_test - -import ( - "fmt" - "testing" - "time" - - "github.com/smartcontractkit/chainlink/core/store/assets" - - "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestJobRunner_resumeRunsSinceLastShutdown(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - rm, cleanup := cltest.NewJobRunner(store) - defer cleanup() - - j := models.NewJob() - i := models.Initiator{Type: models.InitiatorWeb} - j.Initiators = []models.Initiator{i} - json := fmt.Sprintf(`{"until":"%v"}`, utils.ISO8601UTC(time.Now().Add(time.Second))) - j.Tasks = []models.TaskSpec{cltest.NewTask(t, "sleep", json)} - assert.NoError(t, store.CreateJob(&j)) - - sleepingRun := j.NewRun(i) - sleepingRun.Status = models.RunStatusPendingSleep - sleepingRun.TaskRuns[0].Status = models.RunStatusPendingSleep - assert.NoError(t, store.CreateJobRun(&sleepingRun)) - - inProgressRun := j.NewRun(i) - inProgressRun.Status = models.RunStatusInProgress - assert.NoError(t, store.CreateJobRun(&inProgressRun)) - - assert.NoError(t, services.ExportedResumeRunsSinceLastShutdown(rm)) - messages := []*models.ID{} - - rr, open := <-store.RunChannel.Receive() - assert.True(t, open) - messages = append(messages, rr.ID) - - rr, open = <-store.RunChannel.Receive() - assert.True(t, open) - messages = append(messages, rr.ID) - - expectedMessages := []*models.ID{sleepingRun.ID, inProgressRun.ID} - assert.ElementsMatch(t, expectedMessages, messages) -} - -func TestJobRunner_executeRun_correctlyPopulatesFinishedAt(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - j := models.NewJob() - i := models.Initiator{Type: models.InitiatorWeb} - j.Initiators = []models.Initiator{i} - j.Tasks = []models.TaskSpec{ - cltest.NewTask(t, "noop"), - cltest.NewTask(t, "nooppend"), - } - assert.NoError(t, store.CreateJob(&j)) - - run := j.NewRun(i) - require.NoError(t, store.CreateJobRun(&run)) - - require.NoError(t, services.ExportedExecuteRun(&run, store)) - assert.False(t, run.Result.ErrorMessage.Valid) - assert.False(t, run.FinishedAt.Valid) - assert.Equal(t, models.RunStatusInProgress, run.Status) - - require.NoError(t, services.ExportedExecuteRun(&run, store)) - assert.False(t, run.Result.ErrorMessage.Valid) - assert.False(t, run.FinishedAt.Valid) - assert.Equal(t, models.RunStatusPendingConfirmations, run.Status) -} - -func TestJobRunner_executeRun_correctlyAddsLinkEarnings(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - j := models.NewJob() - i := models.Initiator{Type: models.InitiatorWeb} - j.Initiators = []models.Initiator{i} - j.Tasks = []models.TaskSpec{ - cltest.NewTask(t, "noop"), - } - assert.NoError(t, store.CreateJob(&j)) - run := j.NewRun(i) - run.Payment = assets.NewLink(1) - require.NoError(t, store.CreateJobRun(&run)) - require.NoError(t, services.ExportedExecuteRun(&run, store)) - - actual, err := store.LinkEarnedFor(&j) - require.NoError(t, err) - assert.Equal(t, assets.NewLink(1), actual) -} - -func TestJobRunner_ChannelForRun_equalityBetweenRuns(t *testing.T) { - t.Parallel() - - store, cleanup := cltest.NewStore(t) - defer cleanup() - rm, cleanup := cltest.NewJobRunner(store) - defer cleanup() - - job := cltest.NewJobWithWebInitiator() - initr := job.Initiators[0] - run1 := job.NewRun(initr) - run2 := job.NewRun(initr) - - chan1a := services.ExportedChannelForRun(rm, run1.ID) - chan2 := services.ExportedChannelForRun(rm, run2.ID) - chan1b := services.ExportedChannelForRun(rm, run1.ID) - - assert.NotEqual(t, chan1a, chan2) - assert.Equal(t, chan1a, chan1b) - assert.NotEqual(t, chan2, chan1b) -} - -func TestJobRunner_ChannelForRun_sendAfterClosing(t *testing.T) { - t.Parallel() - - s, cleanup := cltest.NewStore(t) - defer cleanup() - rm, cleanup := cltest.NewJobRunner(s) - defer cleanup() - assert.NoError(t, rm.Start()) - - j := cltest.NewJobWithWebInitiator() - assert.NoError(t, s.CreateJob(&j)) - initr := j.Initiators[0] - jr := j.NewRun(initr) - assert.NoError(t, s.CreateJobRun(&jr)) - - chan1 := services.ExportedChannelForRun(rm, jr.ID) - chan1 <- struct{}{} - cltest.WaitForJobRunToComplete(t, s, jr) - - gomega.NewGomegaWithT(t).Eventually(func() chan<- struct{} { - return services.ExportedChannelForRun(rm, jr.ID) - }).Should(gomega.Not(gomega.Equal(chan1))) // eventually deletes the channel - - chan2 := services.ExportedChannelForRun(rm, jr.ID) - chan2 <- struct{}{} // does not panic -} - -func TestJobRunner_ChannelForRun_equalityWithoutClosing(t *testing.T) { - t.Parallel() - - s, cleanup := cltest.NewStore(t) - defer cleanup() - rm, cleanup := cltest.NewJobRunner(s) - defer cleanup() - assert.NoError(t, rm.Start()) - - j := cltest.NewJobWithWebInitiator() - j.Tasks = []models.TaskSpec{cltest.NewTask(t, "nooppend")} - assert.NoError(t, s.CreateJob(&j)) - initr := j.Initiators[0] - jr := j.NewRun(initr) - assert.NoError(t, s.CreateJobRun(&jr)) - - chan1 := services.ExportedChannelForRun(rm, jr.ID) - - chan1 <- struct{}{} - cltest.WaitForJobRunToPendConfirmations(t, s, jr) - - chan2 := services.ExportedChannelForRun(rm, jr.ID) - assert.Equal(t, chan1, chan2) -} - -func TestJobRunner_Stop(t *testing.T) { - t.Parallel() - - s, cleanup := cltest.NewStore(t) - defer cleanup() - rm, cleanup := cltest.NewJobRunner(s) - defer cleanup() - j := cltest.NewJobWithWebInitiator() - initr := j.Initiators[0] - jr := j.NewRun(initr) - - require.NoError(t, rm.Start()) - - services.ExportedChannelForRun(rm, jr.ID) - assert.Equal(t, 1, services.ExportedWorkerCount(rm)) - - rm.Stop() - - gomega.NewGomegaWithT(t).Eventually(func() int { - return services.ExportedWorkerCount(rm) - }).Should(gomega.Equal(0)) -} diff --git a/core/services/job_subscriber.go b/core/services/job_subscriber.go index 6a9361de32d..cb083ed5488 100644 --- a/core/services/job_subscriber.go +++ b/core/services/job_subscriber.go @@ -4,12 +4,15 @@ import ( "fmt" "sync" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/logger" + "chainlink/core/store" + "chainlink/core/store/models" + "go.uber.org/multierr" ) +//go:generate mockery -name JobSubscriber -output ../internal/mocks/ -case=underscore + // JobSubscriber listens for push notifications of event logs from the ethereum // node's websocket for specific jobs by subscribing to ethLogs. type JobSubscriber interface { @@ -24,12 +27,14 @@ type jobSubscriber struct { store *store.Store jobSubscriptions map[string]JobSubscription jobsMutex *sync.RWMutex + runManager RunManager } // NewJobSubscriber returns a new job subscriber. -func NewJobSubscriber(store *store.Store) JobSubscriber { +func NewJobSubscriber(store *store.Store, runManager RunManager) JobSubscriber { return &jobSubscriber{ store: store, + runManager: runManager, jobSubscriptions: map[string]JobSubscription{}, jobsMutex: &sync.RWMutex{}, } @@ -42,7 +47,7 @@ func (js *jobSubscriber) AddJob(job models.JobSpec, bn *models.Head) error { return nil } - sub, err := StartJobSubscription(job, bn, js.store) + sub, err := StartJobSubscription(job, bn, js.store, js.runManager) if err != nil { return err } @@ -56,6 +61,7 @@ func (js *jobSubscriber) RemoveJob(ID *models.ID) error { sub, ok := js.jobSubscriptions[ID.String()] delete(js.jobSubscriptions, ID.String()) js.jobsMutex.Unlock() + if !ok { return fmt.Errorf("JobSubscriber#RemoveJob: job %s not found", ID) } @@ -67,6 +73,7 @@ func (js *jobSubscriber) RemoveJob(ID *models.ID) error { func (js *jobSubscriber) Jobs() []models.JobSpec { js.jobsMutex.RLock() defer js.jobsMutex.RUnlock() + var jobs []models.JobSpec for _, sub := range js.jobSubscriptions { jobs = append(jobs, sub.Job) @@ -77,14 +84,15 @@ func (js *jobSubscriber) Jobs() []models.JobSpec { func (js *jobSubscriber) addSubscription(sub JobSubscription) { js.jobsMutex.Lock() defer js.jobsMutex.Unlock() + js.jobSubscriptions[sub.Job.ID.String()] = sub } // Connect connects the jobs to the ethereum node by creating corresponding subscriptions. func (js *jobSubscriber) Connect(bn *models.Head) error { var merr error - err := js.store.Jobs(func(j models.JobSpec) bool { - merr = multierr.Append(merr, js.AddJob(j, bn)) + err := js.store.Jobs(func(j *models.JobSpec) bool { + merr = multierr.Append(merr, js.AddJob(*j, bn)) return true }) return multierr.Append(merr, err) @@ -95,6 +103,7 @@ func (js *jobSubscriber) Connect(bn *models.Head) error { func (js *jobSubscriber) Disconnect() { js.jobsMutex.Lock() defer js.jobsMutex.Unlock() + for _, sub := range js.jobSubscriptions { sub.Unsubscribe() } @@ -103,17 +112,8 @@ func (js *jobSubscriber) Disconnect() { // OnNewHead resumes all pending job runs based on the new head activity. func (js *jobSubscriber) OnNewHead(head *models.Head) { - height := head.ToInt() - - err := js.store.UnscopedJobRunsWithStatus(func(run *models.JobRun) { - err := ResumeConfirmingTask(run, js.store.Unscoped(), height) - if err != nil { - logger.Errorf("JobSubscriber.OnNewHead: %v", err) - } - - }, models.RunStatusPendingConnection, models.RunStatusPendingConfirmations) - + err := js.runManager.ResumeAllConfirming(head.ToInt()) if err != nil { - logger.Errorf("error fetching pending job runs: %v", err) + logger.Errorw("Failed to resume confirming tasks on new head", "error", err) } } diff --git a/core/services/job_subscriber_test.go b/core/services/job_subscriber_test.go index 2a7ae2e2355..02f074c694a 100644 --- a/core/services/job_subscriber_test.go +++ b/core/services/job_subscriber_test.go @@ -1,305 +1,112 @@ package services_test import ( - "fmt" "math/big" "testing" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/services" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestJobSubscriber_Connect_WithJobs(t *testing.T) { +func TestJobSubscriber_OnNewHead(t *testing.T) { t.Parallel() - store, el, cleanup := cltest.NewJobSubscriber(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - eth := cltest.MockEthOnStore(t, store) - eth.Register("eth_getLogs", []models.Log{}) - eth.Register("eth_getLogs", []models.Log{}) - j1 := cltest.NewJobWithLogInitiator() - j2 := cltest.NewJobWithLogInitiator() - assert.Nil(t, store.CreateJob(&j1)) - assert.Nil(t, store.CreateJob(&j2)) - eth.RegisterSubscription("logs") - eth.RegisterSubscription("logs") + runManager := new(mocks.RunManager) + jobSubscriber := services.NewJobSubscriber(store, runManager) - assert.Nil(t, el.Connect(cltest.Head(1))) - eth.EventuallyAllCalled(t) -} + runManager.On("ResumeAllConfirming", big.NewInt(1337)).Return(nil) -func newAddr() common.Address { - return cltest.NewAddress() + jobSubscriber.OnNewHead(cltest.Head(1337)) + + runManager.AssertExpectations(t) } -func TestJobSubscriber_reconnectLoop_Resubscribing(t *testing.T) { +func TestJobSubscriber_AddJob_RemoveJob(t *testing.T) { t.Parallel() store, cleanup := cltest.NewStore(t) defer cleanup() + cltest.MockEthOnStore(t, store) - eth := cltest.MockEthOnStore(t, store) - eth.Register("eth_getLogs", []models.Log{}) - eth.Register("eth_getLogs", []models.Log{}) - eth.Register("eth_getLogs", []models.Log{}) - eth.Register("eth_getLogs", []models.Log{}) + runManager := new(mocks.RunManager) + jobSubscriber := services.NewJobSubscriber(store, runManager) - j1 := cltest.NewJobWithLogInitiator() - j2 := cltest.NewJobWithLogInitiator() - assert.Nil(t, store.CreateJob(&j1)) - assert.Nil(t, store.CreateJob(&j2)) + jobSpec := cltest.NewJobWithLogInitiator() + err := jobSubscriber.AddJob(jobSpec, cltest.Head(321)) + require.NoError(t, err) - eth.RegisterSubscription("logs") - eth.RegisterSubscription("logs") + assert.Len(t, jobSubscriber.Jobs(), 1) - el := services.NewJobSubscriber(store) - assert.Nil(t, el.Connect(cltest.Head(1))) - assert.Equal(t, 2, len(el.Jobs())) - el.Disconnect() - assert.Equal(t, 0, len(el.Jobs())) + err = jobSubscriber.RemoveJob(jobSpec.ID) + require.NoError(t, err) + + assert.Len(t, jobSubscriber.Jobs(), 0) + + runManager.AssertExpectations(t) - eth.RegisterSubscription("logs") - eth.RegisterSubscription("logs") - assert.Nil(t, el.Connect(cltest.Head(2))) - assert.Equal(t, 2, len(el.Jobs())) - el.Disconnect() - assert.Equal(t, 0, len(el.Jobs())) - eth.EventuallyAllCalled(t) } -func TestJobSubscriber_AttachedToHeadTracker(t *testing.T) { +func TestJobSubscriber_AddJob_NotLogInitiatedError(t *testing.T) { t.Parallel() - g := gomega.NewGomegaWithT(t) - store, el, cleanup := cltest.NewJobSubscriber(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - eth := cltest.MockEthOnStore(t, store) - j1 := cltest.NewJobWithLogInitiator() - j2 := cltest.NewJobWithLogInitiator() - assert.Nil(t, store.CreateJob(&j1)) - assert.Nil(t, store.CreateJob(&j2)) - - eth.RegisterSubscription("logs") - eth.RegisterSubscription("logs") - eth.Register("eth_chainId", store.Config.ChainID()) - - ht := services.NewHeadTracker(store, []strpkg.HeadTrackable{el}) - assert.Nil(t, ht.Start()) - g.Eventually(func() int { return len(el.Jobs()) }).Should(gomega.Equal(2)) - eth.EventuallyAllCalled(t) -} + runManager := new(mocks.RunManager) + jobSubscriber := services.NewJobSubscriber(store, runManager) -func TestJobSubscriber_AddJob_Listening(t *testing.T) { - t.Parallel() - sharedAddr := newAddr() - noAddr := common.Address{} - - tests := []struct { - name string - initType string - initrAddr common.Address - logAddr common.Address - wantCount int - topic0 common.Hash - data hexutil.Bytes - }{ - {"ethlog matching address", "ethlog", sharedAddr, sharedAddr, 1, common.Hash{}, hexutil.Bytes{}}, - {"ethlog all address", "ethlog", noAddr, newAddr(), 1, common.Hash{}, hexutil.Bytes{}}, - {"runlog v0 matching address", "runlog", sharedAddr, sharedAddr, 1, models.RunLogTopic0original, cltest.StringToVersionedLogData0(t, "id", `{"value":"100"}`)}, - {"runlog v20190123 w/o address", "runlog", noAddr, newAddr(), 1, models.RunLogTopic20190123withFullfillmentParams, cltest.StringToVersionedLogData20190123withFulfillmentParams(t, "id", `{"value":"100"}`)}, - {"runlog v20190123 matching address", "runlog", sharedAddr, sharedAddr, 1, models.RunLogTopic20190123withFullfillmentParams, cltest.StringToVersionedLogData20190123withFulfillmentParams(t, "id", `{"value":"100"}`)}, - {"runlog w non-matching topic", "runlog", sharedAddr, sharedAddr, 0, common.Hash{}, cltest.StringToVersionedLogData20190123withFulfillmentParams(t, "id", `{"value":"100"}`)}, - {"runlog v20190207 w/o address", "runlog", noAddr, newAddr(), 1, models.RunLogTopic20190207withoutIndexes, cltest.StringToVersionedLogData20190207withoutIndexes(t, "id", cltest.NewAddress(), `{"value":"100"}`)}, - {"runlog v20190207 matching address", "runlog", sharedAddr, sharedAddr, 1, models.RunLogTopic20190207withoutIndexes, cltest.StringToVersionedLogData20190207withoutIndexes(t, "id", cltest.NewAddress(), `{"value":"100"}`)}, - {"runlog w non-matching topic", "runlog", sharedAddr, sharedAddr, 0, common.Hash{}, cltest.StringToVersionedLogData20190207withoutIndexes(t, "id", cltest.NewAddress(), `{"value":"100"}`)}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - store, el, cleanup := cltest.NewJobSubscriber(t) - defer cleanup() - - eth := cltest.MockEthOnStore(t, store) - eth.Register("eth_getLogs", []models.Log{}) - eth.Register("eth_chainId", store.Config.ChainID()) - logChan := make(chan models.Log, 1) - eth.RegisterSubscription("logs", logChan) - - job := cltest.NewJob() - initr := models.Initiator{Type: test.initType} - initr.Address = test.initrAddr - job.Initiators = []models.Initiator{initr} - require.NoError(t, store.CreateJob(&job)) - el.AddJob(job, cltest.Head(1)) - - ht := services.NewHeadTracker(store, []strpkg.HeadTrackable{el}) - require.NoError(t, ht.Start()) - - logChan <- models.Log{ - Address: test.logAddr, - Data: test.data, - Topics: []common.Hash{ - test.topic0, - models.IDToTopic(job.ID), - newAddr().Hash(), - common.BigToHash(big.NewInt(0)), - }, - } - - cltest.WaitForRuns(t, job, store, test.wantCount) - - eth.EventuallyAllCalled(t) - }) - } + job := models.JobSpec{} + err := jobSubscriber.AddJob(job, cltest.Head(1)) + require.NoError(t, err) } -func TestJobSubscriber_RemoveJob_RunLog(t *testing.T) { +func TestJobSubscriber_RemoveJob_NotFoundError(t *testing.T) { t.Parallel() - store, el, cleanup := cltest.NewJobSubscriber(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - eth := cltest.MockEthOnStore(t, store) - eth.Register("eth_getLogs", []models.Log{}) - eth.Register("eth_chainId", store.Config.ChainID()) - logChan := make(chan models.Log, 1) - eth.RegisterSubscription("logs", logChan) - - addr := newAddr() - job := cltest.NewJob() - initr := models.Initiator{Type: "runlog"} - initr.Address = addr - job.Initiators = []models.Initiator{initr} - require.NoError(t, store.CreateJob(&job)) - el.AddJob(job, cltest.Head(1)) - require.Len(t, el.Jobs(), 1) - - require.NoError(t, el.RemoveJob(job.ID)) - require.Len(t, el.Jobs(), 0) - - ht := services.NewHeadTracker(store, []strpkg.HeadTrackable{el}) - require.NoError(t, ht.Start()) - - // asserts that JobSubscriber unsubscribed the job specific channel - require.True(t, sendingOnClosedChannel(func() { - logChan <- models.Log{} - })) - - cltest.WaitForRuns(t, job, store, 0) - eth.EventuallyAllCalled(t) + runManager := new(mocks.RunManager) + jobSubscriber := services.NewJobSubscriber(store, runManager) + + err := jobSubscriber.RemoveJob(models.NewID()) + require.Error(t, err) } -func TestJobSubscriber_RemoveJob_EthLog(t *testing.T) { +func TestJobSubscriber_Connect_Disconnect(t *testing.T) { t.Parallel() - store, el, cleanup := cltest.NewJobSubscriber(t) + store, cleanup := cltest.NewStore(t) defer cleanup() + runManager := new(mocks.RunManager) + jobSubscriber := services.NewJobSubscriber(store, runManager) + eth := cltest.MockEthOnStore(t, store) eth.Register("eth_getLogs", []models.Log{}) - eth.Register("eth_chainId", store.Config.ChainID()) - logChan := make(chan models.Log, 1) - eth.RegisterSubscription("logs", logChan) - - addr := newAddr() - job := cltest.NewJob() - initr := models.Initiator{Type: "ethlog"} - initr.Address = addr - job.Initiators = []models.Initiator{initr} - require.NoError(t, store.CreateJob(&job)) - el.AddJob(job, cltest.Head(1)) - require.Len(t, el.Jobs(), 1) - - require.NoError(t, el.RemoveJob(job.ID)) - require.Len(t, el.Jobs(), 0) - - ht := services.NewHeadTracker(store, []strpkg.HeadTrackable{el}) - require.NoError(t, ht.Start()) - - // asserts that JobSubscriber unsubscribed the job specific channel - require.True(t, sendingOnClosedChannel(func() { - logChan <- models.Log{} - })) - - cltest.WaitForRuns(t, job, store, 0) + eth.Register("eth_getLogs", []models.Log{}) + + jobSpec1 := cltest.NewJobWithLogInitiator() + jobSpec2 := cltest.NewJobWithLogInitiator() + assert.Nil(t, store.CreateJob(&jobSpec1)) + assert.Nil(t, store.CreateJob(&jobSpec2)) + eth.RegisterSubscription("logs") + eth.RegisterSubscription("logs") + + assert.Nil(t, jobSubscriber.Connect(cltest.Head(491))) eth.EventuallyAllCalled(t) -} -func sendingOnClosedChannel(callback func()) (rval bool) { - defer func() { - if r := recover(); r != nil { - rerror := r.(error) - rval = rerror.Error() == "send on closed channel" - } - }() - callback() - return false -} + assert.Len(t, jobSubscriber.Jobs(), 2) -func TestJobSubscriber_OnNewHead_ResumePendingConfirmationsAndPendingConnections(t *testing.T) { - t.Parallel() + jobSubscriber.Disconnect() - block := cltest.NewBlockHeader(10) - prettyLabel := func(archived bool, rs models.RunStatus) string { - if archived { - return fmt.Sprintf("archived:%s", string(rs)) - } - return string(rs) - } - - tests := []struct { - status models.RunStatus - archived bool - wantSend bool - }{ - {models.RunStatusPendingConnection, false, true}, - {models.RunStatusPendingConnection, true, true}, - {models.RunStatusPendingConfirmations, false, true}, - {models.RunStatusPendingConfirmations, true, true}, - {models.RunStatusInProgress, false, false}, - {models.RunStatusInProgress, true, false}, - {models.RunStatusPendingBridge, false, false}, - {models.RunStatusPendingBridge, true, false}, - {models.RunStatusPendingSleep, false, false}, - {models.RunStatusPendingSleep, true, false}, - {models.RunStatusCompleted, false, false}, - {models.RunStatusCompleted, true, false}, - } - - for _, test := range tests { - t.Run(prettyLabel(test.archived, test.status), func(t *testing.T) { - store, js, cleanup := cltest.NewJobSubscriber(t) - defer cleanup() - - mockRunChannel := cltest.NewMockRunChannel() - store.RunChannel = mockRunChannel - - job := cltest.NewJobWithWebInitiator() - require.NoError(t, store.CreateJob(&job)) - initr := job.Initiators[0] - run := job.NewRun(initr) - run.ApplyResult(models.RunResult{Status: test.status}) - require.NoError(t, store.CreateJobRun(&run)) - - if test.archived { - require.NoError(t, store.ArchiveJob(job.ID)) - } - - js.OnNewHead(block.ToHead()) - if test.wantSend { - assert.Equal(t, 1, len(mockRunChannel.Runs)) - } else { - assert.Equal(t, 0, len(mockRunChannel.Runs)) - } - }) - } + assert.Len(t, jobSubscriber.Jobs(), 0) } diff --git a/core/services/mock_services/job_subscriber.go b/core/services/mock_services/job_subscriber.go deleted file mode 100644 index cb7de255f38..00000000000 --- a/core/services/mock_services/job_subscriber.go +++ /dev/null @@ -1,103 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/smartcontractkit/chainlink/core/services (interfaces: JobSubscriber) - -// Package mock_services is a generated GoMock package. -package mock_services - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - models "github.com/smartcontractkit/chainlink/core/store/models" -) - -// MockJobSubscriber is a mock of JobSubscriber interface -type MockJobSubscriber struct { - ctrl *gomock.Controller - recorder *MockJobSubscriberMockRecorder -} - -// MockJobSubscriberMockRecorder is the mock recorder for MockJobSubscriber -type MockJobSubscriberMockRecorder struct { - mock *MockJobSubscriber -} - -// NewMockJobSubscriber creates a new mock instance -func NewMockJobSubscriber(ctrl *gomock.Controller) *MockJobSubscriber { - mock := &MockJobSubscriber{ctrl: ctrl} - mock.recorder = &MockJobSubscriberMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockJobSubscriber) EXPECT() *MockJobSubscriberMockRecorder { - return m.recorder -} - -// AddJob mocks base method -func (m *MockJobSubscriber) AddJob(arg0 models.JobSpec, arg1 *models.Head) error { - ret := m.ctrl.Call(m, "AddJob", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddJob indicates an expected call of AddJob -func (mr *MockJobSubscriberMockRecorder) AddJob(arg0, arg1 interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddJob", reflect.TypeOf((*MockJobSubscriber)(nil).AddJob), arg0, arg1) -} - -// Connect mocks base method -func (m *MockJobSubscriber) Connect(arg0 *models.Head) error { - ret := m.ctrl.Call(m, "Connect", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Connect indicates an expected call of Connect -func (mr *MockJobSubscriberMockRecorder) Connect(arg0 interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockJobSubscriber)(nil).Connect), arg0) -} - -// Disconnect mocks base method -func (m *MockJobSubscriber) Disconnect() { - m.ctrl.Call(m, "Disconnect") -} - -// Disconnect indicates an expected call of Disconnect -func (mr *MockJobSubscriberMockRecorder) Disconnect() *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnect", reflect.TypeOf((*MockJobSubscriber)(nil).Disconnect)) -} - -// Jobs mocks base method -func (m *MockJobSubscriber) Jobs() []models.JobSpec { - ret := m.ctrl.Call(m, "Jobs") - ret0, _ := ret[0].([]models.JobSpec) - return ret0 -} - -// Jobs indicates an expected call of Jobs -func (mr *MockJobSubscriberMockRecorder) Jobs() *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Jobs", reflect.TypeOf((*MockJobSubscriber)(nil).Jobs)) -} - -// OnNewHead mocks base method -func (m *MockJobSubscriber) OnNewHead(arg0 *models.Head) { - m.ctrl.Call(m, "OnNewHead", arg0) -} - -// OnNewHead indicates an expected call of OnNewHead -func (mr *MockJobSubscriberMockRecorder) OnNewHead(arg0 interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnNewHead", reflect.TypeOf((*MockJobSubscriber)(nil).OnNewHead), arg0) -} - -// RemoveJob mocks base method -func (m *MockJobSubscriber) RemoveJob(arg0 *models.ID) error { - ret := m.ctrl.Call(m, "RemoveJob", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemoveJob indicates an expected call of RemoveJob -func (mr *MockJobSubscriberMockRecorder) RemoveJob(arg0 interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveJob", reflect.TypeOf((*MockJobSubscriber)(nil).RemoveJob), arg0) -} diff --git a/core/services/reaper.go b/core/services/reaper.go index 711461fb89c..409710eb982 100644 --- a/core/services/reaper.go +++ b/core/services/reaper.go @@ -3,9 +3,9 @@ package services import ( "time" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/orm" + "chainlink/core/logger" + "chainlink/core/store" + "chainlink/core/store/orm" ) type storeReaper struct { diff --git a/core/services/reaper_test.go b/core/services/reaper_test.go index 1329823c283..5a2a1caa7bf 100644 --- a/core/services/reaper_test.go +++ b/core/services/reaper_test.go @@ -4,10 +4,11 @@ import ( "testing" "time" + "chainlink/core/internal/cltest" + "chainlink/core/services" + "chainlink/core/store/models" + "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/core/services/run_executor.go b/core/services/run_executor.go new file mode 100644 index 00000000000..07ebbafb449 --- /dev/null +++ b/core/services/run_executor.go @@ -0,0 +1,116 @@ +package services + +import ( + "fmt" + "time" + + "chainlink/core/adapters" + "chainlink/core/logger" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + + "github.com/pkg/errors" +) + +//go:generate mockery -name RunExecutor -output ../internal/mocks/ -case=underscore + +// RunExecutor handles the actual running of the job tasks +type RunExecutor interface { + Execute(*models.ID) error +} + +type runExecutor struct { + store *store.Store +} + +// NewRunExecutor initializes a RunExecutor. +func NewRunExecutor(store *store.Store) RunExecutor { + return &runExecutor{ + store: store, + } +} + +// Execute performs the work associate with a job run +func (je *runExecutor) Execute(runID *models.ID) error { + run, err := je.store.Unscoped().FindJobRun(runID) + if err != nil { + return errors.Wrapf(err, "error finding run %s", runID) + } + + for taskIndex := range run.TaskRuns { + taskRun := &run.TaskRuns[taskIndex] + if !run.Status.Runnable() { + logger.Debugw("Run execution blocked", run.ForLogger("task", taskRun.ID.String())...) + break + } + + if taskRun.Status.Completed() { + continue + } + + if meetsMinimumConfirmations(&run, taskRun, run.ObservedHeight) { + start := time.Now() + + result := je.executeTask(&run, taskRun) + + taskRun.ApplyOutput(result) + run.ApplyOutput(result) + + elapsed := time.Since(start).Seconds() + + logger.Debugw(fmt.Sprintf("Executed task %s", taskRun.TaskSpec.Type), run.ForLogger("task", taskRun.ID.String(), "elapsed", elapsed)...) + + } else { + logger.Debugw("Pausing run pending confirmations", + run.ForLogger("required_height", taskRun.MinimumConfirmations)..., + ) + taskRun.Status = models.RunStatusPendingConfirmations + run.Status = models.RunStatusPendingConfirmations + + } + + if err := je.store.ORM.SaveJobRun(&run); errors.Cause(err) == orm.OptimisticUpdateConflictError { + logger.Debugw("Optimistic update conflict while updating run", run.ForLogger()...) + return nil + } else if err != nil { + return err + } + } + + if run.Status.Finished() { + logger.Debugw("All tasks complete for run", run.ForLogger()...) + } + return nil +} + +func (je *runExecutor) executeTask(run *models.JobRun, taskRun *models.TaskRun) models.RunOutput { + taskCopy := taskRun.TaskSpec // deliberately copied to keep mutations local + + params, err := models.Merge(taskCopy.Params, run.Overrides) + if err != nil { + return models.NewRunOutputError(err) + } + taskCopy.Params = params + + adapter, err := adapters.For(taskCopy, je.store.Config, je.store.ORM) + if err != nil { + return models.NewRunOutputError(err) + } + + previousTaskRun := run.PreviousTaskRun() + + previousTaskInput := models.JSON{} + if previousTaskRun != nil { + previousTaskInput = previousTaskRun.Result.Data + } + + data, err := models.Merge(run.Overrides, previousTaskInput, taskRun.Result.Data) + if err != nil { + return models.NewRunOutputError(err) + } + + input := *models.NewRunInput(run.ID, data, taskRun.Status) + result := adapter.Perform(input, je.store) + return result +} diff --git a/core/services/run_executor_test.go b/core/services/run_executor_test.go new file mode 100644 index 00000000000..c59843174db --- /dev/null +++ b/core/services/run_executor_test.go @@ -0,0 +1,178 @@ +package services_test + +import ( + "math/big" + "testing" + "time" + + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/null" + "chainlink/core/services" + "chainlink/core/store/assets" + "chainlink/core/store/models" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunExecutor_Execute(t *testing.T) { + t.Parallel() + + store, cleanup := cltest.NewStore(t) + defer cleanup() + + runExecutor := services.NewRunExecutor(store) + + j := models.NewJob() + i := models.Initiator{Type: models.InitiatorWeb} + j.Initiators = []models.Initiator{i} + j.Tasks = []models.TaskSpec{ + cltest.NewTask(t, "noop"), + } + assert.NoError(t, store.CreateJob(&j)) + + run := j.NewRun(i) + run.Payment = assets.NewLink(9117) + require.NoError(t, store.CreateJobRun(&run)) + + err := runExecutor.Execute(run.ID) + require.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusCompleted, run.Status) + require.Len(t, run.TaskRuns, 1) + assert.Equal(t, models.RunStatusCompleted, run.TaskRuns[0].Status) + + actual, err := store.LinkEarnedFor(&j) + require.NoError(t, err) + assert.Equal(t, assets.NewLink(9117), actual) +} + +func TestRunExecutor_Execute_Pending(t *testing.T) { + t.Parallel() + + store, cleanup := cltest.NewStore(t) + defer cleanup() + + runExecutor := services.NewRunExecutor(store) + + j := models.NewJob() + i := models.Initiator{Type: models.InitiatorWeb} + j.Initiators = []models.Initiator{i} + j.Tasks = []models.TaskSpec{ + cltest.NewTask(t, "noop"), + cltest.NewTask(t, "nooppend"), + } + assert.NoError(t, store.CreateJob(&j)) + + run := j.NewRun(i) + run.Payment = assets.NewLink(9117) + require.NoError(t, store.CreateJobRun(&run)) + + err := runExecutor.Execute(run.ID) + require.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusPendingConfirmations, run.Status) + require.Len(t, run.TaskRuns, 2) + assert.Equal(t, models.RunStatusCompleted, run.TaskRuns[0].Status) + assert.Equal(t, models.RunStatusPendingConfirmations, run.TaskRuns[1].Status) + + actual, err := store.LinkEarnedFor(&j) + require.NoError(t, err) + assert.Nil(t, actual) +} + +func TestRunExecutor_Execute_RunNotFoundError(t *testing.T) { + t.Parallel() + + store, cleanup := cltest.NewStore(t) + defer cleanup() + + runExecutor := services.NewRunExecutor(store) + + err := runExecutor.Execute(models.NewID()) + require.Error(t, err) +} + +func TestRunExecutor_Execute_CancelActivelyRunningTask(t *testing.T) { + t.Parallel() + + store, cleanup := cltest.NewStore(t) + defer cleanup() + clock := cltest.NewTriggerClock(t) + store.Clock = clock + + runExecutor := services.NewRunExecutor(store) + + j := models.NewJob() + i := models.Initiator{Type: models.InitiatorWeb} + j.Initiators = []models.Initiator{i} + j.Tasks = []models.TaskSpec{ + cltest.NewTask(t, "sleep", `{"until": 2147483647}`), + cltest.NewTask(t, "noop"), + } + assert.NoError(t, store.CreateJob(&j)) + + run := j.NewRun(i) + run.Payment = assets.NewLink(19238) + require.NoError(t, store.CreateJobRun(&run)) + + go func() { + err := runExecutor.Execute(run.ID) + require.NoError(t, err) + }() + + // FIXME: Can't think of a better way to do this + // Make sure Execute has some time to start the sleep task + time.Sleep(300 * time.Millisecond) + + runQueue := new(mocks.RunQueue) + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, clock) + runManager.Cancel(run.ID) + + clock.Trigger() + + run, err := store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusCancelled, run.Status) + + require.Len(t, run.TaskRuns, 2) + assert.Equal(t, models.RunStatusCancelled, run.TaskRuns[0].Status) + assert.Equal(t, models.RunStatusUnstarted, run.TaskRuns[1].Status) + + actual, err := store.LinkEarnedFor(&j) + require.NoError(t, err) + assert.Nil(t, actual) +} + +func TestRunExecutor_InitialTaskLacksConfirmations(t *testing.T) { + t.Parallel() + + store, cleanup := cltest.NewStore(t) + defer cleanup() + + runExecutor := services.NewRunExecutor(store) + + j := cltest.NewJobWithWebInitiator() + j.Tasks = []models.TaskSpec{cltest.NewTask(t, "noop")} + assert.NoError(t, store.CreateJob(&j)) + + run := j.NewRun(j.Initiators[0]) + txHash := cltest.NewHash() + run.RunRequest.TxHash = &txHash + run.TaskRuns[0].MinimumConfirmations = null.Uint32From(10) + run.CreationHeight = models.NewBig(big.NewInt(0)) + run.ObservedHeight = run.CreationHeight + require.NoError(t, store.CreateJobRun(&run)) + require.NoError(t, runExecutor.Execute(run.ID)) + + run, err := store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusPendingConfirmations, run.Status) + require.Len(t, run.TaskRuns, 1) + assert.Equal(t, models.RunStatusPendingConfirmations, run.TaskRuns[0].Status) +} diff --git a/core/services/run_manager.go b/core/services/run_manager.go new file mode 100644 index 00000000000..3bd15ddf6a1 --- /dev/null +++ b/core/services/run_manager.go @@ -0,0 +1,363 @@ +package services + +import ( + "fmt" + "math/big" + + "chainlink/core/adapters" + "chainlink/core/logger" + clnull "chainlink/core/null" + "chainlink/core/store" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/utils" + + "github.com/pkg/errors" +) + +// RecurringScheduleJobError contains the field for the error message. +type RecurringScheduleJobError struct { + msg string +} + +// Error returns the error message for the run. +func (err RecurringScheduleJobError) Error() string { + return err.msg +} + +//go:generate mockery -name RunManager -output ../internal/mocks/ -case=underscore + +// RunManager supplies methods for queueing, resuming and cancelling jobs in +// the RunQueue +type RunManager interface { + Create( + jobSpecID *models.ID, + initiator *models.Initiator, + data *models.JSON, + creationHeight *big.Int, + runRequest *models.RunRequest) (*models.JobRun, error) + CreateErrored( + jobSpecID *models.ID, + initiator models.Initiator, + err error) (*models.JobRun, error) + ResumePending( + runID *models.ID, + input models.BridgeRunResult) error + Cancel(runID *models.ID) (*models.JobRun, error) + + ResumeAllInProgress() error + ResumeAllConfirming(currentBlockHeight *big.Int) error + ResumeAllConnecting() error +} + +// runManager implements RunManager +type runManager struct { + orm *orm.ORM + runQueue RunQueue + txManager store.TxManager + config orm.ConfigReader + clock utils.AfterNower +} + +func newRun( + job *models.JobSpec, + initiator *models.Initiator, + data *models.JSON, + currentHeight *big.Int, + payment *assets.Link, + config orm.ConfigReader, + orm *orm.ORM, + clock utils.AfterNower, + txManager store.TxManager) (*models.JobRun, error) { + + now := clock.Now() + if !job.Started(now) { + return nil, RecurringScheduleJobError{ + msg: fmt.Sprintf("Job runner: Job %v unstarted: %v before job's start time %v", job.ID, now, job.EndAt), + } + } + + if job.Ended(now) { + return nil, RecurringScheduleJobError{ + msg: fmt.Sprintf("Job runner: Job %v ended: %v past job's end time %v", job.ID, now, job.EndAt), + } + } + + run := job.NewRun(*initiator) + run.Overrides = *data + run.CreationHeight = models.NewBig(currentHeight) + run.ObservedHeight = models.NewBig(currentHeight) + + if !MeetsMinimumPayment(&job.MinPayment, payment) { + logger.Infow("Rejecting run with insufficient payment", + run.ForLogger( + "input_payment", payment, + "required_payment", job.MinPayment)...) + + err := fmt.Errorf( + "Rejecting job %s with payment %s below job-specific-minimum threshold (%s)", + job.ID, + payment, + job.MinPayment.Text(10)) + run.SetError(err) + } + + cost := &assets.Link{} + cost.Set(&job.MinPayment) + for i, taskRun := range run.TaskRuns { + adapter, err := adapters.For(taskRun.TaskSpec, config, orm) + + if err != nil { + run.SetError(err) + return &run, nil + } + + if job.MinPayment.IsZero() { + mp := adapter.MinContractPayment() + if mp != nil { + cost.Add(cost, mp) + } + } + + if currentHeight != nil { + run.TaskRuns[i].MinimumConfirmations = clnull.Uint32From( + utils.MaxUint32( + config.MinIncomingConfirmations(), + taskRun.TaskSpec.Confirmations.Uint32, + adapter.MinConfs()), + ) + } + } + + // payment is only present for runs triggered by runlogs + if payment != nil { + if cost.Cmp(payment) > 0 { + logger.Debugw("Rejecting run with insufficient payment", + run.ForLogger( + "input_payment", payment, + "required_payment", cost)...) + + err := fmt.Errorf( + "Rejecting job %s with payment %s below minimum threshold (%s)", + job.ID, + payment, + config.MinimumContractPayment().Text(10)) + run.SetError(err) + } + } + + if len(run.TaskRuns) == 0 { + run.SetError(fmt.Errorf("invariant for job %s: no tasks to run in NewRun", job.ID)) + } + + if !run.Status.Runnable() { + return &run, nil + } + + return &run, nil +} + +// NewRunManager returns a new job manager +func NewRunManager( + runQueue RunQueue, + config orm.ConfigReader, + orm *orm.ORM, + txManager store.TxManager, + clock utils.AfterNower) RunManager { + return &runManager{ + orm: orm, + runQueue: runQueue, + txManager: txManager, + config: config, + clock: clock, + } +} + +// CreateErrored creates a run that is in the errored state. This is a +// special case where this job cannot run but we want to create the run record +// so the error is more visible to the node operator. +func (jm *runManager) CreateErrored( + jobSpecID *models.ID, + initiator models.Initiator, + runErr error) (*models.JobRun, error) { + job, err := jm.orm.Unscoped().FindJob(jobSpecID) + if err != nil { + return nil, errors.Wrap(err, "failed to find job spec") + } + + run := job.NewRun(initiator) + run.SetError(runErr) + return &run, jm.orm.CreateJobRun(&run) +} + +// Create immediately persists a JobRun and sends it to the RunQueue for +// execution. +func (jm *runManager) Create( + jobSpecID *models.ID, + initiator *models.Initiator, + data *models.JSON, + creationHeight *big.Int, + runRequest *models.RunRequest, +) (*models.JobRun, error) { + logger.Debugw(fmt.Sprintf("New run triggered by %s", initiator.Type), + "job", jobSpecID.String(), + "creation_height", creationHeight.String(), + ) + + job, err := jm.orm.Unscoped().FindJob(jobSpecID) + if err != nil { + return nil, errors.Wrap(err, "failed to find job spec") + } + + if job.Archived() { + return nil, RecurringScheduleJobError{ + msg: fmt.Sprintf("Trying to run archived job %s", job.ID), + } + } + + run, err := newRun(&job, initiator, data, creationHeight, runRequest.Payment, jm.config, jm.orm, jm.clock, jm.txManager) + if err != nil { + return nil, errors.Wrap(err, "newRun failed") + } + + run.RunRequest = *runRequest + run.Status = models.RunStatusInProgress + + if err := jm.orm.CreateJobRun(run); err != nil { + return nil, errors.Wrap(err, "CreateJobRun failed") + } + + logger.Debugw( + fmt.Sprintf("Executing run originally initiated by %s", run.Initiator.Type), + run.ForLogger()..., + ) + jm.runQueue.Run(run) + return run, nil +} + +// ResumeAllConfirming wakes up all jobs that were sleeping because they were +// waiting for block confirmations. +func (jm *runManager) ResumeAllConfirming(currentBlockHeight *big.Int) error { + return jm.orm.UnscopedJobRunsWithStatus(func(run *models.JobRun) { + currentTaskRun := run.NextTaskRun() + if currentTaskRun == nil { + jm.updateWithError(run, "Attempting to resume confirming run with no remaining tasks %s", run.ID) + return + } + + run.ObservedHeight = models.NewBig(currentBlockHeight) + logger.Debugw(fmt.Sprintf("New head #%s resuming run", currentBlockHeight), run.ForLogger()...) + + validateMinimumConfirmations(run, currentTaskRun, run.ObservedHeight, jm.txManager) + + err := jm.updateAndTrigger(run) + if err != nil { + logger.Errorw("Error saving run", run.ForLogger("error", err)...) + } + }, models.RunStatusPendingConnection, models.RunStatusPendingConfirmations) +} + +// ResumeAllConnecting wakes up all tasks that have gone to sleep because they +// needed an ethereum client connection. +func (jm *runManager) ResumeAllConnecting() error { + return jm.orm.UnscopedJobRunsWithStatus(func(run *models.JobRun) { + logger.Debugw("New connection resuming run", run.ForLogger()...) + + currentTaskRun := run.NextTaskRun() + if currentTaskRun == nil { + jm.updateWithError(run, "Attempting to resume connecting run with no remaining tasks %s", run.ID) + return + } + + currentTaskRun.Status = models.RunStatusInProgress + run.Status = models.RunStatusInProgress + err := jm.updateAndTrigger(run) + if err != nil { + logger.Errorw("Error saving run", run.ForLogger("error", err)...) + } + }, models.RunStatusPendingConnection, models.RunStatusPendingConfirmations) +} + +// ResumePendingTask wakes up a task that required a response from a bridge adapter. +func (jm *runManager) ResumePending( + runID *models.ID, + input models.BridgeRunResult, +) error { + run, err := jm.orm.Unscoped().FindJobRun(runID) + if err != nil { + return err + } + + logger.Debugw("External adapter resuming run", run.ForLogger("input_data", input.Data)...) + + if !run.Status.PendingBridge() { + return fmt.Errorf("Attempting to resume non pending run %s", run.ID) + } + + currentTaskRun := run.NextTaskRun() + if currentTaskRun == nil { + return jm.updateWithError(&run, "Attempting to resume pending run with no remaining tasks %s", run.ID) + } + + data, err := models.Merge(run.Overrides, input.Data) + if err != nil { + return jm.updateWithError(&run, "Error while merging onto overrides for run %s", run.ID) + } + run.Overrides = data + + currentTaskRun.ApplyBridgeRunResult(input) + run.ApplyBridgeRunResult(input) + + return jm.updateAndTrigger(&run) +} + +// ResumeAllInProgress queries the db for job runs that should be resumed +// since a previous node shutdown. +// +// As a result of its reliance on the database, it must run before anything +// persists a job RunStatus to the db to ensure that it only captures pending and in progress +// jobs as a result of the last shutdown, and not as a result of what's happening now. +// +// To recap: This must run before anything else writes job run status to the db, +// ie. tries to run a job. +func (jm *runManager) ResumeAllInProgress() error { + return jm.orm.UnscopedJobRunsWithStatus(jm.runQueue.Run, models.RunStatusInProgress, models.RunStatusPendingSleep) +} + +// Cancel suspends a running task. +func (jm *runManager) Cancel(runID *models.ID) (*models.JobRun, error) { + run, err := jm.orm.FindJobRun(runID) + if err != nil { + return nil, err + } + + logger.Debugw("Cancelling run", run.ForLogger()...) + if run.Status.Finished() { + return nil, fmt.Errorf("Cannot cancel a run that has already finished") + } + + run.Cancel() + return &run, jm.orm.SaveJobRun(&run) +} + +func (jm *runManager) updateWithError(run *models.JobRun, msg string, args ...interface{}) error { + run.SetError(fmt.Errorf(msg, args...)) + logger.Error(fmt.Sprintf(msg, args...)) + + if err := jm.orm.SaveJobRun(run); err != nil { + logger.Errorw("Error saving run", run.ForLogger("error", err)...) + return err + } + return nil +} + +func (jm *runManager) updateAndTrigger(run *models.JobRun) error { + if err := jm.orm.SaveJobRun(run); err != nil { + return err + } + if run.Status == models.RunStatusInProgress { + jm.runQueue.Run(run) + } + return nil +} diff --git a/core/services/run_manager_test.go b/core/services/run_manager_test.go new file mode 100644 index 00000000000..d9806cca347 --- /dev/null +++ b/core/services/run_manager_test.go @@ -0,0 +1,634 @@ +package services_test + +import ( + "math/big" + "testing" + "time" + + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + clnull "chainlink/core/null" + "chainlink/core/services" + "chainlink/core/store/models" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "gopkg.in/guregu/null.v3" +) + +func TestRunManager_ResumePending(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + runQueue := new(mocks.RunQueue) + runQueue.On("Run", mock.Anything).Maybe().Return(nil) + + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, store.Clock) + + job := cltest.NewJob() + require.NoError(t, store.CreateJob(&job)) + input := cltest.JSONFromString(t, `{"address":"0xdfcfc2b9200dbb10952c2b7cce60fc7260e03c6f"}`) + + t.Run("reject a run with an invalid state", func(t *testing.T) { + run := &models.JobRun{ID: models.NewID(), JobSpecID: job.ID} + require.NoError(t, store.CreateJobRun(run)) + err := runManager.ResumePending(run.ID, models.BridgeRunResult{}) + assert.Error(t, err) + }) + + t.Run("reject a run with no tasks", func(t *testing.T) { + run := models.JobRun{ID: models.NewID(), JobSpecID: job.ID, Status: models.RunStatusPendingBridge} + require.NoError(t, store.CreateJobRun(&run)) + err := runManager.ResumePending(run.ID, models.BridgeRunResult{}) + assert.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusErrored, run.Status) + }) + + t.Run("input with error errors run", func(t *testing.T) { + runID := models.NewID() + run := models.JobRun{ + ID: runID, + JobSpecID: job.ID, + Status: models.RunStatusPendingBridge, + TaskRuns: []models.TaskRun{models.TaskRun{ID: models.NewID(), JobRunID: runID}}, + } + require.NoError(t, store.CreateJobRun(&run)) + + err := runManager.ResumePending(run.ID, models.BridgeRunResult{Status: models.RunStatusErrored}) + assert.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusErrored, run.Status) + assert.True(t, run.FinishedAt.Valid) + assert.Len(t, run.TaskRuns, 1) + assert.Equal(t, models.RunStatusErrored, run.TaskRuns[0].Status) + }) + + t.Run("completed input with remaining tasks should put task into in-progress", func(t *testing.T) { + runID := models.NewID() + run := models.JobRun{ + ID: runID, + JobSpecID: job.ID, + Status: models.RunStatusPendingBridge, + TaskRuns: []models.TaskRun{models.TaskRun{ID: models.NewID(), JobRunID: runID}, models.TaskRun{ID: models.NewID(), JobRunID: runID}}, + } + require.NoError(t, store.CreateJobRun(&run)) + + err := runManager.ResumePending(run.ID, models.BridgeRunResult{Data: input, Status: models.RunStatusCompleted}) + assert.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.NoError(t, err) + assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) + assert.Len(t, run.TaskRuns, 2) + assert.Equal(t, string(models.RunStatusCompleted), string(run.TaskRuns[0].Status)) + }) + + t.Run("completed input with no remaining tasks should get marked as complete", func(t *testing.T) { + runID := models.NewID() + run := models.JobRun{ + ID: runID, + JobSpecID: job.ID, + Status: models.RunStatusPendingBridge, + TaskRuns: []models.TaskRun{models.TaskRun{ID: models.NewID(), JobRunID: runID}}, + } + require.NoError(t, store.CreateJobRun(&run)) + + err := runManager.ResumePending(run.ID, models.BridgeRunResult{Data: input, Status: models.RunStatusCompleted}) + assert.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, string(models.RunStatusCompleted), string(run.Status)) + assert.True(t, run.FinishedAt.Valid) + assert.Len(t, run.TaskRuns, 1) + assert.Equal(t, string(models.RunStatusCompleted), string(run.TaskRuns[0].Status)) + }) + + runQueue.AssertExpectations(t) +} + +func TestRunManager_ResumeAllConfirming(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + runQueue := new(mocks.RunQueue) + runQueue.On("Run", mock.Anything).Maybe().Return(nil) + + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, store.Clock) + + job := cltest.NewJob() + require.NoError(t, store.CreateJob(&job)) + + t.Run("reject a run with no tasks", func(t *testing.T) { + run := models.JobRun{ + ID: models.NewID(), + JobSpecID: job.ID, + Status: models.RunStatusPendingConfirmations, + } + require.NoError(t, store.CreateJobRun(&run)) + + err := runManager.ResumeAllConfirming(nil) + assert.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusErrored, run.Status) + }) + + creationHeight := models.NewBig(big.NewInt(0)) + + t.Run("leave in pending if not enough confirmations have been met yet", func(t *testing.T) { + run := models.JobRun{ + ID: models.NewID(), + JobSpecID: job.ID, + CreationHeight: creationHeight, + Status: models.RunStatusPendingConfirmations, + TaskRuns: []models.TaskRun{models.TaskRun{ + ID: models.NewID(), + MinimumConfirmations: clnull.Uint32From(2), + TaskSpec: models.TaskSpec{ + Type: adapters.TaskTypeNoOp, + }, + }}, + } + require.NoError(t, store.CreateJobRun(&run)) + + err := runManager.ResumeAllConfirming(creationHeight.ToInt()) + require.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusPendingConfirmations, run.Status) + assert.Equal(t, uint32(1), run.TaskRuns[0].Confirmations.Uint32) + }) + + t.Run("input, should go from pending -> in progress and save the input", func(t *testing.T) { + run := models.JobRun{ + ID: models.NewID(), + JobSpecID: job.ID, + CreationHeight: creationHeight, + Status: models.RunStatusPendingConfirmations, + TaskRuns: []models.TaskRun{models.TaskRun{ + ID: models.NewID(), + MinimumConfirmations: clnull.Uint32From(1), + TaskSpec: models.TaskSpec{ + Type: adapters.TaskTypeNoOp, + }, + }}, + } + require.NoError(t, store.CreateJobRun(&run)) + + observedHeight := big.NewInt(1) + err := runManager.ResumeAllConfirming(observedHeight) + require.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) + }) + + runQueue.AssertExpectations(t) +} + +func TestRunManager_ResumeAllConnecting(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + runQueue := new(mocks.RunQueue) + runQueue.On("Run", mock.Anything).Maybe().Return(nil) + + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, store.Clock) + + job := cltest.NewJob() + require.NoError(t, store.CreateJob(&job)) + + t.Run("reject a run with no tasks", func(t *testing.T) { + run := models.JobRun{ + ID: models.NewID(), + JobSpecID: job.ID, + Status: models.RunStatusPendingConnection, + } + require.NoError(t, store.CreateJobRun(&run)) + + err := runManager.ResumeAllConnecting() + assert.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusErrored, run.Status) + }) + + t.Run("input, should go from pending -> in progress", func(t *testing.T) { + run := models.JobRun{ + ID: models.NewID(), + JobSpecID: job.ID, + Status: models.RunStatusPendingConnection, + TaskRuns: []models.TaskRun{models.TaskRun{ + ID: models.NewID(), + }}, + } + require.NoError(t, store.CreateJobRun(&run)) + err := runManager.ResumeAllConnecting() + assert.NoError(t, err) + + run, err = store.FindJobRun(run.ID) + require.NoError(t, err) + assert.Equal(t, models.RunStatusInProgress, run.Status) + }) +} + +func TestRunManager_ResumeAllConnecting_NotEnoughConfirmations(t *testing.T) { + t.Parallel() + app, cleanup := cltest.NewApplication(t) + defer cleanup() + + store := app.Store + eth := cltest.MockEthOnStore(t, store) + eth.Register("eth_chainId", store.Config.ChainID()) + + app.Start() + + job := cltest.NewJobWithRunLogInitiator() + job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} + require.NoError(t, store.CreateJob(&job)) + + initiator := job.Initiators[0] + run := job.NewRun(initiator) + run.Status = models.RunStatusPendingConnection + run.CreationHeight = models.NewBig(big.NewInt(0)) + run.ObservedHeight = run.CreationHeight + run.TaskRuns[0].MinimumConfirmations = clnull.Uint32From(807) + run.TaskRuns[0].Status = models.RunStatusPendingConnection + require.NoError(t, store.CreateJobRun(&run)) + + app.RunManager.ResumeAllConnecting() + + cltest.WaitForJobRunToPendConfirmations(t, store, run) +} + +func TestRunManager_Create(t *testing.T) { + t.Parallel() + app, cleanup := cltest.NewApplication(t) + defer cleanup() + + store := app.Store + eth := cltest.MockEthOnStore(t, store) + eth.Register("eth_chainId", store.Config.ChainID()) + + app.Start() + + job := cltest.NewJobWithRunLogInitiator() + job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} // empty params + require.NoError(t, store.CreateJob(&job)) + + requestID := "RequestID" + initr := job.Initiators[0] + rr := models.NewRunRequest() + rr.RequestID = &requestID + data := cltest.JSONFromString(t, `{"random": "input"}`) + jr, err := app.RunManager.Create(job.ID, &initr, &data, nil, rr) + require.NoError(t, err) + updatedJR := cltest.WaitForJobRunToComplete(t, store, *jr) + assert.Equal(t, rr.RequestID, updatedJR.RunRequest.RequestID) +} + +func TestRunManager_Create_DoesNotSaveToTaskSpec(t *testing.T) { + t.Parallel() + app, cleanup := cltest.NewApplication(t) + defer cleanup() + + store := app.Store + eth := cltest.MockEthOnStore(t, store) + eth.Register("eth_chainId", store.Config.ChainID()) + + app.Start() + + job := cltest.NewJobWithWebInitiator() + job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} // empty params + require.NoError(t, store.CreateJob(&job)) + + initr := job.Initiators[0] + data := cltest.JSONFromString(t, `{"random": "input"}`) + jr, err := app.RunManager.Create(job.ID, &initr, &data, nil, &models.RunRequest{}) + require.NoError(t, err) + cltest.WaitForJobRunToComplete(t, store, *jr) + + retrievedJob, err := store.FindJob(job.ID) + require.NoError(t, err) + require.Len(t, job.Tasks, 1) + require.Len(t, retrievedJob.Tasks, 1) + assert.Equal(t, job.Tasks[0].Params, retrievedJob.Tasks[0].Params) +} + +func TestRunManager_Create_fromRunLog_Happy(t *testing.T) { + t.Parallel() + + initiatingTxHash := cltest.NewHash() + triggeringBlockHash := cltest.NewHash() + otherBlockHash := cltest.NewHash() + + tests := []struct { + name string + logBlockHash common.Hash + receiptBlockHash common.Hash + wantStatus models.RunStatus + }{ + { + name: "main chain", + logBlockHash: triggeringBlockHash, + receiptBlockHash: triggeringBlockHash, + wantStatus: models.RunStatusCompleted, + }, + { + name: "ommered chain", + logBlockHash: triggeringBlockHash, + receiptBlockHash: otherBlockHash, + wantStatus: models.RunStatusErrored, + }, + } + + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + config, cfgCleanup := cltest.NewConfig(t) + defer cfgCleanup() + minimumConfirmations := uint32(2) + config.Set("MIN_INCOMING_CONFIRMATIONS", minimumConfirmations) + app, cleanup := cltest.NewApplicationWithConfig(t, config) + defer cleanup() + + eth := app.MockEthCallerSubscriber() + store := app.GetStore() + eth.Context("app.Start()", func(eth *cltest.EthMock) { + eth.Register("eth_chainId", store.Config.ChainID()) + }) + app.Start() + + job := cltest.NewJobWithRunLogInitiator() + job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} + require.NoError(t, store.CreateJob(&job)) + + creationHeight := big.NewInt(1) + requestID := "RequestID" + initr := job.Initiators[0] + rr := models.NewRunRequest() + rr.RequestID = &requestID + rr.TxHash = &initiatingTxHash + rr.BlockHash = &test.logBlockHash + data := cltest.JSONFromString(t, `{"random": "input"}`) + jr, err := app.RunManager.Create(job.ID, &initr, &data, creationHeight, rr) + require.NoError(t, err) + + run := cltest.WaitForJobRunToPendConfirmations(t, app.Store, *jr) + assert.Equal(t, models.RunStatusPendingConfirmations, run.TaskRuns[0].Status) + assert.Equal(t, models.RunStatusPendingConfirmations, run.Status) + + confirmedReceipt := models.TxReceipt{ + Hash: initiatingTxHash, + BlockHash: &test.receiptBlockHash, + BlockNumber: cltest.Int(3), + } + eth.Context("validateOnMainChain", func(eth *cltest.EthMock) { + eth.Register("eth_getTransactionReceipt", confirmedReceipt) + }) + + err = app.RunManager.ResumeAllConfirming(big.NewInt(2)) + require.NoError(t, err) + run = cltest.WaitForJobRunStatus(t, store, *jr, test.wantStatus) + assert.Equal(t, rr.RequestID, run.RunRequest.RequestID) + assert.Equal(t, minimumConfirmations, run.TaskRuns[0].MinimumConfirmations.Uint32) + assert.True(t, run.TaskRuns[0].MinimumConfirmations.Valid) + assert.Equal(t, minimumConfirmations, run.TaskRuns[0].Confirmations.Uint32, "task run should track its current confirmations") + assert.True(t, run.TaskRuns[0].Confirmations.Valid) + + assert.True(t, eth.AllCalled(), eth.Remaining()) + }) + } +} + +func TestRunManager_Create_fromRunLog_ConnectToLaggingEthNode(t *testing.T) { + t.Parallel() + + initiatingTxHash := cltest.NewHash() + triggeringBlockHash := cltest.NewHash() + + config, cfgCleanup := cltest.NewConfig(t) + defer cfgCleanup() + minimumConfirmations := uint32(2) + config.Set("MIN_INCOMING_CONFIRMATIONS", minimumConfirmations) + app, cleanup := cltest.NewApplicationWithConfig(t, config) + defer cleanup() + + eth := app.MockEthCallerSubscriber() + app.MockStartAndConnect() + + store := app.GetStore() + job := cltest.NewJobWithRunLogInitiator() + job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} + require.NoError(t, store.CreateJob(&job)) + + requestID := "RequestID" + initr := job.Initiators[0] + rr := models.NewRunRequest() + rr.RequestID = &requestID + rr.TxHash = &initiatingTxHash + rr.BlockHash = &triggeringBlockHash + + futureCreationHeight := big.NewInt(9) + pastCurrentHeight := big.NewInt(1) + + data := cltest.JSONFromString(t, `{"random": "input"}`) + jr, err := app.RunManager.Create(job.ID, &initr, &data, futureCreationHeight, rr) + require.NoError(t, err) + cltest.WaitForJobRunToPendConfirmations(t, app.Store, *jr) + + err = app.RunManager.ResumeAllConfirming(pastCurrentHeight) + require.NoError(t, err) + updatedJR := cltest.WaitForJobRunToPendConfirmations(t, app.Store, *jr) + assert.True(t, updatedJR.TaskRuns[0].Confirmations.Valid) + assert.Equal(t, uint32(0), updatedJR.TaskRuns[0].Confirmations.Uint32) + assert.True(t, eth.AllCalled(), eth.Remaining()) +} + +func TestRunManager_ResumeConfirmingTasks(t *testing.T) { + t.Parallel() + + tests := []struct { + status models.RunStatus + }{ + {models.RunStatusPendingConnection}, + {models.RunStatusPendingConfirmations}, + } + + for _, test := range tests { + t.Run(string(test.status), func(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + job := cltest.NewJobWithWebInitiator() + require.NoError(t, store.CreateJob(&job)) + initr := job.Initiators[0] + run := job.NewRun(initr) + run.Status = test.status + require.NoError(t, store.CreateJobRun(&run)) + + runQueue := new(mocks.RunQueue) + runQueue.On("Run", mock.Anything).Return(nil) + + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, store.Clock) + runManager.ResumeAllConfirming(big.NewInt(3821)) + + runQueue.AssertExpectations(t) + }) + } +} + +func TestRunManager_ResumeAllInProgress(t *testing.T) { + t.Parallel() + + tests := []struct { + status models.RunStatus + }{ + {models.RunStatusInProgress}, + {models.RunStatusPendingSleep}, + } + + for _, test := range tests { + t.Run(string(test.status), func(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + job := cltest.NewJobWithWebInitiator() + require.NoError(t, store.CreateJob(&job)) + initr := job.Initiators[0] + run := job.NewRun(initr) + run.Status = test.status + require.NoError(t, store.CreateJobRun(&run)) + + runQueue := new(mocks.RunQueue) + runQueue.On("Run", mock.Anything).Return(nil) + + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, store.Clock) + runManager.ResumeAllInProgress() + + runQueue.AssertExpectations(t) + }) + } +} + +// XXX: In progress tasks that are archived should still be run as they have been paid for +func TestRunManager_ResumeAllInProgress_Archived(t *testing.T) { + t.Parallel() + + tests := []struct { + status models.RunStatus + }{ + {models.RunStatusInProgress}, + {models.RunStatusPendingSleep}, + } + + for _, test := range tests { + t.Run(string(test.status), func(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + job := cltest.NewJobWithWebInitiator() + require.NoError(t, store.CreateJob(&job)) + initr := job.Initiators[0] + run := job.NewRun(initr) + run.Status = test.status + run.DeletedAt = null.TimeFrom(time.Now()) + require.NoError(t, store.CreateJobRun(&run)) + + runQueue := new(mocks.RunQueue) + runQueue.On("Run", mock.Anything).Return(nil) + + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, store.Clock) + runManager.ResumeAllInProgress() + + runQueue.AssertExpectations(t) + }) + } +} + +func TestRunManager_ResumeAllInProgress_NotInProgress(t *testing.T) { + t.Parallel() + + tests := []struct { + status models.RunStatus + }{ + {models.RunStatusPendingConnection}, + {models.RunStatusPendingConfirmations}, + {models.RunStatusPendingBridge}, + {models.RunStatusCompleted}, + {models.RunStatusCancelled}, + } + + for _, test := range tests { + t.Run(string(test.status), func(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + job := cltest.NewJobWithWebInitiator() + require.NoError(t, store.CreateJob(&job)) + initr := job.Initiators[0] + run := job.NewRun(initr) + run.Status = test.status + require.NoError(t, store.CreateJobRun(&run)) + + runQueue := new(mocks.RunQueue) + runQueue.On("Run", mock.Anything).Maybe().Return(nil) + + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, store.Clock) + runManager.ResumeAllInProgress() + + runQueue.AssertExpectations(t) + }) + } +} + +func TestRunManager_ResumeAllInProgress_NotInProgressAndArchived(t *testing.T) { + t.Parallel() + + tests := []struct { + status models.RunStatus + }{ + {models.RunStatusPendingConnection}, + {models.RunStatusPendingConfirmations}, + {models.RunStatusPendingBridge}, + {models.RunStatusCompleted}, + {models.RunStatusCancelled}, + } + + for _, test := range tests { + t.Run(string(test.status), func(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + job := cltest.NewJobWithWebInitiator() + require.NoError(t, store.CreateJob(&job)) + initr := job.Initiators[0] + run := job.NewRun(initr) + run.Status = test.status + run.DeletedAt = null.TimeFrom(time.Now()) + require.NoError(t, store.CreateJobRun(&run)) + + runQueue := new(mocks.RunQueue) + runQueue.On("Run", mock.Anything).Maybe().Return(nil) + + runManager := services.NewRunManager(runQueue, store.Config, store.ORM, store.TxManager, store.Clock) + runManager.ResumeAllInProgress() + + runQueue.AssertExpectations(t) + }) + } +} diff --git a/core/services/run_queue.go b/core/services/run_queue.go new file mode 100644 index 00000000000..c8542169357 --- /dev/null +++ b/core/services/run_queue.go @@ -0,0 +1,114 @@ +package services + +import ( + "fmt" + "sync" + "time" + + "chainlink/core/logger" + "chainlink/core/store/models" +) + +//go:generate mockery -name RunQueue -output ../internal/mocks/ -case=underscore + +// RunQueue safely handles coordinating job runs. +type RunQueue interface { + Start() error + Stop() + Run(*models.JobRun) + + WorkerCount() int +} + +type runQueue struct { + workersMutex sync.RWMutex + workers map[string]int + workersWg sync.WaitGroup + + runExecutor RunExecutor + + runsQueued uint + runsExecuted uint + quit chan struct{} +} + +// NewRunQueue initializes a RunQueue. +func NewRunQueue(runExecutor RunExecutor) RunQueue { + return &runQueue{ + quit: make(chan struct{}), + workers: make(map[string]int), + runExecutor: runExecutor, + } +} + +// Start prepares the job runner for accepting runs to execute. +func (rq *runQueue) Start() error { + go rq.statisticsLogger() + return nil +} + +func (rq *runQueue) statisticsLogger() { + ticker := time.NewTicker(5 * time.Minute) + for { + select { + case <-ticker.C: + rq.workersMutex.RLock() + logger.Debugw("Run queue statistics", "runs_executed", rq.runsExecuted, "runs_queued", rq.runsQueued, "worker_count", len(rq.workers)) + rq.workersMutex.RUnlock() + case <-rq.quit: + ticker.Stop() + return + } + } +} + +// Stop closes all open worker channels. +func (rq *runQueue) Stop() { + rq.quit <- struct{}{} + rq.workersWg.Wait() +} + +// Run tells the job runner to start executing a job +func (rq *runQueue) Run(run *models.JobRun) { + runID := run.ID.String() + + rq.workersMutex.Lock() + if queueCount, present := rq.workers[runID]; present { + rq.runsQueued += 1 + rq.workers[runID] = queueCount + 1 + rq.workersMutex.Unlock() + return + } + rq.runsExecuted += 1 + rq.workers[runID] = 1 + rq.workersMutex.Unlock() + + rq.workersWg.Add(1) + go func() { + for { + rq.workersMutex.Lock() + queueCount := rq.workers[runID] + if queueCount <= 0 { + delete(rq.workers, runID) + rq.workersMutex.Unlock() + break + } + rq.workers[runID] = queueCount - 1 + rq.workersMutex.Unlock() + + if err := rq.runExecutor.Execute(run.ID); err != nil { + logger.Errorw(fmt.Sprint("Error executing run ", runID), "error", err) + } + } + + rq.workersWg.Done() + }() +} + +// WorkerCount returns the number of workers currently processing a job run +func (rq *runQueue) WorkerCount() int { + rq.workersMutex.RLock() + defer rq.workersMutex.RUnlock() + + return len(rq.workers) +} diff --git a/core/services/run_queue_test.go b/core/services/run_queue_test.go new file mode 100644 index 00000000000..0312c1c9371 --- /dev/null +++ b/core/services/run_queue_test.go @@ -0,0 +1,127 @@ +package services_test + +import ( + "testing" + + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/services" + "chainlink/core/store/models" + + "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" +) + +func TestRunQueue(t *testing.T) { + t.Parallel() + g := gomega.NewGomegaWithT(t) + + runExecutor := new(mocks.RunExecutor) + runQueue := services.NewRunQueue(runExecutor) + + executeJobChannel := make(chan struct{}) + + runQueue.Start() + defer runQueue.Stop() + + runExecutor.On("Execute", mock.Anything). + Return(nil, nil). + Run(func(mock.Arguments) { + executeJobChannel <- struct{}{} + }) + + runQueue.Run(&models.JobRun{ID: models.NewID()}) + + g.Eventually(func() int { + return runQueue.WorkerCount() + }).Should(gomega.Equal(1)) + + cltest.CallbackOrTimeout(t, "Execute", func() { + <-executeJobChannel + }) + + runExecutor.AssertExpectations(t) + + g.Eventually(func() int { + return runQueue.WorkerCount() + }).Should(gomega.Equal(0)) +} + +func TestRunQueue_OneWorkerPerRun(t *testing.T) { + t.Parallel() + g := gomega.NewGomegaWithT(t) + + runExecutor := new(mocks.RunExecutor) + runQueue := services.NewRunQueue(runExecutor) + + executeJobChannel := make(chan struct{}) + + runQueue.Start() + defer runQueue.Stop() + + runExecutor.On("Execute", mock.Anything). + Return(nil, nil). + Run(func(mock.Arguments) { + executeJobChannel <- struct{}{} + }) + + runQueue.Run(&models.JobRun{ID: models.NewID()}) + runQueue.Run(&models.JobRun{ID: models.NewID()}) + + g.Eventually(func() int { + return runQueue.WorkerCount() + }).Should(gomega.Equal(2)) + + cltest.CallbackOrTimeout(t, "Execute", func() { + <-executeJobChannel + <-executeJobChannel + }) + + runExecutor.AssertExpectations(t) + + g.Eventually(func() int { + return runQueue.WorkerCount() + }).Should(gomega.Equal(0)) +} + +func TestRunQueue_OneWorkerForSameRunTriggeredMultipleTimes(t *testing.T) { + t.Parallel() + g := gomega.NewGomegaWithT(t) + + runExecutor := new(mocks.RunExecutor) + runQueue := services.NewRunQueue(runExecutor) + + executeJobChannel := make(chan struct{}) + + runQueue.Start() + defer runQueue.Stop() + + runExecutor.On("Execute", mock.Anything). + Return(nil, nil). + Run(func(mock.Arguments) { + executeJobChannel <- struct{}{} + }) + + id := models.NewID() + runQueue.Run(&models.JobRun{ID: id}) + runQueue.Run(&models.JobRun{ID: id}) + + g.Eventually(func() int { + return runQueue.WorkerCount() + }).Should(gomega.Equal(1)) + + g.Consistently(func() int { + return runQueue.WorkerCount() + }).Should(gomega.BeNumerically("<", 2)) + + cltest.CallbackOrTimeout(t, "Execute", func() { + <-executeJobChannel + <-executeJobChannel + }) + + runExecutor.AssertExpectations(t) + + g.Eventually(func() int { + return runQueue.WorkerCount() + }).Should(gomega.Equal(0)) +} diff --git a/core/services/runs.go b/core/services/runs.go index 36cfeba2b75..c0315425dad 100644 --- a/core/services/runs.go +++ b/core/services/runs.go @@ -4,370 +4,70 @@ import ( "fmt" "math/big" - "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/logger" - clnull "github.com/smartcontractkit/chainlink/core/null" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/logger" + clnull "chainlink/core/null" + "chainlink/core/store" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/utils" ) -// ExecuteJob saves and immediately begins executing a run for a specified job -// if it is ready. -func ExecuteJob( - job models.JobSpec, - initiator models.Initiator, - input models.RunResult, - creationHeight *big.Int, - store *store.Store) (*models.JobRun, error) { - return ExecuteJobWithRunRequest( - job, - initiator, - input, - creationHeight, - store, - models.NewRunRequest(), - ) -} - -// ExecuteJobWithRunRequest saves and immediately begins executing a run -// for a specified job if it is ready, assigning the passed initiator run. -func ExecuteJobWithRunRequest( - job models.JobSpec, - initiator models.Initiator, - input models.RunResult, - creationHeight *big.Int, - store *store.Store, - runRequest models.RunRequest) (*models.JobRun, error) { - - logger.Debugw(fmt.Sprintf("New run triggered by %s", initiator.Type), - "job", job.ID, - "input_status", input.Status, - "creation_height", creationHeight, - ) - - run, err := NewRun(job, initiator, input, creationHeight, store, runRequest.Payment) - if err != nil { - return nil, errors.Wrap(err, "NewRun failed") - } - - run.RunRequest = runRequest - return run, createAndTrigger(run, store) -} - // MeetsMinimumPayment is a helper that returns true if jobrun received // sufficient payment (more than jobspec's MinimumPayment) to be considered successful func MeetsMinimumPayment( expectedMinJobPayment *assets.Link, actualRunPayment *assets.Link) bool { // input.Payment is always present for runs triggered by ethlogs - if actualRunPayment == nil || expectedMinJobPayment == nil || expectedMinJobPayment.IsZero() { + if actualRunPayment == nil || expectedMinJobPayment.IsZero() { return true } return expectedMinJobPayment.Cmp(actualRunPayment) < 1 } -// NewRun returns a run from an input job, in an initial state ready for -// processing by the job runner system -func NewRun( - job models.JobSpec, - initiator models.Initiator, - input models.RunResult, - currentHeight *big.Int, - store *store.Store, - payment *assets.Link) (*models.JobRun, error) { - - now := store.Clock.Now() - if !job.Started(now) { - return nil, RecurringScheduleJobError{ - msg: fmt.Sprintf("Job runner: Job %s unstarted: %v before job's start time %v", job.ID.String(), now, job.EndAt), - } - } - - if job.Ended(now) { - return nil, RecurringScheduleJobError{ - msg: fmt.Sprintf("Job runner: Job %s ended: %v past job's end time %v", job.ID.String(), now, job.EndAt), - } - } - - run := job.NewRun(initiator) +func validateMinimumConfirmations(run *models.JobRun, taskRun *models.TaskRun, currentHeight *models.Big, txManager store.TxManager) { + updateTaskRunConfirmations(currentHeight, run, taskRun) - if input.HasError() { - run.SetError(input.GetError()) - return &run, nil - } + if !meetsMinimumConfirmations(run, taskRun, run.ObservedHeight) { + logger.Debugw("Pausing run pending confirmations", + run.ForLogger("required_height", taskRun.MinimumConfirmations)..., + ) - run.Overrides = input.Data - run.CreationHeight = models.NewBig(currentHeight) - run.ObservedHeight = models.NewBig(currentHeight) + taskRun.Status = models.RunStatusPendingConfirmations + run.Status = models.RunStatusPendingConfirmations - if !MeetsMinimumPayment(job.MinPayment, payment) { - logger.Infow("Rejecting run with insufficient payment", []interface{}{ - "run", run.ID, - "job", run.JobSpecID, - "input_payment", payment, - "required_payment", job.MinPayment, - }...) + } else if err := validateOnMainChain(run, taskRun, txManager); err != nil { + logger.Warnw("Failure while trying to validate chain", + run.ForLogger("error", err)..., + ) - err := fmt.Errorf( - "Rejecting job %s with payment %s below job-specific-minimum threshold (%s)", - job.ID, - payment, - job.MinPayment.Text(10)) + taskRun.SetError(err) run.SetError(err) - } - - cost := &assets.Link{} - if job.MinPayment != nil { - cost.Set(job.MinPayment) - } - for i, taskRun := range run.TaskRuns { - adapter, err := adapters.For(taskRun.TaskSpec, store) - - if err != nil { - run.SetError(err) - return &run, nil - } - - if job.MinPayment == nil || job.MinPayment.IsZero() { - mp := adapter.MinContractPayment() - if mp != nil { - cost.Add(cost, mp) - } - } - - if currentHeight != nil { - run.TaskRuns[i].MinimumConfirmations = clnull.Uint32From( - utils.MaxUint32( - store.Config.MinIncomingConfirmations(), - taskRun.TaskSpec.Confirmations.Uint32, - adapter.MinConfs()), - ) - } - } - - // payment is always present for runs triggered by ethlogs - if payment != nil { - if cost.Cmp(payment) > 0 { - logger.Debugw("Rejecting run with insufficient payment", []interface{}{ - "run", run.ID, - "job", run.JobSpecID, - "input_payment", payment, - "required_payment", cost, - }...) - err := fmt.Errorf( - "Rejecting job %s with payment %s below minimum threshold (%s)", - job.ID, - payment, - store.Config.MinimumContractPayment().Text(10)) - run.SetError(err) - } - } - - if len(run.TaskRuns) == 0 { - run.SetError(fmt.Errorf("invariant for job %s: no tasks to run in NewRun", job.ID.String())) - } - - if !run.Status.Runnable() { - return &run, nil - } - - initialTask := run.TaskRuns[0] - validateMinimumConfirmations(&run, &initialTask, run.CreationHeight, store) - return &run, nil -} - -// ResumeConfirmingTask resumes a confirming run if the minimum confirmations have been met -func ResumeConfirmingTask( - run *models.JobRun, - store *store.Store, - currentBlockHeight *big.Int, -) error { - - logger.Debugw("New head resuming run", run.ForLogger()...) - - if !run.Status.PendingConfirmations() && !run.Status.PendingConnection() { - return fmt.Errorf("Attempt to resume non confirming task") - } - - currentTaskRun := run.NextTaskRun() - if currentTaskRun == nil { - return fmt.Errorf("Attempting to resume confirming run with no remaining tasks %s", run.ID.String()) - } - - if currentBlockHeight == nil { - return fmt.Errorf("Attempting to resume confirming run with no currentBlockHeight %s", run.ID.String()) - } - - run.ObservedHeight = models.NewBig(currentBlockHeight) - - validateMinimumConfirmations(run, currentTaskRun, run.ObservedHeight, store) - return updateAndTrigger(run, store) -} - -// ResumeConnectingTask resumes a run that was left in pending_connection. -func ResumeConnectingTask( - run *models.JobRun, - store *store.Store, -) error { - - logger.Debugw("New connection resuming run", run.ForLogger()...) - - if !run.Status.PendingConnection() { - return fmt.Errorf("Attempt to resume non connecting task") - } - - currentTaskRun := run.NextTaskRun() - if currentTaskRun == nil { - return fmt.Errorf("Attempting to resume connecting run with no remaining tasks %s", run.ID.String()) - } - - run.Status = models.RunStatusInProgress - return updateAndTrigger(run, store) -} - -// ResumePendingTask takes the body provided from an external adapter, -// saves it for the next task to process, then tells the job runner to execute -// it -func ResumePendingTask( - run *models.JobRun, - store *store.Store, - input models.RunResult, -) error { - logger.Debugw("External adapter resuming job", []interface{}{ - "run", run.ID.String(), - "job", run.JobSpecID.String(), - "status", run.Status, - "input_data", input.Data, - "input_result", input.Status, - }...) - - if !run.Status.PendingBridge() { - return fmt.Errorf("Attempting to resume non pending run %s", run.ID.String()) - } - - currentTaskRun := run.NextTaskRun() - if currentTaskRun == nil { - return fmt.Errorf("Attempting to resume pending run with no remaining tasks %s", run.ID.String()) - } - - run.Overrides.Merge(input.Data) - - currentTaskRun.ApplyResult(input) - run.ApplyResult(input) - - return updateAndTrigger(run, store) -} - -func prepareAdapter( - taskRun *models.TaskRun, - data models.JSON, - store *store.Store, -) (*adapters.PipelineAdapter, error) { - taskCopy := taskRun.TaskSpec // deliberately copied to keep mutations local - - merged, err := taskCopy.Params.Merge(data) - if err != nil { - return nil, err + } else { + run.Status = models.RunStatusInProgress } - taskCopy.Params = merged - - return adapters.For(taskCopy, store) } -// QueueSleepingTask creates a go routine which will wake up the job runner -// once the sleep's time has elapsed -func QueueSleepingTask( - run *models.JobRun, - store *store.Store, -) error { - if !run.Status.PendingSleep() { - return fmt.Errorf("Attempting to resume non sleeping run %s", run.ID.String()) - } - - currentTaskRun := run.NextTaskRun() - if currentTaskRun == nil { - return fmt.Errorf("Attempting to resume sleeping run with no remaining tasks %s", run.ID.String()) - } - - if !currentTaskRun.Status.PendingSleep() { - return fmt.Errorf("Attempting to resume sleeping run with non sleeping task %s", run.ID.String()) +func validateOnMainChain(run *models.JobRun, taskRun *models.TaskRun, txManager store.TxManager) error { + txhash := run.RunRequest.TxHash + if txhash == nil || !taskRun.MinimumConfirmations.Valid || taskRun.MinimumConfirmations.Uint32 == 0 { + return nil } - adapter, err := prepareAdapter(currentTaskRun, run.Overrides, store) + receipt, err := txManager.GetTxReceipt(*txhash) if err != nil { - currentTaskRun.SetError(err) - run.SetError(err) - return store.SaveJobRun(run) - } - - if sleepAdapter, ok := adapter.BaseAdapter.(*adapters.Sleep); ok { - return performTaskSleep(run, currentTaskRun, sleepAdapter, store) + return err } - - return fmt.Errorf("Attempting to resume non sleeping task for run %s (%s)", run.ID.String(), currentTaskRun.TaskSpec.Type) -} - -func performTaskSleep( - run *models.JobRun, - task *models.TaskRun, - adapter *adapters.Sleep, - store *store.Store) error { - - duration := adapter.Duration() - if duration <= 0 { - logger.Debugw("Sleep duration has already elapsed, completing task", run.ForLogger()...) - task.Status = models.RunStatusCompleted - run.Status = models.RunStatusInProgress - return updateAndTrigger(run, store) + if invalidRequest(run.RunRequest, receipt) { + return fmt.Errorf( + "TxHash %s initiating run %s not on main chain; presumably has been uncled", + txhash.Hex(), + run.ID.String(), + ) } - - // XXX: This is to eliminate data race that occurs because slices share their - // underlying array even in copies - runCopy := *run - runCopy.TaskRuns = make([]models.TaskRun, len(run.TaskRuns)) - copy(runCopy.TaskRuns, run.TaskRuns) - - go func(run models.JobRun) { - logger.Debugw("Task sleeping...", run.ForLogger()...) - - <-store.Clock.After(duration) - - task := run.NextTaskRun() - task.Status = models.RunStatusCompleted - run.Status = models.RunStatusInProgress - - logger.Debugw("Waking job up after sleep", run.ForLogger()...) - - if err := updateAndTrigger(&run, store); err != nil { - logger.Errorw("Error resuming sleeping job:", "error", err) - } - }(runCopy) - return nil } -func validateMinimumConfirmations( - run *models.JobRun, - taskRun *models.TaskRun, - currentHeight *models.Big, - store *store.Store) { - - updateTaskRunConfirmations(currentHeight, run, taskRun) - if !meetsMinimumConfirmations(run, taskRun, run.ObservedHeight) { - logger.Debugw("Run cannot continue because it lacks sufficient confirmations", []interface{}{"run", run.ID.String(), "required_height", taskRun.MinimumConfirmations}...) - run.Status = models.RunStatusPendingConfirmations - } else if err := validateOnMainChain(run, taskRun, store); err != nil { - run.SetError(err) - } else { - logger.Debugw("Adding next task to job run queue", []interface{}{"run", run.ID.String(), "nextTask", taskRun.TaskSpec.Type}...) - run.Status = models.RunStatusInProgress - } -} - func updateTaskRunConfirmations(currentHeight *models.Big, jr *models.JobRun, taskRun *models.TaskRun) { if !taskRun.MinimumConfirmations.Valid || jr.CreationHeight == nil || currentHeight == nil { return @@ -381,26 +81,6 @@ func updateTaskRunConfirmations(currentHeight *models.Big, jr *models.JobRun, ta taskRun.Confirmations = clnull.Uint32From(uint32(diff.Int64())) } -func validateOnMainChain(jr *models.JobRun, taskRun *models.TaskRun, store *store.Store) error { - txhash := jr.RunRequest.TxHash - if txhash == nil || !taskRun.MinimumConfirmations.Valid || taskRun.MinimumConfirmations.Uint32 == 0 { - return nil - } - - receipt, err := store.TxManager.GetTxReceipt(*txhash) - if err != nil { - return err - } - if invalidRequest(jr.RunRequest, receipt) { - return fmt.Errorf( - "TxHash %s initiating run %s not on main chain; presumably has been uncled", - txhash.Hex(), - jr.ID.String(), - ) - } - return nil -} - func invalidRequest(request models.RunRequest, receipt *models.TxReceipt) bool { return receipt.Unconfirmed() || (request.BlockHash != nil && *request.BlockHash != *receipt.BlockHash) @@ -426,37 +106,3 @@ func blockConfirmations(currentHeight, creationHeight *models.Big) *big.Int { } return confs } - -func updateAndTrigger(run *models.JobRun, store *store.Store) error { - if err := store.SaveJobRun(run); err != nil { - return err - } - return triggerIfReady(run, store) -} - -func createAndTrigger(run *models.JobRun, store *store.Store) error { - if err := store.CreateJobRun(run); err != nil { - return errors.Wrap(err, "CreateJobRun failed") - } - return triggerIfReady(run, store) -} - -func triggerIfReady(run *models.JobRun, store *store.Store) error { - if run.Status == models.RunStatusInProgress { - logger.Debugw(fmt.Sprintf("Executing run originally initiated by %s", run.Initiator.Type), run.ForLogger()...) - return store.RunChannel.Send(run.ID) - } - - logger.Debugw(fmt.Sprintf("Pausing run originally initiated by %s", run.Initiator.Type), run.ForLogger()...) - return nil -} - -// RecurringScheduleJobError contains the field for the error message. -type RecurringScheduleJobError struct { - msg string -} - -// Error returns the error message for the run. -func (err RecurringScheduleJobError) Error() string { - return err.msg -} diff --git a/core/services/runs_test.go b/core/services/runs_test.go deleted file mode 100644 index bc036c279b3..00000000000 --- a/core/services/runs_test.go +++ /dev/null @@ -1,830 +0,0 @@ -package services_test - -import ( - "bytes" - "fmt" - "math" - "math/big" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - clnull "github.com/smartcontractkit/chainlink/core/null" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - null "gopkg.in/guregu/null.v3" -) - -func TestNewRun(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - input := models.JSON{Result: gjson.Parse(`{"address":"0xdfcfc2b9200dbb10952c2b7cce60fc7260e03c6f"}`)} - - _, bt := cltest.NewBridgeType(t, "timecube", "http://http://timecube.2enp.com/") - bt.MinimumContractPayment = assets.NewLink(10) - require.NoError(t, store.CreateBridgeType(bt)) - - creationHeight := big.NewInt(1000) - - jobSpec := models.NewJob() - jobSpec.Tasks = []models.TaskSpec{{ - Type: "timecube", - }} - jobSpec.Initiators = []models.Initiator{{ - Type: models.InitiatorEthLog, - }} - - inputResult := models.RunResult{Data: input} - run, err := services.NewRun(jobSpec, jobSpec.Initiators[0], inputResult, creationHeight, store, nil) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) - assert.Len(t, run.TaskRuns, 1) - assert.Equal(t, input, run.Overrides) - assert.False(t, run.TaskRuns[0].Confirmations.Valid) -} - -func TestNewRun_MeetsMinimumPayment(t *testing.T) { - tests := []struct { - name string - MinJobPayment *assets.Link - RunPayment *assets.Link - meetsMinPayment bool - }{ - {"insufficient payment", assets.NewLink(100), assets.NewLink(10), false}, - {"sufficient payment (strictly greater)", assets.NewLink(1), assets.NewLink(10), true}, - {"sufficient payment (equal)", assets.NewLink(10), assets.NewLink(10), true}, - {"runs that do not accept payments must return true", assets.NewLink(10), nil, true}, - {"return true when minpayment is not specified in jobspec", nil, assets.NewLink(0), true}, - } - - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - actual := services.MeetsMinimumPayment(test.MinJobPayment, test.RunPayment) - assert.Equal(t, test.meetsMinPayment, actual) - }) - } -} - -func TestNewRun_jobSpecMinPayment(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - input := models.JSON{Result: gjson.Parse(`{"address":"0xdfcfc2b9200dbb10952c2b7cce60fc7260e03c6f"}`)} - - tests := []struct { - name string - payment *assets.Link - minPayment *assets.Link - expectedStatus models.RunStatus - }{ - {"payment < min payment", assets.NewLink(9), assets.NewLink(10), models.RunStatusErrored}, - {"payment = min payment", assets.NewLink(10), assets.NewLink(10), models.RunStatusInProgress}, - {"payment > min payment", assets.NewLink(11), assets.NewLink(10), models.RunStatusInProgress}, - {"payment is nil", nil, assets.NewLink(10), models.RunStatusInProgress}, - {"minPayment is nil", nil, nil, models.RunStatusInProgress}, - } - - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - jobSpec := models.NewJob() - jobSpec.Tasks = []models.TaskSpec{{ - Type: "noop", - }} - jobSpec.Initiators = []models.Initiator{{ - Type: models.InitiatorEthLog, - }} - jobSpec.MinPayment = test.minPayment - - inputResult := models.RunResult{Data: input} - - run, err := services.NewRun(jobSpec, jobSpec.Initiators[0], inputResult, nil, store, test.payment) - assert.NoError(t, err) - assert.Equal(t, string(test.expectedStatus), string(run.Status)) - }) - } -} - -func TestNewRun_taskSumPayment(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - _, bta := cltest.NewBridgeType(t, "timecube_a", "http://http://timecube.2enp.com/") - bta.MinimumContractPayment = assets.NewLink(8) - require.NoError(t, store.CreateBridgeType(bta)) - - _, btb := cltest.NewBridgeType(t, "timecube_b", "http://http://timecube.2enp.com/") - btb.MinimumContractPayment = assets.NewLink(7) - require.NoError(t, store.CreateBridgeType(btb)) - - store.Config.Set("MINIMUM_CONTRACT_PAYMENT", "1") - - input := models.JSON{Result: gjson.Parse(`{"address":"0xdfcfc2b9200dbb10952c2b7cce60fc7260e03c6f"}`)} - - tests := []struct { - name string - payment *assets.Link - expectedStatus models.RunStatus - }{ - {"payment < min payment", assets.NewLink(15), models.RunStatusErrored}, - {"payment = min payment", assets.NewLink(16), models.RunStatusInProgress}, - {"payment > min payment", assets.NewLink(17), models.RunStatusInProgress}, - {"payment is nil", nil, models.RunStatusInProgress}, - } - - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - jobSpec := models.NewJob() - jobSpec.Tasks = []models.TaskSpec{ - {Type: "timecube_a"}, - {Type: "timecube_b"}, - {Type: "ethtx"}, - {Type: "noop"}, - } - jobSpec.Initiators = []models.Initiator{{ - Type: models.InitiatorEthLog, - }} - - inputResult := models.RunResult{Data: input} - - run, err := services.NewRun(jobSpec, jobSpec.Initiators[0], inputResult, nil, store, test.payment) - assert.NoError(t, err) - assert.Equal(t, string(test.expectedStatus), string(run.Status)) - }) - } -} - -func TestNewRun_minimumConfirmations(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - input := models.JSON{Result: gjson.Parse(`{"address":"0xdfcfc2b9200dbb10952c2b7cce60fc7260e03c6f"}`)} - inputResult := models.RunResult{Data: input} - - creationHeight := big.NewInt(1000) - - tests := []struct { - name string - configConfirmations uint32 - taskConfirmations uint32 - expectedStatus models.RunStatus - }{ - {"creates runnable job", 0, 0, models.RunStatusInProgress}, - {"requires minimum task confirmations", 2, 0, models.RunStatusPendingConfirmations}, - {"requires minimum config confirmations", 0, 2, models.RunStatusPendingConfirmations}, - } - - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - store.Config.Set("MIN_INCOMING_CONFIRMATIONS", test.configConfirmations) - - jobSpec := cltest.NewJobWithLogInitiator() - jobSpec.Tasks[0].Confirmations = clnull.Uint32From(test.taskConfirmations) - - run, err := services.NewRun( - jobSpec, - jobSpec.Initiators[0], - inputResult, - creationHeight, - store, - nil) - assert.NoError(t, err) - assert.Equal(t, string(test.expectedStatus), string(run.Status)) - require.Len(t, run.TaskRuns, 1) - max := utils.MaxUint32(test.taskConfirmations, test.configConfirmations) - assert.Equal(t, max, run.TaskRuns[0].MinimumConfirmations.Uint32) - }) - } -} - -func TestNewRun_startAtAndEndAt(t *testing.T) { - pastTime := cltest.ParseNullableTime(t, "2000-01-01T00:00:00.000Z") - futureTime := cltest.ParseNullableTime(t, "3000-01-01T00:00:00.000Z") - nullTime := null.Time{Valid: false} - - tests := []struct { - name string - startAt null.Time - endAt null.Time - errored bool - }{ - {"job not started", futureTime, nullTime, true}, - {"job started", pastTime, futureTime, false}, - {"job with no time range", nullTime, nullTime, false}, - {"job ended", nullTime, pastTime, true}, - } - - store, cleanup := cltest.NewStore(t) - defer cleanup() - clock := cltest.UseSettableClock(store) - clock.SetTime(time.Now()) - - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - job := cltest.NewJobWithWebInitiator() - job.StartAt = test.startAt - job.EndAt = test.endAt - assert.Nil(t, store.CreateJob(&job)) - - _, err := services.NewRun(job, job.Initiators[0], models.RunResult{}, nil, store, nil) - if test.errored { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestNewRun_noTasksErrorsInsteadOfPanic(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - job := cltest.NewJobWithWebInitiator() - job.Tasks = []models.TaskSpec{} - require.NoError(t, store.CreateJob(&job)) - - jr, err := services.NewRun(job, job.Initiators[0], models.RunResult{}, nil, store, nil) - assert.NoError(t, err) - assert.True(t, jr.Status.Errored()) - assert.True(t, jr.Result.HasError()) -} - -func TestResumePendingTask(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - // reject a run with an invalid state - jobID := models.NewID() - runID := models.NewID() - run := &models.JobRun{ - ID: runID, - JobSpecID: jobID, - } - err := services.ResumePendingTask(run, store, models.RunResult{}) - assert.Error(t, err) - - // reject a run with no tasks - run = &models.JobRun{ - ID: runID, - JobSpecID: jobID, - Status: models.RunStatusPendingBridge, - } - err = services.ResumePendingTask(run, store, models.RunResult{}) - assert.Error(t, err) - - // input with error errors run - run.TaskRuns = []models.TaskRun{models.TaskRun{ID: models.NewID(), JobRunID: runID}} - err = services.ResumePendingTask(run, store, models.RunResult{CachedJobRunID: runID, Status: models.RunStatusErrored}) - assert.Error(t, err) - assert.True(t, run.FinishedAt.Valid) - - // completed input with remaining tasks should put task into pending - run = &models.JobRun{ - ID: runID, - JobSpecID: jobID, - Status: models.RunStatusPendingBridge, - TaskRuns: []models.TaskRun{models.TaskRun{ID: models.NewID(), JobRunID: runID}, models.TaskRun{ID: models.NewID(), JobRunID: runID}}, - } - input := models.JSON{Result: gjson.Parse(`{"address":"0xdfcfc2b9200dbb10952c2b7cce60fc7260e03c6f"}`)} - err = services.ResumePendingTask(run, store, models.RunResult{CachedJobRunID: runID, Data: input, Status: models.RunStatusCompleted}) - assert.Error(t, err) - assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) - assert.Len(t, run.TaskRuns, 2) - assert.Equal(t, run.ID, run.TaskRuns[0].Result.CachedJobRunID) - assert.Equal(t, string(models.RunStatusCompleted), string(run.TaskRuns[0].Result.Status)) - - // completed input with no remaining tasks should get marked as complete - run = &models.JobRun{ - ID: runID, - JobSpecID: jobID, - Status: models.RunStatusPendingBridge, - TaskRuns: []models.TaskRun{models.TaskRun{ID: models.NewID(), JobRunID: runID}}, - } - err = services.ResumePendingTask(run, store, models.RunResult{CachedJobRunID: runID, Data: input, Status: models.RunStatusCompleted}) - assert.Error(t, err) - assert.Equal(t, string(models.RunStatusCompleted), string(run.Status)) - assert.True(t, run.FinishedAt.Valid) - assert.Len(t, run.TaskRuns, 1) - assert.Equal(t, run.ID, run.TaskRuns[0].Result.CachedJobRunID) - assert.Equal(t, string(models.RunStatusCompleted), string(run.TaskRuns[0].Result.Status)) -} - -func TestResumeConfirmingTask(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - // reject a run with an invalid state - jobID := models.NewID() - runID := models.NewID() - run := &models.JobRun{ - ID: runID, - JobSpecID: jobID, - } - err := services.ResumeConfirmingTask(run, store, nil) - assert.Error(t, err) - - // reject a run with no tasks - run = &models.JobRun{ - ID: runID, - JobSpecID: jobID, - Status: models.RunStatusPendingConfirmations, - } - err = services.ResumeConfirmingTask(run, store, nil) - assert.Error(t, err) - - jobSpec := models.JobSpec{ID: models.NewID()} - require.NoError(t, store.ORM.CreateJob(&jobSpec)) - - // leave in pending if not enough confirmations have been met yet - creationHeight := models.NewBig(big.NewInt(0)) - run = &models.JobRun{ - ID: models.NewID(), - JobSpecID: jobSpec.ID, - CreationHeight: creationHeight, - Status: models.RunStatusPendingConfirmations, - TaskRuns: []models.TaskRun{models.TaskRun{ - ID: models.NewID(), - MinimumConfirmations: clnull.Uint32From(2), - TaskSpec: models.TaskSpec{ - JobSpecID: jobSpec.ID, - Type: adapters.TaskTypeNoOp, - }, - }}, - } - require.NoError(t, store.CreateJobRun(run)) - err = services.ResumeConfirmingTask(run, store, creationHeight.ToInt()) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusPendingConfirmations), string(run.Status)) - assert.Equal(t, uint32(1), run.TaskRuns[0].Confirmations.Uint32) - - // input, should go from pending -> in progress and save the input - run = &models.JobRun{ - ID: models.NewID(), - JobSpecID: jobSpec.ID, - CreationHeight: creationHeight, - Status: models.RunStatusPendingConfirmations, - TaskRuns: []models.TaskRun{models.TaskRun{ - ID: models.NewID(), - MinimumConfirmations: clnull.Uint32From(1), - TaskSpec: models.TaskSpec{ - JobSpecID: jobSpec.ID, - Type: adapters.TaskTypeNoOp, - }, - }}, - } - observedHeight := big.NewInt(1) - require.NoError(t, store.CreateJobRun(run)) - err = services.ResumeConfirmingTask(run, store, observedHeight) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) -} - -func TestResumeConnectingTask(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - - // reject a run with an invalid state - jobID := models.NewID() - runID := models.NewID() - run := &models.JobRun{ - ID: runID, - JobSpecID: jobID, - } - err := services.ResumeConnectingTask(run, store) - assert.Error(t, err) - - // reject a run with no tasks - run = &models.JobRun{ - ID: runID, - JobSpecID: jobID, - Status: models.RunStatusPendingConnection, - } - err = services.ResumeConnectingTask(run, store) - assert.Error(t, err) - - jobSpec := models.JobSpec{ID: models.NewID()} - require.NoError(t, store.ORM.CreateJob(&jobSpec)) - - taskSpec := models.TaskSpec{Type: adapters.TaskTypeNoOp, JobSpecID: jobSpec.ID} - // input, should go from pending -> in progress and save the input - run = &models.JobRun{ - ID: models.NewID(), - JobSpecID: jobSpec.ID, - Status: models.RunStatusPendingConnection, - TaskRuns: []models.TaskRun{models.TaskRun{ - ID: models.NewID(), - TaskSpec: taskSpec, - }}, - } - require.NoError(t, store.CreateJobRun(run)) - err = services.ResumeConnectingTask(run, store) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) -} - -func sleepAdapterParams(t testing.TB, n int) models.JSON { - d := time.Duration(n) - json := []byte(fmt.Sprintf(`{"until":%v}`, time.Now().Add(d*time.Second).Unix())) - return cltest.ParseJSON(t, bytes.NewBuffer(json)) -} - -func TestQueueSleepingTask(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - store.Clock = cltest.NeverClock{} - - // reject a run with an invalid state - jobID := models.NewID() - runID := models.NewID() - run := &models.JobRun{ - ID: runID, - JobSpecID: jobID, - } - err := services.QueueSleepingTask(run, store) - assert.Error(t, err) - - // reject a run with no tasks - run = &models.JobRun{ - ID: runID, - JobSpecID: jobID, - Status: models.RunStatusPendingSleep, - } - err = services.QueueSleepingTask(run, store) - assert.Error(t, err) - - jobSpec := models.JobSpec{ID: models.NewID()} - require.NoError(t, store.ORM.CreateJob(&jobSpec)) - - // reject a run that is sleeping but its task is not - run = &models.JobRun{ - ID: models.NewID(), - JobSpecID: jobSpec.ID, - Status: models.RunStatusPendingSleep, - TaskRuns: []models.TaskRun{models.TaskRun{ - ID: models.NewID(), - TaskSpec: models.TaskSpec{Type: adapters.TaskTypeSleep, JobSpecID: jobSpec.ID}, - }}, - } - require.NoError(t, store.CreateJobRun(run)) - err = services.QueueSleepingTask(run, store) - assert.Error(t, err) - - // error decoding params into adapter - inputFromTheFuture := cltest.ParseJSON(t, bytes.NewBuffer([]byte(`{"until": -1}`))) - run = &models.JobRun{ - ID: models.NewID(), - JobSpecID: jobSpec.ID, - Status: models.RunStatusPendingSleep, - TaskRuns: []models.TaskRun{ - models.TaskRun{ - ID: models.NewID(), - Status: models.RunStatusPendingSleep, - TaskSpec: models.TaskSpec{ - JobSpecID: jobSpec.ID, - Type: adapters.TaskTypeSleep, - Params: inputFromTheFuture, - }, - }, - }, - } - require.NoError(t, store.CreateJobRun(run)) - err = services.QueueSleepingTask(run, store) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusErrored), string(run.TaskRuns[0].Status)) - assert.Equal(t, string(models.RunStatusErrored), string(run.Status)) - - // mark run as pending, task as completed if duration has already elapsed - run = &models.JobRun{ - ID: models.NewID(), - JobSpecID: jobSpec.ID, - Status: models.RunStatusPendingSleep, - TaskRuns: []models.TaskRun{models.TaskRun{ - ID: models.NewID(), - Status: models.RunStatusPendingSleep, - TaskSpec: models.TaskSpec{Type: adapters.TaskTypeSleep, JobSpecID: jobSpec.ID}, - }}, - } - require.NoError(t, store.CreateJobRun(run)) - err = services.QueueSleepingTask(run, store) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusCompleted), string(run.TaskRuns[0].Status)) - assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) - - runRequest, open := <-store.RunChannel.Receive() - assert.True(t, open) - assert.Equal(t, run.ID, runRequest.ID) - -} - -func TestQueueSleepingTaskA_CompletesSleepingTaskAfterDurationElapsed_Happy(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - store.Clock = cltest.NeverClock{} - - jobSpec := models.JobSpec{ID: models.NewID()} - require.NoError(t, store.ORM.CreateJob(&jobSpec)) - - // queue up next run if duration has not elapsed yet - clock := cltest.UseSettableClock(store) - store.Clock = clock - clock.SetTime(time.Time{}) - - inputFromTheFuture := sleepAdapterParams(t, 60) - run := &models.JobRun{ - ID: models.NewID(), - JobSpecID: jobSpec.ID, - Status: models.RunStatusPendingSleep, - TaskRuns: []models.TaskRun{ - models.TaskRun{ - ID: models.NewID(), - Status: models.RunStatusPendingSleep, - TaskSpec: models.TaskSpec{ - JobSpecID: jobSpec.ID, - Type: adapters.TaskTypeSleep, - Params: inputFromTheFuture, - }, - }, - }, - } - require.NoError(t, store.CreateJobRun(run)) - err := services.QueueSleepingTask(run, store) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusPendingSleep), string(run.TaskRuns[0].Status)) - assert.Equal(t, string(models.RunStatusPendingSleep), string(run.Status)) - - // force the duration elapse - clock.SetTime((time.Time{}).Add(math.MaxInt64)) - runRequest, open := <-store.RunChannel.Receive() - assert.True(t, open) - assert.Equal(t, run.ID, runRequest.ID) - - *run, err = store.ORM.FindJobRun(run.ID) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusCompleted), string(run.TaskRuns[0].Status)) - assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) -} - -func TestQueueSleepingTaskA_CompletesSleepingTaskAfterDurationElapsed_Archived(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - store.Clock = cltest.NeverClock{} - - jobSpec := models.JobSpec{ID: models.NewID()} - require.NoError(t, store.ORM.CreateJob(&jobSpec)) - - // queue up next run if duration has not elapsed yet - clock := cltest.UseSettableClock(store) - store.Clock = clock - clock.SetTime(time.Time{}) - - inputFromTheFuture := sleepAdapterParams(t, 60) - run := &models.JobRun{ - ID: models.NewID(), - JobSpecID: jobSpec.ID, - Status: models.RunStatusPendingSleep, - TaskRuns: []models.TaskRun{ - models.TaskRun{ - ID: models.NewID(), - Status: models.RunStatusPendingSleep, - TaskSpec: models.TaskSpec{ - JobSpecID: jobSpec.ID, - Type: adapters.TaskTypeSleep, - Params: inputFromTheFuture, - }, - }, - }, - } - require.NoError(t, store.CreateJobRun(run)) - require.NoError(t, store.ArchiveJob(jobSpec.ID)) - - unscoped := store.Unscoped() - err := services.QueueSleepingTask(run, unscoped) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusPendingSleep), string(run.TaskRuns[0].Status)) - assert.Equal(t, string(models.RunStatusPendingSleep), string(run.Status)) - - // force the duration elapse - clock.SetTime((time.Time{}).Add(math.MaxInt64)) - runRequest, open := <-store.RunChannel.Receive() - assert.True(t, open) - assert.Equal(t, run.ID, runRequest.ID) - - require.Error(t, utils.JustError(store.FindJobRun(run.ID)), "archived runs should not be visible to normal store") - - *run, err = unscoped.FindJobRun(run.ID) - assert.NoError(t, err) - assert.Equal(t, string(models.RunStatusCompleted), string(run.TaskRuns[0].Status)) - assert.Equal(t, string(models.RunStatusInProgress), string(run.Status)) -} - -func TestExecuteJob_DoesNotSaveToTaskSpec(t *testing.T) { - t.Parallel() - app, cleanup := cltest.NewApplication(t) - defer cleanup() - - store := app.Store - eth := cltest.MockEthOnStore(t, store) - eth.Register("eth_chainId", store.Config.ChainID()) - - app.Start() - - job := cltest.NewJobWithWebInitiator() - job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} // empty params - require.NoError(t, store.CreateJob(&job)) - - initr := job.Initiators[0] - jr, err := services.ExecuteJob( - job, - initr, - cltest.RunResultWithData(`{"random": "input"}`), - nil, - store, - ) - require.NoError(t, err) - cltest.WaitForJobRunToComplete(t, store, *jr) - - retrievedJob, err := store.FindJob(job.ID) - require.NoError(t, err) - require.Len(t, job.Tasks, 1) - require.Len(t, retrievedJob.Tasks, 1) - assert.Equal(t, job.Tasks[0].Params, retrievedJob.Tasks[0].Params) -} - -func TestExecuteJobWithRunRequest(t *testing.T) { - t.Parallel() - app, cleanup := cltest.NewApplication(t) - defer cleanup() - - store := app.Store - eth := cltest.MockEthOnStore(t, store) - eth.Register("eth_chainId", store.Config.ChainID()) - - app.Start() - - job := cltest.NewJobWithRunLogInitiator() - job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} // empty params - require.NoError(t, store.CreateJob(&job)) - - requestID := "RequestID" - initr := job.Initiators[0] - rr := models.NewRunRequest() - rr.RequestID = &requestID - jr, err := services.ExecuteJobWithRunRequest( - job, - initr, - cltest.RunResultWithData(`{"random": "input"}`), - nil, - store, - rr, - ) - require.NoError(t, err) - updatedJR := cltest.WaitForJobRunToComplete(t, store, *jr) - assert.Equal(t, rr.RequestID, updatedJR.RunRequest.RequestID) -} - -func TestExecuteJobWithRunRequest_fromRunLog_Happy(t *testing.T) { - t.Parallel() - - initiatingTxHash := cltest.NewHash() - triggeringBlockHash := cltest.NewHash() - otherBlockHash := cltest.NewHash() - - tests := []struct { - name string - logBlockHash common.Hash - receiptBlockHash common.Hash - wantStatus models.RunStatus - }{ - { - name: "main chain", - logBlockHash: triggeringBlockHash, - receiptBlockHash: triggeringBlockHash, - wantStatus: models.RunStatusCompleted, - }, - { - name: "ommered chain", - logBlockHash: triggeringBlockHash, - receiptBlockHash: otherBlockHash, - wantStatus: models.RunStatusErrored, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - config, cfgCleanup := cltest.NewConfig(t) - defer cfgCleanup() - minimumConfirmations := uint32(2) - config.Set("MIN_INCOMING_CONFIRMATIONS", minimumConfirmations) - app, cleanup := cltest.NewApplicationWithConfig(t, config) - defer cleanup() - - eth := app.MockEthCallerSubscriber() - app.Start() - - store := app.GetStore() - job := cltest.NewJobWithRunLogInitiator() - job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} - require.NoError(t, store.CreateJob(&job)) - - creationHeight := big.NewInt(1) - requestID := "RequestID" - initr := job.Initiators[0] - rr := models.NewRunRequest() - rr.RequestID = &requestID - rr.TxHash = &initiatingTxHash - rr.BlockHash = &test.logBlockHash - jr, err := services.ExecuteJobWithRunRequest( - job, - initr, - cltest.RunResultWithData(`{"random": "input"}`), - creationHeight, - store, - rr, - ) - require.NoError(t, err) - cltest.WaitForJobRunToPendConfirmations(t, app.Store, *jr) - - confirmedReceipt := models.TxReceipt{ - Hash: initiatingTxHash, - BlockHash: &test.receiptBlockHash, - BlockNumber: cltest.Int(3), - } - eth.Context("validateOnMainChain", func(ethMock *cltest.EthMock) { - eth.Register("eth_getTransactionReceipt", confirmedReceipt) - }) - - err = services.ResumeConfirmingTask(jr, store, big.NewInt(2)) - require.NoError(t, err) - updatedJR := cltest.WaitForJobRunStatus(t, store, *jr, test.wantStatus) - assert.Equal(t, rr.RequestID, updatedJR.RunRequest.RequestID) - assert.Equal(t, minimumConfirmations, updatedJR.TaskRuns[0].MinimumConfirmations.Uint32) - assert.True(t, updatedJR.TaskRuns[0].MinimumConfirmations.Valid) - assert.Equal(t, minimumConfirmations, updatedJR.TaskRuns[0].Confirmations.Uint32, "task run should track its current confirmations") - assert.True(t, updatedJR.TaskRuns[0].Confirmations.Valid) - assert.True(t, eth.AllCalled(), eth.Remaining()) - }) - } -} - -func TestExecuteJobWithRunRequest_fromRunLog_ConnectToLaggingEthNode(t *testing.T) { - t.Parallel() - - initiatingTxHash := cltest.NewHash() - triggeringBlockHash := cltest.NewHash() - - config, cfgCleanup := cltest.NewConfig(t) - defer cfgCleanup() - minimumConfirmations := uint32(2) - config.Set("MIN_INCOMING_CONFIRMATIONS", minimumConfirmations) - app, cleanup := cltest.NewApplicationWithConfig(t, config) - defer cleanup() - - eth := app.MockEthCallerSubscriber() - app.MockStartAndConnect() - - store := app.GetStore() - job := cltest.NewJobWithRunLogInitiator() - job.Tasks = []models.TaskSpec{cltest.NewTask(t, "NoOp")} - require.NoError(t, store.CreateJob(&job)) - - requestID := "RequestID" - initr := job.Initiators[0] - rr := models.NewRunRequest() - rr.RequestID = &requestID - rr.TxHash = &initiatingTxHash - rr.BlockHash = &triggeringBlockHash - - futureCreationHeight := big.NewInt(9) - pastCurrentHeight := big.NewInt(1) - - jr, err := services.ExecuteJobWithRunRequest( - job, - initr, - cltest.RunResultWithData(`{"random": "input"}`), - futureCreationHeight, - store, - rr, - ) - require.NoError(t, err) - cltest.WaitForJobRunToPendConfirmations(t, app.Store, *jr) - - err = services.ResumeConfirmingTask(jr, store, pastCurrentHeight) - require.NoError(t, err) - updatedJR := cltest.WaitForJobRunToPendConfirmations(t, app.Store, *jr) - assert.True(t, updatedJR.TaskRuns[0].Confirmations.Valid) - assert.Equal(t, uint32(0), updatedJR.TaskRuns[0].Confirmations.Uint32) - assert.True(t, eth.AllCalled(), eth.Remaining()) -} diff --git a/core/services/scheduler.go b/core/services/scheduler.go index 05e2b79817b..2a7f3b9778a 100644 --- a/core/services/scheduler.go +++ b/core/services/scheduler.go @@ -4,11 +4,12 @@ import ( "errors" "sync" + "chainlink/core/logger" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/mrwonko/cron" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" ) // Scheduler contains fields for Recurring and OneTime for occurrences, @@ -18,20 +19,23 @@ type Scheduler struct { Recurring *Recurring OneTime *OneTime store *store.Store + runManager RunManager startedMutex sync.RWMutex started bool } // NewScheduler initializes the Scheduler instances with both Recurring // and OneTime fields since jobs can contain tasks which utilize both. -func NewScheduler(store *store.Store) *Scheduler { +func NewScheduler(store *store.Store, runManager RunManager) *Scheduler { return &Scheduler{ - Recurring: NewRecurring(store), + Recurring: NewRecurring(runManager), OneTime: &OneTime{ - Store: store, - Clock: store.Clock, + Store: store, + Clock: store.Clock, + RunManager: runManager, }, - store: store, + store: store, + runManager: runManager, } } @@ -53,7 +57,7 @@ func (s *Scheduler) Start() error { } s.started = true - return s.store.Jobs(func(j models.JobSpec) bool { + return s.store.Jobs(func(j *models.JobSpec) bool { s.addJob(j) return true }) @@ -71,9 +75,9 @@ func (s *Scheduler) Stop() { } } -func (s *Scheduler) addJob(job models.JobSpec) { - s.Recurring.AddJob(job) - s.OneTime.AddJob(job) +func (s *Scheduler) addJob(job *models.JobSpec) { + s.Recurring.AddJob(*job) + s.OneTime.AddJob(*job) } // AddJob is the governing function for Recurring and OneTime, @@ -84,23 +88,22 @@ func (s *Scheduler) AddJob(job models.JobSpec) { if !s.started { return } - s.addJob(job) + s.addJob(&job) } // Recurring is used for runs that need to execute on a schedule, // and is configured with cron. // Instances of Recurring must be initialized using NewRecurring(). type Recurring struct { - Cron Cron - Clock utils.Nower - store *store.Store + Cron Cron + Clock utils.Nower + runManager RunManager } // NewRecurring create a new instance of Recurring, ready to use. -func NewRecurring(store *store.Store) *Recurring { +func NewRecurring(runManager RunManager) *Recurring { return &Recurring{ - store: store, - Clock: store.Clock, + runManager: runManager, } } @@ -120,29 +123,22 @@ func (r *Recurring) Stop() { // AddJob looks for "cron" initiators, adds them to cron's schedule // for execution when specified. func (r *Recurring) AddJob(job models.JobSpec) { - for _, i := range job.InitiatorsFor(models.InitiatorCron) { - initr := i - if !job.Ended(r.Clock.Now()) { - archived := false - r.Cron.AddFunc(string(initr.Schedule), func() { - if archived || r.store.Archived(job.ID) { - archived = true - return - } - _, err := ExecuteJob(job, initr, models.RunResult{}, nil, r.store) - if err != nil && !expectedRecurringScheduleJobError(err) { - logger.Errorw(err.Error()) - } - }) - } + for _, initr := range job.InitiatorsFor(models.InitiatorCron) { + r.Cron.AddFunc(string(initr.Schedule), func() { + _, err := r.runManager.Create(job.ID, &initr, &models.JSON{}, nil, &models.RunRequest{}) + if err != nil && !expectedRecurringScheduleJobError(err) { + logger.Errorw(err.Error()) + } + }) } } // OneTime represents runs that are to be executed only once. type OneTime struct { - Store *store.Store - Clock utils.Afterer - done chan struct{} + Store *store.Store + Clock utils.Afterer + RunManager RunManager + done chan struct{} } // Start allocates a channel for the "done" field with an empty struct. @@ -153,8 +149,13 @@ func (ot *OneTime) Start() error { // AddJob runs the job at the time specified for the "runat" initiator. func (ot *OneTime) AddJob(job models.JobSpec) { - for _, initr := range job.InitiatorsFor(models.InitiatorRunAt) { - go ot.RunJobAt(initr, job) + for _, initiator := range job.InitiatorsFor(models.InitiatorRunAt) { + if !initiator.Time.Valid { + logger.Errorf("RunJobAt: JobSpec %s must have initiator with valid run at time: %v", job.ID, initiator) + continue + } + + go ot.RunJobAt(initiator, job) } } @@ -165,27 +166,17 @@ func (ot *OneTime) Stop() { // RunJobAt wait until the Stop() function has been called on the run // or the specified time for the run is after the present time. -func (ot *OneTime) RunJobAt(initr models.Initiator, job models.JobSpec) { - if !initr.Time.Valid { - logger.Errorf("RunJobAt: JobSpec %s must have initiator with valid run at time: %v", job.ID.String(), initr) - return - } +func (ot *OneTime) RunJobAt(initiator models.Initiator, job models.JobSpec) { select { case <-ot.done: - case <-ot.Clock.After(utils.DurationFromNow(initr.Time.Time)): - if ot.Store.Archived(job.ID) { - return - } - if err := ot.Store.MarkRan(&initr, true); err != nil { + case <-ot.Clock.After(utils.DurationFromNow(initiator.Time.Time)): + _, err := ot.RunManager.Create(job.ID, &initiator, &models.JSON{}, nil, &models.RunRequest{}) + if err != nil { logger.Error(err.Error()) return } - _, err := ExecuteJob(job, initr, models.RunResult{}, nil, ot.Store) - if err != nil { + if err := ot.Store.MarkRan(&initiator, true); err != nil { logger.Error(err.Error()) - if err := ot.Store.MarkRan(&initr, false); err != nil { - logger.Error(err.Error()) - } } } } diff --git a/core/services/scheduler_test.go b/core/services/scheduler_test.go index 442c30c8d23..fbbd2062035 100644 --- a/core/services/scheduler_test.go +++ b/core/services/scheduler_test.go @@ -4,14 +4,12 @@ import ( "testing" "time" - "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/stretchr/testify/assert" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/services" + + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/tevino/abool" - null "gopkg.in/guregu/null.v3" ) func TestScheduler_Start_LoadingRecurringJobs(t *testing.T) { @@ -25,322 +23,102 @@ func TestScheduler_Start_LoadingRecurringJobs(t *testing.T) { jobWoCron := cltest.NewJob() require.NoError(t, store.CreateJob(&jobWoCron)) - sched := services.NewScheduler(store) - require.NoError(t, sched.Start()) - defer sched.Stop() - - cltest.WaitForRunsAtLeast(t, jobWCron, store, 1) - cltest.WaitForRuns(t, jobWoCron, store, 0) -} - -func TestScheduler_AddJob_WhenStopped(t *testing.T) { - t.Parallel() - - store, cleanup := cltest.NewStore(t) - defer cleanup() - sched := services.NewScheduler(store) - defer sched.Stop() - - j := cltest.NewJobWithSchedule("* * * * *") - assert.Nil(t, store.CreateJob(&j)) - sched.AddJob(j) - - cltest.WaitForRuns(t, j, store, 0) -} - -func TestScheduler_Start_AddingUnstartedJob(t *testing.T) { - store, cleanupStore := cltest.NewStore(t) - defer cleanupStore() - clock := cltest.UseSettableClock(store) - - startAt := cltest.ParseISO8601(t, "3000-01-01T00:00:00.000Z") - j := cltest.NewJobWithSchedule("* * * * *") - j.StartAt = cltest.NullableTime(startAt) - assert.Nil(t, store.CreateJob(&j)) + executeJobChannel := make(chan struct{}) + runManager := new(mocks.RunManager) + runManager.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, nil). + Twice(). + Run(func(mock.Arguments) { + executeJobChannel <- struct{}{} + }) - sched := services.NewScheduler(store) - defer sched.Stop() - assert.Nil(t, sched.Start()) + sched := services.NewScheduler(store, runManager) + require.NoError(t, sched.Start()) - gomega.NewGomegaWithT(t).Consistently(func() int { - runs, err := store.JobRunsFor(j.ID) - assert.NoError(t, err) - return len(runs) - }, (2 * time.Second)).Should(gomega.Equal(0)) + cltest.CallbackOrTimeout(t, "Create", func() { + <-executeJobChannel + <-executeJobChannel + }, 3*time.Second) - clock.SetTime(startAt) + sched.Stop() - cltest.WaitForRunsAtLeast(t, j, store, 2) + runManager.AssertExpectations(t) } func TestRecurring_AddJob(t *testing.T) { - nullTime := cltest.NullTime(t, nil) - pastTime := cltest.NullTime(t, "2000-01-01T00:00:00.000Z") - futureTime := cltest.NullTime(t, "3000-01-01T00:00:00.000Z") - tests := []struct { - name string - startAt null.Time - endAt null.Time - wantEntries int - wantRuns int - }{ - {"before start at", futureTime, nullTime, 1, 0}, - {"before end at", nullTime, futureTime, 1, 1}, - {"after start at", pastTime, nullTime, 1, 1}, - {"after end at", nullTime, pastTime, 0, 0}, - {"no range", nullTime, nullTime, 1, 1}, - {"start at after end at", futureTime, pastTime, 0, 0}, - } - - store, cleanup := cltest.NewStore(t) - defer cleanup() - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - r := services.NewRecurring(store) - cron := cltest.NewMockCron() - r.Cron = cron - defer r.Stop() - - job := cltest.NewJobWithSchedule("* * * * *") - job.StartAt = test.startAt - job.EndAt = test.endAt - - require.NoError(t, store.CreateJob(&job)) - r.AddJob(job) - - assert.Equal(t, test.wantEntries, len(cron.Entries)) - - cron.RunEntries() - jobRuns, err := store.JobRunsFor(job.ID) - assert.NoError(t, err) - assert.Equal(t, test.wantRuns, len(jobRuns)) - }) - } -} - -func TestRecurring_AddJob_Archived(t *testing.T) { - store, cleanup := cltest.NewStore(t) - defer cleanup() - r := services.NewRecurring(store) + executeJobChannel := make(chan struct{}, 1) + runManager := new(mocks.RunManager) + runManager.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, nil). + Run(func(mock.Arguments) { + executeJobChannel <- struct{}{} + }). + Twice() + + r := services.NewRecurring(runManager) cron := cltest.NewMockCron() r.Cron = cron - defer r.Stop() job := cltest.NewJobWithSchedule("* * * * *") - require.NoError(t, store.CreateJob(&job)) r.AddJob(job) cron.RunEntries() - count, err := store.Unscoped().JobRunsCountFor(job.ID) - require.NoError(t, err) - assert.Equal(t, 1, count) - - require.NoError(t, store.ArchiveJob(job.ID)) - cron.RunEntries() - - count, err = store.Unscoped().JobRunsCountFor(job.ID) - require.NoError(t, err) - assert.Equal(t, 1, count) -} - -func TestOneTime_AddJob(t *testing.T) { - nullTime := cltest.NullTime(t, nil) - pastTime := cltest.NullTime(t, "2000-01-01T00:00:00.000Z") - futureTime := cltest.NullTime(t, "3000-01-01T00:00:00.000Z") - pastRunTime := time.Now().Add(time.Hour * -1) - tests := []struct { - name string - startAt null.Time - endAt null.Time - runAt time.Time - wantCompleted bool - }{ - {"run at before start at", futureTime, nullTime, pastRunTime, false}, - {"run at before end at", nullTime, futureTime, pastRunTime, true}, - {"run at after start at", pastTime, nullTime, pastRunTime, true}, - {"run at after end at", nullTime, pastTime, pastRunTime, false}, - {"no range", nullTime, nullTime, pastRunTime, true}, - {"start at after end at", futureTime, pastTime, pastRunTime, false}, - } - - store, cleanup := cltest.NewStore(t) - defer cleanup() - jobRunner, cleanup := cltest.NewJobRunner(store) - defer cleanup() - jobRunner.Start() - - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - ot := services.OneTime{ - Clock: store.Clock, - Store: store, - } - require.NoError(t, ot.Start()) - defer ot.Stop() - - j := cltest.NewJobWithRunAtInitiator(test.runAt) - require.Nil(t, store.CreateJob(&j)) - - j.StartAt = test.startAt - j.EndAt = test.endAt - - ot.AddJob(j) - - tester := func() bool { - completed := false - jobRuns, err := store.JobRunsFor(j.ID) - require.NoError(t, err) - if (len(jobRuns) > 0) && (jobRuns[0].Status == models.RunStatusCompleted) { - completed = true - } - return completed - } - - if test.wantCompleted { - gomega.NewGomegaWithT(t).Eventually(tester).Should(gomega.Equal(true)) - } else { - gomega.NewGomegaWithT(t).Consistently(tester).Should(gomega.Equal(false)) - } - - }) - } -} - -func TestOneTime_RunJobAt_StopJobBeforeExecution(t *testing.T) { - t.Parallel() - store, cleanup := cltest.NewStore(t) - defer cleanup() + cltest.CallbackOrTimeout(t, "Create", func() { + <-executeJobChannel + }, 3*time.Second) - ot := services.OneTime{ - Clock: &cltest.NeverClock{}, - Store: store, - } - ot.Start() - j := cltest.NewJobWithRunAtInitiator(time.Now().Add(time.Hour)) - assert.Nil(t, store.CreateJob(&j)) - initr := j.Initiators[0] + cron.RunEntries() - finished := abool.New() - go func() { - ot.RunJobAt(initr, j) - finished.Set() - }() + cltest.CallbackOrTimeout(t, "Create", func() { + <-executeJobChannel + }, 3*time.Second) - ot.Stop() + r.Stop() - gomega.NewGomegaWithT(t).Eventually(func() bool { - return finished.IsSet() - }).Should(gomega.Equal(true)) - jobRuns, err := store.JobRunsFor(j.ID) - assert.NoError(t, err) - assert.Equal(t, 0, len(jobRuns)) + runManager.AssertExpectations(t) } -func TestOneTime_RunJobAt_ExecuteLateJob(t *testing.T) { - t.Parallel() - +func TestOneTime_AddJob(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ot := services.OneTime{ - Clock: store.Clock, - Store: store, - } - j := cltest.NewJobWithRunAtInitiator(time.Now().Add(time.Hour * -1)) - assert.Nil(t, store.CreateJob(&j)) - initr := j.Initiators[0] - initr.ID = j.Initiators[0].ID - initr.JobSpecID = j.ID - - finished := abool.New() - go func() { - ot.RunJobAt(initr, j) - finished.Set() - }() - - gomega.NewGomegaWithT(t).Eventually(func() bool { - return finished.IsSet() - }).Should(gomega.Equal(true)) - jobRuns, err := store.JobRunsFor(j.ID) - assert.NoError(t, err) - assert.Equal(t, 1, len(jobRuns)) -} - -func TestOneTime_RunJobAt_RunTwice(t *testing.T) { - t.Parallel() + executeJobChannel := make(chan struct{}) + runManager := new(mocks.RunManager) + runManager.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, nil). + Once(). + Run(func(mock.Arguments) { + executeJobChannel <- struct{}{} + }) - store, cleanup := cltest.NewStore(t) - defer cleanup() + clock := cltest.NewTriggerClock(t) ot := services.OneTime{ - Clock: store.Clock, - Store: store, + Clock: clock, + Store: store, + RunManager: runManager, } + require.NoError(t, ot.Start()) j := cltest.NewJobWithRunAtInitiator(time.Now()) - assert.NoError(t, store.CreateJob(&j)) - ot.RunJobAt(j.Initiators[0], j) + require.Nil(t, store.CreateJob(&j)) - j2, err := ot.Store.FindJob(j.ID) - require.NoError(t, err) - require.Len(t, j2.Initiators, 1) - ot.RunJobAt(j2.Initiators[0], j2) + ot.AddJob(j) - jobRuns, err := store.JobRunsFor(j.ID) - assert.NoError(t, err) - assert.Equal(t, 1, len(jobRuns)) -} - -func TestOneTime_RunJobAt_UnstartedRun(t *testing.T) { - t.Parallel() - - store, cleanup := cltest.NewStore(t) - defer cleanup() - - ot := services.OneTime{ - Clock: store.Clock, - Store: store, - } - - j := cltest.NewJobWithRunAtInitiator(time.Now()) - j.EndAt = cltest.NullTime(t, "2000-01-01T00:10:00.000Z") - assert.NoError(t, store.CreateJob(&j)) + clock.Trigger() - ot.RunJobAt(j.Initiators[0], j) + cltest.CallbackOrTimeout(t, "ws client restarts", func() { + <-executeJobChannel + }, 3*time.Second) - j2, err := store.FindJob(j.ID) - require.NoError(t, err) - require.Len(t, j2.Initiators, 1) - assert.Equal(t, false, j2.Initiators[0].Ran) -} + // This should block because if OneTime works it won't listen on the channel again + go clock.Trigger() -func TestOneTime_RunJobAt_ArchivedRun(t *testing.T) { - t.Parallel() - - store, cleanup := cltest.NewStore(t) - defer cleanup() + // Sleep for some time to make sure another call isn't made + time.Sleep(1 * time.Second) - ot := services.OneTime{ - Clock: cltest.InstantClock{}, - Store: store, - } + ot.Stop() - j := cltest.NewJobWithRunAtInitiator(time.Now()) - j.EndAt = cltest.NullTime(t, "2000-01-01T00:10:00.000Z") - require.NoError(t, store.CreateJob(&j)) - require.NoError(t, store.ArchiveJob(j.ID)) - - ot.RunJobAt(j.Initiators[0], j) - - unscoped := store.Unscoped() - j2, err := unscoped.FindJob(j.ID) - require.NoError(t, err) - require.Len(t, j2.Initiators, 1) - assert.Equal(t, false, j2.Initiators[0].Ran) - count, err := unscoped.JobRunsCountFor(j.ID) - require.NoError(t, err) - assert.Equal(t, 0, count) + runManager.AssertExpectations(t) } diff --git a/core/services/signatures/ethdss/ethdss.go b/core/services/signatures/ethdss/ethdss.go index 4764c2837dd..52bc2bcbf62 100644 --- a/core/services/signatures/ethdss/ethdss.go +++ b/core/services/signatures/ethdss/ethdss.go @@ -23,8 +23,9 @@ import ( "errors" "math/big" - "github.com/smartcontractkit/chainlink/core/services/signatures/ethschnorr" - "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "chainlink/core/services/signatures/ethschnorr" + "chainlink/core/services/signatures/secp256k1" + "go.dedis.ch/kyber/v3" "go.dedis.ch/kyber/v3/share" ) diff --git a/core/services/signatures/ethdss/ethdss_test.go b/core/services/signatures/ethdss/ethdss_test.go index ec344053669..6e51759708f 100644 --- a/core/services/signatures/ethdss/ethdss_test.go +++ b/core/services/signatures/ethdss/ethdss_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/core/services/signatures/cryptotest" - "github.com/smartcontractkit/chainlink/core/services/signatures/ethschnorr" - "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "chainlink/core/services/signatures/cryptotest" + "chainlink/core/services/signatures/ethschnorr" + "chainlink/core/services/signatures/secp256k1" "go.dedis.ch/kyber/v3" dkg "go.dedis.ch/kyber/v3/share/dkg/rabin" diff --git a/core/services/signatures/ethschnorr/ethschnorr.go b/core/services/signatures/ethschnorr/ethschnorr.go index 30ce38efc9f..3bd1e29a560 100644 --- a/core/services/signatures/ethschnorr/ethschnorr.go +++ b/core/services/signatures/ethschnorr/ethschnorr.go @@ -19,7 +19,7 @@ import ( "fmt" "math/big" - "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "chainlink/core/services/signatures/secp256k1" "go.dedis.ch/kyber/v3" ) diff --git a/core/services/signatures/ethschnorr/ethschnorr_test.go b/core/services/signatures/ethschnorr/ethschnorr_test.go index 1004ecbc044..e2cf156c204 100644 --- a/core/services/signatures/ethschnorr/ethschnorr_test.go +++ b/core/services/signatures/ethschnorr/ethschnorr_test.go @@ -15,8 +15,8 @@ import ( "go.dedis.ch/kyber/v3" "go.dedis.ch/kyber/v3/group/curve25519" - "github.com/smartcontractkit/chainlink/core/services/signatures/cryptotest" - "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "chainlink/core/services/signatures/cryptotest" + "chainlink/core/services/signatures/secp256k1" ) var numSignatures = 5 diff --git a/core/services/signatures/secp256k1/curve_test.go b/core/services/signatures/secp256k1/curve_test.go index 3c6b21257ed..e4802ea3d0e 100644 --- a/core/services/signatures/secp256k1/curve_test.go +++ b/core/services/signatures/secp256k1/curve_test.go @@ -1,8 +1,9 @@ package secp256k1 import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) var group = &Secp256k1{} diff --git a/core/services/signatures/secp256k1/field_test.go b/core/services/signatures/secp256k1/field_test.go index 2839a3dbaa7..0a5cf4bcf9c 100644 --- a/core/services/signatures/secp256k1/field_test.go +++ b/core/services/signatures/secp256k1/field_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/core/services/signatures/cryptotest" + "chainlink/core/services/signatures/cryptotest" ) var numFieldSamples = 10 diff --git a/core/services/signatures/secp256k1/point_test.go b/core/services/signatures/secp256k1/point_test.go index 020ff8a2c22..0dcaf7e8f6f 100644 --- a/core/services/signatures/secp256k1/point_test.go +++ b/core/services/signatures/secp256k1/point_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/core/services/signatures/cryptotest" + "chainlink/core/services/signatures/cryptotest" ) var numPointSamples = 10 diff --git a/core/services/signatures/secp256k1/scalar_test.go b/core/services/signatures/secp256k1/scalar_test.go index b08dc01ad3f..dfa815cd4d9 100644 --- a/core/services/signatures/secp256k1/scalar_test.go +++ b/core/services/signatures/secp256k1/scalar_test.go @@ -12,7 +12,7 @@ import ( "go.dedis.ch/kyber/v3" "go.dedis.ch/kyber/v3/group/curve25519" - "github.com/smartcontractkit/chainlink/core/services/signatures/cryptotest" + "chainlink/core/services/signatures/cryptotest" ) var numScalarSamples = 10 diff --git a/core/services/subscription.go b/core/services/subscription.go index a8bc31ecbc0..eec82cc6e89 100644 --- a/core/services/subscription.go +++ b/core/services/subscription.go @@ -5,13 +5,14 @@ import ( "math/big" "time" + "chainlink/core/logger" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + ethereum "github.com/ethereum/go-ethereum" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" "go.uber.org/multierr" ) @@ -29,7 +30,7 @@ type JobSubscription struct { // StartJobSubscription constructs a JobSubscription which listens for and // tracks event logs corresponding to the specified job. Ignores any errors if // there is at least one successful subscription to an initiator log. -func StartJobSubscription(job models.JobSpec, head *models.Head, store *strpkg.Store) (JobSubscription, error) { +func StartJobSubscription(job models.JobSpec, head *models.Head, store *strpkg.Store, runManager RunManager) (JobSubscription, error) { var merr error var unsubscribers []Unsubscriber @@ -40,7 +41,7 @@ func StartJobSubscription(job models.JobSpec, head *models.Head, store *strpkg.S ) for _, initr := range initrs { - unsubscriber, err := NewInitiatorSubscription(initr, job, store, head, ReceiveLogRequest) + unsubscriber, err := NewInitiatorSubscription(initr, job, store, runManager, head, ReceiveLogRequest) if err == nil { unsubscribers = append(unsubscribers, unsubscriber) } else { @@ -68,10 +69,11 @@ func (js JobSubscription) Unsubscribe() { // to the callback. type InitiatorSubscription struct { *ManagedSubscription - Job models.JobSpec - Initiator models.Initiator - store *strpkg.Store - callback func(*strpkg.Store, models.LogRequest) + runManager RunManager + JobSpecID models.ID + Initiator models.Initiator + store *strpkg.Store + callback func(*strpkg.Store, RunManager, models.LogRequest) } // NewInitiatorSubscription creates a new InitiatorSubscription that feeds received @@ -80,8 +82,9 @@ func NewInitiatorSubscription( initr models.Initiator, job models.JobSpec, store *strpkg.Store, + runManager RunManager, head *models.Head, - callback func(*strpkg.Store, models.LogRequest), + callback func(*strpkg.Store, RunManager, models.LogRequest), ) (InitiatorSubscription, error) { nextHead := head.NextInt() // Exclude current block from subscription if replayFromBlock := store.Config.ReplayFromBlock(); replayFromBlock >= 0 { @@ -97,10 +100,11 @@ func NewInitiatorSubscription( } sub := InitiatorSubscription{ - Job: job, - Initiator: initr, - store: store, - callback: callback, + JobSpecID: *job.ID, + runManager: runManager, + Initiator: initr, + store: store, + callback: callback, } managedSub, err := NewManagedSubscription(store, filter, sub.dispatchLog) @@ -114,36 +118,31 @@ func NewInitiatorSubscription( } func (sub InitiatorSubscription) dispatchLog(log models.Log) { - logger.Debugw(fmt.Sprintf("Log for %v initiator for job %s", sub.Initiator.Type, sub.Job.ID.String()), - "txHash", log.TxHash.Hex(), "logIndex", log.Index, "blockNumber", log.BlockNumber, "job", sub.Job.ID.String()) + logger.Debugw(fmt.Sprintf("Log for %v initiator for job %s", sub.Initiator.Type, sub.JobSpecID.String()), + "txHash", log.TxHash.Hex(), "logIndex", log.Index, "blockNumber", log.BlockNumber, "job", sub.JobSpecID.String()) base := models.InitiatorLogEvent{ - JobSpec: sub.Job, + JobSpecID: sub.JobSpecID, Initiator: sub.Initiator, Log: log, } - sub.callback(sub.store, base.LogRequest()) + sub.callback(sub.store, sub.runManager, base.LogRequest()) } func loggerLogListening(initr models.Initiator, blockNumber *big.Int) { - msg := fmt.Sprintf( - "Listening for %v from block %v for address %v for job %s", - initr.Type, - presenters.FriendlyBigInt(blockNumber), - utils.LogListeningAddress(initr.Address), - initr.JobSpecID.String()) - logger.Infow(msg) + msg := fmt.Sprintf("Listening for %v from block %v", initr.Type, presenters.FriendlyBigInt(blockNumber)) + logger.Infow(msg, "address", utils.LogListeningAddress(initr.Address), "jobID", initr.JobSpecID.String()) } // ReceiveLogRequest parses the log and runs the job indicated by a RunLog or // ServiceAgreementExecutionLog. (Both log events have the same format.) -func ReceiveLogRequest(store *strpkg.Store, le models.LogRequest) { +func ReceiveLogRequest(store *strpkg.Store, runManager RunManager, le models.LogRequest) { if !le.Validate() { return } if le.GetLog().Removed { - logger.Debugw("Skipping run for removed log", "log", le.GetLog(), "jobId", le.GetJobSpec().ID.String()) + logger.Debugw("Skipping run for removed log", "log", le.GetLog(), "jobId", le.GetJobSpecID().String()) return } @@ -154,29 +153,31 @@ func ReceiveLogRequest(store *strpkg.Store, le models.LogRequest) { return } - runJob(store, le, data) + runJob(store, runManager, le, data) } -func runJob(store *strpkg.Store, le models.LogRequest, data models.JSON) { - input := models.RunResult{Data: data} +func runJob(store *strpkg.Store, runManager RunManager, le models.LogRequest, data models.JSON) { + jobSpecID := le.GetJobSpecID() + initiator := le.GetInitiator() + if err := le.ValidateRequester(); err != nil { - input.SetError(err) + if _, err := runManager.CreateErrored(jobSpecID, initiator, err); err != nil { + logger.Errorw(err.Error()) + } logger.Errorw(err.Error(), le.ForLogger()...) + return } + rr, err := le.RunRequest() if err != nil { - input.SetError(err) + if _, err := runManager.CreateErrored(jobSpecID, initiator, err); err != nil { + logger.Errorw(err.Error()) + } logger.Errorw(err.Error(), le.ForLogger()...) + return } - _, err = ExecuteJobWithRunRequest( - le.GetJobSpec(), - le.GetInitiator(), - input, - le.BlockNumber(), - store.Unscoped(), - rr, - ) + _, err = runManager.Create(jobSpecID, &initiator, &data, le.BlockNumber(), &rr) if err != nil { logger.Errorw(err.Error(), le.ForLogger()...) } @@ -225,7 +226,7 @@ func (sub ManagedSubscription) Unsubscribe() { // timedUnsubscribe attempts to unsubscribe but aborts abruptly after a time delay // unblocking the application. This is an effort to mitigate the occasional // indefinite block described here from go-ethereum: -// https://github.com/smartcontractkit/chainlink/pull/600#issuecomment-426320971 +// https://chainlink/pull/600#issuecomment-426320971 func timedUnsubscribe(unsubscriber Unsubscriber) { unsubscribed := make(chan struct{}) go func() { diff --git a/core/services/subscription_test.go b/core/services/subscription_test.go index 7e35ebdb0c7..edc91da5512 100644 --- a/core/services/subscription_test.go +++ b/core/services/subscription_test.go @@ -8,15 +8,18 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" - "github.com/golang/mock/gomock" + + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/services" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/internal/mocks" - "github.com/smartcontractkit/chainlink/core/services" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -34,9 +37,10 @@ func TestServices_NewInitiatorSubscription_BackfillLogs(t *testing.T) { eth.RegisterSubscription("logs") var count int32 - callback := func(*strpkg.Store, models.LogRequest) { atomic.AddInt32(&count, 1) } + callback := func(*strpkg.Store, services.RunManager, models.LogRequest) { atomic.AddInt32(&count, 1) } fromBlock := cltest.Head(0) - sub, err := services.NewInitiatorSubscription(initr, job, store, fromBlock, callback) + jm := new(mocks.RunManager) + sub, err := services.NewInitiatorSubscription(initr, job, store, jm, fromBlock, callback) assert.NoError(t, err) defer sub.Unsubscribe() @@ -59,8 +63,9 @@ func TestServices_NewInitiatorSubscription_BackfillLogs_WithNoHead(t *testing.T) eth.RegisterSubscription("logs") var count int32 - callback := func(*strpkg.Store, models.LogRequest) { atomic.AddInt32(&count, 1) } - sub, err := services.NewInitiatorSubscription(initr, job, store, nil, callback) + callback := func(*strpkg.Store, services.RunManager, models.LogRequest) { atomic.AddInt32(&count, 1) } + jm := new(mocks.RunManager) + sub, err := services.NewInitiatorSubscription(initr, job, store, jm, nil, callback) assert.NoError(t, err) defer sub.Unsubscribe() @@ -84,9 +89,10 @@ func TestServices_NewInitiatorSubscription_PreventsDoubleDispatch(t *testing.T) eth.RegisterSubscription("logs", logsChan) var count int32 - callback := func(*strpkg.Store, models.LogRequest) { atomic.AddInt32(&count, 1) } + callback := func(*strpkg.Store, services.RunManager, models.LogRequest) { atomic.AddInt32(&count, 1) } head := cltest.Head(0) - sub, err := services.NewInitiatorSubscription(initr, job, store, head, callback) + jm := new(mocks.RunManager) + sub, err := services.NewInitiatorSubscription(initr, job, store, jm, head, callback) assert.NoError(t, err) defer sub.Unsubscribe() @@ -111,7 +117,7 @@ func TestServices_ReceiveLogRequest_IgnoredLogWithRemovedFlag(t *testing.T) { require.NoError(t, store.CreateJob(&jobSpec)) log := models.InitiatorLogEvent{ - JobSpec: jobSpec, + JobSpecID: *jobSpec.ID, Log: models.Log{ Removed: true, }, @@ -121,14 +127,191 @@ func TestServices_ReceiveLogRequest_IgnoredLogWithRemovedFlag(t *testing.T) { err := store.ORM.DB.Model(&models.JobRun{}).Count(&originalCount).Error require.NoError(t, err) - services.ReceiveLogRequest(store, log) + jm := new(mocks.RunManager) + services.ReceiveLogRequest(store, jm, log) + jm.AssertExpectations(t) +} + +func TestServices_StartJobSubscription(t *testing.T) { + t.Parallel() + + sharedAddr := cltest.NewAddress() + noAddr := common.Address{} - gomega.NewGomegaWithT(t).Consistently(func() int { - count := 0 - err := store.ORM.DB.Model(&models.JobRun{}).Count(&count).Error - require.NoError(t, err) - return count - originalCount - }).Should(gomega.Equal(0)) + tests := []struct { + name string + initType string + initrAddr common.Address + logAddr common.Address + topic0 common.Hash + data hexutil.Bytes + }{ + { + "ethlog matching address", + "ethlog", + sharedAddr, + sharedAddr, + common.Hash{}, + hexutil.Bytes{}, + }, + { + "ethlog all address", + "ethlog", + noAddr, + cltest.NewAddress(), + common.Hash{}, + hexutil.Bytes{}, + }, + { + "runlog v0 matching address", + "runlog", + sharedAddr, + sharedAddr, + models.RunLogTopic0original, + cltest.StringToVersionedLogData0(t, + "id", + `{"value":"100"}`, + ), + }, + { + "runlog v20190123 w/o address", + "runlog", + noAddr, + cltest.NewAddress(), + models.RunLogTopic20190123withFullfillmentParams, + cltest.StringToVersionedLogData20190123withFulfillmentParams(t, "id", `{"value":"100"}`), + }, + { + "runlog v20190123 matching address", + "runlog", + sharedAddr, + sharedAddr, + models.RunLogTopic20190123withFullfillmentParams, + cltest.StringToVersionedLogData20190123withFulfillmentParams(t, "id", `{"value":"100"}`), + }, + { + "runlog v20190207 w/o address", + "runlog", + noAddr, + cltest.NewAddress(), + models.RunLogTopic20190207withoutIndexes, + cltest.StringToVersionedLogData20190207withoutIndexes(t, "id", cltest.NewAddress(), `{"value":"100"}`), + }, + { + "runlog v20190207 matching address", + "runlog", + sharedAddr, + sharedAddr, + models.RunLogTopic20190207withoutIndexes, + cltest.StringToVersionedLogData20190207withoutIndexes(t, "id", cltest.NewAddress(), `{"value":"100"}`), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + eth := cltest.MockEthOnStore(t, store) + eth.Register("eth_getLogs", []models.Log{}) + logChan := make(chan models.Log, 1) + eth.RegisterSubscription("logs", logChan) + + job := cltest.NewJob() + initr := models.Initiator{Type: test.initType} + initr.Address = test.initrAddr + job.Initiators = []models.Initiator{initr} + require.NoError(t, store.CreateJob(&job)) + + executeJobChannel := make(chan struct{}) + + runManager := new(mocks.RunManager) + runManager.On("Create", job.ID, mock.Anything, mock.Anything, big.NewInt(0), mock.Anything). + Return(nil, nil). + Run(func(mock.Arguments) { + executeJobChannel <- struct{}{} + }) + + subscription, err := services.StartJobSubscription(job, cltest.Head(91), store, runManager) + require.NoError(t, err) + assert.NotNil(t, subscription) + + logChan <- models.Log{ + Address: test.logAddr, + Data: test.data, + Topics: []common.Hash{ + test.topic0, + models.IDToTopic(job.ID), + cltest.NewAddress().Hash(), + common.BigToHash(big.NewInt(0)), + }, + } + + cltest.CallbackOrTimeout(t, "Create", func() { + <-executeJobChannel + }) + + runManager.AssertExpectations(t) + eth.EventuallyAllCalled(t) + }) + } +} + +func TestServices_StartJobSubscription_RunlogNoTopicMatch(t *testing.T) { + t.Parallel() + + sharedAddr := cltest.NewAddress() + + tests := []struct { + name string + data hexutil.Bytes + }{ + { + "runlog w non-matching topic", + cltest.StringToVersionedLogData20190123withFulfillmentParams(t, "id", `{"value":"100"}`)}, + { + "runlog w non-matching topic", + cltest.StringToVersionedLogData20190207withoutIndexes(t, "id", cltest.NewAddress(), `{"value":"100"}`), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store, cleanup := cltest.NewStore(t) + defer cleanup() + + eth := cltest.MockEthOnStore(t, store) + eth.Register("eth_getLogs", []models.Log{}) + logChan := make(chan models.Log, 1) + eth.RegisterSubscription("logs", logChan) + + job := cltest.NewJob() + initr := models.Initiator{Type: "runlog"} + initr.Address = sharedAddr + job.Initiators = []models.Initiator{initr} + require.NoError(t, store.CreateJob(&job)) + + runManager := new(mocks.RunManager) + + subscription, err := services.StartJobSubscription(job, cltest.Head(91), store, runManager) + require.NoError(t, err) + assert.NotNil(t, subscription) + + logChan <- models.Log{ + Address: sharedAddr, + Data: test.data, + Topics: []common.Hash{ + common.Hash{}, + models.IDToTopic(job.ID), + cltest.NewAddress().Hash(), + common.BigToHash(big.NewInt(0)), + }, + } + + runManager.AssertExpectations(t) + eth.EventuallyAllCalled(t) + }) + } } func TestServices_NewInitiatorSubscription_ReplayFromBlock(t *testing.T) { @@ -137,10 +320,8 @@ func TestServices_NewInitiatorSubscription_ReplayFromBlock(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - txmMock := mocks.NewMockTxManager(ctrl) - store.TxManager = txmMock + txManager := new(mocks.TxManager) + store.TxManager = txManager cases := []struct { name string @@ -174,17 +355,28 @@ func TestServices_NewInitiatorSubscription_ReplayFromBlock(t *testing.T) { log := cltest.LogFromFixture(t, "testdata/subscription_logs.json") - txmMock.EXPECT().SubscribeToLogs(gomock.Any(), expectedQuery).Return(cltest.EmptyMockSubscription(), nil) - txmMock.EXPECT().GetLogs(expectedQuery).Return([]models.Log{log}, nil) + txManager.On("SubscribeToLogs", mock.Anything, expectedQuery).Return(cltest.EmptyMockSubscription(), nil) + txManager.On("GetLogs", expectedQuery).Return([]models.Log{log}, nil) + + executeJobChannel := make(chan struct{}) + + runManager := new(mocks.RunManager) + runManager.On("Create", job.ID, mock.Anything, mock.Anything, big.NewInt(0), mock.Anything). + Return(nil, nil). + Run(func(mock.Arguments) { + executeJobChannel <- struct{}{} + }) var wg sync.WaitGroup wg.Add(1) - callback := func(*strpkg.Store, models.LogRequest) { wg.Done() } + callback := func(*strpkg.Store, services.RunManager, models.LogRequest) { wg.Done() } - _, err := services.NewInitiatorSubscription(initr, job, store, currentHead, callback) + _, err := services.NewInitiatorSubscription(initr, job, store, runManager, currentHead, callback) require.NoError(t, err) wg.Wait() + + txManager.AssertExpectations(t) }) } } diff --git a/core/services/synchronization/presenters.go b/core/services/synchronization/presenters.go index afaff673bba..b088a7ce931 100644 --- a/core/services/synchronization/presenters.go +++ b/core/services/synchronization/presenters.go @@ -3,11 +3,12 @@ package synchronization import ( "encoding/json" + clnull "chainlink/core/null" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" - clnull "github.com/smartcontractkit/chainlink/core/null" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" null "gopkg.in/guregu/null.v3" ) diff --git a/core/services/synchronization/presenters_test.go b/core/services/synchronization/presenters_test.go index 41ca6e0da89..3ba55ee6182 100644 --- a/core/services/synchronization/presenters_test.go +++ b/core/services/synchronization/presenters_test.go @@ -5,10 +5,11 @@ import ( "io/ioutil" "testing" + clnull "chainlink/core/null" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common" - clnull "github.com/smartcontractkit/chainlink/core/null" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" diff --git a/core/services/synchronization/stats_pusher.go b/core/services/synchronization/stats_pusher.go index bc61b2ba46f..b96eb952abb 100644 --- a/core/services/synchronization/stats_pusher.go +++ b/core/services/synchronization/stats_pusher.go @@ -7,13 +7,14 @@ import ( "sync" "time" + "chainlink/core/logger" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/utils" + "github.com/jinzhu/gorm" "github.com/jpillora/backoff" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/utils" ) // StatsPusher polls for events and pushes them via a WebSocketClient @@ -120,8 +121,6 @@ func (sp *StatsPusher) eventLoop(parentCtx context.Context) { } func (sp *StatsPusher) pusherLoop(parentCtx context.Context) error { - logger.Debugw("Started StatsPusher") - for { select { case <-sp.waker: @@ -135,7 +134,6 @@ func (sp *StatsPusher) pusherLoop(parentCtx context.Context) error { return err } case <-parentCtx.Done(): - logger.Debugw("StatsPusher got done signal, shutting down") return nil } } diff --git a/core/services/synchronization/stats_pusher_test.go b/core/services/synchronization/stats_pusher_test.go index e8026dd08e3..10e67e14779 100644 --- a/core/services/synchronization/stats_pusher_test.go +++ b/core/services/synchronization/stats_pusher_test.go @@ -3,10 +3,11 @@ package synchronization_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services/synchronization" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" + "chainlink/core/internal/cltest" + "chainlink/core/services/synchronization" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -36,7 +37,7 @@ func TestStatsPusher(t *testing.T) { }) cltest.WaitForSyncEventCount(t, store.ORM, 0) - jr.ApplyResult(models.RunResult{Status: models.RunStatusCompleted}) + jr.Status = models.RunStatusCompleted require.NoError(t, store.SaveJobRun(&jr)) assert.Equal(t, 1, lenSyncEvents(t, store.ORM)) diff --git a/core/services/synchronization/web_socket_client.go b/core/services/synchronization/web_socket_client.go index 45b3c0f5d0c..ca9337a5ce8 100644 --- a/core/services/synchronization/web_socket_client.go +++ b/core/services/synchronization/web_socket_client.go @@ -9,9 +9,10 @@ import ( "sync" "time" + "chainlink/core/logger" + "chainlink/core/utils" + "github.com/gorilla/websocket" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/utils" ) var ( diff --git a/core/services/synchronization/web_socket_client_test.go b/core/services/synchronization/web_socket_client_test.go index e31300fe39a..7a0005a40aa 100644 --- a/core/services/synchronization/web_socket_client_test.go +++ b/core/services/synchronization/web_socket_client_test.go @@ -6,8 +6,9 @@ import ( "testing" "time" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services/synchronization" + "chainlink/core/internal/cltest" + "chainlink/core/services/synchronization" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/core/services/validators.go b/core/services/validators.go index 004aa007086..967433f4769 100644 --- a/core/services/validators.go +++ b/core/services/validators.go @@ -2,15 +2,17 @@ package services import ( "fmt" + "net/url" "strings" "time" + "chainlink/core/adapters" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/asaskevich/govalidator" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" ) // ValidateJob checks the job and its associated Initiators and Tasks for any @@ -46,11 +48,12 @@ func ValidateBridgeType(bt *models.BridgeTypeRequest, store *store.Store) error if _, err := models.NewTaskType(bt.Name.String()); err != nil { fe.Merge(err) } - if isURL := govalidator.IsURL(bt.URL.String()); !isURL { + if _, err := url.Parse(bt.URL.String()); err != nil { fe.Add("Invalid URL format") + fe.Merge(err) } ts := models.TaskSpec{Type: bt.Name} - if a, _ := adapters.For(ts, store); a != nil { + if a, _ := adapters.For(ts, store.Config, store.ORM); a != nil { fe.Add(fmt.Sprintf("Adapter %v already exists", bt.Name)) } return fe.CoerceEmptyToNil() @@ -135,7 +138,7 @@ func validateServiceAgreementInitiator(i models.Initiator, j models.JobSpec) err } func validateTask(task models.TaskSpec, store *store.Store) error { - adapter, err := adapters.For(task, store) + adapter, err := adapters.For(task, store.Config, store.ORM) if !store.Config.Dev() { if _, ok := adapter.BaseAdapter.(*adapters.Sleep); ok { return errors.New("Sleep Adapter is not implemented yet") @@ -152,7 +155,7 @@ func ValidateServiceAgreement(sa models.ServiceAgreement, store *store.Store) er fe := models.NewJSONAPIErrors() config := store.Config - if sa.Encumbrance.Payment == nil { + if sa.Encumbrance.Payment.IsZero() { fe.Add("Service agreement encumbrance error: No payment amount set") } else if sa.Encumbrance.Payment.Cmp(config.MinimumContractPayment()) == -1 { fe.Add(fmt.Sprintf("Service agreement encumbrance error: Payment amount is below minimum %v", config.MinimumContractPayment().String())) diff --git a/core/services/validators_test.go b/core/services/validators_test.go index 7bc1e167f82..65312ba4d53 100644 --- a/core/services/validators_test.go +++ b/core/services/validators_test.go @@ -6,11 +6,12 @@ import ( "testing" "time" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/services" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -125,10 +126,16 @@ func TestValidateAdapter(t *testing.T) { models.NewJSONAPIErrorsWith("Task Type validation: name invalid/adapter contains invalid characters"), }, { - "invalid adapter url", - "adapterwithinvalidurl", + "valid url", + "adapterwithvalidurl", cltest.WebURL(t, "//denergy"), - models.NewJSONAPIErrorsWith("Invalid URL format"), + nil, + }, + { + "valid docker url", + "adapterwithdockerurl", + cltest.WebURL(t, "http://chainlink_cmc-adapter_1:8080"), + nil, }, {"new external adapter", "gdaxprice", bt.URL, nil}, } diff --git a/core/services/vrf/vrf.go b/core/services/vrf/vrf.go index 669e9977b9d..58e81b36089 100644 --- a/core/services/vrf/vrf.go +++ b/core/services/vrf/vrf.go @@ -32,8 +32,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/services/signatures/secp256k1" + "chainlink/core/utils" + "go.dedis.ch/kyber/v3" ) diff --git a/core/services/vrf/vrf_test.go b/core/services/vrf/vrf_test.go index 3b7e4e8ec56..138d6f9175f 100644 --- a/core/services/vrf/vrf_test.go +++ b/core/services/vrf/vrf_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/services/signatures/secp256k1" + "chainlink/core/utils" ) func TestVRF_IsSquare(t *testing.T) { diff --git a/core/store/assets/currencies.go b/core/store/assets/currencies.go index 51891ac1181..79da64b6dd3 100644 --- a/core/store/assets/currencies.go +++ b/core/store/assets/currencies.go @@ -1,7 +1,9 @@ package assets import ( + "chainlink/core/utils" "database/sql/driver" + "errors" "fmt" "math/big" @@ -9,6 +11,8 @@ import ( "github.com/willf/pad" ) +var ErrNoQuotesForCurrency = errors.New("cannot unmarshal json.Number into currency") + func format(i *big.Int, precision int) string { v := "1" + pad.Right("", precision, "0") d := &big.Int{} @@ -83,6 +87,23 @@ func (l *Link) MarshalText() ([]byte, error) { return (*big.Int)(l).MarshalText() } +// MarshalJSON implements the json.Marshaler interface. +func (l Link) MarshalJSON() ([]byte, error) { + value, err := l.MarshalText() + if err != nil { + return nil, err + } + return []byte(fmt.Sprintf(`"%s"`, value)), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (l *Link) UnmarshalJSON(data []byte) error { + if utils.IsQuoted(data) { + return l.UnmarshalText(utils.RemoveQuotes(data)) + } + return ErrNoQuotesForCurrency +} + // UnmarshalText implements the encoding.TextUnmarshaler interface. func (l *Link) UnmarshalText(text []byte) error { if _, ok := l.SetString(string(text), 10); !ok { @@ -161,11 +182,28 @@ func (e *Eth) SetString(s string, base int) (*Eth, bool) { return (*Eth)(w), ok } +// MarshalJSON implements the json.Marshaler interface. +func (e Eth) MarshalJSON() ([]byte, error) { + value, err := e.MarshalText() + if err != nil { + return nil, err + } + return []byte(fmt.Sprintf(`"%s"`, value)), nil +} + // MarshalText implements the encoding.TextMarshaler interface. func (e *Eth) MarshalText() ([]byte, error) { return e.ToInt().MarshalText() } +// UnmarshalJSON implements the json.Unmarshaler interface. +func (e *Eth) UnmarshalJSON(data []byte) error { + if utils.IsQuoted(data) { + return e.UnmarshalText(utils.RemoveQuotes(data)) + } + return ErrNoQuotesForCurrency +} + // UnmarshalText implements the encoding.TextUnmarshaler interface. func (e *Eth) UnmarshalText(text []byte) error { if _, ok := e.SetString(string(text), 10); !ok { diff --git a/core/store/assets/currencies_test.go b/core/store/assets/currencies_test.go index 6c1a87c7688..079f30ddcbd 100644 --- a/core/store/assets/currencies_test.go +++ b/core/store/assets/currencies_test.go @@ -4,7 +4,8 @@ import ( "encoding/json" "testing" - "github.com/smartcontractkit/chainlink/core/store/assets" + "chainlink/core/store/assets" + "github.com/stretchr/testify/assert" ) @@ -56,11 +57,11 @@ func TestAssets_Link_UnmarshalJsonError(t *testing.T) { link := assets.Link{} - err := json.Unmarshal([]byte(`"a"`), &link) - assert.EqualError(t, err, "assets: cannot unmarshal \"a\" into a *assets.Link") + err := json.Unmarshal([]byte(`"x"`), &link) + assert.EqualError(t, err, "assets: cannot unmarshal \"x\" into a *assets.Link") err = json.Unmarshal([]byte(`1`), &link) - assert.EqualError(t, err, "json: cannot unmarshal number into Go value of type *assets.Link") + assert.Equal(t, assets.ErrNoQuotesForCurrency, err) } func TestAssets_NewEthAndString(t *testing.T) { @@ -118,9 +119,9 @@ func TestAssets_Eth_UnmarshalJsonError(t *testing.T) { eth := assets.Eth{} - err := json.Unmarshal([]byte(`"a"`), ð) - assert.EqualError(t, err, "assets: cannot unmarshal \"a\" into a *assets.Eth") + err := json.Unmarshal([]byte(`"x"`), ð) + assert.EqualError(t, err, "assets: cannot unmarshal \"x\" into a *assets.Eth") err = json.Unmarshal([]byte(`1`), ð) - assert.EqualError(t, err, "json: cannot unmarshal number into Go value of type *assets.Eth") + assert.Equal(t, assets.ErrNoQuotesForCurrency, err) } diff --git a/core/store/eth_client.go b/core/store/eth_client.go index 54d6b6c8138..0621c22f2cf 100644 --- a/core/store/eth_client.go +++ b/core/store/eth_client.go @@ -4,15 +4,16 @@ import ( "context" "math/big" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/utils" + ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" ) -//go:generate mockgen -package=mocks -destination=../internal/mocks/eth_client_mocks.go github.com/smartcontractkit/chainlink/core/store EthClient +//go:generate mockery -name EthClient -output ../internal/mocks/ -case=underscore // EthClient is the interface supplied by EthCallerSubscriber type EthClient interface { diff --git a/core/store/eth_client_test.go b/core/store/eth_client_test.go index c53d12ee886..12ffb9db52f 100644 --- a/core/store/eth_client_test.go +++ b/core/store/eth_client_test.go @@ -6,10 +6,11 @@ import ( "math/big" + "chainlink/core/internal/cltest" + strpkg "chainlink/core/store" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -77,6 +78,8 @@ func TestEthCallerSubscriber_GetNonce(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) + ethMock := app.MockEthCallerSubscriber() ethClientObject := app.Store.TxManager.(*strpkg.EthTxManager).EthClient ethMock.Register("eth_getTransactionCount", "0x0100") @@ -90,6 +93,8 @@ func TestEthCallerSubscriber_SendRawTx(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) + ethMock := app.MockEthCallerSubscriber() ethClientObject := app.Store.TxManager.(*strpkg.EthTxManager).EthClient ethMock.Register("eth_sendRawTransaction", common.Hash{1}) @@ -102,6 +107,7 @@ func TestEthCallerSubscriber_GetEthBalance(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) tests := []struct { name string @@ -129,6 +135,7 @@ func TestEthCallerSubscriber_GetERC20Balance(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) ethMock := app.MockEthCallerSubscriber() ethClientObject := app.Store.TxManager.(*strpkg.EthTxManager).EthClient diff --git a/core/store/key_store.go b/core/store/key_store.go index 98fd0a7e828..adffb5ded42 100644 --- a/core/store/key_store.go +++ b/core/store/key_store.go @@ -5,13 +5,14 @@ import ( "fmt" "math/big" + "chainlink/core/logger" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" "go.uber.org/multierr" ) diff --git a/core/store/key_store_test.go b/core/store/key_store_test.go index 6c01cf1c732..a4e3aa01dac 100644 --- a/core/store/key_store_test.go +++ b/core/store/key_store_test.go @@ -4,7 +4,8 @@ import ( "io/ioutil" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" + "chainlink/core/internal/cltest" + "github.com/stretchr/testify/assert" ) diff --git a/core/store/migrations/migrate.go b/core/store/migrations/migrate.go index 009759dd908..265f81d58ca 100644 --- a/core/store/migrations/migrate.go +++ b/core/store/migrations/migrate.go @@ -3,30 +3,31 @@ package migrations import ( "regexp" + "chainlink/core/store/migrations/migration0" + "chainlink/core/store/migrations/migration1559081901" + "chainlink/core/store/migrations/migration1559767166" + "chainlink/core/store/migrations/migration1560433987" + "chainlink/core/store/migrations/migration1560791143" + "chainlink/core/store/migrations/migration1560881846" + "chainlink/core/store/migrations/migration1560881855" + "chainlink/core/store/migrations/migration1560886530" + "chainlink/core/store/migrations/migration1560924400" + "chainlink/core/store/migrations/migration1564007745" + "chainlink/core/store/migrations/migration1565139192" + "chainlink/core/store/migrations/migration1565210496" + "chainlink/core/store/migrations/migration1565291711" + "chainlink/core/store/migrations/migration1565877314" + "chainlink/core/store/migrations/migration1566498796" + "chainlink/core/store/migrations/migration1566915476" + "chainlink/core/store/migrations/migration1567029116" + "chainlink/core/store/migrations/migration1568280052" + "chainlink/core/store/migrations/migration1568390387" + "chainlink/core/store/migrations/migration1568833756" + "chainlink/core/store/migrations/migration1570087128" + "chainlink/core/store/migrations/migration1570675883" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration0" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1559081901" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1559767166" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1560433987" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1560791143" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1560881846" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1560881855" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1560886530" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1560924400" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1564007745" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1565139192" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1565210496" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1565291711" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1565877314" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1566498796" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1566915476" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1567029116" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1568280052" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1568390387" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1568833756" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1570087128" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1570675883" gormigrate "gopkg.in/gormigrate.v1" ) diff --git a/core/store/migrations/migrate_test.go b/core/store/migrations/migrate_test.go index 3649347cfc9..6afff5bf880 100644 --- a/core/store/migrations/migrate_test.go +++ b/core/store/migrations/migrate_test.go @@ -7,15 +7,16 @@ import ( "testing" "time" + "chainlink/core/internal/cltest" + "chainlink/core/store/assets" + "chainlink/core/store/migrations" + "chainlink/core/store/migrations/migration0" + "chainlink/core/store/migrations/migration1560881855" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/utils" + "github.com/gofrs/uuid" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/migrations" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration0" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1560881855" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" gormigrate "gopkg.in/gormigrate.v1" @@ -45,7 +46,7 @@ func TestMigrate_Migrations(t *testing.T) { db := orm.DB - require.NoError(t, migrations.MigrateTo(db, "1559081901")) + require.NoError(t, migrations.Migrate(db)) assert.True(t, db.HasTable("bridge_types")) assert.True(t, db.HasTable("encumbrances")) @@ -164,7 +165,7 @@ func TestMigrate_Migration1565139192(t *testing.T) { specNoPayment := models.NewJobFromRequest(models.JobSpecRequest{}) specWithPayment := models.NewJobFromRequest(models.JobSpecRequest{ - MinPayment: assets.NewLink(5), + MinPayment: *assets.NewLink(5), }) specOneFound := models.JobSpec{} specTwoFound := models.JobSpec{} @@ -172,9 +173,9 @@ func TestMigrate_Migration1565139192(t *testing.T) { require.NoError(t, db.Create(&specWithPayment).Error) require.NoError(t, db.Create(&specNoPayment).Error) require.NoError(t, db.Where("id = ?", specNoPayment.ID).Find(&specOneFound).Error) - require.Equal(t, assets.NewLink(0), specNoPayment.MinPayment) + require.Equal(t, *assets.NewLink(0), specNoPayment.MinPayment) require.NoError(t, db.Where("id = ?", specWithPayment.ID).Find(&specTwoFound).Error) - require.Equal(t, assets.NewLink(5), specWithPayment.MinPayment) + require.Equal(t, *assets.NewLink(5), specWithPayment.MinPayment) } func TestMigrate_Migration1565210496(t *testing.T) { diff --git a/core/store/migrations/migration0/migrate.go b/core/store/migrations/migration0/migrate.go index 44d0ed9c8a7..d519365a110 100644 --- a/core/store/migrations/migration0/migrate.go +++ b/core/store/migrations/migration0/migrate.go @@ -3,13 +3,15 @@ package migration0 import ( "time" + clnull "chainlink/core/null" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common" "github.com/jinzhu/gorm" "github.com/pkg/errors" - clnull "github.com/smartcontractkit/chainlink/core/null" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" "gopkg.in/guregu/null.v3" + ) // Migrate runs the initial migration diff --git a/core/store/migrations/migration1559081901/migrate.go b/core/store/migrations/migration1559081901/migrate.go index 7129dde564f..a874df65cec 100644 --- a/core/store/migrations/migration1559081901/migrate.go +++ b/core/store/migrations/migration1559081901/migrate.go @@ -1,9 +1,10 @@ package migration1559081901 import ( + "chainlink/core/store/models" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/models" ) func Migrate(tx *gorm.DB) error { diff --git a/core/store/migrations/migration1560791143/migrate.go b/core/store/migrations/migration1560791143/migrate.go index 07b3fcd27bf..00f398cbb49 100644 --- a/core/store/migrations/migration1560791143/migrate.go +++ b/core/store/migrations/migration1560791143/migrate.go @@ -1,9 +1,10 @@ package migration1560791143 import ( + "chainlink/core/store/dbutil" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/dbutil" ) func Migrate(tx *gorm.DB) error { diff --git a/core/store/migrations/migration1560881855/migrate.go b/core/store/migrations/migration1560881855/migrate.go index e2249abc5c0..00435c686a7 100644 --- a/core/store/migrations/migration1560881855/migrate.go +++ b/core/store/migrations/migration1560881855/migrate.go @@ -3,10 +3,11 @@ package migration1560881855 import ( "time" + "chainlink/core/store/assets" + "chainlink/core/store/dbutil" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/dbutil" ) func Migrate(tx *gorm.DB) error { diff --git a/core/store/migrations/migration1560886530/migrate.go b/core/store/migrations/migration1560886530/migrate.go index 045dea7f3c3..e5a6d662506 100644 --- a/core/store/migrations/migration1560886530/migrate.go +++ b/core/store/migrations/migration1560886530/migrate.go @@ -1,13 +1,14 @@ package migration1560886530 import ( + "chainlink/core/store/dbutil" + "chainlink/core/store/migrations/migration0" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/ethereum/go-ethereum/common" "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/dbutil" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration0" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" ) // Migrate converts the heads table to use a surrogate ID and binary hash diff --git a/core/store/migrations/migration1564007745/migrate.go b/core/store/migrations/migration1564007745/migrate.go index d8b71ebd380..48dad61bbb2 100644 --- a/core/store/migrations/migration1564007745/migrate.go +++ b/core/store/migrations/migration1564007745/migrate.go @@ -1,9 +1,10 @@ package migration1564007745 import ( + "chainlink/core/store/models" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/models" ) func Migrate(tx *gorm.DB) error { diff --git a/core/store/migrations/migration1565210496/migrate.go b/core/store/migrations/migration1565210496/migrate.go index 171d573158d..4b3c1a5be9e 100644 --- a/core/store/migrations/migration1565210496/migrate.go +++ b/core/store/migrations/migration1565210496/migrate.go @@ -1,9 +1,10 @@ package migration1565210496 import ( + "chainlink/core/store/dbutil" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/dbutil" ) // Migrate optimizes the JobRuns table for reduced on disk footprint diff --git a/core/store/migrations/migration1565291711/migrate.go b/core/store/migrations/migration1565291711/migrate.go index 0538241cc14..d5b4bb98319 100644 --- a/core/store/migrations/migration1565291711/migrate.go +++ b/core/store/migrations/migration1565291711/migrate.go @@ -1,9 +1,10 @@ package migration1565291711 import ( + "chainlink/core/store/dbutil" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/dbutil" ) // Migrate optimizes the JobRuns table to reduce the cost of IDs diff --git a/core/store/migrations/migration1565877314/migrate.go b/core/store/migrations/migration1565877314/migrate.go index e6645c9cd6e..c8cfc20c1ec 100644 --- a/core/store/migrations/migration1565877314/migrate.go +++ b/core/store/migrations/migration1565877314/migrate.go @@ -3,11 +3,12 @@ package migration1565877314 import ( "net/url" + "chainlink/core/store/migrations/migration0" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration0" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" ) type ExternalInitiator struct { diff --git a/core/store/migrations/migration1566498796/migrate.go b/core/store/migrations/migration1566498796/migrate.go index b59c9551863..44f21137f73 100644 --- a/core/store/migrations/migration1566498796/migrate.go +++ b/core/store/migrations/migration1566498796/migrate.go @@ -1,8 +1,9 @@ package migration1566498796 import ( + "chainlink/core/store/dbutil" + "github.com/jinzhu/gorm" - "github.com/smartcontractkit/chainlink/core/store/dbutil" ) // Migrate optimizes the JobRuns table to reduce the cost of IDs diff --git a/core/store/migrations/migration1567029116/migrate.go b/core/store/migrations/migration1567029116/migrate.go index 4104fd863a4..2a0c8252639 100644 --- a/core/store/migrations/migration1567029116/migrate.go +++ b/core/store/migrations/migration1567029116/migrate.go @@ -1,8 +1,9 @@ package migration1567029116 import ( + "chainlink/core/store/dbutil" + "github.com/jinzhu/gorm" - "github.com/smartcontractkit/chainlink/core/store/dbutil" ) // Migrate optimizes the JobRuns table to reduce the cost of IDs diff --git a/core/store/migrations/migration1568280052/migrate.go b/core/store/migrations/migration1568280052/migrate.go index 08922f12d9f..91f1614c757 100644 --- a/core/store/migrations/migration1568280052/migrate.go +++ b/core/store/migrations/migration1568280052/migrate.go @@ -1,10 +1,11 @@ package migration1568280052 import ( + "chainlink/core/store/migrations/migration1565877314" + "chainlink/core/store/models" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1565877314" - "github.com/smartcontractkit/chainlink/core/store/models" ) type ExternalInitiator struct { diff --git a/core/store/migrations/migration1568390387/migrate.go b/core/store/migrations/migration1568390387/migrate.go index 6b614ced69d..959db1cc207 100644 --- a/core/store/migrations/migration1568390387/migrate.go +++ b/core/store/migrations/migration1568390387/migrate.go @@ -3,9 +3,10 @@ package migration1568390387 import ( "time" + "chainlink/core/store/assets" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/assets" ) type Encumbrance struct { diff --git a/core/store/migrations/migration1568833756/migrate.go b/core/store/migrations/migration1568833756/migrate.go index a42d8f5095d..c22696ba5b1 100644 --- a/core/store/migrations/migration1568833756/migrate.go +++ b/core/store/migrations/migration1568833756/migrate.go @@ -3,10 +3,11 @@ package migration1568833756 import ( "time" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common" "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/models" "gopkg.in/guregu/null.v3" ) diff --git a/core/store/migrations/migration1570087128/migrate.go b/core/store/migrations/migration1570087128/migrate.go index 4205d1e5c16..3e35090eae5 100644 --- a/core/store/migrations/migration1570087128/migrate.go +++ b/core/store/migrations/migration1570087128/migrate.go @@ -1,8 +1,9 @@ package migration1570087128 import ( + "chainlink/core/store/dbutil" + "github.com/jinzhu/gorm" - "github.com/smartcontractkit/chainlink/core/store/dbutil" ) func Migrate(tx *gorm.DB) error { diff --git a/core/store/models/address.go b/core/store/models/address.go index 3b42227cb30..a0068fa3dd0 100644 --- a/core/store/models/address.go +++ b/core/store/models/address.go @@ -6,8 +6,9 @@ import ( "math/big" "strings" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/utils" ) // EIP55Address is a newtype for string which persists an ethereum address in diff --git a/core/store/models/big.go b/core/store/models/big.go index c359541d4a3..a27cbe3c8a9 100644 --- a/core/store/models/big.go +++ b/core/store/models/big.go @@ -5,8 +5,9 @@ import ( "fmt" "math/big" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/chainlink/core/utils" ) // Big stores large integers and can deserialize a variety of inputs. diff --git a/core/store/models/bridge_run_result.go b/core/store/models/bridge_run_result.go new file mode 100644 index 00000000000..4ff548a1724 --- /dev/null +++ b/core/store/models/bridge_run_result.go @@ -0,0 +1,50 @@ +package models + +import ( + "encoding/json" + "errors" + + null "gopkg.in/guregu/null.v3" +) + +// BridgeRunResult handles the parsing of RunResults from external adapters. +type BridgeRunResult struct { + Data JSON `json:"data"` + Status RunStatus `json:"status"` + ErrorMessage null.String `json:"error"` + ExternalPending bool `json:"pending"` + AccessToken string `json:"accessToken"` +} + +// UnmarshalJSON parses the given input and updates the BridgeRunResult in the +// external adapter format. +func (brr *BridgeRunResult) UnmarshalJSON(input []byte) error { + // XXX: This indirection prevents an infinite regress during json.Unmarshal + type biAlias BridgeRunResult + var anon biAlias + err := json.Unmarshal(input, &anon) + *brr = BridgeRunResult(anon) + + if brr.Status == RunStatusErrored || brr.ErrorMessage.Valid { + brr.Status = RunStatusErrored + } else if brr.ExternalPending || brr.Status.PendingBridge() { + brr.Status = RunStatusPendingBridge + } else { + brr.Status = RunStatusCompleted + } + + return err +} + +// HasError returns true if the status is errored or the error message is set +func (brr BridgeRunResult) HasError() bool { + return brr.Status == RunStatusErrored || brr.ErrorMessage.Valid +} + +// GetError returns the error of a BridgeRunResult if it is present. +func (brr BridgeRunResult) GetError() error { + if brr.HasError() { + return errors.New(brr.ErrorMessage.ValueOrZero()) + } + return nil +} diff --git a/core/store/models/bridge_type.go b/core/store/models/bridge_type.go index 7eefe3151b7..315850591b7 100644 --- a/core/store/models/bridge_type.go +++ b/core/store/models/bridge_type.go @@ -4,8 +4,8 @@ import ( "crypto/subtle" "fmt" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/store/assets" + "chainlink/core/utils" ) // BridgeTypeRequest is the incoming record used to create a BridgeType diff --git a/core/store/models/bridge_type_test.go b/core/store/models/bridge_type_test.go index b03769c2aff..d03598fc224 100644 --- a/core/store/models/bridge_type_test.go +++ b/core/store/models/bridge_type_test.go @@ -3,8 +3,9 @@ package models_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/core/store/models/bulk_test.go b/core/store/models/bulk_test.go index 02ae691d19f..db9ad4cb21a 100644 --- a/core/store/models/bulk_test.go +++ b/core/store/models/bulk_test.go @@ -3,7 +3,8 @@ package models_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" ) diff --git a/core/store/models/cbor.go b/core/store/models/cbor.go index bb41e7c6202..94fd8e136b3 100644 --- a/core/store/models/cbor.go +++ b/core/store/models/cbor.go @@ -4,7 +4,8 @@ import ( "bytes" "encoding/json" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/utils" + "github.com/ugorji/go/codec" ) diff --git a/core/store/models/common.go b/core/store/models/common.go index fe2dd2884bb..e9afe5f0a42 100644 --- a/core/store/models/common.go +++ b/core/store/models/common.go @@ -9,9 +9,10 @@ import ( "strings" "time" + "chainlink/core/store/assets" + "github.com/araddon/dateparse" "github.com/jinzhu/gorm" - "github.com/smartcontractkit/chainlink/core/store/assets" "github.com/ethereum/go-ethereum/common" "github.com/mrwonko/cron" @@ -20,12 +21,6 @@ import ( "github.com/ugorji/go/codec" ) -var ( - // ErrorCannotMergeNonObject is returned if a Merge was attempted on a string - // or array JSON value - ErrorCannotMergeNonObject = errors.New("Cannot merge, expected object '{}'") -) - // RunStatus is a string that represents the run status type RunStatus string @@ -47,6 +42,8 @@ const ( RunStatusErrored = RunStatus("errored") // RunStatusCompleted is used for when a run has successfully completed execution. RunStatusCompleted = RunStatus("completed") + // RunStatusCancelled is used to indicate a run is no longer desired. + RunStatusCancelled = RunStatus("cancelled") ) // Unstarted returns true if the status is the initial state. @@ -79,6 +76,11 @@ func (s RunStatus) Completed() bool { return s == RunStatusCompleted } +// Cancelled returns true if the status is RunStatusCancelled. +func (s RunStatus) Cancelled() bool { + return s == RunStatusCancelled +} + // Errored returns true if the status is RunStatusErrored. func (s RunStatus) Errored() bool { return s == RunStatusErrored @@ -91,7 +93,7 @@ func (s RunStatus) Pending() bool { // Finished returns true if the status is final and can't be changed. func (s RunStatus) Finished() bool { - return s.Completed() || s.Errored() + return s.Completed() || s.Errored() || s.Cancelled() } // Runnable returns true if the status is ready to be run. @@ -176,48 +178,35 @@ func (j JSON) MarshalJSON() ([]byte, error) { return []byte("{}"), nil } -// Merge combines the given JSON with the existing JSON. -func (j JSON) Merge(j2 JSON) (JSON, error) { - body := j.Map() - if body == nil || (j.Type != gjson.JSON && j.Type != gjson.Null) { - return JSON{}, ErrorCannotMergeNonObject - } - - for key, value := range j2.Map() { - body[key] = value - } - - cleaned := map[string]interface{}{} - for k, v := range body { - cleaned[k] = v.Value() - } - - b, err := json.Marshal(cleaned) - if err != nil { - return JSON{}, err - } - - var rval JSON - return rval, gjson.Unmarshal(b, &rval) -} - // Bytes returns the raw JSON. func (j JSON) Bytes() []byte { return []byte(j.String()) } // Add returns a new instance of JSON with the new value added. -func (j JSON) Add(key string, val interface{}) (JSON, error) { - var j2 JSON - b, err := json.Marshal(val) - if err != nil { - return j2, err +func (j JSON) Add(insertKey string, insertValue interface{}) (JSON, error) { + output := make(map[string]interface{}) + + switch v := j.Result.Value().(type) { + case map[string]interface{}: + for key, value := range v { + if key != insertKey { + output[key] = value + } + } + output[insertKey] = insertValue + case nil: + output[insertKey] = insertValue + default: + return JSON{}, errors.New("can only add to JSON objects or null") } - str := fmt.Sprintf(`{"%v":%v}`, key, string(b)) - if err = json.Unmarshal([]byte(str), &j2); err != nil { - return j2, err + + bytes, err := json.Marshal(output) + if err != nil { + return JSON{}, err } - return j.Merge(j2) + + return JSON{Result: gjson.ParseBytes(bytes)}, nil } // Delete returns a new instance of JSON with the specified key removed. @@ -472,3 +461,27 @@ type Configuration struct { Name string `gorm:"not null;unique;index"` Value string `gorm:"not null"` } + +// Merge returns a new map with all keys merged from right to left +func Merge(inputs ...JSON) (JSON, error) { + output := make(map[string]interface{}) + + for _, input := range inputs { + switch v := input.Result.Value().(type) { + case map[string]interface{}: + for key, value := range v { + output[key] = value + } + case nil: + default: + return JSON{}, errors.New("can only merge JSON objects") + } + } + + bytes, err := json.Marshal(output) + if err != nil { + return JSON{}, err + } + + return JSON{Result: gjson.ParseBytes(bytes)}, nil +} diff --git a/core/store/models/common_test.go b/core/store/models/common_test.go index bf763d0d3c4..dfd2a669317 100644 --- a/core/store/models/common_test.go +++ b/core/store/models/common_test.go @@ -7,9 +7,10 @@ import ( "testing" "time" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ugorji/go/codec" @@ -81,7 +82,7 @@ func TestJSON_Merge(t *testing.T) { j1 := cltest.JSONFromString(t, test.original) j2 := cltest.JSONFromString(t, test.input) - merged, err := j1.Merge(j2) + merged, err := models.Merge(j1, j2) if test.wantError { require.Error(t, err) } else { @@ -94,7 +95,7 @@ func TestJSON_Merge(t *testing.T) { } func TestJSON_MergeNull(t *testing.T) { - merged, err := models.JSON{}.Merge(models.JSON{}) + merged, err := models.Merge(models.JSON{}, models.JSON{}) require.NoError(t, err) assert.Equal(t, `{}`, merged.String()) } diff --git a/core/store/models/eth.go b/core/store/models/eth.go index 57a1d100284..0e713c89e59 100755 --- a/core/store/models/eth.go +++ b/core/store/models/eth.go @@ -10,11 +10,12 @@ import ( "regexp" "time" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/jinzhu/gorm" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/tidwall/gjson" "go.uber.org/multierr" null "gopkg.in/guregu/null.v3" @@ -59,6 +60,13 @@ func (log Log) getTopic(idx uint) (common.Hash, error) { return log.Topics[idx], nil } +// logMarshaling represents an ethereum event log. +// +// NOTE: If this is changed, gen_log_json.go must be changed accordingly. It was +// generated by the above "//go:generate gencodec" command, which is currently +// broken. (It seems as though the problem might be that gencodec doesn't work +// with modules-based packages, in which case it could probably be run outside +// chainlink. https://github.com/fjl/gencodec/issues/10) type logMarshaling struct { Data hexutil.Bytes BlockNumber hexutil.Uint64 @@ -95,7 +103,7 @@ type Tx struct { // String implements Stringer for Tx func (tx *Tx) String() string { - return fmt.Sprintf("Tx{ID: %d, From: %s, To: %s, Hash: %s, SentAt: %d}", + return fmt.Sprintf("Tx(ID: %d, From: %s, To: %s, Hash: %s, SentAt: %d)", tx.ID, tx.From.String(), tx.To.String(), diff --git a/core/store/models/eth_test.go b/core/store/models/eth_test.go index d46d628c69f..38020c8c5a1 100644 --- a/core/store/models/eth_test.go +++ b/core/store/models/eth_test.go @@ -8,11 +8,12 @@ import ( "sort" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" diff --git a/core/store/models/external_initiator.go b/core/store/models/external_initiator.go index 45f33845518..f2a226db8e8 100644 --- a/core/store/models/external_initiator.go +++ b/core/store/models/external_initiator.go @@ -6,9 +6,10 @@ import ( "fmt" "strings" + "chainlink/core/utils" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/utils" "golang.org/x/crypto/sha3" ) diff --git a/core/store/models/external_initiator_test.go b/core/store/models/external_initiator_test.go index 52c9c49e491..e5263fccbae 100644 --- a/core/store/models/external_initiator_test.go +++ b/core/store/models/external_initiator_test.go @@ -3,8 +3,9 @@ package models_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" ) diff --git a/core/store/models/id.go b/core/store/models/id.go index c7a806fcc15..166b547d36c 100644 --- a/core/store/models/id.go +++ b/core/store/models/id.go @@ -5,8 +5,9 @@ import ( "fmt" "strings" + "chainlink/core/utils" + uuid "github.com/satori/go.uuid" - "github.com/smartcontractkit/chainlink/core/utils" ) // ID is a UUID that has a custom display format diff --git a/core/store/models/job_run.go b/core/store/models/job_run.go index 06db64f34b9..911e422d7a3 100644 --- a/core/store/models/job_run.go +++ b/core/store/models/job_run.go @@ -1,15 +1,13 @@ package models import ( - "encoding/json" - "errors" "fmt" "time" + clnull "chainlink/core/null" + "chainlink/core/store/assets" + "github.com/ethereum/go-ethereum/common" - clnull "github.com/smartcontractkit/chainlink/core/null" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/tidwall/gjson" null "gopkg.in/guregu/null.v3" ) @@ -67,17 +65,22 @@ func (jr JobRun) ForLogger(kvs ...interface{}) []interface{} { output = append(output, "observed_height", jr.ObservedHeight.ToInt()) } - if jr.Result.HasError() { - output = append(output, "job_error", jr.Result.Error()) + if jr.HasError() { + output = append(output, "job_error", jr.ErrorString()) } - if jr.Status == "completed" { + if jr.Status.Completed() { output = append(output, "link_earned", jr.Payment) } return append(kvs, output...) } +// HasError returns true if this JobRun has errored +func (jr JobRun) HasError() bool { + return jr.Status.Errored() +} + // NextTaskRunIndex returns the position of the next unfinished task func (jr *JobRun) NextTaskRunIndex() (int, bool) { for index, tr := range jr.TaskRuns { @@ -116,38 +119,49 @@ func (jr *JobRun) TasksRemain() bool { // SetError sets this job run to failed and saves the error message func (jr *JobRun) SetError(err error) { jr.Result.ErrorMessage = null.StringFrom(err.Error()) - jr.Result.Status = RunStatusErrored - jr.Status = jr.Result.Status - jr.FinishedAt = null.TimeFrom(time.Now()) + jr.setStatus(RunStatusErrored) +} + +// Cancel sets this run as cancelled, it should no longer be processed. +func (jr *JobRun) Cancel() { + currentTaskRun := jr.NextTaskRun() + if currentTaskRun != nil { + currentTaskRun.Status = RunStatusCancelled + } + jr.setStatus(RunStatusCancelled) +} + +// ApplyOutput updates the JobRun's Result and Status +func (jr *JobRun) ApplyOutput(result RunOutput) { + if result.HasError() { + jr.SetError(result.Error()) + return + } + jr.Result.Data = result.Data() + jr.setStatus(result.Status()) } -// ApplyResult updates the JobRun's Result and Status -func (jr *JobRun) ApplyResult(result RunResult) error { - data, err := jr.Result.Data.Merge(result.Data) - if err != nil { - return err +// ApplyBridgeRunResult saves the input from a BridgeAdapter +func (jr *JobRun) ApplyBridgeRunResult(result BridgeRunResult) { + if result.HasError() { + jr.SetError(result.GetError()) } - jr.Result = result - jr.Result.Data = data - jr.Status = result.Status + jr.Result.Data = result.Data + jr.setStatus(result.Status) +} + +func (jr *JobRun) setStatus(status RunStatus) { + jr.Status = status if jr.Status.Completed() && jr.TasksRemain() { jr.Status = RunStatusInProgress } else if jr.Status.Finished() { jr.FinishedAt = null.TimeFrom(time.Now()) } - return nil } -// JobRunsWithStatus filters passed job runs returning those that have -// the desired status, entirely in memory. -func JobRunsWithStatus(runs []JobRun, status RunStatus) []JobRun { - rval := []JobRun{} - for _, r := range runs { - if r.Status == status { - rval = append(rval, r) - } - } - return rval +// ErrorString returns the error as a string if present, otherwise "". +func (jr *JobRun) ErrorString() string { + return jr.Result.ErrorMessage.ValueOrZero() } // RunRequest stores the fields used to initiate the parent job run. @@ -162,8 +176,8 @@ type RunRequest struct { } // NewRunRequest returns a new RunRequest instance. -func NewRunRequest() RunRequest { - return RunRequest{CreatedAt: time.Now()} +func NewRunRequest() *RunRequest { + return &RunRequest{CreatedAt: time.Now()} } // TaskRun stores the Task and represents the status of the @@ -186,178 +200,35 @@ func (tr TaskRun) String() string { return fmt.Sprintf("TaskRun(%v,%v,%v,%v)", tr.ID.String(), tr.TaskSpec.Type, tr.Status, tr.Result) } -// ForLogger formats the TaskRun info for a common formatting in the log. -func (tr *TaskRun) ForLogger(kvs ...interface{}) []interface{} { - output := []interface{}{ - "type", tr.TaskSpec.Type, - "params", tr.TaskSpec.Params, - "taskrun", tr.ID, - "status", tr.Status, - } - - if tr.Result.HasError() { - output = append(output, "error", tr.Result.Error()) - } - - return append(kvs, output...) -} - // SetError sets this task run to failed and saves the error message func (tr *TaskRun) SetError(err error) { tr.Result.ErrorMessage = null.StringFrom(err.Error()) - tr.Result.Status = RunStatusErrored - tr.Status = tr.Result.Status + tr.Status = RunStatusErrored } -// ApplyResult updates the TaskRun's Result and Status -func (tr *TaskRun) ApplyResult(result RunResult) { - tr.Result = result +// ApplyBridgeRunResult updates the TaskRun's Result and Status +func (tr *TaskRun) ApplyBridgeRunResult(result BridgeRunResult) { + if result.HasError() { + tr.SetError(result.GetError()) + } + tr.Result.Data = result.Data tr.Status = result.Status } -// MarkPendingConfirmations marks the task's status as blocked. -func (tr *TaskRun) MarkPendingConfirmations() { - tr.Status = RunStatusPendingConfirmations - tr.Result.Status = RunStatusPendingConfirmations -} - -// RunResult keeps track of the outcome of a TaskRun or JobRun. It stores the -// Data and ErrorMessage, and contains a field to track the status. -type RunResult struct { - ID uint `json:"-" gorm:"primary_key;auto_increment"` - CachedJobRunID *ID `json:"jobRunId" gorm:"-"` - CachedTaskRunID *ID `json:"taskRunId" gorm:"-"` - Data JSON `json:"data" gorm:"type:text"` - Status RunStatus `json:"status"` - ErrorMessage null.String `json:"error"` -} - -func RunResultComplete(resultVal interface{}) RunResult { - var result RunResult - result.CompleteWithResult(resultVal) - return result -} - -func RunResultError(err error) RunResult { - var result RunResult - result.SetError(err) - return result -} - -// CompleteWithResult saves a value to a RunResult and marks it as completed -func (rr *RunResult) CompleteWithResult(val interface{}) { - rr.Status = RunStatusCompleted - rr.ApplyResult(val) -} - -// ApplyResult saves a value to a RunResult with the key result. -func (rr *RunResult) ApplyResult(val interface{}) { - rr.Add("result", val) -} - -// Add adds a key and result to the RunResult's JSON payload. -func (rr *RunResult) Add(key string, result interface{}) { - data, err := rr.Data.Add(key, result) - if err != nil { - rr.SetError(err) +// ApplyOutput updates the TaskRun's Result and Status +func (tr *TaskRun) ApplyOutput(result RunOutput) { + if result.HasError() { + tr.SetError(result.Error()) return } - rr.Data = data -} - -// SetError marks the result as errored and saves the specified error message -func (rr *RunResult) SetError(err error) { - rr.ErrorMessage = null.StringFrom(err.Error()) - rr.Status = RunStatusErrored -} - -// MarkPendingBridge sets the status to pending_bridge -func (rr *RunResult) MarkPendingBridge() { - rr.Status = RunStatusPendingBridge -} - -// MarkPendingConfirmations sets the status to pending_confirmations. -func (rr *RunResult) MarkPendingConfirmations() { - rr.Status = RunStatusPendingConfirmations -} - -// MarkPendingConnection sets the status to pending_connection. -func (rr *RunResult) MarkPendingConnection() { - rr.Status = RunStatusPendingConnection -} - -// Get searches for and returns the JSON at the given path. -func (rr *RunResult) Get(path string) gjson.Result { - return rr.Data.Get(path) -} - -// ResultString returns the string result of the Data JSON field. -func (rr *RunResult) ResultString() (string, error) { - val := rr.Result() - if val.Type != gjson.String { - return "", fmt.Errorf("non string result") - } - return val.String(), nil -} - -// Result returns the result as a gjson object -func (rr *RunResult) Result() gjson.Result { - return rr.Get("result") + tr.Result.Data = result.Data() + tr.Status = result.Status() } -// HasError returns true if the ErrorMessage is present. -func (rr *RunResult) HasError() bool { - return rr.ErrorMessage.Valid -} - -// Error returns the string value of the ErrorMessage field. -func (rr *RunResult) Error() string { - return rr.ErrorMessage.String -} - -// GetError returns the error of a RunResult if it is present. -func (rr *RunResult) GetError() error { - if rr.HasError() { - return errors.New(rr.ErrorMessage.ValueOrZero()) - } - return nil -} - -// Merge saves the specified result's data onto the receiving RunResult. The -// input result's data takes preference over the receivers'. -func (rr *RunResult) Merge(in RunResult) error { - var err error - rr.Data, err = rr.Data.Merge(in.Data) - if err != nil { - return err - } - rr.ErrorMessage = in.ErrorMessage - rr.Status = in.Status - return nil -} - -// BridgeRunResult handles the parsing of RunResults from external adapters. -type BridgeRunResult struct { - RunResult - ExternalPending bool `json:"pending"` - AccessToken string `json:"accessToken"` -} - -// UnmarshalJSON parses the given input and updates the BridgeRunResult in the -// external adapter format. -func (brr *BridgeRunResult) UnmarshalJSON(input []byte) error { - type biAlias BridgeRunResult - var anon biAlias - err := json.Unmarshal(input, &anon) - *brr = BridgeRunResult(anon) - - if brr.Status.Errored() || brr.HasError() { - brr.Status = RunStatusErrored - } else if brr.ExternalPending || brr.Status.PendingBridge() { - brr.Status = RunStatusPendingBridge - } else { - brr.Status = RunStatusCompleted - } - - return err +// RunResult keeps track of the outcome of a TaskRun or JobRun. It stores the +// Data and ErrorMessage. +type RunResult struct { + ID uint `json:"-" gorm:"primary_key;auto_increment"` + Data JSON `json:"data" gorm:"type:text"` + ErrorMessage null.String `json:"error"` } diff --git a/core/store/models/job_run_test.go b/core/store/models/job_run_test.go index c0b73850073..538d3385c90 100644 --- a/core/store/models/job_run_test.go +++ b/core/store/models/job_run_test.go @@ -3,21 +3,19 @@ package models_test import ( "encoding/json" "errors" - "fmt" "math/big" "testing" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/internal/cltest" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" null "gopkg.in/guregu/null.v3" ) -func TestJobRuns_RetrievingFromDBWithError(t *testing.T) { +func TestJobRun_RetrievingFromDBWithError(t *testing.T) { t.Parallel() store, cleanup := cltest.NewStore(t) defer cleanup() @@ -26,17 +24,17 @@ func TestJobRuns_RetrievingFromDBWithError(t *testing.T) { require.NoError(t, store.CreateJob(&job)) jr := job.NewRun(job.Initiators[0]) jr.JobSpecID = job.ID - jr.Result = cltest.RunResultWithError(fmt.Errorf("bad idea")) + jr.Result.ErrorMessage = null.StringFrom("bad idea") err := store.CreateJobRun(&jr) require.NoError(t, err) run, err := store.FindJobRun(jr.ID) - assert.NoError(t, err) - assert.True(t, run.Result.HasError()) - assert.Equal(t, "bad idea", run.Result.Error()) + require.NoError(t, err) + assert.True(t, run.Result.ErrorMessage.Valid) + assert.Equal(t, "bad idea", run.ErrorString()) } -func TestJobRuns_RetrievingFromDBWithData(t *testing.T) { +func TestJobRun_RetrievingFromDBWithData(t *testing.T) { t.Parallel() store, cleanup := cltest.NewStore(t) defer cleanup() @@ -48,17 +46,17 @@ func TestJobRuns_RetrievingFromDBWithData(t *testing.T) { jr := job.NewRun(initr) data := `{"result":"11850.00"}` - jr.Result = cltest.RunResultWithData(data) + jr.Result = models.RunResult{Data: cltest.JSONFromString(t, data)} err = store.CreateJobRun(&jr) assert.NoError(t, err) run, err := store.FindJobRun(jr.ID) assert.NoError(t, err) - assert.False(t, run.Result.HasError()) + assert.False(t, run.Result.ErrorMessage.Valid) assert.JSONEq(t, data, run.Result.Data.String()) } -func TestJobRuns_SavesASyncEvent(t *testing.T) { +func TestJobRun_SavesASyncEvent(t *testing.T) { t.Parallel() config, _ := cltest.NewConfig(t) config.Set("EXPLORER_URL", "http://localhost:4201") @@ -97,7 +95,7 @@ func TestJobRuns_SavesASyncEvent(t *testing.T) { assert.Contains(t, data, "status") } -func TestJobRuns_SkipsEventSaveIfURLBlank(t *testing.T) { +func TestJobRun_SkipsEventSaveIfURLBlank(t *testing.T) { t.Parallel() config, _ := cltest.NewConfig(t) config.Set("EXPLORER_URL", "") @@ -111,7 +109,7 @@ func TestJobRuns_SkipsEventSaveIfURLBlank(t *testing.T) { jr := job.NewRun(initr) data := `{"result":"921.02"}` - jr.Result = cltest.RunResultWithData(data) + jr.Result = models.RunResult{Data: cltest.JSONFromString(t, data)} err = store.CreateJobRun(&jr) assert.NoError(t, err) @@ -124,7 +122,7 @@ func TestJobRuns_SkipsEventSaveIfURLBlank(t *testing.T) { require.Len(t, events, 0) } -func TestForLogger(t *testing.T) { +func TestJobRun_ForLogger(t *testing.T) { t.Parallel() store, cleanup := cltest.NewStore(t) defer cleanup() @@ -135,7 +133,7 @@ func TestForLogger(t *testing.T) { jr.JobSpecID = job.ID linkReward := assets.NewLink(5) - jr.Result = cltest.RunResultWithData(`{"result":"11850.00"}`) + jr.Result = models.RunResult{Data: cltest.JSONFromString(t, `{"result":"11850.00"}`)} jr.Payment = linkReward logsBeforeCompletion := jr.ForLogger() require.Len(t, logsBeforeCompletion, 6) @@ -163,216 +161,47 @@ func TestForLogger(t *testing.T) { assert.Equal(t, logsWithBlockHeights[8], "observed_height") assert.Equal(t, logsWithBlockHeights[9], big.NewInt(10)) - jrErr := job.NewRun(job.Initiators[0]) - jrErr.Result = cltest.RunResultWithError(fmt.Errorf("bad idea")) - logsWithErr := jrErr.ForLogger() - assert.Equal(t, logsWithErr[6], "job_error") - assert.Equal(t, logsWithErr[7], jrErr.Result.Error()) - -} - -func TestJobRun_NextTaskRun(t *testing.T) { - t.Parallel() - - store, cleanup := cltest.NewStore(t) - defer cleanup() - jobRunner, cleanup := cltest.NewJobRunner(store) - defer cleanup() - jobRunner.Start() - - job := cltest.NewJobWithWebInitiator() - job.Tasks = []models.TaskSpec{ - {Type: adapters.TaskTypeNoOp}, - {Type: adapters.TaskTypeNoOpPend}, - {Type: adapters.TaskTypeNoOp}, - } - assert.NoError(t, store.CreateJob(&job)) run := job.NewRun(job.Initiators[0]) - assert.NoError(t, store.CreateJobRun(&run)) - assert.Equal(t, &run.TaskRuns[0], run.NextTaskRun()) - - store.RunChannel.Send(run.ID) - cltest.WaitForJobRunStatus(t, store, run, models.RunStatusPendingConfirmations) - - run, err := store.FindJobRun(run.ID) - assert.NoError(t, err) - assert.Equal(t, &run.TaskRuns[1], run.NextTaskRun()) -} - -func TestRunResult_Value(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - json string - want string - wantErrored bool - }{ - {"string", `{"result": "100", "other": "101"}`, "100", false}, - {"integer", `{"result": 100}`, "", true}, - {"float", `{"result": 100.01}`, "", true}, - {"boolean", `{"result": true}`, "", true}, - {"null", `{"result": null}`, "", true}, - {"no key", `{"other": 100}`, "", true}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var data models.JSON - assert.NoError(t, json.Unmarshal([]byte(test.json), &data)) - rr := models.RunResult{Data: data} - - val, err := rr.ResultString() - assert.Equal(t, test.want, val) - assert.Equal(t, test.wantErrored, (err != nil)) - }) - } -} - -func TestRunResult_Add(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - json string - key string - value interface{} - want string - }{ - {"string", `{"a": "1"}`, "b", "2", `{"a": "1", "b": "2"}`}, - {"int", `{"a": "1"}`, "b", 2, `{"a": "1", "b": 2}`}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var data models.JSON - assert.NoError(t, json.Unmarshal([]byte(test.json), &data)) - rr := models.RunResult{Data: data} - - rr.Add(test.key, test.value) - - assert.JSONEq(t, test.want, rr.Data.String()) - }) - } -} - -func TestRunResult_WithError(t *testing.T) { - t.Parallel() - - rr := models.RunResult{} - - assert.Equal(t, models.RunStatusUnstarted, rr.Status) - - rr.SetError(errors.New("this blew up")) - - assert.Equal(t, models.RunStatusErrored, rr.Status) - assert.Equal(t, cltest.NullString("this blew up"), rr.ErrorMessage) -} - -func TestRunResult_Merge(t *testing.T) { - t.Parallel() - - inProgress := models.RunStatusInProgress - pending := models.RunStatusPendingBridge - errored := models.RunStatusErrored - completed := models.RunStatusCompleted - - nullString := cltest.NullString(nil) - tests := []struct { - name string - originalData string - originalError null.String - originalStatus models.RunStatus - inData string - inError null.String - inStatus models.RunStatus - wantData string - wantErrorMessage null.String - wantStatus models.RunStatus - }{ - {"merging data", - `{"result":"old&busted","unique":"1"}`, nullString, inProgress, - `{"result":"newHotness","and":"!"}`, nullString, inProgress, - `{"result":"newHotness","unique":"1","and":"!"}`, nullString, inProgress}, - {"completed result", - `{"result":"old"}`, nullString, inProgress, - `{}`, nullString, completed, - `{"result":"old"}`, nullString, completed}, - {"error override", - `{"result":"old"}`, nullString, inProgress, - `{}`, cltest.NullString("new problem"), errored, - `{"result":"old"}`, cltest.NullString("new problem"), errored}, - {"pending override", - `{"result":"old"}`, nullString, inProgress, - `{}`, nullString, pending, - `{"result":"old"}`, nullString, pending}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - original := models.RunResult{ - Data: models.JSON{Result: gjson.Parse(test.originalData)}, - ErrorMessage: test.originalError, - Status: test.originalStatus, - } - in := models.RunResult{ - Data: cltest.JSONFromString(t, test.inData), - ErrorMessage: test.inError, - Status: test.inStatus, - } - merged := original - merged.Merge(in) - - assert.JSONEq(t, test.originalData, original.Data.String()) - assert.Equal(t, test.originalError, original.ErrorMessage) - assert.Equal(t, test.originalStatus, original.Status) - - assert.JSONEq(t, test.inData, in.Data.String()) - assert.Equal(t, test.inError, in.ErrorMessage) - assert.Equal(t, test.inStatus, in.Status) - - assert.JSONEq(t, test.wantData, merged.Data.String()) - assert.Equal(t, test.wantErrorMessage, merged.ErrorMessage) - assert.Equal(t, test.wantStatus, merged.Status) - }) - } + run.Status = models.RunStatusErrored + run.Result.ErrorMessage = null.StringFrom("bad idea") + logsWithErr := run.ForLogger() + require.Len(t, logsWithErr, 8) + assert.Equal(t, logsWithErr[6], "job_error") + assert.Equal(t, logsWithErr[7], run.ErrorString()) } -func TestJobRun_ApplyResult_CompletedWithNoTasksRemaining(t *testing.T) { +func TestJobRun_ApplyOutput_CompletedWithNoTasksRemaining(t *testing.T) { t.Parallel() job := cltest.NewJobWithWebInitiator() jobRun := job.NewRun(job.Initiators[0]) - result := models.RunResult{Status: models.RunStatusCompleted} - jobRun.TaskRuns[0].ApplyResult(result) - err := jobRun.ApplyResult(result) - assert.NoError(t, err) + result := models.NewRunOutputComplete(models.JSON{}) + jobRun.TaskRuns[0].ApplyOutput(result) + jobRun.ApplyOutput(result) assert.True(t, jobRun.FinishedAt.Valid) } -func TestJobRun_ApplyResult_CompletedWithTasksRemaining(t *testing.T) { +func TestJobRun_ApplyOutput_CompletedWithTasksRemaining(t *testing.T) { t.Parallel() job := cltest.NewJobWithWebInitiator() jobRun := job.NewRun(job.Initiators[0]) - result := models.RunResult{Status: models.RunStatusCompleted} - err := jobRun.ApplyResult(result) - assert.NoError(t, err) + result := models.NewRunOutputComplete(models.JSON{}) + jobRun.ApplyOutput(result) assert.False(t, jobRun.FinishedAt.Valid) assert.Equal(t, jobRun.Status, models.RunStatusInProgress) } -func TestJobRun_ApplyResult_ErrorSetsFinishedAt(t *testing.T) { +func TestJobRun_ApplyOutput_ErrorSetsFinishedAt(t *testing.T) { t.Parallel() job := cltest.NewJobWithWebInitiator() jobRun := job.NewRun(job.Initiators[0]) jobRun.Status = models.RunStatusErrored - result := models.RunResult{Status: models.RunStatusErrored} - err := jobRun.ApplyResult(result) - assert.NoError(t, err) + result := models.NewRunOutputError(errors.New("oh futz")) + jobRun.ApplyOutput(result) assert.True(t, jobRun.FinishedAt.Valid) } diff --git a/core/store/models/job_spec.go b/core/store/models/job_spec.go index 0263b8e2da2..295e4134fc1 100644 --- a/core/store/models/job_spec.go +++ b/core/store/models/job_spec.go @@ -8,11 +8,12 @@ import ( "strings" "time" + clnull "chainlink/core/null" + "chainlink/core/store/assets" + "github.com/ethereum/go-ethereum/common" "github.com/jinzhu/gorm" "github.com/pkg/errors" - clnull "github.com/smartcontractkit/chainlink/core/null" - "github.com/smartcontractkit/chainlink/core/store/assets" null "gopkg.in/guregu/null.v3" ) @@ -22,7 +23,7 @@ type JobSpecRequest struct { Tasks []TaskSpecRequest `json:"tasks"` StartAt null.Time `json:"startAt"` EndAt null.Time `json:"endAt"` - MinPayment *assets.Link `json:"minPayment"` + MinPayment assets.Link `json:"minPayment"` } // InitiatorRequest represents a schema for incoming initiator requests as used by the API. @@ -64,14 +65,14 @@ type TaskSpecRequest struct { // for a given contract. It contains the Initiators, Tasks (which are the // individual steps to be carried out), StartAt, EndAt, and CreatedAt fields. type JobSpec struct { - ID *ID `json:"id,omitempty" gorm:"primary_key;not null"` - CreatedAt time.Time `json:"createdAt" gorm:"index"` - Initiators []Initiator `json:"initiators"` - MinPayment *assets.Link `json:"minPayment" gorm:"type:varchar(255)"` - Tasks []TaskSpec `json:"tasks"` - StartAt null.Time `json:"startAt" gorm:"index"` - EndAt null.Time `json:"endAt" gorm:"index"` - DeletedAt null.Time `json:"-" gorm:"index"` + ID *ID `json:"id,omitempty" gorm:"primary_key;not null"` + CreatedAt time.Time `json:"createdAt" gorm:"index"` + Initiators []Initiator `json:"initiators"` + MinPayment assets.Link `json:"minPayment" gorm:"type:varchar(255)"` + Tasks []TaskSpec `json:"tasks"` + StartAt null.Time `json:"startAt" gorm:"index"` + EndAt null.Time `json:"endAt" gorm:"index"` + DeletedAt null.Time `json:"-" gorm:"index"` } // GetID returns the ID of this structure for jsonapi serialization. @@ -95,7 +96,7 @@ func NewJob() JobSpec { return JobSpec{ ID: NewID(), CreatedAt: time.Now(), - MinPayment: assets.NewLink(0), + MinPayment: *assets.NewLink(0), } } @@ -118,12 +119,15 @@ func NewJobFromRequest(jsr JobSpecRequest) JobSpec { jobSpec.EndAt = jsr.EndAt jobSpec.StartAt = jsr.StartAt - if jsr.MinPayment != nil { - jobSpec.MinPayment = jsr.MinPayment - } + jobSpec.MinPayment = jsr.MinPayment return jobSpec } +// Archived returns true if the job spec has been soft deleted +func (j JobSpec) Archived() bool { + return j.DeletedAt.Valid +} + // NewRun initializes the job by creating the IDs for the job // and all associated tasks, and setting the CreatedAt field. func (j JobSpec) NewRun(i Initiator) JobRun { @@ -135,7 +139,6 @@ func (j JobSpec) NewRun(i Initiator) JobRun { ID: trid, JobRunID: jrid, TaskSpec: task, - Result: RunResult{CachedTaskRunID: trid, CachedJobRunID: jrid}, } } @@ -147,11 +150,10 @@ func (j JobSpec) NewRun(i Initiator) JobRun { CreatedAt: now, UpdatedAt: now, TaskRuns: taskRuns, - RunRequest: runRequest, + RunRequest: *runRequest, Initiator: i, InitiatorID: i.ID, Status: RunStatusUnstarted, - Result: RunResult{CachedJobRunID: jrid}, } } @@ -184,16 +186,6 @@ func (j JobSpec) InitiatorExternal(name string) *Initiator { return found } -// WebAuthorized returns true if the "web" initiator is present. -func (j JobSpec) WebAuthorized() bool { - for _, initr := range j.Initiators { - if initr.Type == InitiatorWeb { - return true - } - } - return false -} - // IsLogInitiated Returns true if any of the job's initiators are triggered by event logs. func (j JobSpec) IsLogInitiated() bool { for _, initr := range j.Initiators { diff --git a/core/store/models/job_spec_test.go b/core/store/models/job_spec_test.go index e65db434922..d892b4e229d 100644 --- a/core/store/models/job_spec_test.go +++ b/core/store/models/job_spec_test.go @@ -5,10 +5,11 @@ import ( "testing" "time" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" null "gopkg.in/guregu/null.v3" @@ -131,7 +132,7 @@ func TestNewJobFromRequest(t *testing.T) { Tasks: cltest.BuildTaskRequests(t, j1.Tasks), StartAt: j1.StartAt, EndAt: j1.EndAt, - MinPayment: assets.NewLink(5), + MinPayment: *assets.NewLink(5), } j2 := models.NewJobFromRequest(jsr) @@ -141,13 +142,13 @@ func TestNewJobFromRequest(t *testing.T) { assert.NoError(t, err) assert.Len(t, fetched1.Initiators, 1) assert.Len(t, fetched1.Tasks, 1) - assert.Equal(t, fetched1.MinPayment, assets.NewLink(0)) + assert.Equal(t, fetched1.MinPayment, *assets.NewLink(0)) fetched2, err := store.FindJob(j2.ID) assert.NoError(t, err) assert.Len(t, fetched2.Initiators, 1) assert.Len(t, fetched2.Tasks, 1) - assert.Equal(t, fetched2.MinPayment, assets.NewLink(5)) + assert.Equal(t, fetched2.MinPayment, *assets.NewLink(5)) } func TestJobSpec_Save(t *testing.T) { @@ -190,7 +191,7 @@ func TestJobSpec_NewRun(t *testing.T) { taskRun := run.TaskRuns[0] assert.Equal(t, "noop", taskRun.TaskSpec.Type.String()) - adapter, _ := adapters.For(taskRun.TaskSpec, store) + adapter, _ := adapters.For(taskRun.TaskSpec, store.Config, store.ORM) assert.NotNil(t, adapter) assert.JSONEq(t, `{"type":"NoOp","a":1}`, taskRun.TaskSpec.Params.String()) diff --git a/core/store/models/log_events.go b/core/store/models/log_events.go index 5fcbdd1b432..e134dbe63d6 100644 --- a/core/store/models/log_events.go +++ b/core/store/models/log_events.go @@ -6,12 +6,13 @@ import ( "fmt" "math/big" + "chainlink/core/logger" + "chainlink/core/store/assets" + "chainlink/core/utils" + ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/utils" ) // Descriptive indices of a RunLog's Topic array @@ -46,11 +47,11 @@ var ( RunLogTopic20190207withoutIndexes = utils.MustHash("OracleRequest(bytes32,address,bytes32,uint256,address,bytes4,uint256,uint256,bytes)") // ServiceAgreementExecutionLogTopic is the signature for the // Coordinator.RunRequest(...) events which Chainlink nodes watch for. See - // https://github.com/smartcontractkit/chainlink/blob/master/evm/contracts/Coordinator.sol#RunRequest + // https://chainlink/blob/master/evm/contracts/Coordinator.sol#RunRequest ServiceAgreementExecutionLogTopic = utils.MustHash("ServiceAgreementExecution(bytes32,address,uint256,uint256,uint256,bytes)") // ChainlinkFulfilledTopic is the signature for the event emitted after calling // ChainlinkClient.validateChainlinkCallback(requestId). - // https://github.com/smartcontractkit/chainlink/blob/master/evm/contracts/ChainlinkClient.sol + // https://chainlink/blob/master/evm/contracts/ChainlinkClient.sol ChainlinkFulfilledTopic = utils.MustHash("ChainlinkFulfilled(bytes32)") // OracleFullfillmentFunctionID0original is the original function selector for fulfilling Ethereum requests. OracleFullfillmentFunctionID0original = utils.MustHash("fulfillData(uint256,bytes32)").Hex()[:10] @@ -129,7 +130,7 @@ func FilterQueryFactory(i Initiator, from *big.Int) (ethereum.FilterQuery, error // i.e. EthLogEvent, RunLogEvent, ServiceAgreementLogEvent, OracleLogEvent type LogRequest interface { GetLog() Log - GetJobSpec() JobSpec + GetJobSpecID() *ID GetInitiator() Initiator Validate() bool @@ -144,8 +145,8 @@ type LogRequest interface { // InitiatorLogEvent encapsulates all information as a result of a received log from an // InitiatorSubscription. type InitiatorLogEvent struct { + JobSpecID ID Log Log - JobSpec JobSpec Initiator Initiator } @@ -169,9 +170,9 @@ func (le InitiatorLogEvent) GetLog() Log { return le.Log } -// GetJobSpec returns the associated JobSpec -func (le InitiatorLogEvent) GetJobSpec() JobSpec { - return le.JobSpec +// GetJobSpecID returns the associated JobSpecID +func (le InitiatorLogEvent) GetJobSpecID() *ID { + return &le.JobSpecID } // GetInitiator returns the initiator. @@ -183,7 +184,7 @@ func (le InitiatorLogEvent) GetInitiator() Initiator { // formatting in logs (trace statements, not ethereum events). func (le InitiatorLogEvent) ForLogger(kvs ...interface{}) []interface{} { output := []interface{}{ - "job", le.JobSpec.ID.String(), + "job", le.JobSpecID.String(), "log", le.Log.BlockNumber, "initiator", le.Initiator, } @@ -197,8 +198,10 @@ func (le InitiatorLogEvent) ForLogger(kvs ...interface{}) []interface{} { // ToDebug prints this event via logger.Debug. func (le InitiatorLogEvent) ToDebug() { friendlyAddress := utils.LogListeningAddress(le.Initiator.Address) - msg := fmt.Sprintf("Received log from block #%v for address %v for job %v", le.Log.BlockNumber, friendlyAddress, le.JobSpec.ID.String()) - logger.Debugw(msg, le.ForLogger()...) + logger.Debugw( + fmt.Sprintf("Received log from block #%v for address %v", le.Log.BlockNumber, friendlyAddress), + le.ForLogger()..., + ) } // BlockNumber returns the block number for the given InitiatorSubscriptionLogEvent. @@ -256,11 +259,11 @@ type RunLogEvent struct { // Validate returns whether or not the contained log has a properly encoded // job id. func (le RunLogEvent) Validate() bool { - jobSpecID := le.JobSpec.ID + jobSpecID := &le.JobSpecID topic := le.Log.Topics[RequestLogTopicJobID] if IDToTopic(jobSpecID) != topic && IDToHexTopic(jobSpecID) != topic { - logger.Errorw("Run Log didn't have matching job ID", le.ForLogger("id", le.JobSpec.ID.String())...) + logger.Errorw("Run Log didn't have matching job ID", le.ForLogger("id", le.JobSpecID.String())...) return false } return true @@ -359,7 +362,7 @@ func parserFromLog(log Log) (logRequestParser, error) { } parser, ok := topicFactoryMap[topic] if !ok { - return nil, fmt.Errorf("No parser for the RunLogEvent topic %v", topic) + return nil, fmt.Errorf("No parser for the RunLogEvent topic %s", topic.String()) } return parser, nil } diff --git a/core/store/models/log_events_test.go b/core/store/models/log_events_test.go index 5fe9db897ee..dbdcdbc7892 100644 --- a/core/store/models/log_events_test.go +++ b/core/store/models/log_events_test.go @@ -5,12 +5,13 @@ import ( "strings" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/store/assets" + "chainlink/core/store/models" + ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -139,8 +140,8 @@ func TestRequestLogEvent_Validate(t *testing.T) { } logRequest := models.InitiatorLogEvent{ - JobSpec: job, - Log: log, + JobSpecID: *job.ID, + Log: log, Initiator: models.Initiator{ Type: models.InitiatorRunLog, InitiatorParams: models.InitiatorParams{ diff --git a/core/store/models/run_input.go b/core/store/models/run_input.go new file mode 100644 index 00000000000..35a80efc291 --- /dev/null +++ b/core/store/models/run_input.go @@ -0,0 +1,65 @@ +package models + +import ( + "fmt" + + "github.com/tidwall/gjson" +) + +// RunInput represents the input for performing a Task +type RunInput struct { + jobRunID ID + data JSON + status RunStatus +} + +// NewRunInput creates a new RunInput with arbitrary data +func NewRunInput(jobRunID *ID, data JSON, status RunStatus) *RunInput { + return &RunInput{ + jobRunID: *jobRunID, + data: data, + status: status, + } +} + +// NewRunInputWithResult creates a new RunInput with a value in the "result" field +func NewRunInputWithResult(jobRunID *ID, value interface{}, status RunStatus) *RunInput { + data, err := JSON{}.Add("result", value) + if err != nil { + panic(fmt.Sprintf("invariant violated, add should not fail on empty JSON %v", err)) + } + return &RunInput{ + jobRunID: *jobRunID, + data: data, + status: status, + } +} + +// Result returns the result as a gjson object +func (ri RunInput) Result() gjson.Result { + return ri.data.Get("result") +} + +// ResultString returns the string result of the Data JSON field. +func (ri RunInput) ResultString() (string, error) { + val := ri.Result() + if val.Type != gjson.String { + return "", fmt.Errorf("non string result") + } + return val.String(), nil +} + +// Status returns the RunInput's status +func (ri RunInput) Status() RunStatus { + return ri.status +} + +// Data returns the RunInput's data +func (ri RunInput) Data() JSON { + return ri.data +} + +// JobRunID returns this RunInput's JobRunID +func (ri RunInput) JobRunID() *ID { + return &ri.jobRunID +} diff --git a/core/store/models/run_output.go b/core/store/models/run_output.go new file mode 100644 index 00000000000..6d8e6b28951 --- /dev/null +++ b/core/store/models/run_output.go @@ -0,0 +1,106 @@ +package models + +import ( + "fmt" + + "github.com/tidwall/gjson" +) + +// RunOutput represents the result of performing a Task +type RunOutput struct { + data JSON + status RunStatus + err error +} + +// NewRunOutputError returns a new RunOutput with an error +func NewRunOutputError(err error) RunOutput { + return RunOutput{ + status: RunStatusErrored, + err: err, + } +} + +// NewRunOutputCompleteWithResult returns a new RunOutput that is complete and +// contains a result +func NewRunOutputCompleteWithResult(resultVal interface{}) RunOutput { + data, err := JSON{}.Add("result", resultVal) + if err != nil { + panic(fmt.Sprintf("invariant violated, add should not fail on empty JSON %v", err)) + } + return NewRunOutputComplete(data) +} + +// NewRunOutputComplete returns a new RunOutput that is complete and contains +// raw data +func NewRunOutputComplete(data JSON) RunOutput { + return RunOutput{status: RunStatusCompleted, data: data} +} + +// NewRunOutputPendingConfirmations returns a new RunOutput that indicates the +// task is pending confirmations +func NewRunOutputPendingConfirmations() RunOutput { + return RunOutput{status: RunStatusPendingConfirmations} +} + +// NewRunOutputPendingConfirmationsWithData returns a new RunOutput that +// indicates the task is pending confirmations but also has some data that +// needs to be fed in on next invocation +func NewRunOutputPendingConfirmationsWithData(data JSON) RunOutput { + return RunOutput{status: RunStatusPendingConfirmations, data: data} +} + +// NewRunOutputPendingConnection returns a new RunOutput that indicates the +// task got disconnected +func NewRunOutputPendingConnection() RunOutput { + return RunOutput{status: RunStatusPendingConnection} +} + +// NewRunOutputPendingConnectionWithData returns a new RunOutput that +// indicates the task got disconnected but also has some data that needs to be +// fed in on next invocation +func NewRunOutputPendingConnectionWithData(data JSON) RunOutput { + return RunOutput{status: RunStatusPendingConnection, data: data} +} + +// NewRunOutputInProgress returns a new RunOutput that indicates the +// task is still in progress +func NewRunOutputInProgress(data JSON) RunOutput { + return RunOutput{status: RunStatusInProgress, data: data} +} + +// NewRunOutputPendingBridge returns a new RunOutput that indicates the +// task is still in progress +func NewRunOutputPendingBridge() RunOutput { + return RunOutput{status: RunStatusPendingBridge} +} + +// HasError returns true if the status is errored or the error message is set +func (ro RunOutput) HasError() bool { + return ro.status == RunStatusErrored +} + +// Result returns the result as a gjson object +func (ro RunOutput) Result() gjson.Result { + return ro.data.Get("result") +} + +// Get searches for and returns the JSON at the given path. +func (ro RunOutput) Get(path string) gjson.Result { + return ro.data.Get(path) +} + +// Error returns error for this RunOutput +func (ro RunOutput) Error() error { + return ro.err +} + +// Data returns the data held by this RunOutput +func (ro RunOutput) Data() JSON { + return ro.data +} + +// Status returns the status returned from a task +func (ro RunOutput) Status() RunStatus { + return ro.status +} diff --git a/core/store/models/service_agreement.go b/core/store/models/service_agreement.go index 305995b2e7a..0f159bc354e 100644 --- a/core/store/models/service_agreement.go +++ b/core/store/models/service_agreement.go @@ -8,10 +8,11 @@ import ( "math/big" "time" + "chainlink/core/store/assets" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/utils" null "gopkg.in/guregu/null.v3" ) @@ -20,7 +21,7 @@ type Encumbrance struct { // Corresponds to requestDigest in solidity ServiceAgreement struct ID uint `json:"-" gorm:"primary_key;auto_increment"` // Price to request a report based on this agreement - Payment *assets.Link `json:"payment" gorm:"type:varchar(255)"` + Payment assets.Link `json:"payment" gorm:"type:varchar(255)"` // Expiration is the amount of time an oracle has to answer a request Expiration uint64 `json:"expiration"` // Agreement is valid until this time @@ -59,7 +60,7 @@ type ServiceAgreement struct { type ServiceAgreementRequest struct { Initiators []InitiatorRequest `json:"initiators"` Tasks []TaskSpecRequest `json:"tasks"` - Payment *assets.Link `json:"payment"` + Payment assets.Link `json:"payment"` Expiration uint64 `json:"expiration"` EndAt AnyTime `json:"endAt"` Oracles EIP55AddressCollection `json:"oracles"` @@ -185,7 +186,7 @@ func generateSAID(e Encumbrance, digest common.Hash) (common.Hash, error) { func (e Encumbrance) ABI(digest common.Hash) ([]byte, error) { buffer := bytes.Buffer{} var paymentHash common.Hash - if e.Payment != nil { + if !e.Payment.IsZero() { paymentHash = e.Payment.ToHash() } _, err := buffer.Write(paymentHash.Bytes()) diff --git a/core/store/models/service_agreement_test.go b/core/store/models/service_agreement_test.go index 7b533805c66..f235a605b11 100644 --- a/core/store/models/service_agreement_test.go +++ b/core/store/models/service_agreement_test.go @@ -5,11 +5,12 @@ import ( "strings" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,7 +21,7 @@ func TestNewUnsignedServiceAgreementFromRequest(t *testing.T) { name string input string wantDigest string - wantPayment *assets.Link + wantPayment assets.Link }{ { "basic", @@ -34,7 +35,7 @@ func TestNewUnsignedServiceAgreementFromRequest(t *testing.T) { `"aggFulfillSelector":"0x87654321"` + `}`, "0xad12826461f2259eac07e762d9f1d32dd6af2e4ed0797b08cb3d8a8c3c4dd61d", - assets.NewLink(1), + *assets.NewLink(1), }, } for _, test := range tests { @@ -57,7 +58,7 @@ func TestBuildServiceAgreement(t *testing.T) { name string input string wantDigest string - wantPayment *assets.Link + wantPayment assets.Link }{ { "basic", @@ -71,7 +72,7 @@ func TestBuildServiceAgreement(t *testing.T) { `"aggFulfillSelector":"0x87654321"` + `}`, "0xad12826461f2259eac07e762d9f1d32dd6af2e4ed0797b08cb3d8a8c3c4dd61d", - assets.NewLink(1), + *assets.NewLink(1), }, } for _, test := range tests { @@ -101,7 +102,7 @@ func TestEncumbrance_ABI(t *testing.T) { tests := []struct { name string - payment *assets.Link + payment assets.Link expiration int endAt models.AnyTime oracles []models.EIP55Address @@ -110,7 +111,7 @@ func TestEncumbrance_ABI(t *testing.T) { aggFulfillSelector string want string }{ - {"basic", assets.NewLink(1), 2, models.AnyTime{}, nil, + {"basic", *assets.NewLink(1), 2, models.AnyTime{}, nil, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000", "0x00000000", "0x" + "0000000000000000000000000000000000000000000000000000000000000001" + // Payment @@ -120,7 +121,7 @@ func TestEncumbrance_ABI(t *testing.T) { "0000000000000000000000000000000000000000" + // Aggregator address "00000000" + "00000000", // Function selectors }, - {"basic dead beef payment", assets.NewLink(3735928559), 2, models.AnyTime{}, nil, + {"basic dead beef payment", *assets.NewLink(3735928559), 2, models.AnyTime{}, nil, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000", "0x00000000", "0x" + "00000000000000000000000000000000000000000000000000000000deadbeef" + // Payment @@ -130,7 +131,7 @@ func TestEncumbrance_ABI(t *testing.T) { "0000000000000000000000000000000000000000" + // Aggregator address "00000000" + "00000000", // Function selectors }, - {"empty", nil, 0, models.AnyTime{}, nil, + {"empty", *assets.NewLink(0), 0, models.AnyTime{}, nil, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000", "0x00000000", "0x" + "0000000000000000000000000000000000000000000000000000000000000000" + // Payment @@ -140,7 +141,7 @@ func TestEncumbrance_ABI(t *testing.T) { "0000000000000000000000000000000000000000" + // Aggregator address "00000000" + "00000000", // Function selectors }, - {"oracle address", nil, 0, models.AnyTime{}, + {"oracle address", *assets.NewLink(0), 0, models.AnyTime{}, []models.EIP55Address{models.EIP55Address("0xa0788FC17B1dEe36f057c42B6F373A34B014687e")}, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000", "0x00000000", "0x" + @@ -152,7 +153,7 @@ func TestEncumbrance_ABI(t *testing.T) { "0000000000000000000000000000000000000000" + // Aggregator address "00000000" + "00000000", // Function selectors }, - {"different endAt", nil, 0, models.NewAnyTime(endAt), + {"different endAt", *assets.NewLink(0), 0, models.NewAnyTime(endAt), []models.EIP55Address{models.EIP55Address("0xa0788FC17B1dEe36f057c42B6F373A34B014687e")}, "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000", "0x00000000", "0x" + @@ -164,7 +165,7 @@ func TestEncumbrance_ABI(t *testing.T) { "0000000000000000000000000000000000000000" + // Aggregator address "00000000" + "00000000", // Function selectors }, - {name: "aggregator info", payment: nil, expiration: 0, endAt: models.NewAnyTime(endAt), + {name: "aggregator info", expiration: 0, endAt: models.NewAnyTime(endAt), oracles: []models.EIP55Address{ models.EIP55Address("0xa0788FC17B1dEe36f057c42B6F373A34B014687e"), }, @@ -209,7 +210,7 @@ func TestServiceAgreementRequest_UnmarshalJSON(t *testing.T) { name string input string wantDigest string - wantPayment *assets.Link + wantPayment assets.Link }{ { "basic", @@ -224,7 +225,7 @@ func TestServiceAgreementRequest_UnmarshalJSON(t *testing.T) { `"endAt":"2018-06-19T22:17:19Z"}` + `}`, "0x57bf5be3447b9a3f8491b6538b01f828bcfcaf2d685ea90375ed4ec2943f4865", - assets.NewLink(1), + *assets.NewLink(1), }, } for _, test := range tests { diff --git a/core/store/models/signature.go b/core/store/models/signature.go index f820e6f031d..fb51b058ce1 100644 --- a/core/store/models/signature.go +++ b/core/store/models/signature.go @@ -6,9 +6,10 @@ import ( "fmt" "math/big" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/chainlink/core/utils" ) const ( diff --git a/core/store/models/topics_test.go b/core/store/models/topics_test.go index 0b8d702cc09..9e71a9e2588 100644 --- a/core/store/models/topics_test.go +++ b/core/store/models/topics_test.go @@ -3,8 +3,9 @@ package models_test import ( "testing" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/core/store/models/user.go b/core/store/models/user.go index d9dffdcb2cd..3e8d93d65fc 100644 --- a/core/store/models/user.go +++ b/core/store/models/user.go @@ -5,7 +5,7 @@ import ( "regexp" "time" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/utils" ) // User holds the credentials for API user. diff --git a/core/store/models/user_test.go b/core/store/models/user_test.go index 5e271e8bbd2..85f4247d647 100644 --- a/core/store/models/user_test.go +++ b/core/store/models/user_test.go @@ -3,8 +3,9 @@ package models_test import ( "testing" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/stretchr/testify/assert" ) diff --git a/core/store/orm/config.go b/core/store/orm/config.go index d77439f059e..55566946cbd 100644 --- a/core/store/orm/config.go +++ b/core/store/orm/config.go @@ -13,15 +13,16 @@ import ( "strconv" "time" + "chainlink/core/logger" + "chainlink/core/store/assets" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" "github.com/gorilla/securecookie" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/spf13/viper" "go.uber.org/zap" "go.uber.org/zap/zapcore" diff --git a/core/store/orm/config_reader.go b/core/store/orm/config_reader.go index b277b4d62aa..acdfad5e196 100644 --- a/core/store/orm/config_reader.go +++ b/core/store/orm/config_reader.go @@ -5,9 +5,10 @@ import ( "net/url" "time" + "chainlink/core/store/assets" + "github.com/ethereum/go-ethereum/common" "github.com/gin-gonic/contrib/sessions" - "github.com/smartcontractkit/chainlink/core/store/assets" "go.uber.org/zap" ) diff --git a/core/store/orm/config_test.go b/core/store/orm/config_test.go index 553895d38a3..3a41736b36c 100644 --- a/core/store/orm/config_test.go +++ b/core/store/orm/config_test.go @@ -8,9 +8,10 @@ import ( "testing" "time" + "chainlink/core/store/assets" + "chainlink/core/store/migrations/migration1564007745" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/migrations/migration1564007745" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/core/store/orm/locking_strategies_test.go b/core/store/orm/locking_strategies_test.go index 60df8d1fd23..6fff817f55a 100644 --- a/core/store/orm/locking_strategies_test.go +++ b/core/store/orm/locking_strategies_test.go @@ -7,8 +7,9 @@ import ( "testing" "time" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/orm" + "chainlink/core/internal/cltest" + "chainlink/core/store/orm" + "github.com/stretchr/testify/require" ) diff --git a/core/store/orm/log_wrapper.go b/core/store/orm/log_wrapper.go index 441f91fa6bc..94d554e7cd8 100644 --- a/core/store/orm/log_wrapper.go +++ b/core/store/orm/log_wrapper.go @@ -1,7 +1,8 @@ package orm import ( - "github.com/smartcontractkit/chainlink/core/logger" + "chainlink/core/logger" + "go.uber.org/zap" ) diff --git a/core/store/orm/orm.go b/core/store/orm/orm.go index 1ef42acaa88..70fd5511675 100644 --- a/core/store/orm/orm.go +++ b/core/store/orm/orm.go @@ -10,17 +10,18 @@ import ( "strings" "time" + "chainlink/core/logger" + "chainlink/core/store/assets" + "chainlink/core/store/dbutil" + "chainlink/core/store/models" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" // http://doc.gorm.io/database.html#connecting-to-a-database _ "github.com/jinzhu/gorm/dialects/sqlite" // http://doc.gorm.io/database.html#connecting-to-a-database "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/dbutil" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" "go.uber.org/multierr" "gopkg.in/guregu/null.v3" ) @@ -281,10 +282,23 @@ func (orm *ORM) convenientTransaction(callback func(*gorm.DB) error) error { return dbtx.Commit().Error } +// OptimisticUpdateConflictError is returned when a record update failed +// because another update occurred while the model was in memory and the +// differences must be reconciled. +var OptimisticUpdateConflictError = errors.New("conflict while updating record") + // SaveJobRun updates UpdatedAt for a JobRun and saves it func (orm *ORM) SaveJobRun(run *models.JobRun) error { return orm.convenientTransaction(func(dbtx *gorm.DB) error { - return dbtx.Unscoped().Omit("deleted_at").Save(run).Error + result := dbtx.Unscoped(). + Model(run). + Where("updated_at = ?", run.UpdatedAt). + Omit("deleted_at"). + Save(run) + if result.RowsAffected == 0 { + return OptimisticUpdateConflictError + } + return result.Error }) } @@ -298,7 +312,7 @@ func (orm *ORM) LinkEarnedFor(spec *models.JobSpec) (*assets.Link, error) { var earned *assets.Link query := orm.DB.Table("job_runs"). Joins("JOIN job_specs ON job_runs.job_spec_id = job_specs.id"). - Where("job_specs.id = ? AND job_runs.finished_at IS NOT NULL", spec.ID) + Where("job_specs.id = ? AND job_runs.status = ? AND job_runs.finished_at IS NOT NULL", spec.ID, models.RunStatusCompleted) if dbutil.IsPostgres(orm.DB) { query = query.Select("SUM(payment)") @@ -350,7 +364,7 @@ func (orm *ORM) FindServiceAgreement(id string) (models.ServiceAgreement, error) } // Jobs fetches all jobs. -func (orm *ORM) Jobs(cb func(models.JobSpec) bool) error { +func (orm *ORM) Jobs(cb func(*models.JobSpec) bool) error { return Batch(1000, func(offset, limit uint) (uint, error) { jobs := []models.JobSpec{} err := orm.preloadJobs(). @@ -362,7 +376,7 @@ func (orm *ORM) Jobs(cb func(models.JobSpec) bool) error { } for _, j := range jobs { - if !cb(j) { + if !cb(&j) { return 0, nil } } @@ -447,15 +461,6 @@ func (orm *ORM) createJob(tx *gorm.DB, job *models.JobSpec) error { return tx.Create(job).Error } -// Archived returns whether or not a job has been archived. -func (orm *ORM) Archived(id *models.ID) bool { - j, err := orm.Unscoped().FindJob(id) - if err != nil { - return false - } - return j.DeletedAt.Valid -} - // ArchiveJob soft deletes the job and its associated job runs. func (orm *ORM) ArchiveJob(ID *models.ID) error { j, err := orm.FindJob(ID) diff --git a/core/store/orm/orm_test.go b/core/store/orm/orm_test.go index e2291bcfcb6..6d1021db36f 100644 --- a/core/store/orm/orm_test.go +++ b/core/store/orm/orm_test.go @@ -9,15 +9,17 @@ import ( "testing" "time" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/services" + "chainlink/core/services/synchronization" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/services/synchronization" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/guregu/null.v3" @@ -117,36 +119,31 @@ func TestORM_CreateJobRun_CreatesRunRequest(t *testing.T) { assert.Equal(t, 1, requestCount) } -func TestORM_SaveJobRun_DoesNotSaveTaskSpec(t *testing.T) { +func TestORM_SaveJobRun_ArchivedDoesNotRevertDeletedAt(t *testing.T) { t.Parallel() store, cleanup := cltest.NewStore(t) defer cleanup() - job := cltest.NewJobWithSchedule("* * * * *") + job := cltest.NewJobWithWebInitiator() require.NoError(t, store.CreateJob(&job)) jr := job.NewRun(job.Initiators[0]) require.NoError(t, store.CreateJobRun(&jr)) - var err error - jr.TaskRuns[0].TaskSpec.Params, err = jr.TaskRuns[0].TaskSpec.Params.Merge(cltest.JSONFromString(t, `{"random": "input"}`)) - require.NoError(t, err) + require.NoError(t, store.ArchiveJob(job.ID)) + + jr.Status = models.RunStatusInProgress require.NoError(t, store.SaveJobRun(&jr)) - retrievedJob, err := store.FindJob(job.ID) - require.NoError(t, err) - require.Len(t, job.Tasks, 1) - require.Len(t, retrievedJob.Tasks, 1) - assert.JSONEq( - t, - coercedJSON(job.Tasks[0].Params.String()), - retrievedJob.Tasks[0].Params.String()) + require.Error(t, utils.JustError(store.FindJobRun(jr.ID))) + require.NoError(t, utils.JustError(store.Unscoped().FindJobRun(jr.ID))) } -func TestORM_SaveJobRun_ArchivedDoesNotRevertDeletedAt(t *testing.T) { +func TestORM_SaveJobRun_Cancelled(t *testing.T) { t.Parallel() store, cleanup := cltest.NewStore(t) defer cleanup() + store.ORM.SetLogging(true) job := cltest.NewJobWithWebInitiator() require.NoError(t, store.CreateJob(&job)) @@ -154,20 +151,19 @@ func TestORM_SaveJobRun_ArchivedDoesNotRevertDeletedAt(t *testing.T) { jr := job.NewRun(job.Initiators[0]) require.NoError(t, store.CreateJobRun(&jr)) - require.NoError(t, store.ArchiveJob(job.ID)) - jr.Status = models.RunStatusInProgress require.NoError(t, store.SaveJobRun(&jr)) - require.Error(t, utils.JustError(store.FindJobRun(jr.ID))) - require.NoError(t, utils.JustError(store.Unscoped().FindJobRun(jr.ID))) -} + // Save the updated at before saving with cancelled + updatedAt := jr.UpdatedAt -func coercedJSON(v string) string { - if v == "" { - return "{}" - } - return v + jr.Status = models.RunStatusCancelled + require.NoError(t, store.SaveJobRun(&jr)) + + // Restore the previous updated at to simulate a conflict + jr.UpdatedAt = updatedAt + jr.Status = models.RunStatusInProgress + assert.Equal(t, orm.OptimisticUpdateConflictError, store.SaveJobRun(&jr)) } func TestORM_JobRunsFor(t *testing.T) { @@ -216,20 +212,29 @@ func TestORM_LinkEarnedFor(t *testing.T) { initr := job.Initiators[0] jr1 := job.NewRun(initr) + jr1.Status = models.RunStatusCompleted jr1.Payment = assets.NewLink(2) jr1.FinishedAt = null.TimeFrom(time.Now()) require.NoError(t, store.CreateJobRun(&jr1)) jr2 := job.NewRun(initr) + jr2.Status = models.RunStatusCompleted jr2.Payment = assets.NewLink(3) jr2.FinishedAt = null.TimeFrom(time.Now()) require.NoError(t, store.CreateJobRun(&jr2)) jr3 := job.NewRun(initr) + jr3.Status = models.RunStatusCompleted jr3.Payment = assets.NewLink(5) jr3.FinishedAt = null.TimeFrom(time.Now()) require.NoError(t, store.CreateJobRun(&jr3)) jr4 := job.NewRun(initr) + jr4.Status = models.RunStatusCompleted jr4.Payment = assets.NewLink(5) require.NoError(t, store.CreateJobRun(&jr4)) + jr5 := job.NewRun(initr) + jr5.Status = models.RunStatusCancelled + jr5.Payment = assets.NewLink(5) + jr5.FinishedAt = null.TimeFrom(time.Now()) + require.NoError(t, store.CreateJobRun(&jr5)) totalEarned, err := store.LinkEarnedFor(&job) require.NoError(t, err) @@ -708,9 +713,6 @@ func TestORM_PendingBridgeType_alreadyCompleted(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - jobRunner, cleanup := cltest.NewJobRunner(store) - defer cleanup() - jobRunner.Start() _, bt := cltest.NewBridgeType(t) require.NoError(t, store.CreateBridgeType(bt)) @@ -722,7 +724,9 @@ func TestORM_PendingBridgeType_alreadyCompleted(t *testing.T) { run := job.NewRun(initr) require.NoError(t, store.CreateJobRun(&run)) - store.RunChannel.Send(run.ID) + executor := services.NewRunExecutor(store) + require.NoError(t, executor.Execute(run.ID)) + cltest.WaitForJobRunStatus(t, store, run, models.RunStatusCompleted) _, err := store.PendingBridgeType(run) @@ -751,8 +755,10 @@ func TestORM_PendingBridgeType_success(t *testing.T) { func TestORM_GetLastNonce_StormNotFound(t *testing.T) { t.Parallel() + app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) store := app.Store account := cltest.GetAccountAddress(t, store) @@ -1015,29 +1021,36 @@ func TestBulkDeleteRuns(t *testing.T) { require.NoError(t, store.ORM.CreateJob(&job)) initiator := job.Initiators[0] - // matches updated before but none of the statuses + // bulk delete should not delete these because they match the updated before + // but none of the statuses oldIncompleteRun := job.NewRun(initiator) + oldIncompleteRun.Result = models.RunResult{Data: cltest.JSONFromString(t, `{"result": 17}`)} oldIncompleteRun.Status = models.RunStatusInProgress err := orm.CreateJobRun(&oldIncompleteRun) require.NoError(t, err) db.Model(&oldIncompleteRun).UpdateColumn("updated_at", cltest.ParseISO8601(t, "2018-01-01T00:00:00Z")) - // matches one of the statuses and the updated before + // bulk delete *SHOULD* delete these because they match one of the statuses + // and the updated before oldCompletedRun := job.NewRun(initiator) + oldCompletedRun.Result = models.RunResult{Data: cltest.JSONFromString(t, `{"result": 19}`)} oldCompletedRun.Status = models.RunStatusCompleted err = orm.CreateJobRun(&oldCompletedRun) require.NoError(t, err) db.Model(&oldCompletedRun).UpdateColumn("updated_at", cltest.ParseISO8601(t, "2018-01-01T00:00:00Z")) - // matches one of the statuses but not the updated before + // bulk delete should not delete these because they match one of the + // statuses but not the updated before newCompletedRun := job.NewRun(initiator) + newCompletedRun.Result = models.RunResult{Data: cltest.JSONFromString(t, `{"result": 23}`)} newCompletedRun.Status = models.RunStatusCompleted err = orm.CreateJobRun(&newCompletedRun) require.NoError(t, err) db.Model(&newCompletedRun).UpdateColumn("updated_at", cltest.ParseISO8601(t, "2018-01-30T00:00:00Z")) - // matches nothing + // bulk delete should not delete these because none of their attributes match newIncompleteRun := job.NewRun(initiator) + newIncompleteRun.Result = models.RunResult{Data: cltest.JSONFromString(t, `{"result": 71}`)} newIncompleteRun.Status = models.RunStatusCompleted err = orm.CreateJobRun(&newIncompleteRun) require.NoError(t, err) @@ -1063,7 +1076,7 @@ func TestBulkDeleteRuns(t *testing.T) { var resultCount int err = db.Model(&models.RunResult{}).Count(&resultCount).Error assert.NoError(t, err) - assert.Equal(t, 6, resultCount) + assert.Equal(t, 3, resultCount) var requestCount int err = db.Model(&models.RunRequest{}).Count(&requestCount).Error @@ -1212,10 +1225,8 @@ func TestORM_DeduceDialect(t *testing.T) { } func TestORM_SyncDbKeyStoreToDisk(t *testing.T) { - app, cleanup := cltest.NewApplication(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - - store := app.GetStore() orm := store.ORM seed, err := models.NewKeyFromFile("../../internal/fixtures/keys/3cb8e3fd9d27e39a5e9e6852b0e96160061fd4ea.json") diff --git a/core/store/orm/schema.go b/core/store/orm/schema.go index 078633464f1..fdd248393d9 100644 --- a/core/store/orm/schema.go +++ b/core/store/orm/schema.go @@ -7,8 +7,9 @@ import ( "reflect" "time" + "chainlink/core/store/assets" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/store/assets" ) // ConfigSchema records the schema of configuration at the type level diff --git a/core/store/presenters/presenters.go b/core/store/presenters/presenters.go index 29fb24dbf11..646d83f861a 100644 --- a/core/store/presenters/presenters.go +++ b/core/store/presenters/presenters.go @@ -13,16 +13,17 @@ import ( "strings" "time" + "chainlink/core/logger" + "chainlink/core/store" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/tidwall/gjson" "go.uber.org/multierr" ) @@ -35,22 +36,22 @@ const ( ) // ShowEthBalance returns the current Eth Balance for current Account -func ShowEthBalance(store *store.Store) ([]map[string]interface{}, error) { +func ShowEthBalance(store *store.Store) ([]map[string]string, error) { return showBalanceFor(store, ethRequest) } // ShowLinkBalance returns the current Link Balance for current Account -func ShowLinkBalance(store *store.Store) ([]map[string]interface{}, error) { +func ShowLinkBalance(store *store.Store) ([]map[string]string, error) { return showBalanceFor(store, linkRequest) } -func showBalanceFor(store *store.Store, balanceType requestType) ([]map[string]interface{}, error) { +func showBalanceFor(store *store.Store, balanceType requestType) ([]map[string]string, error) { if !store.KeyStore.HasAccounts() { logger.Panic("KeyStore must have an account in order to show balance") } var merr error - info := []map[string]interface{}{} + info := []map[string]string{} for _, account := range store.KeyStore.Accounts() { b, err := showBalanceForAccount(store, account, balanceType) merr = multierr.Append(merr, err) @@ -62,16 +63,16 @@ func showBalanceFor(store *store.Store, balanceType requestType) ([]map[string]i } // ShowEthBalance returns the current Eth Balance for current Account -func showBalanceForAccount(store *store.Store, account accounts.Account, balanceType requestType) (map[string]interface{}, error) { +func showBalanceForAccount(store *store.Store, account accounts.Account, balanceType requestType) (map[string]string, error) { balance, err := getBalance(store, account, balanceType) if err != nil { return nil, err } address := account.Address - keysAndValues := make(map[string]interface{}) + keysAndValues := make(map[string]string) keysAndValues["message"] = fmt.Sprintf("%v Balance for %v: %v", balance.Symbol(), address.Hex(), balance.String()) keysAndValues["balance"] = balance.String() - keysAndValues["address"] = address + keysAndValues["address"] = address.String() if balance.IsZero() && balanceType == ethRequest { return nil, errors.New("0 ETH Balance. Chainlink node not fully functional, please deposit ETH into your address: " + address.Hex()) } @@ -307,9 +308,6 @@ func (job JobSpec) FriendlyEndAt() string { // FriendlyMinPayment returns a formatted string of the Job's // Minimum Link Payment threshold func (job JobSpec) FriendlyMinPayment() string { - if job.MinPayment == nil { - return assets.NewLink(0).Text(10) - } return job.MinPayment.Text(10) } diff --git a/core/store/presenters/presenters_test.go b/core/store/presenters/presenters_test.go index 70b82f511b5..55ef5b38587 100644 --- a/core/store/presenters/presenters_test.go +++ b/core/store/presenters/presenters_test.go @@ -5,8 +5,9 @@ import ( "fmt" "testing" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" ) diff --git a/core/store/store.go b/core/store/store.go index c6316b6de36..a414a99d7c8 100644 --- a/core/store/store.go +++ b/core/store/store.go @@ -9,14 +9,15 @@ import ( "sync" "time" + "chainlink/core/logger" + "chainlink/core/services/synchronization" + "chainlink/core/store/migrations" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/rpc" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services/synchronization" - "github.com/smartcontractkit/chainlink/core/store/migrations" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/tevino/abool" "go.uber.org/multierr" "golang.org/x/time/rate" @@ -29,7 +30,6 @@ type Store struct { Config *orm.Config Clock utils.AfterNower KeyStore *KeyStore - RunChannel RunChannel TxManager TxManager StatsPusher *synchronization.StatsPusher } @@ -154,7 +154,6 @@ func NewStoreWithDialer(config *orm.Config, dialer Dialer) *Store { Config: config, KeyStore: keyStore, ORM: orm, - RunChannel: NewQueuedRunChannel(), TxManager: NewEthTxManager(&EthCallerSubscriber{ethrpc}, config, keyStore, orm), StatsPusher: synchronization.NewStatsPusher(orm, config.ExplorerURL(), config.ExplorerAccessKey(), config.ExplorerSecret()), } @@ -172,7 +171,6 @@ func (s *Store) Start() error { // Close shuts down all of the working parts of the store. func (s *Store) Close() error { - s.RunChannel.Close() return multierr.Combine( s.ORM.Close(), s.StatsPusher.Close(), @@ -229,65 +227,3 @@ func initializeORM(config *orm.Config) (*orm.ORM, error) { orm.SetLogging(config.LogSQLStatements()) return orm, nil } - -// RunRequest is the type that the RunChannel uses to package all the necessary -// pieces to execute a Job Run. -type RunRequest struct { - ID *models.ID -} - -// RunChannel manages and dispatches incoming runs. -type RunChannel interface { - Send(jobRunID *models.ID) error - Receive() <-chan RunRequest - Close() -} - -// QueuedRunChannel manages incoming results and blocks by enqueuing them -// in a queue per run. -type QueuedRunChannel struct { - queue chan RunRequest - closed bool - mutex sync.Mutex -} - -// NewQueuedRunChannel initializes a QueuedRunChannel. -func NewQueuedRunChannel() RunChannel { - return &QueuedRunChannel{ - queue: make(chan RunRequest, 1000), - } -} - -// Send adds another entry to the queue of runs. -func (rq *QueuedRunChannel) Send(jobRunID *models.ID) error { - rq.mutex.Lock() - defer rq.mutex.Unlock() - - if rq.closed { - return errors.New("QueuedRunChannel.Add: cannot add to a closed QueuedRunChannel") - } - - if jobRunID == nil { - return errors.New("QueuedRunChannel.Add: cannot add an empty jobRunID") - } - - rq.queue <- RunRequest{ID: jobRunID} - return nil -} - -// Receive returns a channel for listening to sent runs. -func (rq *QueuedRunChannel) Receive() <-chan RunRequest { - return rq.queue -} - -// Close closes the QueuedRunChannel so that no runs can be added to it without -// throwing an error. -func (rq *QueuedRunChannel) Close() { - rq.mutex.Lock() - defer rq.mutex.Unlock() - - if !rq.closed { - rq.closed = true - close(rq.queue) - } -} diff --git a/core/store/store_test.go b/core/store/store_test.go index 9e5598e4a14..97bb67b636c 100644 --- a/core/store/store_test.go +++ b/core/store/store_test.go @@ -1,18 +1,18 @@ package store_test import ( + "math/big" "path/filepath" "sort" "strings" "testing" - "github.com/golang/mock/gomock" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/internal/mocks" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) @@ -20,17 +20,16 @@ import ( func TestStore_Start(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() - store := app.Store - ctrl := gomock.NewController(t) - defer ctrl.Finish() + txManager := new(mocks.TxManager) + txManager.On("Register", mock.Anything).Return(big.NewInt(3), nil) + store.TxManager = txManager - txmMock := mocks.NewMockTxManager(ctrl) - store.TxManager = txmMock - txmMock.EXPECT().Register(gomock.Any()) assert.NoError(t, store.Start()) + + txManager.AssertExpectations(t) } func TestStore_Close(t *testing.T) { @@ -39,20 +38,7 @@ func TestStore_Close(t *testing.T) { s, cleanup := cltest.NewStore(t) defer cleanup() - s.RunChannel.Send(models.NewID()) - s.RunChannel.Send(models.NewID()) - - _, open := <-s.RunChannel.Receive() - assert.True(t, open) - - _, open = <-s.RunChannel.Receive() - assert.True(t, open) - assert.NoError(t, s.Close()) - - rr, open := <-s.RunChannel.Receive() - assert.Equal(t, store.RunRequest{}, rr) - assert.False(t, open) } func TestStore_SyncDiskKeyStoreToDB_HappyPath(t *testing.T) { @@ -60,6 +46,7 @@ func TestStore_SyncDiskKeyStoreToDB_HappyPath(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) store := app.GetStore() // create key on disk @@ -94,6 +81,7 @@ func TestStore_SyncDiskKeyStoreToDB_MultipleKeys(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) app.AddUnlockedKey() // second account defer cleanup() + require.NoError(t, app.Start()) store := app.GetStore() @@ -161,22 +149,3 @@ func TestStore_SyncDiskKeyStoreToDB_DBKeyAlreadyExists(t *testing.T) { require.Len(t, keys, 1) require.Equal(t, acc.Address.Hex(), keys[0].Address.String()) } - -func TestQueuedRunChannel_Send(t *testing.T) { - t.Parallel() - - rq := store.NewQueuedRunChannel() - - assert.NoError(t, rq.Send(models.NewID())) - rr1 := <-rq.Receive() - assert.NotNil(t, rr1) -} - -func TestQueuedRunChannel_Send_afterClose(t *testing.T) { - t.Parallel() - - rq := store.NewQueuedRunChannel() - rq.Close() - - assert.Error(t, rq.Send(models.NewID())) -} diff --git a/core/store/tx_manager.go b/core/store/tx_manager.go index 8a5ff2d25ac..7413c7fda4a 100644 --- a/core/store/tx_manager.go +++ b/core/store/tx_manager.go @@ -11,19 +11,20 @@ import ( "github.com/gobuffalo/packr" "github.com/pkg/errors" "github.com/tidwall/gjson" - "gopkg.in/guregu/null.v3" + + "chainlink/core/logger" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/utils" ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/tevino/abool" "go.uber.org/multierr" + "gopkg.in/guregu/null.v3" ) // DefaultGasLimit sets the default gas limit for outgoing transactions. @@ -62,7 +63,7 @@ type TxManager interface { GetChainID() (*big.Int, error) } -//go:generate mockgen -package=mocks -destination=../internal/mocks/tx_manager_mocks.go github.com/smartcontractkit/chainlink/core/store TxManager +//go:generate mockery -name TxManager -output ../internal/mocks/ -case=underscore // EthTxManager contains fields for the Ethereum client, the KeyStore, // the local Config for the application, and the database. @@ -577,15 +578,6 @@ func (txm *EthTxManager) processAttempt( switch state { case Safe: - logger.Debugw( - fmt.Sprintf("Tx #%d is %s", attemptIndex, state), - "txHash", txAttempt.Hash.String(), - "txID", txAttempt.TxID, - "receiptBlockNumber", receipt.BlockNumber.ToInt(), - "currentBlockNumber", blockHeight, - "receiptHash", receipt.Hash.Hex(), - ) - txm.updateLastSafeNonce(tx) return receipt, state, txm.handleSafe(tx, attemptIndex) @@ -685,7 +677,8 @@ func (txm *EthTxManager) handleSafe( ethBalance, linkBalance, balanceErr := txm.GetETHAndLINKBalances(tx.From) logger.Infow( - fmt.Sprintf("Tx #%d got minimum confirmations (%d)", attemptIndex, minimumConfirmations), + fmt.Sprintf("Tx #%d is safe", attemptIndex), + "minimumConfirmations", minimumConfirmations, "txHash", txAttempt.Hash.String(), "txID", txAttempt.TxID, "ethBalance", ethBalance, @@ -841,16 +834,16 @@ type Contract struct { ABI abi.ABI } -// GetContract loads the contract JSON file from ../evm/build/contracts +// GetContract loads the contract JSON file from ../evm/dist/artifacts // and parses the ABI JSON contents into an abi.ABI object func GetContract(name string) (*Contract, error) { - box := packr.NewBox("../../evm/build/contracts") + box := packr.NewBox("../../evm/dist/artifacts") jsonFile, err := box.Find(name + ".json") if err != nil { return nil, errors.Wrap(err, "unable to read contract JSON") } - abiBytes := gjson.GetBytes(jsonFile, "abi") + abiBytes := gjson.GetBytes(jsonFile, "compilerOutput.abi") abiParsed, err := abi.JSON(strings.NewReader(abiBytes.Raw)) if err != nil { return nil, err diff --git a/core/store/tx_manager_internal_test.go b/core/store/tx_manager_internal_test.go index e5261078296..63529dc3f44 100644 --- a/core/store/tx_manager_internal_test.go +++ b/core/store/tx_manager_internal_test.go @@ -3,9 +3,10 @@ package store import ( "testing" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" ) diff --git a/core/store/tx_manager_test.go b/core/store/tx_manager_test.go index 809d2d84971..1e9c4a1f193 100644 --- a/core/store/tx_manager_test.go +++ b/core/store/tx_manager_test.go @@ -6,18 +6,20 @@ import ( "math/big" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/store" + strpkg "chainlink/core/store" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" - "github.com/golang/mock/gomock" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/internal/mocks" - "github.com/smartcontractkit/chainlink/core/store" - strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/utils" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "gopkg.in/guregu/null.v3" ) @@ -28,17 +30,14 @@ func TestTxManager_CreateTx_Success(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - eth := mocks.NewMockEthClient(ctrl) + ethClient := new(mocks.EthClient) config := cltest.NewTestConfig(t) keyStore := strpkg.NewKeyStore(config.KeysDir()) account, err := keyStore.NewAccount(cltest.Password) require.NoError(t, err) require.NoError(t, keyStore.Unlock(cltest.Password)) - manager := strpkg.NewEthTxManager(eth, config, keyStore, store.ORM) + manager := strpkg.NewEthTxManager(ethClient, config, keyStore, store.ORM) from := account.Address to := cltest.NewAddress() @@ -48,12 +47,12 @@ func TestTxManager_CreateTx_Success(t *testing.T) { manager.Register(keyStore.Accounts()) require.NoError(t, err) - eth.EXPECT().GetNonce(from).Return(nonce, nil) + ethClient.On("GetNonce", from).Return(nonce, nil) err = manager.Connect(cltest.Head(nonce)) require.NoError(t, err) - eth.EXPECT().SendRawTx(gomock.Any()) + ethClient.On("SendRawTx", mock.Anything).Return(cltest.NewHash(), nil) tx, err := manager.CreateTx(to, data) require.NoError(t, err) @@ -66,6 +65,8 @@ func TestTxManager_CreateTx_Success(t *testing.T) { assert.Equal(t, from, ntx.From) assert.Equal(t, nonce, ntx.Nonce) assert.Len(t, ntx.Attempts, 1) + + ethClient.AssertExpectations(t) } func TestTxManager_CreateTx_RoundRobinSuccess(t *testing.T) { @@ -74,10 +75,7 @@ func TestTxManager_CreateTx_RoundRobinSuccess(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - eth := mocks.NewMockEthClient(ctrl) + ethClient := new(mocks.EthClient) config := cltest.NewTestConfig(t) keyStore := strpkg.NewKeyStore(config.KeysDir()) @@ -89,7 +87,7 @@ func TestTxManager_CreateTx_RoundRobinSuccess(t *testing.T) { require.NoError(t, err) require.NoError(t, keyStore.Unlock(cltest.Password)) - manager := strpkg.NewEthTxManager(eth, config, keyStore, store.ORM) + manager := strpkg.NewEthTxManager(ethClient, config, keyStore, store.ORM) to := cltest.NewAddress() data, err := hex.DecodeString("0000abcdef") @@ -100,12 +98,12 @@ func TestTxManager_CreateTx_RoundRobinSuccess(t *testing.T) { manager.Register(keyStore.Accounts()) require.NoError(t, err) - eth.EXPECT().GetNonce(gomock.Any()).Return(nonce, nil).Times(2) + ethClient.On("GetNonce", mock.Anything).Return(nonce, nil).Times(2) err = manager.Connect(cltest.Head(sentAt)) require.NoError(t, err) - eth.EXPECT().SendRawTx(gomock.Any()).Return(cltest.NewHash(), nil) + ethClient.On("SendRawTx", mock.Anything).Return(cltest.NewHash(), nil) createdTx1, err := manager.CreateTx(to, data) require.NoError(t, err) @@ -115,8 +113,8 @@ func TestTxManager_CreateTx_RoundRobinSuccess(t *testing.T) { assert.Len(t, ntx.Attempts, 1) manager.OnNewHead(cltest.Head(bumpAt)) - eth.EXPECT().GetTxReceipt(createdTx1.Attempts[0].Hash).Return(&models.TxReceipt{}, nil) - eth.EXPECT().SendRawTx(gomock.Any()).Return(cltest.NewHash(), nil) + ethClient.On("GetTxReceipt", createdTx1.Attempts[0].Hash).Return(&models.TxReceipt{}, nil) + ethClient.On("SendRawTx", mock.Anything).Return(cltest.NewHash(), nil) // bump gas receipt, state, err := manager.BumpGasUntilSafe(createdTx1.Attempts[0].Hash) @@ -132,11 +130,13 @@ func TestTxManager_CreateTx_RoundRobinSuccess(t *testing.T) { assert.Equal(t, createdTx1.From, ntx.From) // ensure second tx uses the first account again - eth.EXPECT().SendRawTx(gomock.Any()).Return(cltest.NewHash(), nil) + ethClient.On("SendRawTx", mock.Anything).Return(cltest.NewHash(), nil) createdTx2, err := manager.CreateTx(to, data) assert.NoError(t, err) require.NotEqual(t, createdTx1.From.Hex(), createdTx2.From.Hex(), "should come from a different account") + + ethClient.AssertExpectations(t) } func TestTxManager_CreateTx_BreakTxAttemptLimit(t *testing.T) { @@ -145,10 +145,7 @@ func TestTxManager_CreateTx_BreakTxAttemptLimit(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - eth := mocks.NewMockEthClient(ctrl) + ethClient := new(mocks.EthClient) config := cltest.NewTestConfig(t) config.Set("CHAINLINK_TX_ATTEMPT_LIMIT", 1) @@ -156,7 +153,7 @@ func TestTxManager_CreateTx_BreakTxAttemptLimit(t *testing.T) { account, err := keyStore.NewAccount(cltest.Password) require.NoError(t, err) require.NoError(t, keyStore.Unlock(cltest.Password)) - manager := strpkg.NewEthTxManager(eth, config, keyStore, store.ORM) + manager := strpkg.NewEthTxManager(ethClient, config, keyStore, store.ORM) from := account.Address to := cltest.NewAddress() @@ -169,12 +166,12 @@ func TestTxManager_CreateTx_BreakTxAttemptLimit(t *testing.T) { manager.Register(keyStore.Accounts()) require.NoError(t, err) - eth.EXPECT().GetNonce(from).Return(nonce, nil) + ethClient.On("GetNonce", from).Return(nonce, nil) err = manager.Connect(cltest.Head(sentAt)) require.NoError(t, err) - eth.EXPECT().SendRawTx(gomock.Any()) + ethClient.On("SendRawTx", mock.Anything).Return(cltest.NewHash(), nil) tx, err := manager.CreateTx(to, data) require.NoError(t, err) @@ -189,8 +186,8 @@ func TestTxManager_CreateTx_BreakTxAttemptLimit(t *testing.T) { assert.Len(t, ntx.Attempts, 1) manager.OnNewHead(cltest.Head(bumpAt)) - eth.EXPECT().GetTxReceipt(gomock.Any()).Return(&models.TxReceipt{}, nil) - eth.EXPECT().SendRawTx(gomock.Any()).Return(tx.Attempts[0].Hash, nil) + ethClient.On("GetTxReceipt", mock.Anything).Return(&models.TxReceipt{}, nil) + ethClient.On("SendRawTx", mock.Anything).Return(tx.Attempts[0].Hash, nil) receipt, state, err := manager.BumpGasUntilSafe(tx.Attempts[0].Hash) require.NoError(t, err) @@ -198,13 +195,15 @@ func TestTxManager_CreateTx_BreakTxAttemptLimit(t *testing.T) { assert.Equal(t, strpkg.Unconfirmed, state) manager.OnNewHead(cltest.Head(bumpAgainAt)) - eth.EXPECT().GetTxReceipt(gomock.Any()).Return(&models.TxReceipt{}, nil) - eth.EXPECT().GetTxReceipt(gomock.Any()).Return(&models.TxReceipt{}, nil) + ethClient.On("GetTxReceipt", mock.Anything).Return(&models.TxReceipt{}, nil) + ethClient.On("GetTxReceipt", mock.Anything).Return(&models.TxReceipt{}, nil) receipt, state, err = manager.BumpGasUntilSafe(tx.Attempts[0].Hash) require.NoError(t, err) assert.Nil(t, receipt) assert.Equal(t, strpkg.Unconfirmed, state) + + ethClient.AssertExpectations(t) } func TestTxManager_CreateTx_AttemptErrorDoesNotIncrementNonce(t *testing.T) { @@ -374,16 +373,15 @@ func TestTxManager_CreateTx_NonceTooLowReloadLimit(t *testing.T) { func TestTxManager_CreateTx_ErrPendingConnection(t *testing.T) { t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) + + store, cleanup := cltest.NewStore(t) defer cleanup() - store := app.Store manager := store.TxManager to := cltest.NewAddress() - data, err := hex.DecodeString("0000abcdef") - assert.NoError(t, err) + data := hexutil.MustDecode("0x0000abcdef") - _, err = manager.CreateTx(to, data) + _, err := manager.CreateTx(to, data) assert.Contains(t, err.Error(), strpkg.ErrPendingConnection.Error()) } @@ -867,11 +865,9 @@ func TestTxManager_NextActiveAccount_RoundRobin(t *testing.T) { func TestTxManager_ReloadNonce(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - eth := mocks.NewMockEthClient(ctrl) + ethClient := new(mocks.EthClient) txm := store.NewEthTxManager( - eth, + ethClient, orm.NewConfig(), nil, nil, @@ -882,13 +878,15 @@ func TestTxManager_ReloadNonce(t *testing.T) { ma := store.NewManagedAccount(account, 0) nonce := uint64(234) - eth.EXPECT().GetNonce(from).Return(nonce, nil) + ethClient.On("GetNonce", from).Return(nonce, nil) err := ma.ReloadNonce(txm) assert.NoError(t, err) assert.Equal(t, account.Address, ma.Address) assert.Equal(t, nonce, ma.Nonce()) + + ethClient.AssertExpectations(t) } func TestTxManager_WithdrawLink_HappyPath(t *testing.T) { @@ -930,6 +928,7 @@ func TestTxManager_WithdrawLink_HappyPath(t *testing.T) { transactions, err := app.Store.TxFrom(from) require.NoError(t, err) + require.Len(t, transactions, 1) tx := transactions[0] assert.Equal(t, hash, tx.Hash) assert.Equal(t, nonce, tx.Nonce) @@ -997,17 +996,14 @@ func TestTxManager_LogsETHAndLINKBalancesAfterSuccessfulTx(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - eth := mocks.NewMockEthClient(ctrl) + ethClient := new(mocks.EthClient) config := cltest.NewTestConfig(t) keyStore := strpkg.NewKeyStore(config.KeysDir()) account, err := keyStore.NewAccount(cltest.Password) require.NoError(t, err) require.NoError(t, keyStore.Unlock(cltest.Password)) - manager := strpkg.NewEthTxManager(eth, config, keyStore, store.ORM) + manager := strpkg.NewEthTxManager(ethClient, config, keyStore, store.ORM) from := account.Address to := cltest.NewAddress() @@ -1019,12 +1015,12 @@ func TestTxManager_LogsETHAndLINKBalancesAfterSuccessfulTx(t *testing.T) { manager.Register(keyStore.Accounts()) require.NoError(t, err) - eth.EXPECT().GetNonce(from).Return(nonce, nil) + ethClient.On("GetNonce", from).Return(nonce, nil) err = manager.Connect(cltest.Head(sentAt)) require.NoError(t, err) - eth.EXPECT().SendRawTx(gomock.Any()) + ethClient.On("SendRawTx", mock.Anything).Return(cltest.NewHash(), nil) tx, err := manager.CreateTx(to, data) require.NoError(t, err) @@ -1043,14 +1039,16 @@ func TestTxManager_LogsETHAndLINKBalancesAfterSuccessfulTx(t *testing.T) { BlockNumber: cltest.Int(sentAt), } manager.OnNewHead(cltest.Head(confirmedAt)) - eth.EXPECT().GetTxReceipt(tx.Attempts[0].Hash).Return(&confirmedReceipt, nil) - eth.EXPECT().GetERC20Balance(from, gomock.Any()) - eth.EXPECT().GetEthBalance(from) + ethClient.On("GetTxReceipt", tx.Attempts[0].Hash).Return(&confirmedReceipt, nil) + ethClient.On("GetERC20Balance", from, mock.Anything).Return(nil, nil) + ethClient.On("GetEthBalance", from).Return(nil, nil) receipt, state, err := manager.BumpGasUntilSafe(tx.Attempts[0].Hash) require.NoError(t, err) assert.NotNil(t, receipt) assert.Equal(t, strpkg.Safe, state) + + ethClient.AssertExpectations(t) } func TestTxManager_CreateTxWithGas(t *testing.T) { @@ -1182,3 +1180,46 @@ func TestContract_EncodeMessageCall_errors(t *testing.T) { }) } } + +func TestTxManager_RebroadcastUnconfirmedTxsOnReconnect(t *testing.T) { + t.Parallel() + + store, cleanup := cltest.NewStore(t) + defer cleanup() + + ethClient := new(mocks.EthClient) + + config := cltest.NewTestConfig(t) + config.Set("CHAINLINK_TX_ATTEMPT_LIMIT", 1) + keyStore := strpkg.NewKeyStore(config.KeysDir()) + _, err := keyStore.NewAccount(cltest.Password) + require.NoError(t, err) + require.NoError(t, keyStore.Unlock(cltest.Password)) + manager := strpkg.NewEthTxManager(ethClient, config, keyStore, store.ORM) + + to := cltest.NewAddress() + data, err := hex.DecodeString("0000abcdef") + sentAt := uint64(1) + + manager.Register(keyStore.Accounts()) + require.NoError(t, err) + + ethClient.On("GetNonce", mock.Anything).Times(2).Return(uint64(0), nil) + + err = manager.Connect(cltest.Head(sentAt)) + require.NoError(t, err) + + hash := cltest.NewHash() + ethClient.On("SendRawTx", mock.Anything).Return(hash, nil) + + _, err = manager.CreateTx(to, data) + require.NoError(t, err) + + manager.Disconnect() + + ethClient.On("SendRawTx", mock.Anything).Return(hash, nil) + err = manager.Connect(cltest.Head(sentAt)) + require.NoError(t, err) + + ethClient.AssertExpectations(t) +} diff --git a/core/store/types.go b/core/store/types.go index 13390437959..b2a0c498374 100644 --- a/core/store/types.go +++ b/core/store/types.go @@ -1,7 +1,7 @@ package store import ( - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/store/models" ) // HeadTrackable represents any object that wishes to respond to ethereum events, diff --git a/core/utils/json_normalization_test.go b/core/utils/json_normalization_test.go index 6b9c83881ec..2669372387e 100644 --- a/core/utils/json_normalization_test.go +++ b/core/utils/json_normalization_test.go @@ -4,8 +4,9 @@ import ( "encoding/json" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/utils" + "chainlink/core/internal/cltest" + "chainlink/core/utils" + "github.com/stretchr/testify/assert" ) diff --git a/core/utils/utils_test.go b/core/utils/utils_test.go index 33e26831543..a81a6ac6fc1 100644 --- a/core/utils/utils_test.go +++ b/core/utils/utils_test.go @@ -8,10 +8,11 @@ import ( "testing" "time" + "chainlink/core/internal/cltest" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/multierr" diff --git a/core/web/box_test.go b/core/web/box_test.go index de57e325f43..f14eeea42ba 100644 --- a/core/web/box_test.go +++ b/core/web/box_test.go @@ -5,7 +5,8 @@ import ( "strings" "testing" - "github.com/smartcontractkit/chainlink/core/web" + "chainlink/core/web" + "github.com/stretchr/testify/assert" ) diff --git a/core/web/bridge_types_controller.go b/core/web/bridge_types_controller.go index 6cfa5f1e5c8..31ead1cb23b 100644 --- a/core/web/bridge_types_controller.go +++ b/core/web/bridge_types_controller.go @@ -4,11 +4,12 @@ import ( "fmt" "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/gin-gonic/gin" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" ) // BridgeTypesController manages BridgeType requests in the node. diff --git a/core/web/bridge_types_controller_test.go b/core/web/bridge_types_controller_test.go index 4c357350794..3997936d437 100644 --- a/core/web/bridge_types_controller_test.go +++ b/core/web/bridge_types_controller_test.go @@ -6,11 +6,13 @@ import ( "net/http" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/store" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/web" + "github.com/manyminds/api2go/jsonapi" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/web" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,9 +36,10 @@ func TestBridgeTypesController_Index(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() - bt, err := setupBridgeControllerIndex(t, app) + bt, err := setupBridgeControllerIndex(t, app.Store) assert.NoError(t, err) resp, cleanup := client.Get("/v2/specs?size=x") @@ -75,14 +78,14 @@ func TestBridgeTypesController_Index(t *testing.T) { assert.Equal(t, bt[1].Confirmations, bridges[0].Confirmations, "should have the same Confirmations") } -func setupBridgeControllerIndex(t testing.TB, app *cltest.TestApplication) ([]*models.BridgeType, error) { +func setupBridgeControllerIndex(t testing.TB, store *store.Store) ([]*models.BridgeType, error) { bt1 := &models.BridgeType{ Name: models.MustNewTaskType("testingbridges1"), URL: cltest.WebURL(t, "https://testing.com/bridges"), Confirmations: 0, } - err := app.GetStore().CreateBridgeType(bt1) + err := store.CreateBridgeType(bt1) if err != nil { return nil, err } @@ -92,7 +95,7 @@ func setupBridgeControllerIndex(t testing.TB, app *cltest.TestApplication) ([]*m URL: cltest.WebURL(t, "https://testing.com/tari"), Confirmations: 0, } - err = app.GetStore().CreateBridgeType(bt2) + err = store.CreateBridgeType(bt2) return []*models.BridgeType{bt1, bt2}, err } @@ -101,6 +104,7 @@ func TestBridgeTypesController_Create_Success(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() resp, cleanup := client.Post( @@ -129,6 +133,7 @@ func TestBridgeTypesController_Update_Success(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() bt := &models.BridgeType{ @@ -152,6 +157,7 @@ func TestBridgeController_Show(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() bt := &models.BridgeType{ @@ -181,6 +187,8 @@ func TestBridgeController_Destroy(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() resp, cleanup := client.Delete("/v2/bridge_types/testingbridges1") defer cleanup() @@ -216,6 +224,8 @@ func TestBridgeTypesController_Create_AdapterExistsError(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() resp, cleanup := client.Post( @@ -231,6 +241,8 @@ func TestBridgeTypesController_Create_BindJSONError(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() resp, cleanup := client.Post( @@ -246,6 +258,8 @@ func TestBridgeTypesController_Create_DatabaseError(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() resp, cleanup := client.Post( diff --git a/core/web/bulk_deletes_controller.go b/core/web/bulk_deletes_controller.go index 61f3ae877ed..67011403eeb 100644 --- a/core/web/bulk_deletes_controller.go +++ b/core/web/bulk_deletes_controller.go @@ -3,9 +3,10 @@ package web import ( "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" ) // BulkDeletesController manages background tasks that delete resources given a query diff --git a/core/web/config_controller.go b/core/web/config_controller.go index c3f8e03c787..f76c481ee31 100644 --- a/core/web/config_controller.go +++ b/core/web/config_controller.go @@ -4,10 +4,11 @@ import ( "fmt" "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" ) // ConfigController manages config variables diff --git a/core/web/config_controller_test.go b/core/web/config_controller_test.go index 3e56580945d..5a7cadb36bc 100644 --- a/core/web/config_controller_test.go +++ b/core/web/config_controller_test.go @@ -6,11 +6,12 @@ import ( "testing" "time" + "chainlink/core/internal/cltest" + "chainlink/core/store/assets" + "chainlink/core/store/orm" + "chainlink/core/store/presenters" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/store/presenters" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,6 +21,7 @@ func TestConfigController_Show(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() resp, cleanup := client.Get("/v2/config") diff --git a/core/web/cors_test.go b/core/web/cors_test.go index 47257b2f154..2853f62d0dc 100644 --- a/core/web/cors_test.go +++ b/core/web/cors_test.go @@ -4,7 +4,9 @@ import ( "net/http" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" + "chainlink/core/internal/cltest" + + "github.com/stretchr/testify/require" ) func TestCors_DefaultOrigins(t *testing.T) { @@ -14,6 +16,8 @@ func TestCors_DefaultOrigins(t *testing.T) { config.Set("ALLOW_ORIGINS", "http://localhost:3000,http://localhost:6689") app, appCleanup := cltest.NewApplicationWithConfigAndKey(t, config) defer appCleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() tests := []struct { @@ -53,8 +57,11 @@ func TestCors_OverrideOrigins(t *testing.T) { t.Run(test.origin, func(t *testing.T) { config, _ := cltest.NewConfig(t) config.Set("ALLOW_ORIGINS", test.allow) + app, appCleanup := cltest.NewApplicationWithConfigAndKey(t, config) defer appCleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() headers := map[string]string{"Origin": test.origin} diff --git a/core/web/external_initiators.go b/core/web/external_initiators.go index 45228d8051a..90fa82f4e14 100644 --- a/core/web/external_initiators.go +++ b/core/web/external_initiators.go @@ -6,9 +6,10 @@ import ( "fmt" "net/http" + "chainlink/core/store" + "chainlink/core/store/models" + "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" ) // JobSpecNotice is sent to the External Initiator when JobSpecs are created. diff --git a/core/web/external_initiators_controller.go b/core/web/external_initiators_controller.go index 6a93b1af19f..383d956fba6 100644 --- a/core/web/external_initiators_controller.go +++ b/core/web/external_initiators_controller.go @@ -4,10 +4,11 @@ import ( "errors" "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" ) // ExternalInitiatorsController manages external initiators diff --git a/core/web/external_initiators_controller_test.go b/core/web/external_initiators_controller_test.go index 7adf30fdc18..05602596f36 100644 --- a/core/web/external_initiators_controller_test.go +++ b/core/web/external_initiators_controller_test.go @@ -4,9 +4,10 @@ import ( "bytes" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -16,6 +17,7 @@ func TestExternalInitiatorsController_Create_success(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() @@ -41,6 +43,7 @@ func TestExternalInitiatorsController_Create_invalid(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() @@ -56,6 +59,7 @@ func TestExternalInitiatorsController_Delete(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() @@ -69,6 +73,8 @@ func TestExternalInitiatorsController_DeleteNotFound(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) + err := app.GetStore().CreateExternalInitiator(&models.ExternalInitiator{ AccessKey: "abracadabra", }) diff --git a/core/web/external_initiators_test.go b/core/web/external_initiators_test.go index afb57c79a64..d03abe1b115 100644 --- a/core/web/external_initiators_test.go +++ b/core/web/external_initiators_test.go @@ -5,9 +5,10 @@ import ( "net/http" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/web" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/web" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -72,8 +73,9 @@ func TestNotifyExternalInitiator_Notified(t *testing.T) { } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() + exInitr := struct { Header http.Header Body web.JobSpecNotice @@ -91,13 +93,13 @@ func TestNotifyExternalInitiator_Notified(t *testing.T) { eia := models.NewExternalInitiatorAuthentication() ei, err := models.NewExternalInitiator(eia, &test.ExInitr) require.NoError(t, err) - err = app.GetStore().CreateExternalInitiator(ei) + err = store.CreateExternalInitiator(ei) require.NoError(t, err) - err = app.GetStore().CreateJob(&test.JobSpec) + err = store.CreateJob(&test.JobSpec) require.NoError(t, err) - err = web.NotifyExternalInitiator(test.JobSpec, app.GetStore()) + err = web.NotifyExternalInitiator(test.JobSpec, store) require.NoError(t, err) assert.Equal(t, ei.OutgoingToken, @@ -149,7 +151,7 @@ func TestNotifyExternalInitiator_NotNotified(t *testing.T) { } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { - app, cleanup := cltest.NewApplicationWithKey(t) + store, cleanup := cltest.NewStore(t) defer cleanup() var remoteNotified bool @@ -164,13 +166,13 @@ func TestNotifyExternalInitiator_NotNotified(t *testing.T) { eia := models.NewExternalInitiatorAuthentication() ei, err := models.NewExternalInitiator(eia, &test.ExInitr) require.NoError(t, err) - err = app.GetStore().CreateExternalInitiator(ei) + err = store.CreateExternalInitiator(ei) require.NoError(t, err) - err = app.GetStore().CreateJob(&test.JobSpec) + err = store.CreateJob(&test.JobSpec) require.NoError(t, err) - err = web.NotifyExternalInitiator(test.JobSpec, app.GetStore()) + err = web.NotifyExternalInitiator(test.JobSpec, store) require.NoError(t, err) require.False(t, remoteNotified) diff --git a/core/web/gui_assets_test.go b/core/web/gui_assets_test.go index 03fd135bf7c..37c6041b293 100644 --- a/core/web/gui_assets_test.go +++ b/core/web/gui_assets_test.go @@ -4,7 +4,8 @@ import ( "net/http" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" + "chainlink/core/internal/cltest" + "github.com/stretchr/testify/require" ) @@ -13,6 +14,8 @@ func TestGuiAssets_WildcardIndexHtml(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := &http.Client{} resp, err := client.Get(app.Server.URL + "/") @@ -53,6 +56,8 @@ func TestGuiAssets_WildcardRouteInfo(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := &http.Client{} resp, err := client.Get(app.Server.URL + "/job_specs/abc123/routeInfo.json") @@ -77,6 +82,8 @@ func TestGuiAssets_Exact(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := &http.Client{} resp, err := client.Get(app.Server.URL + "/main.js") diff --git a/core/web/helpers.go b/core/web/helpers.go index 5f31a035fd2..6c6b62a67d0 100644 --- a/core/web/helpers.go +++ b/core/web/helpers.go @@ -4,11 +4,12 @@ import ( "fmt" "net/http" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "github.com/gin-gonic/gin" "github.com/manyminds/api2go/jsonapi" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" ) // StatusCodeForError returns an http status code for an error type. diff --git a/core/web/helpers_test.go b/core/web/helpers_test.go index 14b8a83c53d..71e8ab36cf4 100644 --- a/core/web/helpers_test.go +++ b/core/web/helpers_test.go @@ -4,8 +4,9 @@ import ( "errors" "testing" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/web" + "chainlink/core/store/models" + "chainlink/core/web" + "github.com/stretchr/testify/assert" ) diff --git a/core/web/job_runs_controller.go b/core/web/job_runs_controller.go index d5b43dafd87..ad19cd285d2 100644 --- a/core/web/job_runs_controller.go +++ b/core/web/job_runs_controller.go @@ -5,13 +5,14 @@ import ( "io/ioutil" "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "github.com/gin-gonic/gin" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" ) // JobRunsController manages JobRun requests in the node. @@ -59,7 +60,9 @@ func (jrc *JobRunsController) Create(c *gin.Context) { jsonAPIError(c, http.StatusForbidden, err) } else if data, err := getRunData(c); err != nil { jsonAPIError(c, http.StatusInternalServerError, err) - } else if jr, err := services.ExecuteJob(j, *initiator, models.RunResult{Data: data}, nil, jrc.App.GetStore()); err != nil { + } else if jr, err := jrc.App.Create(j.ID, initiator, &data, nil, &models.RunRequest{}); errors.Cause(err) == orm.ErrorNotFound { + jsonAPIError(c, http.StatusNotFound, errors.New("Job not found")) + } else if err != nil { jsonAPIError(c, http.StatusInternalServerError, err) } else { jsonAPIResponse(c, presenters.JobRun{JobRun: *jr}, "job run") @@ -117,13 +120,13 @@ func (jrc *JobRunsController) Update(c *gin.Context) { authToken := utils.StripBearer(c.Request.Header.Get("Authorization")) unscoped := jrc.App.GetStore().Unscoped() - if id, err := models.NewIDFromString(c.Param("RunID")); err != nil { + if runID, err := models.NewIDFromString(c.Param("RunID")); err != nil { jsonAPIError(c, http.StatusUnprocessableEntity, err) - } else if jr, err := unscoped.FindJobRun(id); errors.Cause(err) == orm.ErrorNotFound { + } else if jr, err := unscoped.FindJobRun(runID); errors.Cause(err) == orm.ErrorNotFound { jsonAPIError(c, http.StatusNotFound, errors.New("Job Run not found")) } else if err != nil { jsonAPIError(c, http.StatusInternalServerError, err) - } else if !jr.Result.Status.PendingBridge() { + } else if !jr.Status.PendingBridge() { jsonAPIError(c, http.StatusMethodNotAllowed, errors.New("Cannot resume a job run that isn't pending")) } else if err := c.ShouldBindJSON(&brr); err != nil { jsonAPIError(c, http.StatusInternalServerError, err) @@ -133,9 +136,27 @@ func (jrc *JobRunsController) Update(c *gin.Context) { jsonAPIError(c, http.StatusInternalServerError, err) } else if !ok { c.AbortWithStatus(http.StatusUnauthorized) - } else if err = services.ResumePendingTask(&jr, unscoped, brr.RunResult); err != nil { + } else if err = jrc.App.ResumePending(runID, brr); errors.Cause(err) == orm.ErrorNotFound { + jsonAPIError(c, http.StatusNotFound, errors.New("Job Run not found")) + } else if err != nil { jsonAPIError(c, http.StatusInternalServerError, err) } else { jsonAPIResponse(c, jr, "job run") } } + +// Cancel stops a Run from continuing. +// Example: +// "/runs/:RunID/cancellation" +func (jrc *JobRunsController) Cancel(c *gin.Context) { + var jr *models.JobRun + if id, err := models.NewIDFromString(c.Param("RunID")); err != nil { + jsonAPIError(c, http.StatusUnprocessableEntity, err) + } else if jr, err = jrc.App.Cancel(id); errors.Cause(err) == orm.ErrorNotFound { + jsonAPIError(c, http.StatusNotFound, errors.New("Job run not found")) + } else if err != nil { + jsonAPIError(c, http.StatusInternalServerError, err) + } else { + jsonAPIResponse(c, presenters.JobRun{JobRun: *jr}, "job run") + } +} diff --git a/core/web/job_runs_controller_test.go b/core/web/job_runs_controller_test.go index b213dda4484..5ad506c7739 100644 --- a/core/web/job_runs_controller_test.go +++ b/core/web/job_runs_controller_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/web" + "github.com/manyminds/api2go/jsonapi" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/web" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -131,9 +132,8 @@ func TestJobRunsController_Create_Success(t *testing.T) { jr := cltest.CreateJobRunViaWeb(t, app, j, `{"result":"100"}`) jr = cltest.WaitForJobRunToComplete(t, app.Store, jr) - val, err := jr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, "100", val) + value := cltest.MustResultString(t, jr.Result) + assert.Equal(t, "100", value) } func TestJobRunsController_Create_Wrong_ExternalInitiator(t *testing.T) { @@ -198,9 +198,8 @@ func TestJobRunsController_Create_ExternalInitiator_Success(t *testing.T) { j, *eia, `{"result":"100"}`, ) jr = cltest.WaitForJobRunToComplete(t, app.Store, jr) - val, err := jr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, "100", val) + value := cltest.MustResultString(t, jr.Result) + assert.Equal(t, "100", value) } func TestJobRunsController_Create_Archived(t *testing.T) { @@ -289,6 +288,8 @@ func TestJobRunsController_Create_InvalidID(t *testing.T) { func TestJobRunsController_Update_Success(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) + eth := app.MockEthCallerSubscriber() + eth.Register("eth_chainId", app.Store.Config.ChainID()) app.Start() defer cleanup() @@ -327,9 +328,8 @@ func TestJobRunsController_Update_Success(t *testing.T) { require.Equal(t, jr.ID, respJobRun.ID) jr = cltest.WaitForJobRunToComplete(t, app.Store, jr) - val, err := jr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, "100", val) + value := cltest.MustResultString(t, jr.Result) + assert.Equal(t, "100", value) }) } } @@ -406,9 +406,8 @@ func TestJobRunsController_Update_WithError(t *testing.T) { assert.Equal(t, jr.ID, respJobRun.ID) jr = cltest.WaitForJobRunStatus(t, app.Store, jr, models.RunStatusErrored) - val, err := jr.Result.ResultString() - assert.NoError(t, err) - assert.Equal(t, "0", val) + value := cltest.MustResultString(t, jr.Result) + assert.Equal(t, "0", value) } func TestJobRunsController_Update_BadInput(t *testing.T) { @@ -517,3 +516,39 @@ func TestJobRunsController_Show_Unauthenticated(t *testing.T) { assert.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Response should be forbidden") } + +func TestJobRunsController_Cancel(t *testing.T) { + t.Parallel() + app, cleanup := cltest.NewApplication(t) + app.Start() + defer cleanup() + + client := app.NewHTTPClient() + + t.Run("invalid run id", func(t *testing.T) { + response, cleanup := client.Put("/v2/runs/xxx/cancellation", nil) + defer cleanup() + cltest.AssertServerResponse(t, response, http.StatusUnprocessableEntity) + }) + + t.Run("missing run", func(t *testing.T) { + resp, cleanup := client.Put("/v2/runs/29023583-0D39-4844-9696-451102590936/cancellation", nil) + defer cleanup() + cltest.AssertServerResponse(t, resp, http.StatusNotFound) + }) + + job := cltest.NewJobWithWebInitiator() + require.NoError(t, app.Store.CreateJob(&job)) + run := job.NewRun(job.Initiators[0]) + require.NoError(t, app.Store.CreateJobRun(&run)) + + t.Run("valid run", func(t *testing.T) { + resp, cleanup := client.Put(fmt.Sprintf("/v2/runs/%s/cancellation", run.ID), nil) + defer cleanup() + cltest.AssertServerResponse(t, resp, http.StatusOK) + + run, err := app.Store.FindJobRun(run.ID) + assert.NoError(t, err) + assert.Equal(t, models.RunStatusCancelled, run.Status) + }) +} diff --git a/core/web/job_specs_controller.go b/core/web/job_specs_controller.go index 622d57bbb47..8fba4fbe9ba 100644 --- a/core/web/job_specs_controller.go +++ b/core/web/job_specs_controller.go @@ -3,12 +3,13 @@ package web import ( "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/store/presenters" + "github.com/gin-gonic/gin" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/store/presenters" ) // JobSpecsController manages JobSpec requests. diff --git a/core/web/job_specs_controller_test.go b/core/web/job_specs_controller_test.go index 5d25089a43a..f967c13e3b5 100644 --- a/core/web/job_specs_controller_test.go +++ b/core/web/job_specs_controller_test.go @@ -7,13 +7,14 @@ import ( "testing" "time" + "chainlink/core/adapters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "chainlink/core/web" + "github.com/manyminds/api2go/jsonapi" - "github.com/smartcontractkit/chainlink/core/adapters" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" - "github.com/smartcontractkit/chainlink/core/web" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,6 +38,7 @@ func TestJobSpecsController_Index_noSort(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() j1, err := setupJobSpecsControllerIndex(app) @@ -85,6 +87,8 @@ func TestJobSpecsController_Index_sortCreatedAt(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() j2 := cltest.NewJobWithWebInitiator() @@ -159,6 +163,8 @@ func TestJobSpecsController_Create_HappyPath(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() resp, cleanup := client.Post("/v2/specs", bytes.NewBuffer(cltest.MustReadFile(t, "testdata/hello_world_job.json"))) @@ -170,15 +176,15 @@ func TestJobSpecsController_Create_HappyPath(t *testing.T) { err := cltest.ParseJSONAPIResponse(t, resp, &j) require.NoError(t, err) - adapter1, _ := adapters.For(j.Tasks[0], app.Store) + adapter1, _ := adapters.For(j.Tasks[0], app.Store.Config, app.Store.ORM) httpGet := adapter1.BaseAdapter.(*adapters.HTTPGet) assert.Equal(t, httpGet.GetURL(), "https://bitstamp.net/api/ticker/") - adapter2, _ := adapters.For(j.Tasks[1], app.Store) + adapter2, _ := adapters.For(j.Tasks[1], app.Store.Config, app.Store.ORM) jsonParse := adapter2.BaseAdapter.(*adapters.JSONParse) assert.Equal(t, []string(jsonParse.Path), []string{"last"}) - adapter4, _ := adapters.For(j.Tasks[3], app.Store) + adapter4, _ := adapters.For(j.Tasks[3], app.Store.Config, app.Store.ORM) signTx := adapter4.BaseAdapter.(*adapters.EthTx) assert.Equal(t, "0x356a04bCe728ba4c62A30294A55E6A8600a320B3", signTx.Address.String()) assert.Equal(t, "0x609ff1bd", signTx.FunctionSelector.String()) @@ -194,7 +200,7 @@ func TestJobSpecsController_Create_HappyPath(t *testing.T) { require.Len(t, j.Initiators, 1) assert.Equal(t, models.InitiatorWeb, j.Initiators[0].Type) - adapter1, _ = adapters.For(j.Tasks[0], app.Store) + adapter1, _ = adapters.For(j.Tasks[0], app.Store.Config, app.Store.ORM) httpGet = adapter1.BaseAdapter.(*adapters.HTTPGet) assert.Equal(t, httpGet.GetURL(), "https://bitstamp.net/api/ticker/") } @@ -244,20 +250,21 @@ func TestJobSpecsController_Create_CaseInsensitiveTypes(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) j := cltest.FixtureCreateJobViaWeb(t, app, "testdata/caseinsensitive_hello_world_job.json") - adapter1, _ := adapters.For(j.Tasks[0], app.Store) + adapter1, _ := adapters.For(j.Tasks[0], app.Store.Config, app.Store.ORM) httpGet := adapter1.BaseAdapter.(*adapters.HTTPGet) assert.Equal(t, httpGet.GetURL(), "https://bitstamp.net/api/ticker/") - adapter2, _ := adapters.For(j.Tasks[1], app.Store) + adapter2, _ := adapters.For(j.Tasks[1], app.Store.Config, app.Store.ORM) jsonParse := adapter2.BaseAdapter.(*adapters.JSONParse) assert.Equal(t, []string(jsonParse.Path), []string{"last"}) assert.Equal(t, "ethbytes32", j.Tasks[2].Type.String()) - adapter4, _ := adapters.For(j.Tasks[3], app.Store) + adapter4, _ := adapters.For(j.Tasks[3], app.Store.Config, app.Store.ORM) signTx := adapter4.BaseAdapter.(*adapters.EthTx) assert.Equal(t, "0x356a04bCe728ba4c62A30294A55E6A8600a320B3", signTx.Address.String()) assert.Equal(t, "0x609ff1bd", signTx.FunctionSelector.String()) @@ -270,6 +277,8 @@ func TestJobSpecsController_Create_NonExistentTaskJob(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() jsonStr := cltest.MustReadFile(t, "testdata/nonexistent_task_job.json") @@ -286,6 +295,8 @@ func TestJobSpecsController_Create_InvalidJob(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() jsonStr := cltest.MustReadFile(t, "testdata/run_at_wo_time_job.json") @@ -302,6 +313,8 @@ func TestJobSpecsController_Create_InvalidCron(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() jsonStr := cltest.MustReadFile(t, "testdata/invalid_cron.json") @@ -318,6 +331,8 @@ func TestJobSpecsController_Create_Initiator_Only(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() jsonStr := cltest.MustReadFile(t, "testdata/initiator_only_job.json") @@ -334,6 +349,8 @@ func TestJobSpecsController_Create_Task_Only(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() jsonStr := cltest.MustReadFile(t, "testdata/task_only_job.json") @@ -349,6 +366,8 @@ func TestJobSpecsController_Create_Task_Only(t *testing.T) { func BenchmarkJobSpecsController_Show(b *testing.B) { app, cleanup := cltest.NewApplication(b) defer cleanup() + require.NoError(b, app.Start()) + client := app.NewHTTPClient() j := setupJobSpecsControllerShow(b, app) @@ -364,6 +383,8 @@ func TestJobSpecsController_Show(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() j := setupJobSpecsControllerShow(t, app) @@ -399,6 +420,8 @@ func TestJobSpecsController_Show_NotFound(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() resp, cleanup := client.Get("/v2/specs/190AE4CE-40B6-4D60-A3DA-061C5ACD32D0") @@ -410,6 +433,8 @@ func TestJobSpecsController_Show_InvalidUuid(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() resp, cleanup := client.Get("/v2/specs/garbage") @@ -420,6 +445,8 @@ func TestJobSpecsController_Show_InvalidUuid(t *testing.T) { func TestJobSpecsController_Show_Unauthenticated(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) + require.NoError(t, app.Start()) + defer cleanup() resp, err := http.Get(app.Server.URL + "/v2/specs/" + "garbage") @@ -431,6 +458,8 @@ func TestJobSpecsController_Destroy(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() job := cltest.NewJobWithLogInitiator() require.NoError(t, app.Store.CreateJob(&job)) diff --git a/core/web/keys_controller.go b/core/web/keys_controller.go index cc6fa8db3eb..41947e1f5ee 100644 --- a/core/web/keys_controller.go +++ b/core/web/keys_controller.go @@ -3,10 +3,11 @@ package web import ( "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" ) // KeysController manages account keys diff --git a/core/web/keys_controller_test.go b/core/web/keys_controller_test.go index a488cb27ed5..8d124ea39e4 100644 --- a/core/web/keys_controller_test.go +++ b/core/web/keys_controller_test.go @@ -5,8 +5,9 @@ import ( "encoding/json" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/stretchr/testify/assert" ) diff --git a/core/web/ping_controller.go b/core/web/ping_controller.go index 8dc80a6825d..195f8edac7d 100644 --- a/core/web/ping_controller.go +++ b/core/web/ping_controller.go @@ -3,8 +3,9 @@ package web import ( "net/http" + "chainlink/core/services" + "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" ) // PingController has the ping endpoint. diff --git a/core/web/ping_controller_test.go b/core/web/ping_controller_test.go index 6e7101a4ddc..61b9a7170eb 100644 --- a/core/web/ping_controller_test.go +++ b/core/web/ping_controller_test.go @@ -4,9 +4,10 @@ import ( "net/http" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/web" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/web" + "github.com/stretchr/testify/require" ) @@ -15,6 +16,7 @@ func TestPingController_Show_APICredentials(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client := app.NewHTTPClient() @@ -29,6 +31,7 @@ func TestPingController_Show_ExternalInitiatorCredentials(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) eia := &models.ExternalInitiatorAuthentication{ AccessKey: "abracadabra", @@ -65,6 +68,7 @@ func TestPingController_Show_NoCredentials(t *testing.T) { app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) client := http.Client{} url := app.Config.ClientNodeURL() + "/v2/ping" diff --git a/core/web/router.go b/core/web/router.go index 4e7db934fde..5aaf0dbc64f 100644 --- a/core/web/router.go +++ b/core/web/router.go @@ -15,6 +15,12 @@ import ( "strings" "time" + "chainlink/core/logger" + "chainlink/core/services" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/orm" + helmet "github.com/danielkov/gin-helmet" "github.com/gin-contrib/cors" "github.com/gin-contrib/expvar" @@ -23,11 +29,6 @@ import ( "github.com/gin-gonic/gin" "github.com/gobuffalo/packr" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" "github.com/ulule/limiter" mgin "github.com/ulule/limiter/drivers/middleware/gin" "github.com/ulule/limiter/drivers/store/memory" @@ -297,6 +298,7 @@ func v2Routes(app services.Application, r *gin.RouterGroup) { authv2.GET("/runs", paginatedRequest(jr.Index)) authv2.GET("/runs/:RunID", jr.Show) + authv2.PUT("/runs/:RunID/cancellation", jr.Cancel) authv2.GET("/service_agreements/:SAID", sa.Show) diff --git a/core/web/router_test.go b/core/web/router_test.go index e87f975a04d..0d53d48c300 100644 --- a/core/web/router_test.go +++ b/core/web/router_test.go @@ -6,9 +6,10 @@ import ( "net/http/httptest" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/web" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/web" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -16,6 +17,7 @@ import ( func TestTokenAuthRequired_NoCredentials(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) router := web.Router(app) ts := httptest.NewServer(router) @@ -30,6 +32,7 @@ func TestTokenAuthRequired_NoCredentials(t *testing.T) { func TestTokenAuthRequired_SessionCredentials(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) router := web.Router(app) ts := httptest.NewServer(router) @@ -45,6 +48,7 @@ func TestTokenAuthRequired_SessionCredentials(t *testing.T) { func TestTokenAuthRequired_TokenCredentials(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) router := web.Router(app) ts := httptest.NewServer(router) @@ -76,6 +80,7 @@ func TestTokenAuthRequired_TokenCredentials(t *testing.T) { func TestTokenAuthRequired_BadTokenCredentials(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) router := web.Router(app) ts := httptest.NewServer(router) @@ -107,6 +112,7 @@ func TestTokenAuthRequired_BadTokenCredentials(t *testing.T) { func TestSessions_RateLimited(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) router := web.Router(app) ts := httptest.NewServer(router) @@ -135,6 +141,7 @@ func TestSessions_RateLimited(t *testing.T) { func TestRouter_LargePOSTBody(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) router := web.Router(app) ts := httptest.NewServer(router) @@ -154,6 +161,8 @@ func TestRouter_LargePOSTBody(t *testing.T) { func TestRouter_GinHelmetHeaders(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) + router := web.Router(app) ts := httptest.NewServer(router) defer ts.Close() diff --git a/core/web/service_agreements_controller.go b/core/web/service_agreements_controller.go index fafe88ab792..a92136cc7cc 100644 --- a/core/web/service_agreements_controller.go +++ b/core/web/service_agreements_controller.go @@ -3,13 +3,14 @@ package web import ( "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "chainlink/core/store/orm" + "chainlink/core/store/presenters" + "github.com/ethereum/go-ethereum/common" "github.com/gin-gonic/gin" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/store/presenters" ) // ServiceAgreementsController manages service agreements. diff --git a/core/web/service_agreements_controller_test.go b/core/web/service_agreements_controller_test.go index b138209d35d..ea864464413 100644 --- a/core/web/service_agreements_controller_test.go +++ b/core/web/service_agreements_controller_test.go @@ -7,9 +7,10 @@ import ( "testing" "time" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,6 +23,8 @@ func TestServiceAgreementsController_Create(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) + eth := cltest.MockEthOnStore(t, app.GetStore()) eth.RegisterSubscription("logs") @@ -73,6 +76,8 @@ func TestServiceAgreementsController_Create_isIdempotent(t *testing.T) { app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() + require.NoError(t, app.Start()) + eth := cltest.MockEthOnStore(t, app.GetStore()) eth.RegisterSubscription("logs") @@ -104,6 +109,8 @@ func TestServiceAgreementsController_Show(t *testing.T) { t.Parallel() app, cleanup := cltest.NewApplication(t) defer cleanup() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() input := cltest.MustReadFile(t, "testdata/hello_world_agreement.json") diff --git a/core/web/sessions_controller.go b/core/web/sessions_controller.go index 90480444d12..fb21c0d0f66 100644 --- a/core/web/sessions_controller.go +++ b/core/web/sessions_controller.go @@ -5,10 +5,11 @@ import ( "fmt" "net/http" + "chainlink/core/services" + "chainlink/core/store/models" + "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/models" "go.uber.org/multierr" ) diff --git a/core/web/sessions_controller_test.go b/core/web/sessions_controller_test.go index baf9081ad87..5c7ede82767 100644 --- a/core/web/sessions_controller_test.go +++ b/core/web/sessions_controller_test.go @@ -8,9 +8,10 @@ import ( "testing" "time" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "github.com/onsi/gomega" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/core/web/transactions_controller.go b/core/web/transactions_controller.go index 9f7ba66e07e..b3702bc1e2d 100644 --- a/core/web/transactions_controller.go +++ b/core/web/transactions_controller.go @@ -3,12 +3,13 @@ package web import ( "net/http" + "chainlink/core/services" + "chainlink/core/store/orm" + "chainlink/core/store/presenters" + "github.com/ethereum/go-ethereum/common" "github.com/gin-gonic/gin" "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/orm" - "github.com/smartcontractkit/chainlink/core/store/presenters" ) // TransactionsController displays Ethereum transactions requests. diff --git a/core/web/transactions_controller_test.go b/core/web/transactions_controller_test.go index a587965619b..70ceebdd6fe 100644 --- a/core/web/transactions_controller_test.go +++ b/core/web/transactions_controller_test.go @@ -5,11 +5,12 @@ import ( "net/http" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/web" + "github.com/manyminds/api2go/jsonapi" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/web" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/core/web/transfer_controller.go b/core/web/transfer_controller.go index b5f85b79fd4..ebaa80d6a3f 100644 --- a/core/web/transfer_controller.go +++ b/core/web/transfer_controller.go @@ -5,13 +5,14 @@ import ( "fmt" "net/http" + "chainlink/core/services" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/common" "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" ) // TransfersController can send LINK tokens to another address diff --git a/core/web/transfer_controller_test.go b/core/web/transfer_controller_test.go index 82d6b1b9c34..787bd9ac1cc 100644 --- a/core/web/transfer_controller_test.go +++ b/core/web/transfer_controller_test.go @@ -6,10 +6,11 @@ import ( "net/http" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/core/web/tx_attempts_controller.go b/core/web/tx_attempts_controller.go index c3ae50ecf8b..700c3c7d3a5 100644 --- a/core/web/tx_attempts_controller.go +++ b/core/web/tx_attempts_controller.go @@ -1,8 +1,9 @@ package web import ( + "chainlink/core/services" + "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" ) // TxAttemptsController lists TxAttempts requests. diff --git a/core/web/tx_attempts_controller_test.go b/core/web/tx_attempts_controller_test.go index f16535a5b8e..17d7f1a0dd2 100644 --- a/core/web/tx_attempts_controller_test.go +++ b/core/web/tx_attempts_controller_test.go @@ -5,10 +5,11 @@ import ( "net/http" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/store/models" + "chainlink/core/web" + "github.com/manyminds/api2go/jsonapi" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/web" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/core/web/user_controller.go b/core/web/user_controller.go index 9d36f633b01..caa4dc16ea6 100644 --- a/core/web/user_controller.go +++ b/core/web/user_controller.go @@ -5,14 +5,15 @@ import ( "fmt" "net/http" + "chainlink/core/services" + "chainlink/core/store" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "github.com/ethereum/go-ethereum/accounts" "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" ) // UserController manages the current Session's User User. @@ -86,9 +87,13 @@ func (c *UserController) updateUserPassword(ctx *gin.Context, user *models.User, func getAccountBalanceFor(ctx *gin.Context, store *store.Store, account accounts.Account) presenters.AccountBalance { txm := store.TxManager if ethBalance, err := txm.GetEthBalance(account.Address); err != nil { + err = fmt.Errorf("Error calling getEthBalance on Ethereum node: %v", err) jsonAPIError(ctx, http.StatusInternalServerError, err) + ctx.Abort() } else if linkBalance, err := txm.GetLINKBalance(account.Address); err != nil { + err = fmt.Errorf("Error calling getLINKBalance on Ethereum node: %v", err) jsonAPIError(ctx, http.StatusInternalServerError, err) + ctx.Abort() } else { return presenters.AccountBalance{ Address: account.Address.Hex(), diff --git a/core/web/user_controller_test.go b/core/web/user_controller_test.go index 8292e81c424..eb0126a2fd8 100644 --- a/core/web/user_controller_test.go +++ b/core/web/user_controller_test.go @@ -5,8 +5,9 @@ import ( "net/http" "testing" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/store/presenters" + "chainlink/core/internal/cltest" + "chainlink/core/store/presenters" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -14,9 +15,10 @@ import ( func TestUserController_UpdatePassword(t *testing.T) { t.Parallel() - appWithUser, cleanup := cltest.NewApplicationWithKey(t) + app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() - client := appWithUser.NewHTTPClient() + require.NoError(t, app.Start()) + client := app.NewHTTPClient() // Invalid request resp, cleanup := client.Patch("/v2/user/password", bytes.NewBufferString("")) @@ -48,9 +50,11 @@ func TestUserController_UpdatePassword(t *testing.T) { func TestUserController_AccountBalances_NoAccounts(t *testing.T) { t.Parallel() - appWithoutAccount, cleanup := cltest.NewApplication(t) + app, cleanup := cltest.NewApplication(t) defer cleanup() - client := appWithoutAccount.NewHTTPClient() + require.NoError(t, app.Start()) + + client := app.NewHTTPClient() resp, cleanup := client.Get("/v2/user/balances") defer cleanup() @@ -66,12 +70,14 @@ func TestUserController_AccountBalances_NoAccounts(t *testing.T) { func TestUserController_AccountBalances_Success(t *testing.T) { t.Parallel() - appWithAccount, cleanup := cltest.NewApplicationWithKey(t) + app, cleanup := cltest.NewApplicationWithKey(t) defer cleanup() - appWithAccount.AddUnlockedKey() - client := appWithAccount.NewHTTPClient() + require.NoError(t, app.Start()) + + app.AddUnlockedKey() + client := app.NewHTTPClient() - ethMock := appWithAccount.MockEthCallerSubscriber() + ethMock := app.MockEthCallerSubscriber() ethMock.Context("first wallet", func(ethMock *cltest.EthMock) { ethMock.Register("eth_getBalance", "0x0100") ethMock.Register("eth_call", "0x0100") @@ -85,7 +91,7 @@ func TestUserController_AccountBalances_Success(t *testing.T) { defer cleanup() require.Equal(t, http.StatusOK, resp.StatusCode) - expectedAccounts := appWithAccount.Store.KeyStore.Accounts() + expectedAccounts := app.Store.KeyStore.Accounts() actualBalances := []presenters.AccountBalance{} err := cltest.ParseJSONAPIResponse(t, resp, &actualBalances) assert.NoError(t, err) diff --git a/core/web/withdrawals_controller.go b/core/web/withdrawals_controller.go index 689be55f334..337481f57e4 100644 --- a/core/web/withdrawals_controller.go +++ b/core/web/withdrawals_controller.go @@ -5,12 +5,13 @@ import ( "fmt" "net/http" + "chainlink/core/services" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "chainlink/core/store/presenters" + "chainlink/core/utils" + "github.com/gin-gonic/gin" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/presenters" - "github.com/smartcontractkit/chainlink/core/utils" ) // WithdrawalsController can send LINK tokens to another address diff --git a/core/web/withdrawals_controller_test.go b/core/web/withdrawals_controller_test.go index 30e6b3df27b..8a53ab4f58e 100644 --- a/core/web/withdrawals_controller_test.go +++ b/core/web/withdrawals_controller_test.go @@ -7,13 +7,14 @@ import ( "net/http" "testing" + "chainlink/core/internal/cltest" + "chainlink/core/internal/mocks" + "chainlink/core/store/assets" + "chainlink/core/store/models" + "github.com/ethereum/go-ethereum/common" - "github.com/golang/mock/gomock" - "github.com/smartcontractkit/chainlink/core/internal/cltest" - "github.com/smartcontractkit/chainlink/core/internal/mocks" - "github.com/smartcontractkit/chainlink/core/store/assets" - "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) // verifyLinkBalanceCheck(t) is used to check that the address checked in a @@ -35,24 +36,19 @@ func TestWithdrawalsController_CreateSuccess(t *testing.T) { defer cleanup() client := app.NewHTTPClient() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - wr := models.WithdrawalRequest{ DestinationAddress: common.HexToAddress("0xDEADEAFDEADEAFDEADEAFDEADEAFDEAD00000000"), Amount: assets.NewLink(1000000000000000000), } subscription := cltest.EmptyMockSubscription() - - txmMock := mocks.NewMockTxManager(ctrl) - txmMock.EXPECT().SubscribeToNewHeads(gomock.Any()).Return(subscription, nil).AnyTimes() - txmMock.EXPECT().GetChainID().Return(big.NewInt(3), nil).AnyTimes() - txmMock.EXPECT().Register(gomock.Any()) - - txmMock.EXPECT().ContractLINKBalance(wr).Return(*wr.Amount, nil) - txmMock.EXPECT().WithdrawLINK(wr).Return(cltest.NewHash(), nil) - app.Store.TxManager = txmMock + txManager := new(mocks.TxManager) + txManager.On("SubscribeToNewHeads", mock.Anything).Maybe().Return(subscription, nil) + txManager.On("GetChainID").Maybe().Return(big.NewInt(3), nil) + txManager.On("Register", mock.Anything).Return(big.NewInt(3), nil) + txManager.On("ContractLINKBalance", wr).Return(*wr.Amount, nil) + txManager.On("WithdrawLINK", wr).Return(cltest.NewHash(), nil) + app.Store.TxManager = txManager oca := common.HexToAddress("0xDEADB3333333F") config.Set("ORACLE_CONTRACT_ADDRESS", &oca) @@ -66,6 +62,7 @@ func TestWithdrawalsController_CreateSuccess(t *testing.T) { defer cleanup() cltest.AssertServerResponse(t, resp, http.StatusOK) + txManager.AssertExpectations(t) } func TestWithdrawalsController_BalanceTooLow(t *testing.T) { diff --git a/evm/.eslintignore b/evm/.eslintignore new file mode 100644 index 00000000000..80697f2272d --- /dev/null +++ b/evm/.eslintignore @@ -0,0 +1 @@ +src/generated \ No newline at end of file diff --git a/evm/@types/globals.d.ts b/evm/@types/globals.d.ts deleted file mode 100644 index 0b157927e95..00000000000 --- a/evm/@types/globals.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare const web3: import('web3') -declare const assert: Chai.Assert -declare const contract: import('mocha').MochaGlobals['describe'] -declare const artifacts: { - require: (contract: string) => any -} diff --git a/evm/@types/truffle-contract.d.ts b/evm/@types/truffle-contract.d.ts deleted file mode 100644 index 6be343e2e0f..00000000000 --- a/evm/@types/truffle-contract.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'truffle-contract' diff --git a/evm/buidler.config.js b/evm/buidler.config.js deleted file mode 100644 index 89b6f7b5442..00000000000 --- a/evm/buidler.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - defaultNetwork: 'buidlerevm', -} diff --git a/evm/compiler.json b/evm/compiler.json new file mode 100644 index 00000000000..fa28db0d00c --- /dev/null +++ b/evm/compiler.json @@ -0,0 +1,20 @@ +{ + "contractsDir": "contracts", + "artifactsDir": "dist/artifacts", + "contracts": "*", + "solcVersion": "0.4.24", + "useDockerisedSolc": false, + "compilerSettings": { + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + } + } +} diff --git a/evm/package.json b/evm/package.json index 601482049e2..2837c5039d7 100644 --- a/evm/package.json +++ b/evm/package.json @@ -1,70 +1,63 @@ { "name": "chainlink", - "version": "0.7.7", + "version": "0.7.9", "license": "MIT", "main": "./dist/src", "scripts": { - "generate-typings": "echo \"\u001b[1;33mWARN: Please import the generated contract factory classes for Coordinator directly from the chainlinkv0.5 package when exported.\u001b[0m\" && typechain --target ethers --outDir src/generated \"{src/LinkToken*,build/contracts/*,./v0.5/build/contracts/{Coordinator,CoordinatorInterface}.json}\"", - "build": "truffle build", - "_comment": "XXX: The definition files should be copied over by the tsc framework! This will break on windows!!", - "postbuild": "yarn generate-typings && yarn tsc && cp -f src/generated/*.d.ts dist/src/generated", + "generate-typings": "typechain --target ethers --outDir src/generated \"{src/LinkToken*,dist/artifacts/*}\"", + "postgenerate-typings": "yarn export-typings src/generated dist/src/generated", + "build": "sol-compiler && yarn generate-typings && tsc", "build:windows": "truffle.cmd build", "depcheck": "echo 'chainlink' && depcheck --ignore-dirs=build/contracts,v0.5,box || true", - "eslint": "eslint --ext .ts,.js test", + "eslint": "eslint --ext .ts, testv2 src", "solhint": "solhint ./contracts/**/*.sol", "lint": "yarn solhint && yarn eslint", - "slither": "truffle compile --quiet && slither .", + "slither": "truffle compile --quiet && slither . && rimraf dist/artifacts", "pretest": "yarn build", - "test:v1": "tsc && truffle test ./dist/test/*.js", - "test:v2": "jest --testTimeout 20000", - "test": "yarn test:v1 && yarn test:v2", - "format": "prettier --write \"{src,test,testv2}/*/**\"", - "prepublishOnly": "yarn build && yarn lint && yarn test", + "test": "jest --testTimeout 80000", + "format": "prettier --write \"{src,testv2}/*/**\"", + "prepublishOnly": "yarn workspace chainlinkv0.5 setup && yarn setup && yarn lint && yarn test", "setup": "ts-node ./scripts/build", - "truffle:migrate:cldev": "truffle migrate --network cldev" + "truffle:migrate:cldev": "truffle migrate --network cldev", + "export-typings": "ts-node scripts/export-generated-contract-factories" }, "dependencies": { - "@types/path-to-regexp": "^1.7.0", - "cbor": "^4.1.1", - "eth-ens-namehash": "^2.0.8", - "ethereumjs-abi": "^0.6.7", - "ethereumjs-util": "^6.1.0", + "cbor": "^5.0.1", + "chainlinkv0.5": "0.0.2", "ethers": "^4.0.37", "link_token": "^1.0.6", - "openzeppelin-solidity": "^1.12.0", - "solidity-cborutils": "^1.0.4", - "truffle-contract": "^4.0.31", - "web3": "^1.2.0" + "openzeppelin-solidity": "^1.12.0" }, "devDependencies": { + "@0x/sol-compiler": "^3.1.15", "@chainlink/eslint-config": "0.0.1", "@chainlink/prettier-config": "0.0.1", - "@nomiclabs/buidler": "^1.0.1", "@types/cbor": "^2.0.0", "@types/chai": "^4.2.4", "@types/debug": "^4.1.5", + "@types/ganache-core": "^2.7.0", "@types/jest": "^24.0.18", "@types/node": "^12.7.5", - "@types/web3": "^1.0.0", + "@types/shelljs": "^0.8.6", "bn.js": "^4.11.0", "chai": "^4.2.0", - "cross-env": "^6.0.0", + "chalk": "^2.4.2", "debug": "^4.1.1", "depcheck": "^0.8.3", "eslint": "^6.3.0", "execa": "^3.2.0", - "ganache-core": "^2.8.0", "jest": "^24.9.0", "jest-circus": "^24.9.0", "prettier": "^1.18.2", - "solc": "0.4.24", + "rimraf": "^3.0.0", + "shelljs": "^0.8.3", "solhint": "^2.1.0", "truffle": "^5.0.25", "ts-jest": "^24.1.0", "ts-node": "^8.4.1", - "typechain": "1.0.1", - "typechain-target-ethers": "^1.0.0-beta.1", - "typescript": "^3.7.0-beta" + "typechain": "1.0.3", + "typechain-target-ethers": "^1.0.1", + "typescript": "^3.7.0" }, "prettier": "@chainlink/prettier-config", "files": [ diff --git a/evm/scripts/build.ts b/evm/scripts/build.ts index cc253887ce1..2680fbbca8e 100755 --- a/evm/scripts/build.ts +++ b/evm/scripts/build.ts @@ -1,9 +1,31 @@ #!/usr/bin/env node import execa from 'execa' +import { ls } from 'shelljs' +import { writeFileSync } from 'fs' +import { join, resolve } from 'path' -const task = /^win/i.test(process.platform) ? 'build:windows' : 'build' +const isWindows = /^win/i.test(process.platform) +const task = isWindows ? 'build:windows' : 'build' execa.commandSync(`yarn -s workspace chainlink run ${task}`, { stdio: 'inherit', }) +if (isWindows) { + remapAbi() +} +/** + * Temporary interop for truffle compile on Windows + * to flatten the compiler output into the JSON file + */ +function remapAbi() { + const artifacts = join('dist', 'artifacts') + const jsons = ls(artifacts) + jsons.forEach(j => { + const jsonPath = resolve(join(artifacts, j)) + const json = require(jsonPath) + const { abi } = json + const newJson = { compilerOutput: { abi }, ...json } + writeFileSync(jsonPath, JSON.stringify(newJson)) + }) +} diff --git a/evm/scripts/export-generated-contract-factories.ts b/evm/scripts/export-generated-contract-factories.ts new file mode 100644 index 00000000000..a4e4069ac5b --- /dev/null +++ b/evm/scripts/export-generated-contract-factories.ts @@ -0,0 +1,126 @@ +import { resolve, join, parse } from 'path' +import chalk from 'chalk' +import { cp, rm, ls, test, mkdir, cat } from 'shelljs' +import { writeFileSync } from 'fs' + +// when this is non-empty, no files will be written +const DRY_RUN = process.env.DRY_RUN + +// logging functions with colour output +const err = (...args: string[]) => console.error(chalk.red(...args)) +const warn = (...args: string[]) => console.warn(chalk.yellow(...args)) +const log = (...args: string[]) => console.log(chalk.green(...args)) +const info = (...args: string[]) => console.log(chalk.grey(...args)) + +function main() { + const [generatedPath, distPath] = [process.argv[2], process.argv[3]].map(p => + resolve(p), + ) + + exportGeneratedContractFactories(generatedPath, distPath) +} +main() + +/** + * Export all generated contract factories and their associated types + * @param generatedPath The path of the generated files + * @param distPath The path of the post-tsc generated files + */ +export function exportGeneratedContractFactories( + generatedPath: string, + distPath: string, +): void { + const dir = getGeneratedFilePaths(generatedPath) + const exportPaths = dir + .map(makeExportPath) + .filter(Boolean) + .join('\n') + info(`Export paths:\n${exportPaths}`) + + if (!DRY_RUN) { + makeBarrelFile(generatedPath, exportPaths) + copyTypings(generatedPath, distPath) + } +} + +/** + * This copies the .d.ts files from the generated phase over, + * since the typescript compiler drops any .d.ts source files during + * compilation + * @param generatedPath The path of the generated files + * @param distPath The path of the post-tsc generated files + */ +function copyTypings(generatedPath: string, distPath: string): void { + mkdir('-p', distPath) + cp(`${generatedPath}/*.d.ts`, distPath) +} + +/** + * Create a barrel file which contains all of the exports. + * This will replace the existing barrel file if it already exists. + * @path the path to create the barrel file + * @param data The data to write to the barrel file + */ +function makeBarrelFile(path: string, data: string): void { + const exportFilePath = join(path, 'index.ts') + warn(`Writing barrel file to ${exportFilePath}`) + writeFileSync(exportFilePath, data) + mergeIndexes(path) +} + +/** + * Making a barrel file makes us end up with two index files, + * since one already is generated (albeit with a .d.ts extension). + * + * This function merges both of them into one index file, and deletes the + * .d.fs one. + * @param path The path of the generated files + */ +function mergeIndexes(path: string): void { + const exportFilePath = join(path, 'index.ts') + const declarationsFilePath = join(path, 'index.d.ts') + const declarationFile = cat(declarationsFilePath) + const exportsFile = cat(exportFilePath) + + writeFileSync(exportFilePath, [declarationFile, exportsFile].join('\n')) + rm(declarationsFilePath) +} + +/** + * Check if the generated directory for smart contract factories exists + * @param path The path of the generated files + */ +function generatedDirExists(path: string): boolean { + log(`Checking if directory: ${path} exists...`) + + return test('-d', path) +} + +/** + * Get all the generated file paths from the generated directory + * @param path The path of the generated files + */ +function getGeneratedFilePaths(path: string): string[] { + if (!generatedDirExists(path)) { + err(`Directory ${path} does not exist. Exiting...`) + process.exit(1) + } + log(`Directory ${path} exists, continuing...`) + return ls(path) +} + +/** + * Create an es6 export of a filename, handles interface and index conflicts naively + * @param fileName The filename to export + */ +function makeExportPath(fileName: string): string { + const { name } = parse(fileName) + + if (name.endsWith('.d')) { + return '' + } else if (name === 'index') { + return '' + } else { + return `export * from './${name}'` + } +} diff --git a/evm/slither.config.json b/evm/slither.config.json index a12e70741ed..d3a75e57930 100644 --- a/evm/slither.config.json +++ b/evm/slither.config.json @@ -1,5 +1,6 @@ { "detectors_to_exclude": "naming-convention,pragma,solc-version", + "truffle_build_directory": "dist/artifacts", "filter_paths": "tests,vendor", "truffle_ignore_compile": true, "truffle_version": "truffle" diff --git a/evm/src/contract.ts b/evm/src/contract.ts index a90c96f0495..6f0718457dd 100644 --- a/evm/src/contract.ts +++ b/evm/src/contract.ts @@ -1,5 +1,5 @@ /** - * The type of any function + * The type of any function that is deployable */ type Deployable = { deploy: (...deployArgs: any[]) => Promise diff --git a/evm/src/debug.ts b/evm/src/debug.ts index a55de396a85..2acc9e42dca 100644 --- a/evm/src/debug.ts +++ b/evm/src/debug.ts @@ -1,5 +1,11 @@ import debug from 'debug' +/** + * This creates a debug logger instance to be used within our internal code. + * @see https://www.npmjs.com/package/debug to see how to use the logger at runtime + * @see wallet.ts makes extensive use of this function. + * @param name The root namespace to assign to the log messages + */ export function makeDebug(name: string): debug.Debugger { return debug(name) } diff --git a/evm/src/helpers.ts b/evm/src/helpers.ts deleted file mode 100644 index 620ffbaf40b..00000000000 --- a/evm/src/helpers.ts +++ /dev/null @@ -1,741 +0,0 @@ -import cbor from 'cbor' -import TruffleContract from 'truffle-contract' -import LinkToken from './LinkToken.json' -import { assertBigNum } from './matchers' -import * as abi from 'ethereumjs-abi' -import BN from 'bn.js' -import * as util from 'ethereumjs-util' - -const HEX_BASE = 16 - -web3.providers.HttpProvider.prototype.sendAsync = - web3.providers.HttpProvider.prototype.send -export const eth = web3.eth - -export interface Roles { - defaultAccount: string - oracleNode: string - oracleNode1: string - oracleNode2: string - oracleNode3: string - stranger: string - consumer: string -} - -export interface Personas { - Default: string - Neil: string - Ned: string - Nelly: string - Carol: string - Eddy: string -} - -interface RolesAndPersonas { - roles: Roles - personas: Personas -} - -/** - * Generate roles and personas for tests along with their corrolated account addresses - */ -export async function initializeRolesAndPersonas(): Promise { - const [ - defaultAccount, - oracleNode1, - oracleNode2, - oracleNode3, - stranger, - consumer, - ] = await eth.getAccounts() - - const personas: Personas = { - Default: defaultAccount, - Neil: oracleNode1, - Ned: oracleNode2, - Nelly: oracleNode3, - Carol: consumer, - Eddy: stranger, - } - - const roles: Roles = { - defaultAccount, - oracleNode: oracleNode1, - oracleNode1, - oracleNode2, - oracleNode3, - stranger, - consumer, - } - - return { personas, roles } -} - -const bNToStringOrIdentity = (a: any): any => (BN.isBN(a) ? a.toString() : a) - -// Deal with transfer amount type truffle doesn't currently handle. (BN) -export const wrappedERC20 = (contract: any): any => ({ - ...contract, - transfer: async (address: any, amount: any) => - contract.transfer(address, bNToStringOrIdentity(amount)), - transferAndCall: async ( - address: any, - amount: any, - payload: any, - options: any, - ) => - contract.transferAndCall( - address, - bNToStringOrIdentity(amount), - payload, - options, - ), -}) - -export const linkContract = async (account: string): Promise => { - if (!account) { - throw Error('No account supplied as a parameter') - } - const receipt = await web3.eth.sendTransaction({ - data: LinkToken.bytecode, - from: account, - gas: 2000000, - }) - const contract = TruffleContract({ abi: LinkToken.abi }) - contract.setProvider(web3.currentProvider) - contract.defaults({ - from: account, - gas: 3500000, - gasPrice: 10000000000, - }) - - return wrappedERC20(await contract.at(receipt.contractAddress)) -} - -export const bigNum = (num: any) => web3.utils.toBN(num) -// TODO: dont call assertions on import -assertBigNum( - bigNum('1'), - bigNum(1), - 'Different representations should give same BNs', -) - -// toWei(n) is n * 10**18, as a BN. -export const toWei = (num: string | number): any => - bigNum(web3.utils.toWei(bigNum(num))) -// TODO: dont call assertions on import -assertBigNum( - toWei('1'), - toWei(1), - 'Different representations should give same BNs', -) - -export const toUtf8 = web3.utils.toUtf8 - -export const keccak = web3.utils.sha3 - -export const hexToInt = (str: string): any => bigNum(str).toNumber() - -export const toHexWithoutPrefix = (arg: any): string => { - if (arg instanceof Buffer || arg instanceof BN) { - return arg.toString('hex') - } else if (arg instanceof Uint8Array) { - return arg.reduce((a, v) => a + v.toString(16).padStart(2, '0'), '') - } else if (Number(arg) === arg) { - return arg.toString(16).padStart(64, '0') - } else { - return Buffer.from(arg, 'ascii').toString('hex') - } -} - -export const toHex = (value: any): string => { - return Ox(toHexWithoutPrefix(value)) -} - -export function Ox(value: any): string { - return value.slice(0, 2) !== '0x' ? `0x${value}` : value -} - -// True if h is a standard representation of a byte array, false otherwise -export const isByteRepresentation = (h: any): boolean => { - return h instanceof Buffer || h instanceof BN || h instanceof Uint8Array -} - -export const getEvents = (contract: any): Promise => - new Promise((resolve, reject) => - contract - .getPastEvents() - .then((events: any) => resolve(events)) - .catch((error: any) => reject(error)), - ) - -export const getLatestEvent = async (contract: any): Promise => { - const events = await getEvents(contract) - return events[events.length - 1] -} - -// link param must be from linkContract(), if amount is a BN -export const requestDataFrom = ( - oc: any, - link: any, - amount: any, - args: any, - options: any, -): any => { - if (!options) { - options = { value: 0 } - } - return link.transferAndCall(oc.address, amount, args, options) -} - -export const functionSelector = (signature: any): string => - '0x' + - keccak(signature) - .slice(2) - .slice(0, 8) - -export const assertActionThrows = (action: any) => - Promise.resolve() - .then(action) - .catch(error => { - assert(error, 'Expected an error to be raised') - assert(error.message, 'Expected an error to be raised') - return error.message - }) - .then(errorMessage => { - assert(errorMessage, 'Expected an error to be raised') - const invalidOpcode = errorMessage.includes('invalid opcode') - const reverted = errorMessage.includes( - 'VM Exception while processing transaction: revert', - ) - assert( - invalidOpcode || reverted, - 'expected following error message to include "invalid JUMP" or ' + - `"revert": "${errorMessage}"`, - ) - // see https://github.com/ethereumjs/testrpc/issues/39 - // for why the "invalid JUMP" is the throw related error when using TestRPC - }) - -export const checkPublicABI = (contract: any, expectedPublic: any) => { - const actualPublic = [] - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const method of contract.abi) { - if (method.type === 'function') { - actualPublic.push(method.name) - } - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const method of actualPublic) { - const index = expectedPublic.indexOf(method) - assert.isAtLeast(index, 0, `#${method} is NOT expected to be public`) - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const method of expectedPublic) { - const index = actualPublic.indexOf(method) - assert.isAtLeast(index, 0, `#${method} is expected to be public`) - } -} - -export const decodeRunABI = (log: any): any => { - const runABI = util.toBuffer(log.data) - const types = ['bytes32', 'address', 'bytes4', 'bytes'] - return abi.rawDecode(types, runABI) -} - -const startMapBuffer = Buffer.from([0xbf]) -const endMapBuffer = Buffer.from([0xff]) - -export const decodeRunRequest = (log: any): any => { - const runABI = util.toBuffer(log.data) - const types = [ - 'address', - 'bytes32', - 'uint256', - 'address', - 'bytes4', - 'uint256', - 'uint256', - 'bytes', - ] - const [ - requester, - requestId, - payment, - callbackAddress, - callbackFunc, - expiration, - version, - data, - ] = abi.rawDecode(types, runABI) - - return { - callbackAddr: Ox(callbackAddress), - callbackFunc: toHex(callbackFunc), - data: autoAddMapDelimiters(data), - dataVersion: version, - expiration: toHex(expiration), - id: toHex(requestId), - jobId: log.topics[1], - payment: toHex(payment), - requester: Ox(requester), - topic: log.topics[0], - } -} - -function autoAddMapDelimiters(data: any): Buffer { - let buffer = data - - if (buffer[0] >> 5 !== 5) { - buffer = Buffer.concat( - [startMapBuffer, buffer, endMapBuffer], - buffer.length + 2, - ) - } - - return buffer -} - -export const decodeDietCBOR = (data: any): any => { - return cbor.decodeFirstSync(autoAddMapDelimiters(data)) -} - -export const runRequestId = (log: any): any => { - const { requestId } = decodeRunRequest(log) - return requestId -} - -export const requestDataBytes = ( - specId: any, - to: any, - fHash: any, - nonce: any, - data: any, -): any => { - const types = [ - 'address', - 'uint256', - 'bytes32', - 'address', - 'bytes4', - 'uint256', - 'uint256', - 'bytes', - ] - const values = [0, 0, specId, to, fHash, nonce, 1, data] - const encoded = abiEncode(types, values) - const funcSelector = functionSelector( - 'oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)', - ) - return funcSelector + encoded -} - -export function abiEncode(types: any, values: any): string { - return abi.rawEncode(types, values).toString('hex') -} - -export const newUint8ArrayFromStr = (str: string): Uint8Array => { - const codePoints = [...str].map(c => c.charCodeAt(0)) - return Uint8Array.from(codePoints) -} - -// newUint8Array returns a uint8array of count bytes from either a hex or -// decimal string, hex strings must begin with 0x -export const newUint8Array = (str: string, count: number): any => { - let result = new Uint8Array(count) - - if (str.startsWith('0x') || str.startsWith('0X')) { - const hexStr = str.slice(2).padStart(count * 2, '0') - for (let i = result.length; i >= 0; i--) { - const offset = i * 2 - result[i] = parseInt(hexStr[offset] + hexStr[offset + 1], HEX_BASE) - } - } else { - const num = bigNum(str) - result = newHash('0x' + num.toString(HEX_BASE)) - } - - return result -} - -// newSignature returns a signature object with v, r, and s broken up -export const newSignature = (str: string): any => { - const oracleSignature = newUint8Array(str, 65) - let v = oracleSignature[64] - if (v < 27) { - v += 27 - } - return { - full: oracleSignature, - r: oracleSignature.slice(0, 32), - s: oracleSignature.slice(32, 64), - v, - } -} - -// newHash returns a 65 byte Uint8Array for representing a hash -export function newHash(str: string): Uint8Array { - return newUint8Array(str, 32) -} - -// newAddress returns a 20 byte Uint8Array for representing an address -export const newAddress = (str: string): Uint8Array => { - return newUint8Array(str, 20) -} - -// lengthTypedArrays sums the length of all specified TypedArrays -export const lengthTypedArrays = ( - ...arrays: Array> -): number => { - return arrays.reduce((a, v) => a + v.length, 0) -} - -export const toBuffer = (uint8a: Uint8Array): Buffer => { - return Buffer.from(uint8a) -} - -// concatTypedArrays recursively concatenates TypedArrays into one big -// TypedArray -// TODO: Does not work recursively -export const concatTypedArrays = ( - ...arrays: Array> -): ArrayLike => { - const size = lengthTypedArrays(...arrays) - const arrayCtor: any = arrays[0].constructor - const result = new arrayCtor(size) - let offset = 0 - arrays.forEach(a => { - result.set(a, offset) - offset += a.length - }) - return result -} - -export const increaseTime5Minutes = async () => { - await web3.currentProvider.send( - { - id: 0, - jsonrpc: '2.0', - method: 'evm_increaseTime', - params: [300], - }, - (error: any) => { - if (error) { - console.log(`Error during helpers.increaseTime5Minutes! ${error}`) - throw error - } - }, - ) -} - -export const sendToEvm = async (evmMethod: string, ...params: any) => { - await web3.currentProvider.send( - { - id: 0, - jsonrpc: '2.0', - method: evmMethod, - params: [...params], - }, - (error: any) => { - if (error) { - console.log(`Error during ${evmMethod}! ${error}`) - throw error - } - }, - ) -} - -export const mineBlocks = async (blocks: number) => { - for (let i = 0; i < blocks; i++) { - await sendToEvm('evm_mine') - } -} - -export const createTxData = ( - selector: string, - types: any, - values: any, -): any => { - const funcSelector = functionSelector(selector) - const encoded = abiEncode([...types], [...values]) - return funcSelector + encoded -} - -export const generateSAID = ({ - payment, - expiration, - endAt, - oracles, - requestDigest, -}: any): Uint8Array => { - const serviceAgreementIDInput = concatTypedArrays( - newHash(payment.toString()), - newHash(expiration.toString()), - newHash(endAt.toString()), - concatTypedArrays( - ...oracles - .map(newAddress) - .map(toHex) - .map(newHash), - ), - requestDigest, - ) - const serviceAgreementIDInputDigest = util.keccak( - toHex(serviceAgreementIDInput), - ) - return newHash(toHex(serviceAgreementIDInputDigest)) -} - -export const recoverPersonalSignature = ( - message: Uint8Array, - signature: any, -): any => { - const personalSignPrefix = newUint8ArrayFromStr( - '\x19Ethereum Signed Message:\n', - ) - const personalSignMessage = Uint8Array.from( - concatTypedArrays( - personalSignPrefix, - newUint8ArrayFromStr(message.length.toString()), - message, - ), - ) - const digest = util.keccak(toBuffer(personalSignMessage)) - const requestDigestPubKey = util.ecrecover( - digest, - signature.v, - toBuffer(signature.r), - toBuffer(signature.s), - ) - return util.pubToAddress(requestDigestPubKey) -} - -export const personalSign = async ( - account: any, - message: any, -): Promise => { - if (!isByteRepresentation(message)) { - throw new Error(`Message ${message} is not a recognized representation of a byte array. - (Can be Buffer, BigNumber, Uint8Array, 0x-prepended hexadecimal string.)`) - } - return newSignature(await web3.eth.sign(toHex(message), account)) -} - -export const executeServiceAgreementBytes = ( - sAID: any, - to: any, - fHash: any, - nonce: any, - data: any, -): any => { - const types = [ - 'address', - 'uint256', - 'bytes32', - 'address', - 'bytes4', - 'uint256', - 'uint256', - 'bytes', - ] - const values = [0, 0, sAID, to, fHash, nonce, 1, data] - const encoded = abiEncode(types, values) - const funcSelector = functionSelector( - 'oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)', - ) - return funcSelector + encoded -} - -// Convenience functions for constructing hexadecimal representations of -// binary serializations. -export const strip0x = (s: string): string => - s.startsWith('0x') ? s.slice(2) : s -export const padHexTo256Bit = (s: string): string => - strip0x(s).padStart(64, '0') -export const padNumTo256Bit = (n: number): string => - padHexTo256Bit(n.toString(16)) - -export const constructStructArgs = ( - fieldNames: string[], - values: any[], -): any => { - assert.equal(fieldNames.length, values.length) - const args: Record = {} - for (let i = 0; i < fieldNames.length; i++) { - args[i] = values[i] - args[fieldNames[i]] = values[i] - } - return args -} - -export const initiateServiceAgreementArgs = ({ - payment, - expiration, - endAt, - oracles, - oracleSignatures, - requestDigest, -}: any): any[] => { - return [ - constructStructArgs( - ['payment', 'expiration', 'endAt', 'oracles', 'requestDigest'], - [ - toHex(newHash(payment.toString())), - toHex(newHash(expiration.toString())), - toHex(newHash(endAt.toString())), - oracles.map(newAddress).map(toHex), - toHex(requestDigest), - ], - ), - constructStructArgs( - ['vs', 'rs', 'ss'], - [ - oracleSignatures.map((os: any) => os.v), - oracleSignatures.map((os: any) => toHex(os.r)), - oracleSignatures.map((os: any) => toHex(os.s)), - ], - ), - ] -} - -// Call coordinator contract to initiate the specified service agreement, and -// get the return value -export const initiateServiceAgreementCall = async ( - coordinator: any, - args: any, -): Promise => - coordinator.initiateServiceAgreement.call( - ...initiateServiceAgreementArgs(args), - ) - -/** Call coordinator contract to initiate the specified service agreement. */ -export const initiateServiceAgreement = async ( - coordinator: any, - args: any, -): Promise => - coordinator.initiateServiceAgreement(...initiateServiceAgreementArgs(args)) - -/** Check that the given service agreement was stored at the correct location */ -export const checkServiceAgreementPresent = async ( - coordinator: any, - { payment, expiration, endAt, requestDigest, id }: any, -): Promise => { - const sa = await coordinator.serviceAgreements.call(id) - assertBigNum(sa[0], bigNum(payment)) - assertBigNum(sa[1], bigNum(expiration)) - assertBigNum(sa[2], bigNum(endAt)) - assert.equal(sa[3], toHex(requestDigest)) - - /// / TODO: - - /// / Web3.js doesn't support generating an artifact for arrays - /// within a struct. / This means that we aren't returned the - /// list of oracles and / can't assert on their values. - /// / - - /// / However, we can pass them into the function to generate the - /// ID / & solidity won't compile unless we pass the correct - /// number and / type of params when initializing the - /// ServiceAgreement struct, / so we have some indirect test - /// coverage. - /// / - /// / https://github.com/ethereum/web3.js/issues/1241 - /// / assert.equal( - /// / sa[2], - /// / ['0x70AEc4B9CFFA7b55C0711b82DD719049d615E21d', - /// / '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07'] - /// / ) -} - -// Check that all values for the struct at this SAID have default values. I.e. -// nothing was changed due to invalid request -export const checkServiceAgreementAbsent = async ( - coordinator: any, - serviceAgreementID: any, -) => { - const sa = await coordinator.serviceAgreements.call( - toHex(serviceAgreementID).slice(0, 66), - ) - assertBigNum(sa[0], bigNum(0)) - assertBigNum(sa[1], bigNum(0)) - assertBigNum(sa[2], bigNum(0)) - assert.equal( - sa[3], - '0x0000000000000000000000000000000000000000000000000000000000000000', - ) -} - -export const newServiceAgreement = async (params: any): Promise => { - const agreement: any = {} - params = params || {} - agreement.payment = params.payment || '1000000000000000000' - agreement.expiration = params.expiration || 300 - agreement.endAt = params.endAt || sixMonthsFromNow() - if (!params.oracles) { - throw Error('No Oracle node address provided') - } - agreement.oracles = params.oracles - agreement.oracleSignatures = [] - agreement.requestDigest = - params.requestDigest || - newHash( - '0xbadc0de5badc0de5badc0de5badc0de5badc0de5badc0de5badc0de5badc0de5', - ) - - const sAID = generateSAID(agreement) - agreement.id = toHex(sAID) - - for (let i = 0; i < agreement.oracles.length; i++) { - const oracle = agreement.oracles[i] - const oracleSignature = await personalSign(oracle, sAID) - const requestDigestAddr = recoverPersonalSignature(sAID, oracleSignature) - assert.equal(oracle.toLowerCase(), toHex(requestDigestAddr)) - agreement.oracleSignatures[i] = oracleSignature - } - return agreement -} - -export function sixMonthsFromNow(): number { - return Math.round(Date.now() / 1000.0) + 6 * 30 * 24 * 60 * 60 -} - -export const fulfillOracleRequest = async ( - oracle: any, - request: any, - response: any, - options: any, -): Promise => { - if (!options) { - options = { value: 0 } - } - - return oracle.fulfillOracleRequest( - request.id, - request.payment, - request.callbackAddr, - request.callbackFunc, - request.expiration, - toHex(response), - options, - ) -} - -export const cancelOracleRequest = async ( - oracle: any, - request: any, - options: any, -): Promise => { - if (!options) { - options = { value: 0 } - } - - return oracle.cancelOracleRequest( - request.id, - request.payment, - request.callbackFunc, - request.expiration, - options, - ) -} diff --git a/evm/src/helpersV2.ts b/evm/src/helpersV2.ts index ccea6ed0b35..308fb06a217 100644 --- a/evm/src/helpersV2.ts +++ b/evm/src/helpersV2.ts @@ -2,11 +2,12 @@ import { ethers } from 'ethers' import { createFundedWallet } from './wallet' import { assert } from 'chai' import { Oracle } from './generated/Oracle' -import { CoordinatorFactory } from './generated/CoordinatorFactory' +import { generated as chainlinkv05 } from 'chainlinkv0.5' import { LinkToken } from './generated/LinkToken' import { makeDebug } from './debug' import cbor from 'cbor' import { EmptyOracle } from './generated/EmptyOracle' +import { OracleFactory } from './generated/OracleFactory' const debug = makeDebug('helpers') @@ -34,6 +35,72 @@ interface RolesAndPersonas { personas: Personas } +/** + * This helper function allows us to make use of ganache snapshots, + * which allows us to snapshot one state instance and revert back to it. + * + * This is used to memoize expensive setup calls typically found in beforeEach hooks when we + * need to setup our state with contract deployments before running assertions. + * + * @param provider The provider that's used within the tests + * @param cb The callback to execute that generates the state we want to snapshot + */ +export function useSnapshot( + provider: ethers.providers.JsonRpcProvider, + cb: () => Promise, +) { + const d = debug.extend('memoizeDeploy') + let hasDeployed = false + let snapshotId = '' + + return async () => { + if (!hasDeployed) { + d('executing deployment..') + await cb() + + d('snapshotting...') + /* eslint-disable-next-line require-atomic-updates */ + snapshotId = await provider.send('evm_snapshot', undefined) + d('snapshot id:%s', snapshotId) + + /* eslint-disable-next-line require-atomic-updates */ + hasDeployed = true + } else { + d('reverting to snapshot: %s', snapshotId) + await provider.send('evm_revert', snapshotId) + + d('re-creating snapshot..') + /* eslint-disable-next-line require-atomic-updates */ + snapshotId = await provider.send('evm_snapshot', undefined) + d('recreated snapshot id:%s', snapshotId) + } + } +} + +/** + * A wrapper function to make generated contracts compatible with truffle test suites. + * + * Note that the returned contract is an instance of ethers.Contract, not a @truffle/contract, so there are slight + * api differences, though largely the same. + * + * @see https://docs.ethers.io/ethers.js/html/api-contract.html + * @param contractFactory The ethers based contract factory to interop with + * @param address The address to supply as the signer + */ +export function create any>( + contractFactory: T, + address: string, +): InstanceType { + const web3Instance = (global as any).web3 + const provider = new ethers.providers.Web3Provider( + web3Instance.currentProvider, + ) + const signer = provider.getSigner(address) + const factory = new contractFactory(signer) + + return factory +} + /** * Generate roles and personas for tests along with their corrolated account addresses */ @@ -297,11 +364,13 @@ export function keccak( return utils.keccak256(...args) } +type TxOptions = Omit + export async function fulfillOracleRequest( oracleContract: Oracle | EmptyOracle, runRequest: RunRequest, response: string, - options: Omit = { + options: TxOptions = { gasLimit: 1000000, // FIXME: incorrect gas estimation }, ): ReturnType { @@ -326,13 +395,18 @@ export async function fulfillOracleRequest( ) } -/** - * The solidity function selector for the given signature - */ -export function functionSelector(signature: string): string { - const fullHash = ethers.utils.id(signature) - assert(fullHash.startsWith('0x')) - return fullHash.slice(0, 2 + 4 * 2) // '0x' + initial 4 bytes, in hex +export async function cancelOracleRequest( + oracleContract: Oracle | EmptyOracle, + request: RunRequest, + options: TxOptions = {}, +): ReturnType { + return oracleContract.cancelOracleRequest( + request.id, + request.payment, + request.callbackFunc, + request.expiration, + options, + ) } export function requestDataBytes( @@ -340,20 +414,11 @@ export function requestDataBytes( to: string, fHash: string, nonce: number, - data: string, -): any { - const types = [ - 'address', - 'uint256', - 'bytes32', - 'address', - 'bytes4', - 'uint256', - 'uint256', - 'bytes', - ] + dataBytes: string, +): string { + const ocFactory = new OracleFactory() - const values = [ + return ocFactory.interface.functions.oracleRequest.encode([ ethers.constants.AddressZero, 0, specId, @@ -361,20 +426,15 @@ export function requestDataBytes( fHash, nonce, 1, - data, - ] - const encoded = ethers.utils.defaultAbiCoder.encode(types, values) - const funcSelector = functionSelector( - 'oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)', - ) - return `${funcSelector}${stripHexPrefix(encoded)}` + dataBytes, + ]) } // link param must be from linkContract(), if amount is a BN export function requestDataFrom( oc: Oracle, link: LinkToken, - amount: number, + amount: ethers.utils.BigNumberish, args: string, options: Omit = {}, ): ReturnType { @@ -399,8 +459,9 @@ export function hexToBuf(hexstr: string): Buffer { return Buffer.from(stripHexPrefix(hexstr), 'hex') } +const { CoordinatorFactory } = chainlinkv05 type Hash = ReturnType -type Coordinator = ReturnType +type Coordinator = ReturnType type ServiceAgreement = Parameters[0] /** diff --git a/evm/src/index.ts b/evm/src/index.ts index 7c568a46432..c0cec65e207 100644 --- a/evm/src/index.ts +++ b/evm/src/index.ts @@ -4,6 +4,6 @@ import * as debug from './debug' import LinkToken from './LinkToken.json' import * as wallet from './wallet' import * as matchers from './matchersV2' -import * as provider from './provider' +import * as generated from './generated' -export { contract, helpers, debug, LinkToken, wallet, matchers, provider } +export { contract, helpers, debug, LinkToken, wallet, matchers, generated } diff --git a/evm/src/matchers.ts b/evm/src/matchers.ts deleted file mode 100644 index 8ff1ffde603..00000000000 --- a/evm/src/matchers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import web3 from 'web3' -import { assert } from 'chai' -import BigNumber from 'bn.js' -import BN from 'bn.js' - -const bigNum = (num: number | BN): BigNumber => web3.utils.toBN(num) - -// Throws if a and b are not equal, as BN's -export const assertBigNum = (a: BN, b: BN, failureMessage?: string) => - assert( - bigNum(a).eq(bigNum(b)), - `BigNum ${a} is not ${b}` + (failureMessage ? ': ' + failureMessage : ''), - ) diff --git a/evm/src/provider.ts b/evm/src/provider.ts deleted file mode 100644 index ef1a02fc512..00000000000 --- a/evm/src/provider.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IEthereumProvider } from '@nomiclabs/buidler/types' -import { JsonRpcProvider } from 'ethers/providers' - -// https://github.com/nomiclabs/buidler/blob/master/packages/buidler-ethers/src/ethers-provider-wrapper.ts -export class EthersProviderWrapper extends JsonRpcProvider { - private readonly _buidlerProvider: IEthereumProvider - constructor(buidlerProvider: IEthereumProvider) { - super() - this._buidlerProvider = buidlerProvider - } - public async send(method: string, params: any): Promise { - const result = await this._buidlerProvider.send(method, params) - // We replicate ethers' behavior. - this.emit('debug', { - action: 'send', - request: { - id: 42, - jsonrpc: '2.0', - method, - params, - }, - response: result, - provider: this, - }) - return result - } -} diff --git a/evm/test/Oracle_test.js b/evm/test/Oracle_test.js deleted file mode 100644 index fcc06312dbc..00000000000 --- a/evm/test/Oracle_test.js +++ /dev/null @@ -1,770 +0,0 @@ -import * as h from '../src/helpers' -import { assertBigNum } from '../src/matchers' -const BasicConsumer = artifacts.require('BasicConsumer.sol') -const GetterSetter = artifacts.require('GetterSetter.sol') -const MaliciousRequester = artifacts.require('MaliciousRequester.sol') -const MaliciousConsumer = artifacts.require('MaliciousConsumer.sol') -const Oracle = artifacts.require('Oracle.sol') - -let roles - -before(async () => { - const rolesAndPersonas = await h.initializeRolesAndPersonas() - - roles = rolesAndPersonas.roles -}) - -contract('Oracle', () => { - const fHash = h.functionSelector('requestedBytes32(bytes32,bytes32)') - const specId = - '0x4c7b7ffb66b344fbaa64995af81e355a00000000000000000000000000000000' - const to = '0x80e29acb842498fe6591f020bd82766dce619d43' - let link, oc, withdraw - - beforeEach(async () => { - link = await h.linkContract(roles.defaultAccount) - oc = await Oracle.new(link.address) - await oc.setFulfillmentPermission(roles.oracleNode, true) - withdraw = async (address, amount, options) => - oc.withdraw(address, amount.toString(), options) - }) - - it('has a limited public interface', () => { - h.checkPublicABI(Oracle, [ - 'EXPIRY_TIME', - 'cancelOracleRequest', - 'fulfillOracleRequest', - 'getAuthorizationStatus', - 'onTokenTransfer', - 'oracleRequest', - 'setFulfillmentPermission', - 'withdraw', - 'withdrawable', - // Ownable methods: - 'owner', - 'renounceOwnership', - 'transferOwnership', - ]) - }) - - describe('#setFulfillmentPermission', () => { - context('when called by the owner', () => { - beforeEach(async () => { - await oc.setFulfillmentPermission(roles.stranger, true, { - from: roles.defaultAccount, - }) - }) - - it('adds an authorized node', async () => { - const authorized = await oc.getAuthorizationStatus(roles.stranger) - assert.equal(true, authorized) - }) - - it('removes an authorized node', async () => { - await oc.setFulfillmentPermission(roles.stranger, false, { - from: roles.defaultAccount, - }) - const authorized = await oc.getAuthorizationStatus(roles.stranger) - assert.equal(false, authorized) - }) - }) - - context('when called by a non-owner', () => { - it('cannot add an authorized node', async () => { - await h.assertActionThrows(async () => { - await oc.setFulfillmentPermission(roles.stranger, true, { - from: roles.stranger, - }) - }) - }) - }) - }) - - describe('#onTokenTransfer', () => { - context('when called from any address but the LINK token', () => { - it('triggers the intended method', async () => { - const callData = h.requestDataBytes(specId, to, fHash, 'id', '') - - await h.assertActionThrows(async () => { - await oc.onTokenTransfer(roles.defaultAccount, 0, callData) - }) - }) - }) - - context('when called from the LINK token', () => { - it('triggers the intended method', async () => { - const callData = h.requestDataBytes(specId, to, fHash, 'id', '') - - const tx = await link.transferAndCall(oc.address, 0, callData, { - value: 0, - }) - assert.equal(3, tx.receipt.rawLogs.length) - }) - - context('with no data', () => { - it('reverts', async () => { - await h.assertActionThrows(async () => { - await link.transferAndCall(oc.address, 0, '0x', { - value: 0, - }) - }) - }) - }) - }) - - context('malicious requester', () => { - let mock, requester - const paymentAmount = h.toWei('1', 'ether') - - beforeEach(async () => { - mock = await MaliciousRequester.new(link.address, oc.address) - await link.transfer(mock.address, paymentAmount) - }) - - it('cannot withdraw from oracle', async () => { - const ocOriginalBalance = await link.balanceOf.call(oc.address) - const mockOriginalBalance = await link.balanceOf.call(mock.address) - - await h.assertActionThrows(async () => { - await mock.maliciousWithdraw() - }) - - const ocNewBalance = await link.balanceOf.call(oc.address) - const mockNewBalance = await link.balanceOf.call(mock.address) - - assertBigNum(ocOriginalBalance, ocNewBalance) - assertBigNum(mockNewBalance, mockOriginalBalance) - }) - - context( - 'if the requester tries to create a requestId for another contract', - () => { - it('the requesters ID will not match with the oracle contract', async () => { - const tx = await mock.maliciousTargetConsumer(to) - const events = await h.getEvents(oc) - const mockRequestId = tx.receipt.rawLogs[0].data - const requestId = events[0].args.requestId - assert.notEqual(mockRequestId, requestId) - }) - - it('the target requester can still create valid requests', async () => { - requester = await BasicConsumer.new( - link.address, - oc.address, - specId, - ) - await link.transfer(requester.address, paymentAmount) - await mock.maliciousTargetConsumer(requester.address) - await requester.requestEthereumPrice('USD') - }) - }, - ) - }) - - it('does not allow recursive calls of onTokenTransfer', async () => { - const requestPayload = h.requestDataBytes(specId, to, fHash, 'id', '') - - const ottSelector = h.functionSelector( - 'onTokenTransfer(address,uint256,bytes)', - ) - const header = - '000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef' + // to - '0000000000000000000000000000000000000000000000000000000000000539' + // amount - '0000000000000000000000000000000000000000000000000000000000000060' + // offset - '0000000000000000000000000000000000000000000000000000000000000136' // length - - const maliciousPayload = ottSelector + header + requestPayload.slice(2) - - await h.assertActionThrows(async () => { - await link.transferAndCall(oc.address, 0, maliciousPayload, { - value: 0, - }) - }) - }) - }) - - describe('#oracleRequest', () => { - context('when called through the LINK token', () => { - const paid = 100 - let log, tx - - beforeEach(async () => { - const args = h.requestDataBytes(specId, to, fHash, 1, '') - tx = await h.requestDataFrom(oc, link, paid, args) - assert.equal(3, tx.receipt.rawLogs.length) - - log = tx.receipt.rawLogs[2] - }) - - it('logs an event', async () => { - assert.equal(oc.address, log.address) - - assert.equal(specId, log.topics[1]) - const req = h.decodeRunRequest(tx.receipt.rawLogs[2]) - assert.equal(roles.defaultAccount.toLowerCase(), req.requester) - assertBigNum(paid, req.payment) - }) - - it('uses the expected event signature', async () => { - // If updating this test, be sure to update models.RunLogTopic. - const eventSignature = - '0xd8d7ecc4800d25fa53ce0372f13a416d98907a7ef3d8d3bdd79cf4fe75529c65' - assert.equal(eventSignature, log.topics[0]) - }) - - it('does not allow the same requestId to be used twice', async () => { - const args2 = h.requestDataBytes(specId, to, fHash, 1, '') - await h.assertActionThrows(async () => { - await h.requestDataFrom(oc, link, paid, args2) - }) - }) - - context( - 'when called with a payload less than 2 EVM words + function selector', - () => { - const funcSelector = h.functionSelector( - 'oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)', - ) - const maliciousData = - funcSelector + - '0000000000000000000000000000000000000000000000000000000000000000000' - - it('throws an error', async () => { - await h.assertActionThrows(async () => { - await h.requestDataFrom(oc, link, paid, maliciousData) - }) - }) - }, - ) - - context('when called with a payload between 3 and 9 EVM words', () => { - const funcSelector = h.functionSelector( - 'oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)', - ) - const maliciousData = - funcSelector + - '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001' - - it('throws an error', async () => { - await h.assertActionThrows(async () => { - await h.requestDataFrom(oc, link, paid, maliciousData) - }) - }) - }) - }) - - context('when not called through the LINK token', () => { - it('reverts', async () => { - await h.assertActionThrows(async () => { - await oc.oracleRequest( - '0x0000000000000000000000000000000000000000', - 0, - specId, - to, - fHash, - 1, - 1, - '0x', - { from: roles.oracleNode }, - ) - }) - }) - }) - }) - - describe('#fulfillOracleRequest', () => { - const response = 'Hi Mom!' - let mock, request - - context('cooperative consumer', () => { - beforeEach(async () => { - mock = await BasicConsumer.new(link.address, oc.address, specId) - const paymentAmount = h.toWei(1) - await link.transfer(mock.address, paymentAmount) - const currency = 'USD' - const tx = await mock.requestEthereumPrice(currency) - request = h.decodeRunRequest(tx.receipt.rawLogs[3]) - }) - - context('when called by an unauthorized node', () => { - beforeEach(async () => { - assert.equal(false, await oc.getAuthorizationStatus(roles.stranger)) - }) - - it('raises an error', async () => { - await h.assertActionThrows(async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: roles.stranger, - }) - }) - }) - }) - - context('when called by an authorized node', () => { - it('raises an error if the request ID does not exist', async () => { - request.id = '0xdeadbeef' - await h.assertActionThrows(async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - }) - }) - - it('sets the value on the requested contract', async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - - const currentValue = await mock.currentPrice.call() - assert.equal(response, h.toUtf8(currentValue)) - }) - - it('does not allow a request to be fulfilled twice', async () => { - const response2 = response + ' && Hello World!!' - - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - - await h.assertActionThrows(async () => { - await h.fulfillOracleRequest(oc, request, response2, { - from: roles.oracleNode, - }) - }) - - const currentValue = await mock.currentPrice.call() - assert.equal(response, h.toUtf8(currentValue)) - }) - }) - - context('when the oracle does not provide enough gas', () => { - // if updating this defaultGasLimit, be sure it matches with the - // defaultGasLimit specified in store/tx_manager.go - const defaultGasLimit = 500000 - - beforeEach(async () => { - assertBigNum(0, await oc.withdrawable.call()) - }) - - it('does not allow the oracle to withdraw the payment', async () => { - await h.assertActionThrows(async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - gas: 70000, - }) - }) - - assertBigNum(0, await oc.withdrawable.call()) - }) - - it(`${defaultGasLimit} is enough to pass the gas requirement`, async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - gas: defaultGasLimit, - }) - - assertBigNum(request.payment, await oc.withdrawable.call()) - }) - }) - }) - - context('with a malicious requester', () => { - beforeEach(async () => { - const paymentAmount = h.toWei(1) - mock = await MaliciousRequester.new(link.address, oc.address) - await link.transfer(mock.address, paymentAmount) - }) - - it('cannot cancel before the expiration', async () => { - await h.assertActionThrows(async () => { - await mock.maliciousRequestCancel( - specId, - h.toHex('doesNothing(bytes32,bytes32)'), - ) - }) - }) - - it('cannot call functions on the LINK token through callbacks', async () => { - await h.assertActionThrows(async () => { - await mock.request( - specId, - link.address, - h.toHex('transfer(address,uint256)'), - ) - }) - }) - - context('requester lies about amount of LINK sent', () => { - it('the oracle uses the amount of LINK actually paid', async () => { - const tx = await mock.maliciousPrice(specId) - const req = h.decodeRunRequest(tx.receipt.rawLogs[3]) - - assert(h.toWei(1).eq(h.bigNum(req.payment))) - }) - }) - }) - - context('with a malicious consumer', () => { - const paymentAmount = h.toWei(1) - - beforeEach(async () => { - mock = await MaliciousConsumer.new(link.address, oc.address) - await link.transfer(mock.address, paymentAmount) - }) - - context('fails during fulfillment', () => { - beforeEach(async () => { - const tx = await mock.requestData( - specId, - h.toHex('assertFail(bytes32,bytes32)'), - ) - request = h.decodeRunRequest(tx.receipt.rawLogs[3]) - }) - - it('allows the oracle node to receive their payment', async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - - const balance = await link.balanceOf.call(roles.oracleNode) - assertBigNum(balance, h.bigNum(0)) - - await withdraw(roles.oracleNode, paymentAmount, { - from: roles.defaultAccount, - }) - const newBalance = await link.balanceOf.call(roles.oracleNode) - assertBigNum(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response2 = 'hack the planet 102' - - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - - await h.assertActionThrows(async () => { - await h.fulfillOracleRequest(oc, request, response2, { - from: roles.oracleNode, - }) - }) - }) - }) - - context('calls selfdestruct', () => { - beforeEach(async () => { - const tx = await mock.requestData( - specId, - h.toHex('doesNothing(bytes32,bytes32)'), - ) - request = h.decodeRunRequest(tx.receipt.rawLogs[3]) - await mock.remove() - }) - - it('allows the oracle node to receive their payment', async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - - const balance = await link.balanceOf.call(roles.oracleNode) - assertBigNum(balance, h.bigNum(0)) - - await withdraw(roles.oracleNode, paymentAmount, { - from: roles.defaultAccount, - }) - const newBalance = await link.balanceOf.call(roles.oracleNode) - assertBigNum(paymentAmount, newBalance) - }) - }) - - context('request is canceled during fulfillment', () => { - beforeEach(async () => { - const tx = await mock.requestData( - specId, - h.toHex('cancelRequestOnFulfill(bytes32,bytes32)'), - ) - request = h.decodeRunRequest(tx.receipt.rawLogs[3]) - - assertBigNum(0, await link.balanceOf.call(mock.address)) - }) - - it('allows the oracle node to receive their payment', async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - - const mockBalance = await link.balanceOf.call(mock.address) - assertBigNum(mockBalance, h.bigNum(0)) - - const balance = await link.balanceOf.call(roles.oracleNode) - assertBigNum(balance, h.bigNum(0)) - - await withdraw(roles.oracleNode, paymentAmount, { - from: roles.defaultAccount, - }) - const newBalance = await link.balanceOf.call(roles.oracleNode) - assertBigNum(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response2 = 'hack the planet 102' - - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - - await h.assertActionThrows(async () => { - await h.fulfillOracleRequest(oc, request, response2, { - from: roles.oracleNode, - }) - }) - }) - }) - - context('tries to steal funds from node', () => { - it('is not successful with call', async () => { - const tx = await mock.requestData( - specId, - h.toHex('stealEthCall(bytes32,bytes32)'), - ) - request = h.decodeRunRequest(tx.receipt.rawLogs[3]) - - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - - assertBigNum(0, await web3.eth.getBalance(mock.address)) - }) - - it('is not successful with send', async () => { - const tx = await mock.requestData( - specId, - h.toHex('stealEthSend(bytes32,bytes32)'), - ) - request = h.decodeRunRequest(tx.receipt.rawLogs[3]) - - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - assertBigNum(0, await web3.eth.getBalance(mock.address)) - }) - - it('is not successful with transfer', async () => { - const tx = await mock.requestData( - specId, - h.toHex('stealEthTransfer(bytes32,bytes32)'), - ) - request = h.decodeRunRequest(tx.receipt.rawLogs[3]) - - await h.fulfillOracleRequest(oc, request, response, { - from: roles.oracleNode, - }) - assertBigNum(0, await web3.eth.getBalance(mock.address)) - }) - }) - }) - }) - - describe('#withdraw', () => { - context('without reserving funds via oracleRequest', () => { - it('does nothing', async () => { - let balance = await link.balanceOf(roles.oracleNode) - assert.equal(0, balance) - await h.assertActionThrows(async () => { - await withdraw(roles.oracleNode, h.toWei(1), { - from: roles.defaultAccount, - }) - }) - balance = await link.balanceOf(roles.oracleNode) - assert.equal(0, balance) - }) - }) - - context('reserving funds via oracleRequest', () => { - const payment = 15 - let request - - beforeEach(async () => { - const mock = await GetterSetter.new() - const args = h.requestDataBytes(specId, mock.address, fHash, 'id', '') - const tx = await h.requestDataFrom(oc, link, payment, args) - assert.equal(3, tx.receipt.rawLogs.length) - request = h.decodeRunRequest(tx.receipt.rawLogs[2]) - }) - - context('but not freeing funds w fulfillOracleRequest', () => { - it('does not transfer funds', async () => { - await h.assertActionThrows(async () => { - await withdraw(roles.oracleNode, payment, { - from: roles.defaultAccount, - }) - }) - const balance = await link.balanceOf(roles.oracleNode) - assert.equal(0, balance) - }) - }) - - context('and freeing funds', () => { - beforeEach(async () => { - await h.fulfillOracleRequest(oc, request, 'Hello World!', { - from: roles.oracleNode, - }) - }) - - it('does not allow input greater than the balance', async () => { - const originalOracleBalance = await link.balanceOf(oc.address) - const originalStrangerBalance = await link.balanceOf(roles.stranger) - const withdrawalAmount = payment + 1 - - assert.isAbove(withdrawalAmount, originalOracleBalance.toNumber()) - await h.assertActionThrows(async () => { - await withdraw(roles.stranger, withdrawalAmount, { - from: roles.defaultAccount, - }) - }) - - const newOracleBalance = await link.balanceOf(oc.address) - const newStrangerBalance = await link.balanceOf(roles.stranger) - - assert.equal( - originalOracleBalance.toNumber(), - newOracleBalance.toNumber(), - ) - assert.equal( - originalStrangerBalance.toNumber(), - newStrangerBalance.toNumber(), - ) - }) - - it('allows transfer of partial balance by owner to specified address', async () => { - const partialAmount = 6 - const difference = payment - partialAmount - await withdraw(roles.stranger, partialAmount, { - from: roles.defaultAccount, - }) - const strangerBalance = await link.balanceOf(roles.stranger) - const oracleBalance = await link.balanceOf(oc.address) - assert.equal(partialAmount, strangerBalance) - assert.equal(difference, oracleBalance) - }) - - it('allows transfer of entire balance by owner to specified address', async () => { - await withdraw(roles.stranger, payment, { - from: roles.defaultAccount, - }) - const balance = await link.balanceOf(roles.stranger) - assert.equal(payment, balance) - }) - - it('does not allow a transfer of funds by non-owner', async () => { - await h.assertActionThrows(async () => { - await withdraw(roles.stranger, payment, { from: roles.stranger }) - }) - const balance = await link.balanceOf(roles.stranger) - assert.equal(0, balance) - }) - }) - }) - }) - - describe('#withdrawable', () => { - let request - - beforeEach(async () => { - const amount = h.toWei(1, 'ether').toString() - const mock = await GetterSetter.new() - const args = h.requestDataBytes(specId, mock.address, fHash, 'id', '') - const tx = await h.requestDataFrom(oc, link, amount, args) - assert.equal(3, tx.receipt.rawLogs.length) - request = h.decodeRunRequest(tx.receipt.rawLogs[2]) - await h.fulfillOracleRequest(oc, request, 'Hello World!', { - from: roles.oracleNode, - }) - }) - - it('returns the correct value', async () => { - const withdrawAmount = await oc.withdrawable.call() - assertBigNum(withdrawAmount, request.payment) - }) - }) - - describe('#cancelOracleRequest', () => { - context('with no pending requests', () => { - it('fails', async () => { - const fakeRequest = { - id: h.toHex(1337), - payment: 0, - callbackFunc: h.functionSelector('requestedBytes32(bytes32,bytes32)'), - expiration: 999999999999, - } - await h.increaseTime5Minutes() - - await h.assertActionThrows(async () => { - await h.cancelOracleRequest(oc, fakeRequest, { from: roles.stranger }) - }) - }) - }) - - context('with a pending request', () => { - const startingBalance = 100 - let request, tx - - beforeEach(async () => { - const requestAmount = 20 - - await link.transfer(roles.consumer, startingBalance) - - const args = h.requestDataBytes(specId, roles.consumer, fHash, 1, '') - tx = await link.transferAndCall(oc.address, requestAmount, args, { - from: roles.consumer, - }) - assert.equal(3, tx.receipt.rawLogs.length) - request = h.decodeRunRequest(tx.receipt.rawLogs[2]) - }) - - it('has correct initial balances', async () => { - const oracleBalance = await link.balanceOf(oc.address) - assertBigNum(request.payment, oracleBalance) - - const consumerAmount = await link.balanceOf(roles.consumer) - assert.equal(startingBalance - request.payment, consumerAmount) - }) - - context('from a stranger', () => { - it('fails', async () => { - await h.assertActionThrows(async () => { - await h.cancelOracleRequest(oc, request, { from: roles.consumer }) - }) - }) - }) - - context('from the requester', () => { - it('refunds the correct amount', async () => { - await h.increaseTime5Minutes() - await h.cancelOracleRequest(oc, request, { from: roles.consumer }) - const balance = await link.balanceOf(roles.consumer) - assert.equal(startingBalance, balance) // 100 - }) - - it('triggers a cancellation event', async () => { - await h.increaseTime5Minutes() - const tx = await h.cancelOracleRequest(oc, request, { - from: roles.consumer, - }) - - assert.equal(tx.receipt.rawLogs.length, 2) - assert.equal(request.id, tx.receipt.rawLogs[0].topics[1]) - }) - - it('fails when called twice', async () => { - await h.increaseTime5Minutes() - await h.cancelOracleRequest(oc, request, { from: roles.consumer }) - - await h.assertActionThrows(async () => { - await h.cancelOracleRequest(oc, request, { from: roles.consumer }) - }) - }) - }) - }) - }) -}) diff --git a/evm/testv2/Aggregator.test.ts b/evm/testv2/Aggregator.test.ts index bd0deb8f4fa..e1a0e20e860 100644 --- a/evm/testv2/Aggregator.test.ts +++ b/evm/testv2/Aggregator.test.ts @@ -1,13 +1,12 @@ import * as h from '../src/helpersV2' import { assertBigNum } from '../src/matchersV2' import { ethers } from 'ethers' -import { EthersProviderWrapper } from '../src/provider' -import env from '@nomiclabs/buidler' import { Instance } from '../src/contract' import { OracleFactory } from '../src/generated/OracleFactory' import { LinkTokenFactory } from '../src/generated/LinkTokenFactory' import { AggregatorFactory } from '../src/generated/AggregatorFactory' import { assert } from 'chai' +import ganache from 'ganache-core' const aggregatorFactory = new AggregatorFactory() const oracleFactory = new OracleFactory() @@ -15,7 +14,7 @@ const linkTokenFactory = new LinkTokenFactory() let personas: h.Personas let defaultAccount: ethers.Wallet -const provider = new EthersProviderWrapper(env.ethereum) +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) beforeAll(async () => { const rolesAndPersonas = await h.initializeRolesAndPersonas(provider) @@ -43,8 +42,7 @@ describe('Aggregator', () => { let oc4: Instance let oracles: Instance[] let jobIds: string[] = [] - - beforeEach(async () => { + const deployment = h.useSnapshot(provider, async () => { link = await linkTokenFactory.connect(defaultAccount).deploy() oc1 = await oracleFactory.connect(defaultAccount).deploy(link.address) oc2 = await oracleFactory.connect(defaultAccount).deploy(link.address) @@ -53,6 +51,10 @@ describe('Aggregator', () => { oracles = [oc1, oc2, oc3] }) + beforeEach(async () => { + await deployment() + }) + it('has a limited public interface', () => { h.checkPublicABI(aggregatorFactory, [ 'authorizedRequesters', diff --git a/evm/testv2/AggregatorProxy.test.ts b/evm/testv2/AggregatorProxy.test.ts index ea39f95051f..ba0447c6a77 100644 --- a/evm/testv2/AggregatorProxy.test.ts +++ b/evm/testv2/AggregatorProxy.test.ts @@ -1,7 +1,5 @@ import * as h from '../src/helpersV2' import { assertBigNum } from '../src/matchersV2' -import env from '@nomiclabs/buidler' -import { EthersProviderWrapper } from '../src/provider' import { ethers } from 'ethers' import { Instance } from '../src/contract' import { LinkTokenFactory } from '../src/generated/LinkTokenFactory' @@ -9,11 +7,12 @@ import { AggregatorFactory } from '../src/generated/AggregatorFactory' import { OracleFactory } from '../src/generated/OracleFactory' import { AggregatorProxyFactory } from '../src/generated/AggregatorProxyFactory' import { assert } from 'chai' +import ganache from 'ganache-core' let personas: h.Personas let defaultAccount: ethers.Wallet -const provider = new EthersProviderWrapper(env.ethereum) +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) const linkTokenFactory = new LinkTokenFactory() const aggregatorFactory = new AggregatorFactory() const oracleFactory = new OracleFactory() @@ -39,22 +38,22 @@ describe('AggregatorProxy', () => { let aggregator2: Instance let oc1: Instance let proxy: Instance - - beforeEach(async () => { + const deployment = h.useSnapshot(provider, async () => { link = await linkTokenFactory.connect(defaultAccount).deploy() - oc1 = await oracleFactory.connect(defaultAccount).deploy(link.address) - aggregator = await aggregatorFactory .connect(defaultAccount) .deploy(link.address, basePayment, 1, [oc1.address], [jobId1]) await link.transfer(aggregator.address, deposit) - proxy = await aggregatorProxyFactory .connect(defaultAccount) .deploy(aggregator.address) }) + beforeEach(async () => { + await deployment() + }) + it('has a limited public interface', () => { h.checkPublicABI(aggregatorProxyFactory, [ 'aggregator', diff --git a/evm/testv2/BasicConsumer.test.ts b/evm/testv2/BasicConsumer.test.ts index 085dca8286a..b1ee78ae533 100644 --- a/evm/testv2/BasicConsumer.test.ts +++ b/evm/testv2/BasicConsumer.test.ts @@ -7,15 +7,14 @@ import { LinkTokenFactory } from '../src/generated/LinkTokenFactory' import { OracleFactory } from '../src/generated/OracleFactory' import { BasicConsumerFactory } from '../src/generated/BasicConsumerFactory' import { Instance } from '../src/contract' -import env from '@nomiclabs/buidler' -import { EthersProviderWrapper } from '../src/provider' +import ganache from 'ganache-core' const basicConsumerFactory = new BasicConsumerFactory() const oracleFactory = new OracleFactory() const linkTokenFactory = new LinkTokenFactory() // create ethers provider from that web3js instance -const provider = new EthersProviderWrapper(env.ethereum) +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) let roles: h.Roles @@ -27,13 +26,11 @@ beforeAll(async () => { describe('BasicConsumer', () => { const specId = '0x4c7b7ffb66b344fbaa64995af81e355a'.padEnd(66, '0') - const currency = 'USD' let link: Instance let oc: Instance let cc: Instance - - beforeEach(async () => { + const deployment = h.useSnapshot(provider, async () => { link = await linkTokenFactory.connect(roles.defaultAccount).deploy() oc = await oracleFactory.connect(roles.oracleNode).deploy(link.address) cc = await basicConsumerFactory @@ -41,6 +38,10 @@ describe('BasicConsumer', () => { .deploy(link.address, oc.address, specId) }) + beforeEach(async () => { + await deployment() + }) + it('has a predictable gas price', async () => { const rec = await provider.getTransactionReceipt(cc.deployTransaction.hash!) assert.isBelow(rec.gasUsed!.toNumber(), 1700000) diff --git a/evm/testv2/ConcreteChainlink.test.ts b/evm/testv2/ConcreteChainlink.test.ts index f4e55086515..5ed149f05b1 100644 --- a/evm/testv2/ConcreteChainlink.test.ts +++ b/evm/testv2/ConcreteChainlink.test.ts @@ -1,32 +1,31 @@ -import { - checkPublicABI, - decodeDietCBOR, - initializeRolesAndPersonas, - hexToBuf, -} from '../src/helpersV2' +import * as h from '../src/helpersV2' import { ConcreteChainlinkFactory } from '../src/generated/ConcreteChainlinkFactory' import { Instance } from '../src/contract' import { ethers } from 'ethers' -import env from '@nomiclabs/buidler' -import { EthersProviderWrapper } from '../src/provider' import { assert } from 'chai' import { makeDebug } from '../src/debug' -const provider = new EthersProviderWrapper(env.ethereum) +import ganache from 'ganache-core' + +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) const concreteChainlinkFactory = new ConcreteChainlinkFactory() const debug = makeDebug('ConcreteChainlink') describe('ConcreteChainlink', () => { let ccl: Instance let defaultAccount: ethers.Wallet - beforeEach(async () => { - defaultAccount = await initializeRolesAndPersonas(provider).then( - r => r.roles.defaultAccount, - ) + const deployment = h.useSnapshot(provider, async () => { + defaultAccount = await h + .initializeRolesAndPersonas(provider) + .then(r => r.roles.defaultAccount) ccl = await concreteChainlinkFactory.connect(defaultAccount).deploy() }) + beforeEach(async () => { + await deployment() + }) + it('has a limited public interface', () => { - checkPublicABI(concreteChainlinkFactory, [ + h.checkPublicABI(concreteChainlinkFactory, [ 'add', 'addBytes', 'addInt', @@ -49,7 +48,7 @@ describe('ConcreteChainlink', () => { it('handles empty payloads', async () => { const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, {}) }) }) @@ -59,7 +58,7 @@ describe('ConcreteChainlink', () => { await ccl.setBuffer('0xA161616162') const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, { a: 'b' }) }) }) @@ -69,7 +68,7 @@ describe('ConcreteChainlink', () => { await ccl.add('first', 'word!!') const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, { first: 'word!!' }) }) @@ -78,7 +77,7 @@ describe('ConcreteChainlink', () => { await ccl.add('second', 'dos') const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, { first: 'uno', @@ -92,8 +91,8 @@ describe('ConcreteChainlink', () => { await ccl.addBytes('first', '0xaabbccddeeff') const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - const expected = hexToBuf('0xaabbccddeeff') + const decoded = await h.decodeDietCBOR(payload) + const expected = h.hexToBuf('0xaabbccddeeff') assert.deepEqual(decoded, { first: expected }) }) @@ -102,10 +101,10 @@ describe('ConcreteChainlink', () => { await ccl.addBytes('second', '0x646F73') const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) - const expectedFirst = hexToBuf('0x756E6F') - const expectedSecond = hexToBuf('0x646F73') + const expectedFirst = h.hexToBuf('0x756E6F') + const expectedSecond = h.hexToBuf('0x646F73') assert.deepEqual(decoded, { first: expectedFirst, second: expectedSecond, @@ -116,7 +115,7 @@ describe('ConcreteChainlink', () => { await ccl.addBytes('first', ethers.utils.toUtf8Bytes('apple')) const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) const expected = ethers.utils.toUtf8Bytes('apple') assert.deepEqual(decoded, { first: expected }) }) @@ -127,7 +126,7 @@ describe('ConcreteChainlink', () => { await ccl.addInt('first', 1) const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, { first: 1 }) }) @@ -136,7 +135,7 @@ describe('ConcreteChainlink', () => { await ccl.addInt('second', 2) const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, { first: 1, @@ -150,7 +149,7 @@ describe('ConcreteChainlink', () => { await ccl.addUint('first', 1) const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, { first: 1 }) }) @@ -159,7 +158,7 @@ describe('ConcreteChainlink', () => { await ccl.addUint('second', 2) const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, { first: 1, @@ -177,7 +176,7 @@ describe('ConcreteChainlink', () => { ]) const tx = await ccl.closeEvent() const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) + const decoded = await h.decodeDietCBOR(payload) assert.deepEqual(decoded, { word: ['seinfeld', '"4"', 'LIFE'] }) }) }) diff --git a/evm/testv2/ConcreteChainlinked.test.ts b/evm/testv2/ConcreteChainlinked.test.ts index d8ca12a37a6..0a29dc50ee1 100644 --- a/evm/testv2/ConcreteChainlinked.test.ts +++ b/evm/testv2/ConcreteChainlinked.test.ts @@ -7,8 +7,7 @@ import { assert } from 'chai' import { ethers } from 'ethers' import { LinkTokenFactory } from '../src/generated/LinkTokenFactory' import { Instance } from '../src/contract' -import env from '@nomiclabs/buidler' -import { EthersProviderWrapper } from '../src/provider' +import ganache from 'ganache-core' const concreteChainlinkedFactory = new ConcreteChainlinkedFactory() const emptyOracleFactory = new EmptyOracleFactory() @@ -16,7 +15,7 @@ const getterSetterFactory = new GetterSetterFactory() const oracleFactory = new OracleFactory() const linkTokenFactory = new LinkTokenFactory() -const provider = new EthersProviderWrapper(env.ethereum) +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) let roles: h.Roles @@ -34,8 +33,7 @@ describe('ConcreteChainlinked', () => { let oc: Instance let newoc: Instance let link: Instance - - beforeEach(async () => { + const deployment = h.useSnapshot(provider, async () => { link = await linkTokenFactory.connect(roles.defaultAccount).deploy() oc = await oracleFactory.connect(roles.defaultAccount).deploy(link.address) newoc = await oracleFactory @@ -47,6 +45,10 @@ describe('ConcreteChainlinked', () => { .deploy(link.address, oc.address) }) + beforeEach(async () => { + await deployment() + }) + describe('#newRequest', () => { it('forwards the information to the oracle contract through the link token', async () => { const tx = await cc.publicNewRequest( @@ -131,16 +133,20 @@ describe('ConcreteChainlinked', () => { describe('#cancelChainlinkRequest', () => { let requestId: string + // a concrete chainlink attached to an empty oracle + let ecc: Instance beforeEach(async () => { - oc = await emptyOracleFactory.connect(roles.defaultAccount).deploy() - cc = await concreteChainlinkedFactory + const emptyOracle = await emptyOracleFactory .connect(roles.defaultAccount) - .deploy(link.address, oc.address) + .deploy() + ecc = await concreteChainlinkedFactory + .connect(roles.defaultAccount) + .deploy(link.address, emptyOracle.address) - const tx = await cc.publicRequest( + const tx = await ecc.publicRequest( specId, - cc.address, + ecc.address, ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), 0, ) @@ -149,7 +155,7 @@ describe('ConcreteChainlinked', () => { }) it('emits an event from the contract showing the run was cancelled', async () => { - const tx = await cc.publicCancelRequest( + const tx = await ecc.publicCancelRequest( requestId, 0, ethers.utils.hexZeroPad('0x', 4), @@ -164,7 +170,7 @@ describe('ConcreteChainlinked', () => { it('throws if given a bogus event ID', async () => { await h.assertActionThrows(async () => { - await cc.publicCancelRequest( + await ecc.publicCancelRequest( ethers.utils.formatBytes32String('bogusId'), 0, ethers.utils.hexZeroPad('0x', 4), diff --git a/evm/testv2/GetterSetter.test.ts b/evm/testv2/GetterSetter.test.ts index 156cbbc9dc5..82d114a65ab 100644 --- a/evm/testv2/GetterSetter.test.ts +++ b/evm/testv2/GetterSetter.test.ts @@ -3,11 +3,10 @@ import { ethers } from 'ethers' import { assert } from 'chai' import { GetterSetterFactory } from '../src/generated/GetterSetterFactory' import { Instance } from '../src/contract' -import env from '@nomiclabs/buidler' -import { EthersProviderWrapper } from '../src/provider' +import ganache from 'ganache-core' const GetterSetterContract = new GetterSetterFactory() -const provider = new EthersProviderWrapper(env.ethereum) +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) let roles: h.Roles @@ -23,9 +22,12 @@ describe('GetterSetter', () => { const bytes32 = ethers.utils.formatBytes32String('Hi Mom!') const uint256 = ethers.utils.bigNumberify(645746535432) let gs: Instance + const deployment = h.useSnapshot(provider, async () => { + gs = await GetterSetterContract.connect(roles.defaultAccount).deploy() + }) beforeEach(async () => { - gs = await GetterSetterContract.connect(roles.defaultAccount).deploy() + await deployment() }) describe('#setBytes32Val', () => { diff --git a/evm/testv2/Oracle.test.ts b/evm/testv2/Oracle.test.ts new file mode 100644 index 00000000000..c856d109c09 --- /dev/null +++ b/evm/testv2/Oracle.test.ts @@ -0,0 +1,885 @@ +import * as h from '../src/helpersV2' +import { assertBigNum } from '../src/matchersV2' +import { BasicConsumerFactory } from '../src/generated/BasicConsumerFactory' +import { GetterSetterFactory } from '../src/generated/GetterSetterFactory' +import { MaliciousRequesterFactory } from '../src/generated/MaliciousRequesterFactory' +import { MaliciousConsumerFactory } from '../src/generated/MaliciousConsumerFactory' +import { OracleFactory } from '../src/generated/OracleFactory' +import { LinkTokenFactory } from '../src/generated/LinkTokenFactory' +import { Instance } from '../src/contract' +import { ethers } from 'ethers' +import { assert } from 'chai' +import ganache from 'ganache-core' + +const basicConsumerFactory = new BasicConsumerFactory() +const getterSetterFactory = new GetterSetterFactory() +const maliciousRequesterFactory = new MaliciousRequesterFactory() +const maliciousConsumerFactory = new MaliciousConsumerFactory() +const oracleFactory = new OracleFactory() +const linkTokenFactory = new LinkTokenFactory() + +let roles: h.Roles +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) + +beforeAll(async () => { + const rolesAndPersonas = await h.initializeRolesAndPersonas(provider) + + roles = rolesAndPersonas.roles +}) + +describe('Oracle', () => { + const fHash = getterSetterFactory.interface.functions.requestedBytes32.sighash + const specId = + '0x4c7b7ffb66b344fbaa64995af81e355a00000000000000000000000000000000' + const to = '0x80e29acb842498fe6591f020bd82766dce619d43' + let link: Instance + let oc: Instance + const deployment = h.useSnapshot(provider, async () => { + link = await linkTokenFactory.connect(roles.defaultAccount).deploy() + oc = await oracleFactory.connect(roles.defaultAccount).deploy(link.address) + await oc.setFulfillmentPermission(roles.oracleNode.address, true) + }) + + beforeEach(async () => { + await deployment() + }) + + it('has a limited public interface', () => { + h.checkPublicABI(oracleFactory, [ + 'EXPIRY_TIME', + 'cancelOracleRequest', + 'fulfillOracleRequest', + 'getAuthorizationStatus', + 'onTokenTransfer', + 'oracleRequest', + 'setFulfillmentPermission', + 'withdraw', + 'withdrawable', + // Ownable methods: + 'owner', + 'renounceOwnership', + 'transferOwnership', + ]) + }) + + describe('#setFulfillmentPermission', () => { + describe('when called by the owner', () => { + beforeEach(async () => { + await oc + .connect(roles.defaultAccount) + .setFulfillmentPermission(roles.stranger.address, true) + }) + + it('adds an authorized node', async () => { + const authorized = await oc.getAuthorizationStatus( + roles.stranger.address, + ) + assert.equal(true, authorized) + }) + + it('removes an authorized node', async () => { + await oc + .connect(roles.defaultAccount) + .setFulfillmentPermission(roles.stranger.address, false) + const authorized = await oc.getAuthorizationStatus( + roles.stranger.address, + ) + assert.equal(false, authorized) + }) + }) + + describe('when called by a non-owner', () => { + it('cannot add an authorized node', async () => { + await h.assertActionThrows(async () => { + await oc + .connect(roles.stranger) + .setFulfillmentPermission(roles.stranger.address, true) + }) + }) + }) + }) + + describe('#onTokenTransfer', () => { + describe('when called from any address but the LINK token', () => { + it('triggers the intended method', async () => { + const callData = h.requestDataBytes(specId, to, fHash, 0, '0x0') + + await h.assertActionThrows(async () => { + await oc.onTokenTransfer(roles.defaultAccount.address, 0, callData) + }) + }) + }) + + describe('when called from the LINK token', () => { + it('triggers the intended method', async () => { + const callData = h.requestDataBytes(specId, to, fHash, 0, '0x0') + + const tx = await link.transferAndCall(oc.address, 0, callData, { + value: 0, + }) + const receipt = await tx.wait() + + assert.equal(3, receipt.logs!.length) + }) + + describe('with no data', () => { + it('reverts', async () => { + await h.assertActionThrows(async () => { + await link.transferAndCall(oc.address, 0, '0x', { + value: 0, + }) + }) + }) + }) + }) + + describe('malicious requester', () => { + let mock: Instance + let requester: Instance + const paymentAmount = h.toWei('1') + + beforeEach(async () => { + mock = await maliciousRequesterFactory + .connect(roles.defaultAccount) + .deploy(link.address, oc.address) + await link.transfer(mock.address, paymentAmount) + }) + + it('cannot withdraw from oracle', async () => { + const ocOriginalBalance = await link.balanceOf(oc.address) + const mockOriginalBalance = await link.balanceOf(mock.address) + + await h.assertActionThrows(async () => { + await mock.maliciousWithdraw() + }) + + const ocNewBalance = await link.balanceOf(oc.address) + const mockNewBalance = await link.balanceOf(mock.address) + + assertBigNum(ocOriginalBalance, ocNewBalance) + assertBigNum(mockNewBalance, mockOriginalBalance) + }) + + describe('if the requester tries to create a requestId for another contract', () => { + it('the requesters ID will not match with the oracle contract', async () => { + const tx = await mock.maliciousTargetConsumer(to) + const receipt = await tx.wait() + + const mockRequestId = receipt.logs![0].data + const requestId = (receipt.events![0].args! as any).requestId + assert.notEqual(mockRequestId, requestId) + }) + + it('the target requester can still create valid requests', async () => { + requester = await basicConsumerFactory + .connect(roles.defaultAccount) + .deploy(link.address, oc.address, specId) + await link.transfer(requester.address, paymentAmount) + await mock.maliciousTargetConsumer(requester.address) + await requester.requestEthereumPrice('USD') + }) + }) + }) + + it('does not allow recursive calls of onTokenTransfer', async () => { + const requestPayload = h.requestDataBytes(specId, to, fHash, 0, '0x0') + + const ottSelector = + oracleFactory.interface.functions.onTokenTransfer.sighash + const header = + '000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef' + // to + '0000000000000000000000000000000000000000000000000000000000000539' + // amount + '0000000000000000000000000000000000000000000000000000000000000060' + // offset + '0000000000000000000000000000000000000000000000000000000000000136' // length + + const maliciousPayload = ottSelector + header + requestPayload.slice(2) + + await h.assertActionThrows(async () => { + await link.transferAndCall(oc.address, 0, maliciousPayload, { + value: 0, + }) + }) + }) + }) + + describe('#oracleRequest', () => { + describe('when called through the LINK token', () => { + const paid = 100 + let log: ethers.providers.Log + let receipt: ethers.providers.TransactionReceipt + + beforeEach(async () => { + const args = h.requestDataBytes(specId, to, fHash, 1, '0x0') + const tx = await h.requestDataFrom(oc, link, paid, args) + receipt = await tx.wait() + assert.equal(3, receipt.logs!.length) + + log = receipt.logs![2] + }) + + it('logs an event', async () => { + assert.equal(oc.address, log.address) + + assert.equal(log.topics[1], specId) + + const req = h.decodeRunRequest(receipt.logs![2]) + assert.equal(roles.defaultAccount.address, req.requester) + assertBigNum(paid, req.payment) + }) + + it('uses the expected event signature', async () => { + // If updating this test, be sure to update models.RunLogTopic. + const eventSignature = + '0xd8d7ecc4800d25fa53ce0372f13a416d98907a7ef3d8d3bdd79cf4fe75529c65' + assert.equal(eventSignature, log.topics[0]) + }) + + it('does not allow the same requestId to be used twice', async () => { + const args2 = h.requestDataBytes(specId, to, fHash, 1, '0x0') + await h.assertActionThrows(async () => { + await h.requestDataFrom(oc, link, paid, args2) + }) + }) + + describe('when called with a payload less than 2 EVM words + function selector', () => { + const funcSelector = + oracleFactory.interface.functions.oracleRequest.sighash + const maliciousData = + funcSelector + + '0000000000000000000000000000000000000000000000000000000000000000000' + + it('throws an error', async () => { + await h.assertActionThrows(async () => { + await h.requestDataFrom(oc, link, paid, maliciousData) + }) + }) + }) + + describe('when called with a payload between 3 and 9 EVM words', () => { + const funcSelector = + oracleFactory.interface.functions.oracleRequest.sighash + const maliciousData = + funcSelector + + '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001' + + it('throws an error', async () => { + await h.assertActionThrows(async () => { + await h.requestDataFrom(oc, link, paid, maliciousData) + }) + }) + }) + }) + + describe('when not called through the LINK token', () => { + it('reverts', async () => { + await h.assertActionThrows(async () => { + await oc + .connect(roles.oracleNode) + .oracleRequest( + '0x0000000000000000000000000000000000000000', + 0, + specId, + to, + fHash, + 1, + 1, + '0x', + ) + }) + }) + }) + }) + + describe('#fulfillOracleRequest', () => { + const response = 'Hi Mom!' + let maliciousRequester: Instance + let basicConsumer: Instance + let maliciousConsumer: Instance + let request: ReturnType + + describe('cooperative consumer', () => { + beforeEach(async () => { + basicConsumer = await basicConsumerFactory + .connect(roles.defaultAccount) + .deploy(link.address, oc.address, specId) + const paymentAmount = h.toWei('1') + await link.transfer(basicConsumer.address, paymentAmount) + const currency = 'USD' + const tx = await basicConsumer.requestEthereumPrice(currency) + const receipt = await tx.wait() + request = h.decodeRunRequest(receipt.logs![3]) + }) + + describe('when called by an unauthorized node', () => { + beforeEach(async () => { + assert.equal( + false, + await oc.getAuthorizationStatus(roles.stranger.address), + ) + }) + + it('raises an error', async () => { + await h.assertActionThrows(async () => { + await h.fulfillOracleRequest( + oc.connect(roles.stranger), + request, + response, + ) + }) + }) + }) + + describe('when called by an authorized node', () => { + it('raises an error if the request ID does not exist', async () => { + request.id = ethers.utils.formatBytes32String('DOESNOTEXIST') + await h.assertActionThrows(async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + }) + }) + + it('sets the value on the requested contract', async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + + const currentValue = await basicConsumer.currentPrice() + assert.equal(response, ethers.utils.parseBytes32String(currentValue)) + }) + + it('does not allow a request to be fulfilled twice', async () => { + const response2 = response + ' && Hello World!!' + + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + + await h.assertActionThrows(async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response2, + ) + }) + + const currentValue = await basicConsumer.currentPrice() + assert.equal(response, ethers.utils.parseBytes32String(currentValue)) + }) + }) + + describe('when the oracle does not provide enough gas', () => { + // if updating this defaultGasLimit, be sure it matches with the + // defaultGasLimit specified in store/tx_manager.go + const defaultGasLimit = 500000 + + beforeEach(async () => { + assertBigNum(0, await oc.withdrawable()) + }) + + it('does not allow the oracle to withdraw the payment', async () => { + await h.assertActionThrows(async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + { + gasLimit: 70000, + }, + ) + }) + + assertBigNum(0, await oc.withdrawable()) + }) + + it(`${defaultGasLimit} is enough to pass the gas requirement`, async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + { + gasLimit: defaultGasLimit, + }, + ) + + assertBigNum(request.payment, await oc.withdrawable()) + }) + }) + }) + + describe('with a malicious requester', () => { + beforeEach(async () => { + const paymentAmount = h.toWei('1') + maliciousRequester = await maliciousRequesterFactory + .connect(roles.defaultAccount) + .deploy(link.address, oc.address) + await link.transfer(maliciousRequester.address, paymentAmount) + }) + + it('cannot cancel before the expiration', async () => { + await h.assertActionThrows(async () => { + await maliciousRequester.maliciousRequestCancel( + specId, + ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), + ) + }) + }) + + it('cannot call functions on the LINK token through callbacks', async () => { + await h.assertActionThrows(async () => { + await maliciousRequester.request( + specId, + link.address, + ethers.utils.toUtf8Bytes('transfer(address,uint256)'), + ) + }) + }) + + describe('requester lies about amount of LINK sent', () => { + it('the oracle uses the amount of LINK actually paid', async () => { + const tx = await maliciousRequester.maliciousPrice(specId) + const receipt = await tx.wait() + const req = h.decodeRunRequest(receipt.logs![3]) + + assert(h.toWei('1').eq(req.payment)) + }) + }) + }) + + describe('with a malicious consumer', () => { + const paymentAmount = h.toWei('1') + + beforeEach(async () => { + maliciousConsumer = await maliciousConsumerFactory + .connect(roles.defaultAccount) + .deploy(link.address, oc.address) + await link.transfer(maliciousConsumer.address, paymentAmount) + }) + + describe('fails during fulfillment', () => { + beforeEach(async () => { + const tx = await maliciousConsumer.requestData( + specId, + ethers.utils.toUtf8Bytes('assertFail(bytes32,bytes32)'), + ) + const receipt = await tx.wait() + request = h.decodeRunRequest(receipt.logs![3]) + }) + + it('allows the oracle node to receive their payment', async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + + const balance = await link.balanceOf(roles.oracleNode.address) + assertBigNum(balance, 0) + + await oc + .connect(roles.defaultAccount) + .withdraw(roles.oracleNode.address, paymentAmount) + + const newBalance = await link.balanceOf(roles.oracleNode.address) + assertBigNum(paymentAmount, newBalance) + }) + + it("can't fulfill the data again", async () => { + const response2 = 'hack the planet 102' + + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + + await h.assertActionThrows(async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response2, + ) + }) + }) + }) + + describe('calls selfdestruct', () => { + beforeEach(async () => { + const tx = await maliciousConsumer.requestData( + specId, + ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), + ) + const receipt = await tx.wait() + request = h.decodeRunRequest(receipt.logs![3]) + await maliciousConsumer.remove() + }) + + it('allows the oracle node to receive their payment', async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + + const balance = await link.balanceOf(roles.oracleNode.address) + assertBigNum(balance, 0) + + await oc + .connect(roles.defaultAccount) + .withdraw(roles.oracleNode.address, paymentAmount) + const newBalance = await link.balanceOf(roles.oracleNode.address) + assertBigNum(paymentAmount, newBalance) + }) + }) + + describe('request is canceled during fulfillment', () => { + beforeEach(async () => { + const tx = await maliciousConsumer.requestData( + specId, + ethers.utils.toUtf8Bytes('cancelRequestOnFulfill(bytes32,bytes32)'), + ) + const receipt = await tx.wait() + request = h.decodeRunRequest(receipt.logs![3]) + + assertBigNum(0, await link.balanceOf(maliciousConsumer.address)) + }) + + it('allows the oracle node to receive their payment', async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + + const mockBalance = await link.balanceOf(maliciousConsumer.address) + assertBigNum(mockBalance, 0) + + const balance = await link.balanceOf(roles.oracleNode.address) + assertBigNum(balance, 0) + + await oc + .connect(roles.defaultAccount) + .withdraw(roles.oracleNode.address, paymentAmount) + const newBalance = await link.balanceOf(roles.oracleNode.address) + assertBigNum(paymentAmount, newBalance) + }) + + it("can't fulfill the data again", async () => { + const response2 = 'hack the planet 102' + + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + + await h.assertActionThrows(async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response2, + ) + }) + }) + }) + + describe('tries to steal funds from node', () => { + it('is not successful with call', async () => { + const tx = await maliciousConsumer.requestData( + specId, + ethers.utils.toUtf8Bytes('stealEthCall(bytes32,bytes32)'), + ) + const receipt = await tx.wait() + request = h.decodeRunRequest(receipt.logs![3]) + + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + + assertBigNum(0, await provider.getBalance(maliciousConsumer.address)) + }) + + it('is not successful with send', async () => { + const tx = await maliciousConsumer.requestData( + specId, + ethers.utils.toUtf8Bytes('stealEthSend(bytes32,bytes32)'), + ) + const receipt = await tx.wait() + request = h.decodeRunRequest(receipt.logs![3]) + + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + assertBigNum(0, await provider.getBalance(maliciousConsumer.address)) + }) + + it('is not successful with transfer', async () => { + const tx = await maliciousConsumer.requestData( + specId, + ethers.utils.toUtf8Bytes('stealEthTransfer(bytes32,bytes32)'), + ) + const receipt = await tx.wait() + request = h.decodeRunRequest(receipt.logs![3]) + + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + response, + ) + assertBigNum(0, await provider.getBalance(maliciousConsumer.address)) + }) + }) + }) + }) + + describe('#withdraw', () => { + describe('without reserving funds via oracleRequest', () => { + it('does nothing', async () => { + let balance = await link.balanceOf(roles.oracleNode.address) + assert.equal(0, balance.toNumber()) + await h.assertActionThrows(async () => { + await oc + .connect(roles.defaultAccount) + .withdraw(roles.oracleNode.address, h.toWei('1')) + }) + balance = await link.balanceOf(roles.oracleNode.address) + assert.equal(0, balance.toNumber()) + }) + }) + + describe('reserving funds via oracleRequest', () => { + const payment = 15 + let request: ReturnType + + beforeEach(async () => { + const mock = await getterSetterFactory + .connect(roles.defaultAccount) + .deploy() + const args = h.requestDataBytes(specId, mock.address, fHash, 0, '0x0') + const tx = await h.requestDataFrom(oc, link, payment, args) + const receipt = await tx.wait() + assert.equal(3, receipt.logs!.length) + request = h.decodeRunRequest(receipt.logs![2]) + }) + + describe('but not freeing funds w fulfillOracleRequest', () => { + it('does not transfer funds', async () => { + await h.assertActionThrows(async () => { + await oc + .connect(roles.defaultAccount) + .withdraw(roles.oracleNode.address, payment) + }) + const balance = await link.balanceOf(roles.oracleNode.address) + assert.equal(0, balance.toNumber()) + }) + }) + + describe('and freeing funds', () => { + beforeEach(async () => { + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + 'Hello World!', + ) + }) + + it('does not allow input greater than the balance', async () => { + const originalOracleBalance = await link.balanceOf(oc.address) + const originalStrangerBalance = await link.balanceOf( + roles.stranger.address, + ) + const withdrawalAmount = payment + 1 + + assert.isAbove(withdrawalAmount, originalOracleBalance.toNumber()) + await h.assertActionThrows(async () => { + await oc + .connect(roles.defaultAccount) + .withdraw(roles.stranger.address, withdrawalAmount) + }) + + const newOracleBalance = await link.balanceOf(oc.address) + const newStrangerBalance = await link.balanceOf( + roles.stranger.address, + ) + + assert.equal( + originalOracleBalance.toNumber(), + newOracleBalance.toNumber(), + ) + assert.equal( + originalStrangerBalance.toNumber(), + newStrangerBalance.toNumber(), + ) + }) + + it('allows transfer of partial balance by owner to specified address', async () => { + const partialAmount = 6 + const difference = payment - partialAmount + await oc + .connect(roles.defaultAccount) + .withdraw(roles.stranger.address, partialAmount) + const strangerBalance = await link.balanceOf(roles.stranger.address) + const oracleBalance = await link.balanceOf(oc.address) + assert.equal(partialAmount, strangerBalance.toNumber()) + assert.equal(difference, oracleBalance.toNumber()) + }) + + it('allows transfer of entire balance by owner to specified address', async () => { + await oc + .connect(roles.defaultAccount) + .withdraw(roles.stranger.address, payment) + const balance = await link.balanceOf(roles.stranger.address) + assert.equal(payment, balance.toNumber()) + }) + + it('does not allow a transfer of funds by non-owner', async () => { + await h.assertActionThrows(async () => { + await oc + .connect(roles.stranger) + .withdraw(roles.stranger.address, payment) + }) + const balance = await link.balanceOf(roles.stranger.address) + assert.isTrue(ethers.constants.Zero.eq(balance)) + }) + }) + }) + }) + + describe('#withdrawable', () => { + let request: ReturnType + + beforeEach(async () => { + const amount = h.toWei('1') + const mock = await getterSetterFactory + .connect(roles.defaultAccount) + .deploy() + const args = h.requestDataBytes(specId, mock.address, fHash, 0, '0x0') + const tx = await h.requestDataFrom(oc, link, amount, args) + const receipt = await tx.wait() + assert.equal(3, receipt.logs!.length) + request = h.decodeRunRequest(receipt.logs![2]) + await h.fulfillOracleRequest( + oc.connect(roles.oracleNode), + request, + 'Hello World!', + ) + }) + + it('returns the correct value', async () => { + const withdrawAmount = await oc.withdrawable() + assertBigNum(withdrawAmount, request.payment) + }) + }) + + describe('#cancelOracleRequest', () => { + describe('with no pending requests', () => { + it('fails', async () => { + const fakeRequest: h.RunRequest = { + id: ethers.utils.formatBytes32String('1337'), + payment: '0', + callbackFunc: + getterSetterFactory.interface.functions.requestedBytes32.sighash, + expiration: '999999999999', + + callbackAddr: '', + data: Buffer.from(''), + dataVersion: 0, + jobId: '', + requester: '', + topic: '', + } + await h.increaseTime5Minutes(provider) + + await h.assertActionThrows(async () => { + await h.cancelOracleRequest(oc.connect(roles.stranger), fakeRequest) + }) + }) + }) + + describe('with a pending request', () => { + const startingBalance = 100 + let request: ReturnType + let receipt: ethers.providers.TransactionReceipt + + beforeEach(async () => { + const requestAmount = 20 + + await link.transfer(roles.consumer.address, startingBalance) + + const args = h.requestDataBytes( + specId, + roles.consumer.address, + fHash, + 1, + '0x0', + ) + const tx = await link + .connect(roles.consumer) + .transferAndCall(oc.address, requestAmount, args) + receipt = await tx.wait() + + assert.equal(3, receipt.logs!.length) + request = h.decodeRunRequest(receipt.logs![2]) + }) + + it('has correct initial balances', async () => { + const oracleBalance = await link.balanceOf(oc.address) + assertBigNum(request.payment, oracleBalance) + + const consumerAmount = await link.balanceOf(roles.consumer.address) + assert.equal( + startingBalance - Number(request.payment), + consumerAmount.toNumber(), + ) + }) + + describe('from a stranger', () => { + it('fails', async () => { + await h.assertActionThrows(async () => { + await h.cancelOracleRequest(oc.connect(roles.consumer), request) + }) + }) + }) + + describe('from the requester', () => { + it('refunds the correct amount', async () => { + await h.increaseTime5Minutes(provider) + await h.cancelOracleRequest(oc.connect(roles.consumer), request) + const balance = await link.balanceOf(roles.consumer.address) + assert.equal(startingBalance, balance.toNumber()) // 100 + }) + + it('triggers a cancellation event', async () => { + await h.increaseTime5Minutes(provider) + const tx = await h.cancelOracleRequest( + oc.connect(roles.consumer), + request, + ) + const receipt = await tx.wait() + + assert.equal(receipt.logs!.length, 2) + assert.equal(request.id, receipt.logs![0].topics[1]) + }) + + it('fails when called twice', async () => { + await h.increaseTime5Minutes(provider) + await h.cancelOracleRequest(oc.connect(roles.consumer), request) + + await h.assertActionThrows(async () => { + await h.cancelOracleRequest(oc.connect(roles.consumer), request) + }) + }) + }) + }) + }) +}) diff --git a/evm/testv2/Pointer.test.ts b/evm/testv2/Pointer.test.ts index a6e7759f4d2..223633a54f7 100644 --- a/evm/testv2/Pointer.test.ts +++ b/evm/testv2/Pointer.test.ts @@ -3,12 +3,12 @@ import { assert } from 'chai' import { PointerFactory } from '../src/generated/PointerFactory' import { LinkTokenFactory } from '../src/generated/LinkTokenFactory' import { Instance } from '../src/contract' -import env from '@nomiclabs/buidler' -import { EthersProviderWrapper } from '../src/provider' +import ganache from 'ganache-core' +import { ethers } from 'ethers' const pointerFactory = new PointerFactory() const linkTokenFactory = new LinkTokenFactory() -const provider = new EthersProviderWrapper(env.ethereum) +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) let roles: h.Roles @@ -21,14 +21,17 @@ beforeAll(async () => { describe('Pointer', () => { let contract: Instance let link: Instance - - beforeEach(async () => { + const deployment = h.useSnapshot(provider, async () => { link = await linkTokenFactory.connect(roles.defaultAccount).deploy() contract = await pointerFactory .connect(roles.defaultAccount) .deploy(link.address) }) + beforeEach(async () => { + await deployment() + }) + it('has a limited public interface', () => { h.checkPublicABI(contract, ['getAddress']) }) diff --git a/evm/testv2/SignedSafeMath.test.ts b/evm/testv2/SignedSafeMath.test.ts index 0a78bc7d652..92b40898b45 100644 --- a/evm/testv2/SignedSafeMath.test.ts +++ b/evm/testv2/SignedSafeMath.test.ts @@ -2,13 +2,12 @@ import * as h from '../src/helpersV2' import { assertBigNum } from '../src/matchersV2' import { ethers } from 'ethers' import { createFundedWallet } from '../src/wallet' -import { EthersProviderWrapper } from '../src/provider' import { ConcreteSignedSafeMathFactory } from '../src/generated/ConcreteSignedSafeMathFactory' import { Instance } from '../src/contract' -import env from '@nomiclabs/buidler' +import ganache from 'ganache-core' const concreteSignedSafeMathFactory = new ConcreteSignedSafeMathFactory() -const provider = new EthersProviderWrapper(env.ethereum) +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) let defaultAccount: ethers.Wallet @@ -21,17 +20,19 @@ describe('SignedSafeMath', () => { // a version of the adder contract where we make all ABI exposed functions constant // TODO: submit upstream PR to support constant contract type generation let adder: Instance - - let response + let response: ethers.utils.BigNumber const INT256_MAX = ethers.utils.bigNumberify( '57896044618658097711785492504343953926634992332820282019728792003956564819967', ) const INT256_MIN = ethers.utils.bigNumberify( '-57896044618658097711785492504343953926634992332820282019728792003956564819968', ) + const deployment = h.useSnapshot(provider, async () => { + adder = await concreteSignedSafeMathFactory.connect(defaultAccount).deploy() + }) beforeEach(async () => { - adder = await concreteSignedSafeMathFactory.connect(defaultAccount).deploy() + await deployment() }) describe('#add', () => { diff --git a/evm/testv2/UpdatableConsumer.test.ts b/evm/testv2/UpdatableConsumer.test.ts index 8c9ab691cc4..d736d253f2b 100644 --- a/evm/testv2/UpdatableConsumer.test.ts +++ b/evm/testv2/UpdatableConsumer.test.ts @@ -8,8 +8,7 @@ import { assert } from 'chai' import { LinkTokenFactory } from '../src/generated/LinkTokenFactory' import { OracleFactory } from '../src/generated/OracleFactory' import { Instance } from '../src/contract' -import env from '@nomiclabs/buidler' -import { EthersProviderWrapper } from '../src/provider' +import ganache from 'ganache-core' const linkTokenFactory = new LinkTokenFactory() const ensRegistryFactory = new ENSRegistryFactory() @@ -17,7 +16,7 @@ const oracleFactory = new OracleFactory() const publicResolverFacotory = new PublicResolverFactory() const updatableConsumerFactory = new UpdatableConsumerFactory() -const provider = new EthersProviderWrapper(env.ethereum) +const provider = new ethers.providers.Web3Provider(ganache.provider() as any) let roles: h.Roles @@ -50,8 +49,7 @@ describe('UpdatableConsumer', () => { let link: Instance let oc: Instance let uc: Instance - - beforeEach(async () => { + const deployment = h.useSnapshot(provider, async () => { link = await linkTokenFactory.connect(roles.defaultAccount).deploy() oc = await oracleFactory.connect(roles.oracleNode).deploy(link.address) ens = await ensRegistryFactory.connect(roles.defaultAccount).deploy() @@ -102,6 +100,10 @@ describe('UpdatableConsumer', () => { .deploy(specId, ens.address, domainNode) }) + beforeEach(async () => { + await deployment() + }) + describe('constructor', () => { it('pulls the token contract address from the resolver', async () => { assert.equal(link.address, await uc.getChainlinkToken()) diff --git a/evm/truffle.js b/evm/truffle.js index 4aa236aad09..d138e780a9f 100644 --- a/evm/truffle.js +++ b/evm/truffle.js @@ -1,4 +1,5 @@ module.exports = { + contracts_build_directory: 'dist/artifacts', compilers: { solc: { version: '0.4.24', diff --git a/evm/tsconfig.json b/evm/tsconfig.json index eea5e1d9343..76e5591d014 100644 --- a/evm/tsconfig.json +++ b/evm/tsconfig.json @@ -3,12 +3,10 @@ "incremental": true, "target": "es2019", "module": "commonjs", - "allowJs": true, "declaration": true, "sourceMap": true, "skipLibCheck": true, "outDir": "./dist", - "noEmitOnError": false, "resolveJsonModule": true, "strict": true, "noUnusedLocals": true, @@ -18,5 +16,5 @@ "moduleResolution": "node", "esModuleInterop": true }, - "include": ["src", "@types", "test", "testv2"] + "include": ["src", "testv2"] } diff --git a/evm/v0.5/.gitignore b/evm/v0.5/.gitignore index d6838e4effe..25a7ff8dacd 100644 --- a/evm/v0.5/.gitignore +++ b/evm/v0.5/.gitignore @@ -1,3 +1,4 @@ node_modules/ build/ !contracts/vendor +src/generated diff --git a/evm/v0.5/compiler.json b/evm/v0.5/compiler.json new file mode 100644 index 00000000000..e07fe488b0c --- /dev/null +++ b/evm/v0.5/compiler.json @@ -0,0 +1,20 @@ +{ + "contractsDir": "contracts", + "artifactsDir": "dist/artifacts", + "contracts": "*", + "solcVersion": "0.5.0", + "useDockerisedSolc": false, + "compilerSettings": { + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + } + } +} diff --git a/evm/v0.5/contracts/dev/Coordinator.sol b/evm/v0.5/contracts/dev/Coordinator.sol index d83fefaa803..28345ff37e2 100644 --- a/evm/v0.5/contracts/dev/Coordinator.sol +++ b/evm/v0.5/contracts/dev/Coordinator.sol @@ -230,12 +230,14 @@ contract Coordinator is ChainlinkRequestInterface, CoordinatorInterface { sA.aggFulfillSelector, _requestId, callback.sAId, msg.sender, _data)); require(ok, "aggregator.fulfill failed"); require(aggResponse.length > 0, "probably wrong address/selector"); - (bool aggSuccess, bool aggComplete, bytes memory response) = abi.decode( - aggResponse, (bool, bool, bytes)); + (bool aggSuccess, bool aggComplete, bytes memory response, int256[] memory paymentAmounts) = abi.decode( // solhint-disable-line + aggResponse, (bool, bool, bytes, int256[])); require(aggSuccess, string(response)); if (aggComplete) { + require(paymentAmounts.length == sA.oracles.length, "wrong paymentAmounts.length"); for (uint256 oIdx = 0; oIdx < sA.oracles.length; oIdx++) { // pay oracles - withdrawableTokens[sA.oracles[oIdx]] += sA.payment / sA.oracles.length; + withdrawableTokens[sA.oracles[oIdx]] = uint256(int256( + withdrawableTokens[sA.oracles[oIdx]]) + paymentAmounts[oIdx]); } // solhint-disable-next-line avoid-low-level-calls (bool success,) = callback.addr.call(abi.encodeWithSelector( // report final result callback.functionId, _requestId, abi.decode(response, (bytes32)))); diff --git a/evm/v0.5/contracts/tests/EmptyAggregator.sol b/evm/v0.5/contracts/tests/EmptyAggregator.sol index 4351fa20245..77f6d7b2a44 100644 --- a/evm/v0.5/contracts/tests/EmptyAggregator.sol +++ b/evm/v0.5/contracts/tests/EmptyAggregator.sol @@ -25,7 +25,8 @@ contract EmptyAggregator { function fulfill(bytes32 _requestId, bytes32 _saId, address _oracle, bytes32 _fulfillment) - public returns (bool success, bool complete, bytes memory response) { + public returns (bool success, bool complete, bytes memory response, + int256[] memory paymentAmounts) { success = true; complete = true; response = abi.encode(_fulfillment); diff --git a/evm/v0.5/contracts/tests/MeanAggregator.sol b/evm/v0.5/contracts/tests/MeanAggregator.sol index f993eeac6ed..9ecc722fc68 100644 --- a/evm/v0.5/contracts/tests/MeanAggregator.sol +++ b/evm/v0.5/contracts/tests/MeanAggregator.sol @@ -9,10 +9,10 @@ contract MeanAggregator { // Relies on Coordinator's authorization of the oracles (no need to track // oracle authorization in this contract.) - mapping(bytes32 /* service agreement ID */ => uint256) numOracles; - mapping(bytes32 /* request ID */ => mapping(address /* oracle */ => bool)) - reported; + mapping(bytes32 /* service agreement ID */ => uint256) payment; + mapping(bytes32 /* service agreement ID */ => address[]) oracles; mapping(bytes32 /* request ID */ => uint256) numberReported; + mapping(bytes32 /* request ID */ => mapping(address => uint256)) reportingOrder; // Current total for given request, divided by number of oracles reporting mapping(bytes32 /* request ID */ => uint256) average; @@ -22,35 +22,52 @@ contract MeanAggregator { function initiateJob( bytes32 _sAId, CoordinatorInterface.ServiceAgreement memory _sa) public returns (bool success, bytes memory message) { - if (numOracles[_sAId] != 0) { + if (oracles[_sAId].length != 0) { return (false, bytes("job already initiated")); } if (_sa.oracles.length == 0) { return (false, bytes("must depend on at least one oracle")); } - numOracles[_sAId] = _sa.oracles.length; + oracles[_sAId] = _sa.oracles; + payment[_sAId] = _sa.payment; success = true; } function fulfill(bytes32 _requestId, bytes32 _sAId, address _oracle, bytes32 _value) public - returns (bool success, bool complete, bytes memory response) { - if (reported[_requestId][_oracle]) { - return (false, false, "oracle already reported"); + returns (bool success, bool complete, bytes memory response, + int256[] memory paymentAmounts) { + if (reportingOrder[_requestId][_oracle] != 0 || + numberReported[_requestId] == oracles[_sAId].length) { + return (false, false, "oracle already reported", paymentAmounts); } - uint256 oDividend = uint256(_value) / numOracles[_sAId]; - uint256 oRemainder = uint256(_value) % numOracles[_sAId]; + uint256 oDividend = uint256(_value) / oracles[_sAId].length; + uint256 oRemainder = uint256(_value) % oracles[_sAId].length; uint256 newRemainder = remainder[_requestId] + oRemainder; - uint256 newAverage = average[_requestId] + oDividend + (newRemainder / numOracles[_sAId]); + uint256 newAverage = average[_requestId] + oDividend + (newRemainder / oracles[_sAId].length); assert(newAverage >= average[_requestId]); // No overflow average[_requestId] = newAverage; - remainder[_requestId] = newRemainder % numOracles[_sAId]; + remainder[_requestId] = newRemainder % oracles[_sAId].length; numberReported[_requestId] += 1; + reportingOrder[_requestId][_oracle] = numberReported[_requestId]; success = true; - reported[_requestId][_oracle] = true; - complete = (numberReported[_requestId] == numOracles[_sAId]); + complete = (numberReported[_requestId] == oracles[_sAId].length); if (complete) { response = abi.encode(average[_requestId]); + paymentAmounts = calculatePayments(_sAId, _requestId); } } + + function calculatePayments(bytes32 _sAId, bytes32 _requestId) private returns (int256[] memory paymentAmounts) { + paymentAmounts = new int256[](oracles[_sAId].length); + uint256 numOracles = oracles[_sAId].length; + uint256 totalPayment = payment[_sAId]; + for (uint256 oIdx = 0; oIdx < oracles[_sAId].length; oIdx++) { + // Linearly-decaying payout determined by each oracle's reportingIdx + uint256 reportingIdx = reportingOrder[_requestId][oracles[_sAId][oIdx]] - 1; + paymentAmounts[oIdx] = int256(2*(totalPayment/numOracles) - ( + (totalPayment * ((2*reportingIdx) + 1)) / (numOracles**2))); + delete reportingOrder[_requestId][oracles[_sAId][oIdx]]; + } + } } diff --git a/evm/v0.5/package.json b/evm/v0.5/package.json index 67301cb1eff..749cb6aa428 100644 --- a/evm/v0.5/package.json +++ b/evm/v0.5/package.json @@ -1,40 +1,47 @@ { "name": "chainlinkv0.5", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", + "main": "./dist/src", + "typings": "./dist/src", "scripts": { - "build": "truffle build", - "build.windows": "truffle.cmd build", + "generate-typings": "typechain --target ethers --outDir src/generated \"{test/support/LinkToken*,dist/artifacts/*}\"", + "postgenerate-typings": "yarn workspace chainlink export-typings v0.5/src/generated v0.5/dist/src/generated", + "build:windows": "truffle.cmd build && yarn generate-typings && (tsc || true)", + "build": "sol-compiler && yarn generate-typings && (tsc || true)", "depcheck": "echo \"chainlinkv0.5\" && depcheck --ignore-dirs=build/contracts || true", "eslint": "eslint --ext .ts,.js test", "solhint": "solhint ./contracts/**/*.sol", "lint": "yarn eslint && yarn solhint", "format": "prettier --write \"{src,test}/**/*\"", - "slither": "truffle compile --quiet && slither .", + "slither": "truffle compile --quiet && slither . && rimraf dist/artifacts", "setup": "ts-node ./scripts/build", + "prepublishOnly": "yarn setup && yarn lint && yarn test", "test": "truffle test" }, "dependencies": { "bn.js": "^4.11.0", - "cbor": "^4.1.1", + "cbor": "^5.0.1", "ethereumjs-abi": "^0.6.8", "ethereumjs-util": "^6.1.0", - "truffle-contract": "^4.0.31" + "truffle-contract": "^4.0.31", + "typescript": "^3.7.0-beta" }, "devDependencies": { - "@babel/core": "^7.5.5", + "@babel/core": "^7.6.4", + "@0x/sol-compiler": "^3.1.15", "@babel/plugin-proposal-class-properties": "^7.3.0", "@babel/polyfill": "^7.2.5", "@babel/preset-env": "^7.6.0", "@babel/preset-typescript": "^7.1.0", - "@babel/register": "^7.0.0", + "@babel/register": "^7.6.2", "@chainlink/eslint-config": "0.0.1", "@chainlink/prettier-config": "0.0.1", "@machinomy/types-truffle-contract": "^0.2.0", "@types/cbor": "^2.0.0", "@types/ethereumjs-abi": "^0.6.3", "chai": "^4.2.0", - "cross-env": "^6.0.0", + "cross-env": "^6.0.3", "depcheck": "^0.8.3", "eslint": "^6.3.0", "execa": "^3.2.0", @@ -44,10 +51,13 @@ "solhint": "^2.1.0", "truffle": "^5.0.25", "ts-node": "^8.4.1", + "typechain": "^1.0.3", + "typechain-target-ethers": "^1.0.1", "types-bn": "^0.0.1" }, "files": [ - "contracts" + "contracts", + "dist" ], "prettier": "@chainlink/prettier-config" } diff --git a/evm/v0.5/slither.config.json b/evm/v0.5/slither.config.json index e24750c1d07..3cc46fdd845 100644 --- a/evm/v0.5/slither.config.json +++ b/evm/v0.5/slither.config.json @@ -1,5 +1,6 @@ { "detectors_to_exclude": "naming-convention,pragma,solc-version", + "truffle_build_directory": "dist/artifacts", "filter_paths": "dev,tests,vendor", "truffle_ignore_compile": true, "truffle_version": "truffle" diff --git a/evm/v0.5/src/index.ts b/evm/v0.5/src/index.ts new file mode 100644 index 00000000000..bda4ef3786d --- /dev/null +++ b/evm/v0.5/src/index.ts @@ -0,0 +1,3 @@ +import * as generated from './generated' + +export { generated } diff --git a/evm/v0.5/test/Coordinator_test.js b/evm/v0.5/test/Coordinator_test.js index e272c0d6942..f85034855e8 100644 --- a/evm/v0.5/test/Coordinator_test.js +++ b/evm/v0.5/test/Coordinator_test.js @@ -714,9 +714,15 @@ contract('Coordinator', () => { it('oracle balances are updated', async () => { // Given the 3 oracles from the SA, each should have the following balance after fulfillment - const expected = h.bigNum('333333333333333333') + const expected1 = h.bigNum('555555555555555555') + const expected2 = h.bigNum('333333333333333333') + const expected3 = h.bigNum('111111111111111111') const balance1 = await coordinator.withdrawableTokens.call(oracle1) - assertBigNum(expected, balance1) + const balance2 = await coordinator.withdrawableTokens.call(oracle2) + const balance3 = await coordinator.withdrawableTokens.call(oracle3) + assertBigNum(expected1, balance1) + assertBigNum(expected2, balance2) + assertBigNum(expected3, balance3) }) }) diff --git a/evm/v0.5/test/support/LinkToken.json b/evm/v0.5/test/support/LinkToken.json new file mode 100644 index 00000000000..45c4cb218ab --- /dev/null +++ b/evm/v0.5/test/support/LinkToken.json @@ -0,0 +1,163 @@ +{ + "bytecode": "0x6060604052341561000f57600080fd5b5b600160a060020a03331660009081526001602052604090206b033b2e3c9fd0803ce800000090555b5b610c51806100486000396000f300606060405236156100b75763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde0381146100bc578063095ea7b31461014757806318160ddd1461017d57806323b872dd146101a2578063313ce567146101de5780634000aea014610207578063661884631461028057806370a08231146102b657806395d89b41146102e7578063a9059cbb14610372578063d73dd623146103a8578063dd62ed3e146103de575b600080fd5b34156100c757600080fd5b6100cf610415565b60405160208082528190810183818151815260200191508051906020019080838360005b8381101561010c5780820151818401525b6020016100f3565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610169600160a060020a036004351660243561044c565b604051901515815260200160405180910390f35b341561018857600080fd5b610190610499565b60405190815260200160405180910390f35b34156101ad57600080fd5b610169600160a060020a03600435811690602435166044356104a9565b604051901515815260200160405180910390f35b34156101e957600080fd5b6101f16104f8565b60405160ff909116815260200160405180910390f35b341561021257600080fd5b61016960048035600160a060020a03169060248035919060649060443590810190830135806020601f820181900481020160405190810160405281815292919060208401838380828437509496506104fd95505050505050565b604051901515815260200160405180910390f35b341561028b57600080fd5b610169600160a060020a036004351660243561054c565b604051901515815260200160405180910390f35b34156102c157600080fd5b610190600160a060020a0360043516610648565b60405190815260200160405180910390f35b34156102f257600080fd5b6100cf610667565b60405160208082528190810183818151815260200191508051906020019080838360005b8381101561010c5780820151818401525b6020016100f3565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037d57600080fd5b610169600160a060020a036004351660243561069e565b604051901515815260200160405180910390f35b34156103b357600080fd5b610169600160a060020a03600435166024356106eb565b604051901515815260200160405180910390f35b34156103e957600080fd5b610190600160a060020a0360043581169060243516610790565b60405190815260200160405180910390f35b60408051908101604052600f81527f436861696e4c696e6b20546f6b656e0000000000000000000000000000000000602082015281565b600082600160a060020a03811615801590610479575030600160a060020a031681600160a060020a031614155b151561048457600080fd5b61048e84846107bd565b91505b5b5092915050565b6b033b2e3c9fd0803ce800000081565b600082600160a060020a038116158015906104d6575030600160a060020a031681600160a060020a031614155b15156104e157600080fd5b6104ec85858561082a565b91505b5b509392505050565b601281565b600083600160a060020a0381161580159061052a575030600160a060020a031681600160a060020a031614155b151561053557600080fd5b6104ec85858561093c565b91505b5b509392505050565b600160a060020a033381166000908152600260209081526040808320938616835292905290812054808311156105a957600160a060020a0333811660009081526002602090815260408083209388168352929052908120556105e0565b6105b9818463ffffffff610a2316565b600160a060020a033381166000908152600260209081526040808320938916835292905220555b600160a060020a0333811660008181526002602090815260408083209489168084529490915290819020547f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925915190815260200160405180910390a3600191505b5092915050565b600160a060020a0381166000908152600160205260409020545b919050565b60408051908101604052600481527f4c494e4b00000000000000000000000000000000000000000000000000000000602082015281565b600082600160a060020a038116158015906106cb575030600160a060020a031681600160a060020a031614155b15156106d657600080fd5b61048e8484610a3a565b91505b5b5092915050565b600160a060020a033381166000908152600260209081526040808320938616835292905290812054610723908363ffffffff610afa16565b600160a060020a0333811660008181526002602090815260408083209489168084529490915290819020849055919290917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591905190815260200160405180910390a35060015b92915050565b600160a060020a038083166000908152600260209081526040808320938516835292905220545b92915050565b600160a060020a03338116600081815260026020908152604080832094871680845294909152808220859055909291907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259085905190815260200160405180910390a35060015b92915050565b600160a060020a03808416600081815260026020908152604080832033909516835293815283822054928252600190529182205461086e908463ffffffff610a2316565b600160a060020a0380871660009081526001602052604080822093909355908616815220546108a3908463ffffffff610afa16565b600160a060020a0385166000908152600160205260409020556108cc818463ffffffff610a2316565b600160a060020a03808716600081815260026020908152604080832033861684529091529081902093909355908616917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9086905190815260200160405180910390a3600191505b509392505050565b60006109488484610a3a565b5083600160a060020a031633600160a060020a03167fe19260aff97b920c7df27010903aeb9c8d2be5d310a2c67824cf3f15396e4c16858560405182815260406020820181815290820183818151815260200191508051906020019080838360005b838110156109c35780820151818401525b6020016109aa565b50505050905090810190601f1680156109f05780820380516001836020036101000a031916815260200191505b50935050505060405180910390a3610a0784610b14565b15610a1757610a17848484610b23565b5b5060015b9392505050565b600082821115610a2f57fe5b508082035b92915050565b600160a060020a033316600090815260016020526040812054610a63908363ffffffff610a2316565b600160a060020a033381166000908152600160205260408082209390935590851681522054610a98908363ffffffff610afa16565b600160a060020a0380851660008181526001602052604090819020939093559133909116907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9085905190815260200160405180910390a35060015b92915050565b600082820183811015610b0957fe5b8091505b5092915050565b6000813b908111905b50919050565b82600160a060020a03811663a4c0ed363385856040518463ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018084600160a060020a0316600160a060020a0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610bbd5780820151818401525b602001610ba4565b50505050905090810190601f168015610bea5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b1515610c0a57600080fd5b6102c65a03f11515610c1b57600080fd5b5050505b505050505600a165627a7a72305820c5f438ff94e5ddaf2058efa0019e246c636c37a622e04bb67827c7374acad8d60029", + "abi": [ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{ "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_spender", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_from", "type": "address" }, + { "name": "_to", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{ "name": "", "type": "uint8" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_to", "type": "address" }, + { "name": "_value", "type": "uint256" }, + { "name": "_data", "type": "bytes" } + ], + "name": "transferAndCall", + "outputs": [{ "name": "success", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_spender", "type": "address" }, + { "name": "_subtractedValue", "type": "uint256" } + ], + "name": "decreaseApproval", + "outputs": [{ "name": "success", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "_owner", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "name": "balance", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{ "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_to", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "name": "success", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "_spender", "type": "address" }, + { "name": "_addedValue", "type": "uint256" } + ], + "name": "increaseApproval", + "outputs": [{ "name": "success", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "_owner", "type": "address" }, + { "name": "_spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "name": "remaining", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "from", "type": "address" }, + { "indexed": true, "name": "to", "type": "address" }, + { "indexed": false, "name": "value", "type": "uint256" }, + { "indexed": false, "name": "data", "type": "bytes" } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "owner", "type": "address" }, + { "indexed": true, "name": "spender", "type": "address" }, + { "indexed": false, "name": "value", "type": "uint256" } + ], + "name": "Approval", + "type": "event" + } + ] +} diff --git a/evm/v0.5/test/support/helpers.ts b/evm/v0.5/test/support/helpers.ts index 79719b63a0b..7d0d6bb55a9 100644 --- a/evm/v0.5/test/support/helpers.ts +++ b/evm/v0.5/test/support/helpers.ts @@ -4,7 +4,7 @@ import * as abi from 'ethereumjs-abi' import * as util from 'ethereumjs-util' import { FunctionFragment, ParamType } from 'ethers/utils/abi-coder' import TruffleContract from 'truffle-contract' -import { linkToken } from './linkToken' +import linkToken from './LinkToken.json' import { assertBigNum } from './matchers' // https://github.com/ethereum/web3.js/issues/1119#issuecomment-394217563 @@ -209,7 +209,10 @@ export const assertActionThrows = (action: any, messageContains?: RegExp) => return error.message }) .then(errorMessage => { - assert(errorMessage, 'Expected an error to be raised') + assert( + errorMessage && errorMessage.includes, + 'Expected an error to be raised', + ) const invalidOpcode = errorMessage.includes('invalid opcode') const reverted = errorMessage.includes( 'VM Exception while processing transaction: revert', diff --git a/evm/v0.5/test/support/linkToken.ts b/evm/v0.5/test/support/linkToken.ts deleted file mode 100644 index 3ae3a5af6d9..00000000000 --- a/evm/v0.5/test/support/linkToken.ts +++ /dev/null @@ -1,165 +0,0 @@ -export const linkToken = { - // bytecode from verification on https://etherscan.io/address/0x514910771af9ca656af840dff83e8264ecf986ca#contracts - bytecode: - '0x6060604052341561000f57600080fd5b5b600160a060020a03331660009081526001602052604090206b033b2e3c9fd0803ce800000090555b5b610c51806100486000396000f300606060405236156100b75763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde0381146100bc578063095ea7b31461014757806318160ddd1461017d57806323b872dd146101a2578063313ce567146101de5780634000aea014610207578063661884631461028057806370a08231146102b657806395d89b41146102e7578063a9059cbb14610372578063d73dd623146103a8578063dd62ed3e146103de575b600080fd5b34156100c757600080fd5b6100cf610415565b60405160208082528190810183818151815260200191508051906020019080838360005b8381101561010c5780820151818401525b6020016100f3565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610169600160a060020a036004351660243561044c565b604051901515815260200160405180910390f35b341561018857600080fd5b610190610499565b60405190815260200160405180910390f35b34156101ad57600080fd5b610169600160a060020a03600435811690602435166044356104a9565b604051901515815260200160405180910390f35b34156101e957600080fd5b6101f16104f8565b60405160ff909116815260200160405180910390f35b341561021257600080fd5b61016960048035600160a060020a03169060248035919060649060443590810190830135806020601f820181900481020160405190810160405281815292919060208401838380828437509496506104fd95505050505050565b604051901515815260200160405180910390f35b341561028b57600080fd5b610169600160a060020a036004351660243561054c565b604051901515815260200160405180910390f35b34156102c157600080fd5b610190600160a060020a0360043516610648565b60405190815260200160405180910390f35b34156102f257600080fd5b6100cf610667565b60405160208082528190810183818151815260200191508051906020019080838360005b8381101561010c5780820151818401525b6020016100f3565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037d57600080fd5b610169600160a060020a036004351660243561069e565b604051901515815260200160405180910390f35b34156103b357600080fd5b610169600160a060020a03600435166024356106eb565b604051901515815260200160405180910390f35b34156103e957600080fd5b610190600160a060020a0360043581169060243516610790565b60405190815260200160405180910390f35b60408051908101604052600f81527f436861696e4c696e6b20546f6b656e0000000000000000000000000000000000602082015281565b600082600160a060020a03811615801590610479575030600160a060020a031681600160a060020a031614155b151561048457600080fd5b61048e84846107bd565b91505b5b5092915050565b6b033b2e3c9fd0803ce800000081565b600082600160a060020a038116158015906104d6575030600160a060020a031681600160a060020a031614155b15156104e157600080fd5b6104ec85858561082a565b91505b5b509392505050565b601281565b600083600160a060020a0381161580159061052a575030600160a060020a031681600160a060020a031614155b151561053557600080fd5b6104ec85858561093c565b91505b5b509392505050565b600160a060020a033381166000908152600260209081526040808320938616835292905290812054808311156105a957600160a060020a0333811660009081526002602090815260408083209388168352929052908120556105e0565b6105b9818463ffffffff610a2316565b600160a060020a033381166000908152600260209081526040808320938916835292905220555b600160a060020a0333811660008181526002602090815260408083209489168084529490915290819020547f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925915190815260200160405180910390a3600191505b5092915050565b600160a060020a0381166000908152600160205260409020545b919050565b60408051908101604052600481527f4c494e4b00000000000000000000000000000000000000000000000000000000602082015281565b600082600160a060020a038116158015906106cb575030600160a060020a031681600160a060020a031614155b15156106d657600080fd5b61048e8484610a3a565b91505b5b5092915050565b600160a060020a033381166000908152600260209081526040808320938616835292905290812054610723908363ffffffff610afa16565b600160a060020a0333811660008181526002602090815260408083209489168084529490915290819020849055919290917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591905190815260200160405180910390a35060015b92915050565b600160a060020a038083166000908152600260209081526040808320938516835292905220545b92915050565b600160a060020a03338116600081815260026020908152604080832094871680845294909152808220859055909291907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259085905190815260200160405180910390a35060015b92915050565b600160a060020a03808416600081815260026020908152604080832033909516835293815283822054928252600190529182205461086e908463ffffffff610a2316565b600160a060020a0380871660009081526001602052604080822093909355908616815220546108a3908463ffffffff610afa16565b600160a060020a0385166000908152600160205260409020556108cc818463ffffffff610a2316565b600160a060020a03808716600081815260026020908152604080832033861684529091529081902093909355908616917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9086905190815260200160405180910390a3600191505b509392505050565b60006109488484610a3a565b5083600160a060020a031633600160a060020a03167fe19260aff97b920c7df27010903aeb9c8d2be5d310a2c67824cf3f15396e4c16858560405182815260406020820181815290820183818151815260200191508051906020019080838360005b838110156109c35780820151818401525b6020016109aa565b50505050905090810190601f1680156109f05780820380516001836020036101000a031916815260200191505b50935050505060405180910390a3610a0784610b14565b15610a1757610a17848484610b23565b5b5060015b9392505050565b600082821115610a2f57fe5b508082035b92915050565b600160a060020a033316600090815260016020526040812054610a63908363ffffffff610a2316565b600160a060020a033381166000908152600160205260408082209390935590851681522054610a98908363ffffffff610afa16565b600160a060020a0380851660008181526001602052604090819020939093559133909116907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9085905190815260200160405180910390a35060015b92915050565b600082820183811015610b0957fe5b8091505b5092915050565b6000813b908111905b50919050565b82600160a060020a03811663a4c0ed363385856040518463ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018084600160a060020a0316600160a060020a0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610bbd5780820151818401525b602001610ba4565b50505050905090810190601f168015610bea5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b1515610c0a57600080fd5b6102c65a03f11515610c1b57600080fd5b5050505b505050505600a165627a7a72305820c5f438ff94e5ddaf2058efa0019e246c636c37a622e04bb67827c7374acad8d60029', - abi: [ - { - constant: true, - inputs: [], - name: 'name', - outputs: [{ name: '', type: 'string' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { name: '_spender', type: 'address' }, - { name: '_value', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ name: '', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'totalSupply', - outputs: [{ name: '', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { name: '_from', type: 'address' }, - { name: '_to', type: 'address' }, - { name: '_value', type: 'uint256' }, - ], - name: 'transferFrom', - outputs: [{ name: '', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'decimals', - outputs: [{ name: '', type: 'uint8' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { name: '_to', type: 'address' }, - { name: '_value', type: 'uint256' }, - { name: '_data', type: 'bytes' }, - ], - name: 'transferAndCall', - outputs: [{ name: 'success', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { name: '_spender', type: 'address' }, - { name: '_subtractedValue', type: 'uint256' }, - ], - name: 'decreaseApproval', - outputs: [{ name: 'success', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [{ name: '_owner', type: 'address' }], - name: 'balanceOf', - outputs: [{ name: 'balance', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'symbol', - outputs: [{ name: '', type: 'string' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { name: '_to', type: 'address' }, - { name: '_value', type: 'uint256' }, - ], - name: 'transfer', - outputs: [{ name: 'success', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { name: '_spender', type: 'address' }, - { name: '_addedValue', type: 'uint256' }, - ], - name: 'increaseApproval', - outputs: [{ name: 'success', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [ - { name: '_owner', type: 'address' }, - { name: '_spender', type: 'address' }, - ], - name: 'allowance', - outputs: [{ name: 'remaining', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - anonymous: false, - inputs: [ - { indexed: true, name: 'from', type: 'address' }, - { indexed: true, name: 'to', type: 'address' }, - { indexed: false, name: 'value', type: 'uint256' }, - { indexed: false, name: 'data', type: 'bytes' }, - ], - name: 'Transfer', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, name: 'owner', type: 'address' }, - { indexed: true, name: 'spender', type: 'address' }, - { indexed: false, name: 'value', type: 'uint256' }, - ], - name: 'Approval', - type: 'event', - }, - ], -} diff --git a/evm/v0.5/truffle.js b/evm/v0.5/truffle.js index 11dae0e66dc..e98767d6e26 100644 --- a/evm/v0.5/truffle.js +++ b/evm/v0.5/truffle.js @@ -4,6 +4,7 @@ require('@babel/register')({ require('@babel/polyfill') module.exports = { + contracts_build_directory: 'dist/artifacts', compilers: { solc: { version: '0.5.0', diff --git a/evm/v0.5/tsconfig.json b/evm/v0.5/tsconfig.json new file mode 100644 index 00000000000..0a3d149eea8 --- /dev/null +++ b/evm/v0.5/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "incremental": true, + "target": "es2019", + "module": "commonjs", + "allowJs": true, + "declaration": true, + "declarationDir": "./dist", + "sourceMap": true, + "skipLibCheck": true, + "outDir": "./dist", + "noEmitOnError": false, + "resolveJsonModule": true, + "moduleResolution": "node", + "esModuleInterop": true, + "typeRoots": ["src/generated"] + }, + "include": ["test", "src"] +} diff --git a/examples/echo_server/.eslintignore b/examples/echo_server/.eslintignore deleted file mode 100644 index 2dc187a55fb..00000000000 --- a/examples/echo_server/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -contracts -node_modules \ No newline at end of file diff --git a/examples/echo_server/.eslintrc.js b/examples/echo_server/.eslintrc.js deleted file mode 100644 index d8c6c7ba920..00000000000 --- a/examples/echo_server/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - extends: [ - '@chainlink/eslint-config/node', - '@chainlink/eslint-config/truffle', - ], -} diff --git a/examples/echo_server/.prettierignore b/examples/echo_server/.prettierignore deleted file mode 100644 index ff685c12273..00000000000 --- a/examples/echo_server/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -.prettierignore -.gitignore -.eslintignore -**/*.sol -screenshot.jpg -create_ethlog_job \ No newline at end of file diff --git a/examples/echo_server/README.md b/examples/echo_server/README.md deleted file mode 100644 index 229003ed0d3..00000000000 --- a/examples/echo_server/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Echo Server - -Using Chainlink (CL), this application simply echos incoming ethereum logs -as JSON, listened to by a CL job. It is intended to demonstrate the the first -step to bridging on chain to off chain activity. - -![Log Echo Server](screenshot.jpg?raw=true 'Log Echo Server') - -## Configure and run [Chainlink development environment](../README.md) - -## Run EthLog (Raw Ethereum Logs) - -Uses an `ethlog` initiator to echo all log events. An `ethlog` initiator starts -a job anytime a log event occurs. It can optionally be filtered by an `address`. - -1. Complete the [Run Chainlink Development Environment](../README.md#run-chainlink-development-environment) steps. -2. `./create_ethlog_job` to create the Chainlink (CL) job -3. `yarn install` -4. `node echo.js` -5. `yarn truffle migrate` in another window -6. `node send_ethlog_transaction.js` -7. Wait for log to show up in echo server - -## Run RunLog (Chainlink Specific Ethereum Logs) - -Uses a `runlog` initiator to echo Chainlink log events with the matching job id. - -1. Complete the [Run Chainlink Development Environment](../README.md#run-chainlink-development-environment) steps. -2. `yarn install` -3. `node echo.js` -4. `yarn truffle migrate` in another window -5. `node send_runlog_transaction.js` -6. Wait for log to show up in echo server -7. Investigate migrations/5_run_log.js for insight - -## Further Reading - -Please see the other examples in the repo, and take care to -identify the difference between an `HttpPost` task and a bridge to an external -adapter. The latter allows an asynchronous response from a service to -then potentially write back to the chain. - -## Development - -To run the tests, call `./node_modules/.bin/truffle --network=test test` diff --git a/examples/echo_server/broadcast_logs_job.json b/examples/echo_server/broadcast_logs_job.json deleted file mode 100644 index 2a15e3e50cc..00000000000 --- a/examples/echo_server/broadcast_logs_job.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "_comment": "An ethlog with no address listens to all addresses.", - "initiators": [{ "type": "ethlog" }], - "tasks": [ - { "type": "HttpPost", "params": { "post": "http://localhost:6690" } } - ] -} diff --git a/examples/echo_server/clmigration.js b/examples/echo_server/clmigration.js deleted file mode 100644 index f96d66b5703..00000000000 --- a/examples/echo_server/clmigration.js +++ /dev/null @@ -1,19 +0,0 @@ -// clmigration provides two key helpers for Chainlink development: -// 1. wraps migrations that to be skipped in the test environment, since we -// recreate every contract beforeEach test, and hit other APIs in our migration process. -// 2. Prepare plumbing for correct async/await behavior, -// in spite of https://github.com/trufflesuite/truffle/issues/501 - -module.exports = function(callback) { - return function(deployer, network) { - if (network === 'test') { - console.log('===== SKIPPING MIGRATIONS IN TEST ENVIRONMENT =====') - } else { - deployer - .then(async () => { - return callback(deployer, network) - }) - .catch(console.log) - } - } -} diff --git a/examples/echo_server/contracts/EthLog.sol b/examples/echo_server/contracts/EthLog.sol deleted file mode 100644 index 929383b58f8..00000000000 --- a/examples/echo_server/contracts/EthLog.sol +++ /dev/null @@ -1,9 +0,0 @@ -pragma solidity 0.4.24; - -contract EthLog { - event LogEvent(bytes32 indexed jobId); - - function logEvent() public { - emit LogEvent("hello_chainlink"); - } -} diff --git a/examples/echo_server/contracts/LinkToken.sol b/examples/echo_server/contracts/LinkToken.sol deleted file mode 100644 index e67ad4037c4..00000000000 --- a/examples/echo_server/contracts/LinkToken.sol +++ /dev/null @@ -1,3 +0,0 @@ -pragma solidity 0.4.24; - -import "link_token/contracts/LinkToken.sol"; diff --git a/examples/echo_server/contracts/Migrations.sol b/examples/echo_server/contracts/Migrations.sol deleted file mode 100644 index cffe8b95161..00000000000 --- a/examples/echo_server/contracts/Migrations.sol +++ /dev/null @@ -1,23 +0,0 @@ -pragma solidity 0.4.24; - -contract Migrations { - address public owner; - uint public last_completed_migration; - - modifier restricted() { - if (msg.sender == owner) _; - } - - constructor() public { - owner = msg.sender; - } - - function setCompleted(uint completed) public restricted { - last_completed_migration = completed; - } - - function upgrade(address new_address) public restricted { - Migrations upgraded = Migrations(new_address); - upgraded.setCompleted(last_completed_migration); - } -} diff --git a/examples/echo_server/contracts/Oracle.sol b/examples/echo_server/contracts/Oracle.sol deleted file mode 100644 index 8b00e54e486..00000000000 --- a/examples/echo_server/contracts/Oracle.sol +++ /dev/null @@ -1,3 +0,0 @@ -pragma solidity 0.4.24; - -import "chainlink/contracts/Oracle.sol"; \ No newline at end of file diff --git a/examples/echo_server/contracts/RunLog.sol b/examples/echo_server/contracts/RunLog.sol deleted file mode 100644 index 7799dfda75c..00000000000 --- a/examples/echo_server/contracts/RunLog.sol +++ /dev/null @@ -1,27 +0,0 @@ -pragma solidity 0.4.24; - -import "chainlink/contracts/Chainlinked.sol"; - -contract RunLog is Chainlinked { - uint256 constant private ORACLE_PAYMENT = 1 * LINK; - - bytes32 private jobId; - - constructor(address _link, address _oracle, bytes32 _jobId) public { - setLinkToken(_link); - setOracle(_oracle); - jobId = _jobId; - } - - function request() public { - Chainlink.Request memory req = newRequest(jobId, this, this.fulfill.selector); - req.add("msg", "hello_chainlink"); - chainlinkRequest(req, ORACLE_PAYMENT); - } - - function fulfill(bytes32 _externalId, bytes32 _data) - public - recordChainlinkFulfillment(_externalId) - {} // solhint-disable-line no-empty-blocks - -} diff --git a/examples/echo_server/create_ethlog_job b/examples/echo_server/create_ethlog_job deleted file mode 100755 index b6279c3bb3f..00000000000 --- a/examples/echo_server/create_ethlog_job +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -mkdir tmp - -set -e - -curl -c tmp/cookiefile \ - -d '{"email":"notreal@fakeemail.ch", "password":"twochains"}' \ - -X POST -H 'Content-Type: application/json' \ - http://localhost:6688/sessions \ - >/dev/null - -curl -sS -X POST -H 'Content-Type: application/json' \ - -b tmp/cookiefile \ - -d @./broadcast_logs_job.json http://localhost:6688/v2/specs - diff --git a/examples/echo_server/echo.js b/examples/echo_server/echo.js deleted file mode 100755 index de1fd9bddd0..00000000000 --- a/examples/echo_server/echo.js +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-var-requires */ - -let echoes = 0 -const express = require('express') -const bodyParser = require('body-parser') -const app = express() -let PORT = process.argv[2] -app.use(bodyParser.json()) - -app.get('/count', function(req, res) { - res.json(echoes) -}) - -app.all('*', function(req, res) { - echoes += 1 - console.log({ headers: req.headers, body: req.body }) - res.json({ headers: req.headers, body: req.body }) -}) - -if (isNaN(parseFloat(PORT))) { - console.log('defaulting to 6690') - PORT = 6690 -} - -app.listen(PORT, function() { - console.log('listening on port ' + PORT) -}) diff --git a/examples/echo_server/migrations/1_initial_migration.js b/examples/echo_server/migrations/1_initial_migration.js deleted file mode 100644 index a7a4fdbc824..00000000000 --- a/examples/echo_server/migrations/1_initial_migration.js +++ /dev/null @@ -1,5 +0,0 @@ -const Migrations = artifacts.require('Migrations') - -module.exports = function(deployer) { - deployer.deploy(Migrations) -} diff --git a/examples/echo_server/migrations/2_eth_log.js b/examples/echo_server/migrations/2_eth_log.js deleted file mode 100644 index dbe0c8e3938..00000000000 --- a/examples/echo_server/migrations/2_eth_log.js +++ /dev/null @@ -1,5 +0,0 @@ -const EthLog = artifacts.require('EthLog') - -module.exports = function(deployer) { - deployer.deploy(EthLog) -} diff --git a/examples/echo_server/migrations/3_link_token.js b/examples/echo_server/migrations/3_link_token.js deleted file mode 100644 index 703e5033e02..00000000000 --- a/examples/echo_server/migrations/3_link_token.js +++ /dev/null @@ -1,5 +0,0 @@ -const LinkToken = artifacts.require('LinkToken') - -module.exports = function(deployer) { - deployer.deploy(LinkToken) -} diff --git a/examples/echo_server/migrations/4_chainlink_oracle.js b/examples/echo_server/migrations/4_chainlink_oracle.js deleted file mode 100644 index 63f53fc82d6..00000000000 --- a/examples/echo_server/migrations/4_chainlink_oracle.js +++ /dev/null @@ -1,6 +0,0 @@ -const LinkToken = artifacts.require('LinkToken') -const Oracle = artifacts.require('Oracle') - -module.exports = function(deployer) { - deployer.deploy(Oracle, LinkToken.address) -} diff --git a/examples/echo_server/migrations/5_run_log.js b/examples/echo_server/migrations/5_run_log.js deleted file mode 100644 index 7b4ad89b7ca..00000000000 --- a/examples/echo_server/migrations/5_run_log.js +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -const clmigration = require('../clmigration.js') -const request = require('request-promise').defaults({ jar: true }) -const LinkToken = artifacts.require('LinkToken') -const Oracle = artifacts.require('Oracle') -const RunLog = artifacts.require('RunLog') - -const sessionsUrl = 'http://localhost:6688/sessions' -const specsUrl = 'http://localhost:6688/v2/specs' -const credentials = { email: 'notreal@fakeemail.ch', password: 'twochains' } -const job = { - _comment: - 'A runlog has a jobid baked into the contract so chainlink knows which job to run.', - initiators: [{ type: 'runlog' }], - tasks: [{ type: 'HttpPost', params: { url: 'http://localhost:6690' } }], -} - -module.exports = clmigration(async function(truffleDeployer) { - await request.post(sessionsUrl, { json: credentials }) - const body = await request.post(specsUrl, { json: job }) - console.log(`Deploying Consumer Contract with JobID ${body.data.id}`) - let jobid = body.data.id - if (jobid && jobid.slice(0, 2) != '0x') { - jobid = `0x${jobid}` // hack to prefix 0x to satisfy bytes32 requirement - } - await truffleDeployer.deploy(RunLog, LinkToken.address, Oracle.address, jobid) -}) diff --git a/examples/echo_server/migrations/6_transfer_link.js b/examples/echo_server/migrations/6_transfer_link.js deleted file mode 100644 index 046af0335b3..00000000000 --- a/examples/echo_server/migrations/6_transfer_link.js +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -const clmigration = require('../clmigration.js') -const LinkToken = artifacts.require('LinkToken') -const RunLog = artifacts.require('RunLog') -const devnetAddress = '0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f' - -module.exports = clmigration(function() { - LinkToken.deployed().then(async function(linkInstance) { - await RunLog.deployed() - .then(async function(runLogInstance) { - await linkInstance.transfer( - runLogInstance.address, - web3.utils.toWei('1000'), - ) - await linkInstance.transfer(devnetAddress, web3.utils.toWei('1000')) - }) - .catch(console.log) - }) -}) diff --git a/examples/echo_server/package.json b/examples/echo_server/package.json deleted file mode 100644 index 64e55debc8d..00000000000 --- a/examples/echo_server/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@chainlink/example-echo-server", - "version": "0.6.0", - "license": "MIT", - "private": true, - "scripts": { - "start": "node echo.js", - "depcheck": "echo \"@chainlink/example-echo-server\" && depcheck --ignore-dirs=contracts || true", - "eslint": "eslint --ext .js,.ts .", - "solhint": "solhint ./contracts/**/*.sol", - "lint": "yarn eslint && yarn solhint", - "format": "prettier --write \"**/*\"", - "setup": "echo \"No setup required for @chainlink/example-echo-server\"", - "test": "truffle test" - }, - "dependencies": { - "@babel/polyfill": "^7.2.5", - "@babel/register": "^7.0.0", - "body-parser": "^1.18.3", - "cbor": "^4.0.0", - "chainlink": "^0.6.5", - "express": "^4.16.2", - "link_token": "^1.0.6", - "request-promise": "^4.2.2", - "truffle": "^5.0.25", - "web3": "^1.0.0-beta.55" - }, - "devDependencies": { - "@chainlink/eslint-config": "0.0.1", - "@chainlink/prettier-config": "0.0.1", - "depcheck": "^0.8.3", - "eslint": "^6.3.0", - "prettier": "^1.18.2", - "solc": "0.4.24", - "solhint": "^2.1.0" - }, - "prettier": "@chainlink/prettier-config" -} diff --git a/examples/echo_server/screenshot.jpg b/examples/echo_server/screenshot.jpg deleted file mode 100644 index 7232ff3a5cf..00000000000 Binary files a/examples/echo_server/screenshot.jpg and /dev/null differ diff --git a/examples/echo_server/send_ethlog_transaction.js b/examples/echo_server/send_ethlog_transaction.js deleted file mode 100755 index 1cdb5371873..00000000000 --- a/examples/echo_server/send_ethlog_transaction.js +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-var-requires */ - -const Web3 = require('web3') -const path = require('path') -const EthLogJSON = require(path.join(__dirname, 'build/contracts/EthLog.json')) - -const provider = new Web3.providers.HttpProvider('http://localhost:18545') -const devnetAddress = '0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f' - -const web3 = new Web3(provider) -const networkId = Object.keys(EthLogJSON.networks)[0] -const wc3 = new web3.eth.Contract( - EthLogJSON.abi, - EthLogJSON.networks[networkId].address, -) - -wc3.methods.logEvent().send( - { - from: devnetAddress, - }, - (error, transactionHash) => { - if (error) { - console.error('encountered error: ', error, transactionHash) - } else { - console.log('sent tx: ', transactionHash) - } - }, -) diff --git a/examples/echo_server/send_runlog_transaction.js b/examples/echo_server/send_runlog_transaction.js deleted file mode 100755 index 0daa6435ada..00000000000 --- a/examples/echo_server/send_runlog_transaction.js +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-var-requires */ - -const Web3 = require('web3') -const path = require('path') -const RunLogJSON = require(path.join(__dirname, 'build/contracts/RunLog.json')) - -const provider = new Web3.providers.HttpProvider('http://localhost:18545') -const devnetAddress = '0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f' -const web3 = new Web3(provider) -const networkId = Object.keys(RunLogJSON.networks)[0] -const wc3 = new web3.eth.Contract( - RunLogJSON.abi, - RunLogJSON.networks[networkId].address, -) - -wc3.methods.request().send( - { - from: devnetAddress, - gas: 200000, - }, - (error, transactionHash) => { - if (error) { - console.error('encountered error: ', error, transactionHash) - } else { - console.log('sent tx: ', transactionHash) - } - }, -) diff --git a/examples/echo_server/test/RunLog_test.js b/examples/echo_server/test/RunLog_test.js deleted file mode 100644 index 0588eb0a966..00000000000 --- a/examples/echo_server/test/RunLog_test.js +++ /dev/null @@ -1,21 +0,0 @@ -const LinkToken = artifacts.require('LinkToken') -const Oracle = artifacts.require('Oracle') -const RunLog = artifacts.require('RunLog') - -contract('RunLog', () => { - const arbitraryJobID = - '0x0000000000000000000000000000000000000000000000000000000000000001' - let link, logger, oc - - beforeEach(async () => { - link = await LinkToken.new() - oc = await Oracle.new(link.address) - logger = await RunLog.new(link.address, oc.address, arbitraryJobID) - await link.transfer(logger.address, web3.utils.toWei('1')) - }) - - it('has a limited public interface', async () => { - const tx = await logger.request() - assert.equal(4, tx.receipt.rawLogs.length) - }) -}) diff --git a/examples/echo_server/truffle.js b/examples/echo_server/truffle.js deleted file mode 100644 index 96f3f191a6a..00000000000 --- a/examples/echo_server/truffle.js +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -require('@babel/register') -require('@babel/polyfill') - -module.exports = { - networks: { - cldev: { - host: '127.0.0.1', - port: 18545, - network_id: '*', - gas: 4700000, - }, - }, - compilers: { - solc: { - version: '0.4.24', - }, - }, -} diff --git a/examples/twilio_sms/.eslintignore b/examples/twilio_sms/.eslintignore deleted file mode 100644 index c6bf72cf137..00000000000 --- a/examples/twilio_sms/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -contracts \ No newline at end of file diff --git a/examples/twilio_sms/.eslintrc.js b/examples/twilio_sms/.eslintrc.js deleted file mode 100644 index d8c6c7ba920..00000000000 --- a/examples/twilio_sms/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - extends: [ - '@chainlink/eslint-config/node', - '@chainlink/eslint-config/truffle', - ], -} diff --git a/examples/twilio_sms/.prettierignore b/examples/twilio_sms/.prettierignore deleted file mode 100644 index 6d316116044..00000000000 --- a/examples/twilio_sms/.prettierignore +++ /dev/null @@ -1,5 +0,0 @@ -.prettierignore -.gitignore -**/*.sol -HitMeOnMyBeeper.jpg -.eslintignore diff --git a/examples/twilio_sms/HitMeOnMyBeeper.jpg b/examples/twilio_sms/HitMeOnMyBeeper.jpg deleted file mode 100644 index 876df977a66..00000000000 Binary files a/examples/twilio_sms/HitMeOnMyBeeper.jpg and /dev/null differ diff --git a/examples/twilio_sms/README.md b/examples/twilio_sms/README.md deleted file mode 100644 index f416e75b529..00000000000 --- a/examples/twilio_sms/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Make Money, Hit Me On My Beeper - -[![Hit Me On My Beeper](HitMeOnMyBeeper.jpg?raw=true 'Hit Me On My Beeper')](https://www.youtube.com/watch?v=-4Wu-zSndlw&t=13s) - -In this example, we will send a text message when a contract address receives ether. -This will link Ethereum events (on-chain) with the [Twilio](https://www.twilio.com) SMS service -(off-chain) using Chainlink (CL) - -## Configure and run [Chainlink development environment](../README.md#run-chainlink-development-environment) - -## Sign up for a free Twilio account - -- https://www.twilio.com/try-twilio - -## Run Node Server - -This Node JS server relays messages from Chainlink jobs to Twilio. - -- `yarn install` -- `node twilio.js ` - - i.e. `./twilio.js AC97fade171cxxxxxxxxxxxxxxxxxxxxxx ffab4f5ecc65acxxxxxxefe99xxxxx "+1 786-555-5555" 3055555555`. - -## Create Chainlink Job and Smart Contract (where you will send the money) - -- `truffle migrate`, remember the contract address (0x...) of the migration above. -- `./create_twilio_job_for 0x...` pass contract address as argument - -## Send Money - -- `./send_money_to 0x...` pass contract address as argument - -## Celebrate - -[![Hit Me On My Beeper](HitMeOnMyBeeper.jpg?raw=true 'Hit Me On My Beeper')](https://www.youtube.com/watch?v=-4Wu-zSndlw&t=13s) diff --git a/examples/twilio_sms/contracts/GetMoney.sol b/examples/twilio_sms/contracts/GetMoney.sol deleted file mode 100644 index 6bde57e710d..00000000000 --- a/examples/twilio_sms/contracts/GetMoney.sol +++ /dev/null @@ -1,11 +0,0 @@ -pragma solidity 0.4.24; - -contract GetMoney { - address[] public payees; - event LogMoney(uint256 indexed amount); - - function receive() public payable { - payees.push(msg.sender); - emit LogMoney(msg.value); - } -} diff --git a/examples/twilio_sms/contracts/Migrations.sol b/examples/twilio_sms/contracts/Migrations.sol deleted file mode 100644 index cffe8b95161..00000000000 --- a/examples/twilio_sms/contracts/Migrations.sol +++ /dev/null @@ -1,23 +0,0 @@ -pragma solidity 0.4.24; - -contract Migrations { - address public owner; - uint public last_completed_migration; - - modifier restricted() { - if (msg.sender == owner) _; - } - - constructor() public { - owner = msg.sender; - } - - function setCompleted(uint completed) public restricted { - last_completed_migration = completed; - } - - function upgrade(address new_address) public restricted { - Migrations upgraded = Migrations(new_address); - upgraded.setCompleted(last_completed_migration); - } -} diff --git a/examples/twilio_sms/create_twilio_job_for b/examples/twilio_sms/create_twilio_job_for deleted file mode 100755 index 1910413a40b..00000000000 --- a/examples/twilio_sms/create_twilio_job_for +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env node - -if (process.argv.length <= 2) { - console.log('Usage: ' + __filename + ' ') - process.exit(-1) -} - -const job = { - _comment: 'An ethlog with a specific address only listens to that address.', - initiators: [{ type: 'ethlog', address: process.argv[2] }], - tasks: [{ type: 'HttpPost', params: { post: 'http://localhost:6691' } }], -} - -const request = require('request').defaults({ jar: true }) -let sessionsUrl = 'http://localhost:6688/sessions' -let credentials = { email: 'notreal@fakeemail.ch', password: 'twochains' } -request.post(sessionsUrl, { json: credentials }) -request.post( - { - url: 'http://localhost:6688/v2/specs', - body: job, - json: true, - }, - function(err, response, body) { - console.log(body) - }, -) diff --git a/examples/twilio_sms/migrations/1_initial_migration.js b/examples/twilio_sms/migrations/1_initial_migration.js deleted file mode 100644 index a01c6d6fbbd..00000000000 --- a/examples/twilio_sms/migrations/1_initial_migration.js +++ /dev/null @@ -1,5 +0,0 @@ -const Migrations = artifacts.require('./Migrations.sol') - -module.exports = function(deployer) { - deployer.deploy(Migrations) -} diff --git a/examples/twilio_sms/migrations/2_get_money.js b/examples/twilio_sms/migrations/2_get_money.js deleted file mode 100644 index 463efeee99a..00000000000 --- a/examples/twilio_sms/migrations/2_get_money.js +++ /dev/null @@ -1,5 +0,0 @@ -const GetMoney = artifacts.require('./GetMoney.sol') - -module.exports = function(deployer) { - deployer.deploy(GetMoney) -} diff --git a/examples/twilio_sms/package.json b/examples/twilio_sms/package.json deleted file mode 100644 index 23175cb02d5..00000000000 --- a/examples/twilio_sms/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "private": true, - "name": "@chainlink/example-twilio-sms", - "version": "0.6.0", - "license": "MIT", - "scripts": { - "depcheck": "echo \"@chainlink/example-twilio-sms\" && depcheck --ignore-dirs=contracts || true", - "eslint": "eslint --ext .js,.ts .", - "solhint": "solhint ./contracts/**/*.sol", - "lint": "yarn eslint && yarn solhint", - "format": "prettier --write \"**/*\"", - "setup": "echo \"No setup required for @chainlink/example-twillio-sms\"", - "test": "truffle test" - }, - "devDependencies": { - "@chainlink/eslint-config": "0.0.1", - "@chainlink/prettier-config": "0.0.1", - "body-parser": "^1.18.3", - "depcheck": "^0.8.3", - "eslint": "^6.3.0", - "express": "^4.16.2", - "prettier": "^1.18.2", - "request": "^2.83.0", - "solhint": "^2.1.0", - "truffle": "^5.0.25", - "truffle-contract": "^4.0.31", - "twilio": "^3.34.0" - }, - "prettier": "@chainlink/prettier-config" -} diff --git a/examples/twilio_sms/send_money_to b/examples/twilio_sms/send_money_to deleted file mode 100755 index f9bb29081b8..00000000000 --- a/examples/twilio_sms/send_money_to +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node - -if (process.argv.length <= 2) { - console.log('Usage: ' + __filename + ' ') - process.exit(-1) -} - -const web3 = require('web3'), - contract = require('truffle-contract'), - path = require('path') -GetMoneyJSON = require(path.join(__dirname, 'build/contracts/GetMoney.json')) - -var provider = new web3.providers.HttpProvider('http://localhost:18545') -var GetMoney = contract(GetMoneyJSON) -GetMoney.setProvider(provider) -var devnetAddress = '0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f' - -GetMoney.deployed() - .then(function(instance) { - return instance.receive({ value: 305, from: devnetAddress }) - }) - .then(console.log, console.log) diff --git a/examples/twilio_sms/truffle.js b/examples/twilio_sms/truffle.js deleted file mode 100644 index 0d28cffea8d..00000000000 --- a/examples/twilio_sms/truffle.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ - -module.exports = { - networks: { - development: { - host: '127.0.0.1', - port: 18545, - network_id: '*', - gas: 4700000, - }, - }, -} diff --git a/examples/twilio_sms/twilio.js b/examples/twilio_sms/twilio.js deleted file mode 100755 index 999a8b121ca..00000000000 --- a/examples/twilio_sms/twilio.js +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-var-requires */ - -if (process.argv.length <= 3) { - console.log( - `Usage: ${__filename} `, - ) - process.exit(-1) -} - -const express = require('express') -const bodyParser = require('body-parser') -const app = express() -const PORT = 6691 - -const [accountSid, authToken, from, to] = process.argv.slice(2) -const client = require('twilio')(accountSid, authToken) - -app.use(bodyParser.json()) -app.all('*', function(req, res) { - const log = req.body - const amount = parseInt(log.topics[1], 16) - const message = - 'Hello Chainlink! You just sent received ' + - amount + - ' wei at ' + - log.address - console.log( - 'Sending text from ' + from + ' to ' + to + ' with message: ' + message, - ) - client.messages - .create({ - from: from, - to: to, - body: message, - }) - .then(null, console.log) - - res.json({ body: { status: 'ok' } }) -}) - -app.listen(PORT, function() { - console.log('listening on port ' + PORT + ' for incoming ether') -}) diff --git a/examples/uptime_sla/.babelrc b/examples/uptime_sla/.babelrc deleted file mode 100644 index ae031e09f66..00000000000 --- a/examples/uptime_sla/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "presets": ["@babel/preset-env"], - "plugins": [ - "@babel/plugin-proposal-export-namespace-from", - "@babel/plugin-proposal-throw-expressions", - "@babel/plugin-proposal-class-properties" - ] -} diff --git a/examples/uptime_sla/.eslintignore b/examples/uptime_sla/.eslintignore deleted file mode 100644 index c6bf72cf137..00000000000 --- a/examples/uptime_sla/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -contracts \ No newline at end of file diff --git a/examples/uptime_sla/.eslintrc.js b/examples/uptime_sla/.eslintrc.js deleted file mode 100644 index d8c6c7ba920..00000000000 --- a/examples/uptime_sla/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - extends: [ - '@chainlink/eslint-config/node', - '@chainlink/eslint-config/truffle', - ], -} diff --git a/examples/uptime_sla/.prettierignore b/examples/uptime_sla/.prettierignore deleted file mode 100644 index 2179226c66f..00000000000 --- a/examples/uptime_sla/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -.node-xmlhttprequest-sync-78767 -**/*.sol -.prettierignore -.gitignore -deploy -.eslintignore diff --git a/examples/uptime_sla/README.md b/examples/uptime_sla/README.md deleted file mode 100644 index b3249a2d602..00000000000 --- a/examples/uptime_sla/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Uptime Service Level Agreement - -An example SLA that uses ChainLink to determine the release of payment. - -When the contract is deployed a client, service provider, and start time are specified. Additionally a deposit is made. The end of the contract is set to 30 days after the start time. - -After the contract is created anyone can request updates from the oracle for the contract. If the oracle reports that the uptime is below 99.99% then the deposit is released to the client. If the rate is still above 99.99% after the contract ends, and the deposit has not been released, the deposit is sent to the service provider. - -```solidity -function report(uint256 _requestId, uint256 _rate) - public - recordChainlinkFulfillment(_requestId) - { - if (_rate < uptimeThreshold) { - client.send(this.balance); - } else if (block.timestamp >= endAt) { - serviceProvider.send(this.balance); - } - } -``` - -# ChainLink - -Initiator: `runLog` - -Job Pipeline: `httpGet` => `jsonParse` => `multiply` => `ethUint256` => `ethTx` - -This contract displays ChainLinks ability to pull in data from outside data feeds and format it to be used by Ethereum contracts. - -A float value is pulled out of a nested JSON object and multiplied to a precision level that is useful for the contract. - -The ChainLink Job is configured to not take any specific URL or JSON path, so that this oracle and job can be reused for other APIs. Both `url` and `path` are passed into the oracle by the SLA contract, specifically which data point to use is passed into the contract: - -```solidity -function updateUptime(string _when) public { - Chainlink.Request memory req = newRequest(jobId, this, "report(uint256,uint256)"); - req.add("get", "https://status.heroku.com/api/ui/availabilities"); - string[] memory path = new string[](4); - path[0] = "data"; - path[1] = _when; //pick which data point in the array you want to examine - path[2] = "attributes"; - path[3] = "calculation"; - req.add("path", path); - chainlinkRequest(req, LINK(1)); -} -``` - -The API returns the percentage as a float, for example the current value is `0.999999178716033`. The `multiply` adapter takes that result and multiplies it by 1000, which [a parameter specified in the `times` field](https://github.com/smartcontractkit/hello_chainlink/blob/4b42f127ddeca6541ac2aba1803f458d0a3bf460/uptime_sla/http_json_x10000_job.json). The result is `9999`, allowing the contract to check for "four nines" of uptime. - -## Configure and run [Chainlink development environment](../README.md#run-chainlink-development-environment) - -## Run and update the Uptime SLA contract. - -1. `yarn install` -2. `./deploy` in another window -3. `./send_sla_transaction.js` to trigger an update to the SLA -4. `./get_uptime.js` get the latest uptime - -## Development - -To run the tests, call `./node_modules/.bin/truffle --network=test test` diff --git a/examples/uptime_sla/clmigration.js b/examples/uptime_sla/clmigration.js deleted file mode 100644 index f96d66b5703..00000000000 --- a/examples/uptime_sla/clmigration.js +++ /dev/null @@ -1,19 +0,0 @@ -// clmigration provides two key helpers for Chainlink development: -// 1. wraps migrations that to be skipped in the test environment, since we -// recreate every contract beforeEach test, and hit other APIs in our migration process. -// 2. Prepare plumbing for correct async/await behavior, -// in spite of https://github.com/trufflesuite/truffle/issues/501 - -module.exports = function(callback) { - return function(deployer, network) { - if (network === 'test') { - console.log('===== SKIPPING MIGRATIONS IN TEST ENVIRONMENT =====') - } else { - deployer - .then(async () => { - return callback(deployer, network) - }) - .catch(console.log) - } - } -} diff --git a/examples/uptime_sla/contracts/LinkToken.sol b/examples/uptime_sla/contracts/LinkToken.sol deleted file mode 100644 index e67ad4037c4..00000000000 --- a/examples/uptime_sla/contracts/LinkToken.sol +++ /dev/null @@ -1,3 +0,0 @@ -pragma solidity 0.4.24; - -import "link_token/contracts/LinkToken.sol"; diff --git a/examples/uptime_sla/contracts/Migrations.sol b/examples/uptime_sla/contracts/Migrations.sol deleted file mode 100644 index cffe8b95161..00000000000 --- a/examples/uptime_sla/contracts/Migrations.sol +++ /dev/null @@ -1,23 +0,0 @@ -pragma solidity 0.4.24; - -contract Migrations { - address public owner; - uint public last_completed_migration; - - modifier restricted() { - if (msg.sender == owner) _; - } - - constructor() public { - owner = msg.sender; - } - - function setCompleted(uint completed) public restricted { - last_completed_migration = completed; - } - - function upgrade(address new_address) public restricted { - Migrations upgraded = Migrations(new_address); - upgraded.setCompleted(last_completed_migration); - } -} diff --git a/examples/uptime_sla/contracts/Oracle.sol b/examples/uptime_sla/contracts/Oracle.sol deleted file mode 100644 index 8b00e54e486..00000000000 --- a/examples/uptime_sla/contracts/Oracle.sol +++ /dev/null @@ -1,3 +0,0 @@ -pragma solidity 0.4.24; - -import "chainlink/contracts/Oracle.sol"; \ No newline at end of file diff --git a/examples/uptime_sla/contracts/UptimeSLA.sol b/examples/uptime_sla/contracts/UptimeSLA.sol deleted file mode 100644 index e6c23f65194..00000000000 --- a/examples/uptime_sla/contracts/UptimeSLA.sol +++ /dev/null @@ -1,54 +0,0 @@ -pragma solidity 0.4.24; - -import "chainlink/contracts/Chainlinked.sol"; - -contract UptimeSLA is Chainlinked { - uint256 constant private ORACLE_PAYMENT = 1 * LINK; - uint256 constant public uptimeThreshold = 9999; // solhint-disable-line const-name-snakecase - bytes32 private jobId; - uint256 private endAt; - address private client; - address private serviceProvider; - uint256 public uptime; - - constructor( - address _client, - address _serviceProvider, - address _link, - address _oracle, - bytes32 _jobId - ) public payable { - client = _client; - serviceProvider = _serviceProvider; - endAt = block.timestamp.add(30 days); // solhint-disable-line not-rely-on-time - jobId = _jobId; - - setLinkToken(_link); - setOracle(_oracle); - } - - function updateUptime(string _when) public { - Chainlink.Request memory req = newRequest(jobId, this, this.report.selector); - req.add("get", "https://status.heroku.com/api/ui/availabilities"); - string[] memory path = new string[](4); - path[0] = "data"; - path[1] = _when; - path[2] = "attributes"; - path[3] = "calculation"; - req.addStringArray("path", path); - chainlinkRequest(req, ORACLE_PAYMENT); - } - - function report(bytes32 _externalId, uint256 _uptime) - public - recordChainlinkFulfillment(_externalId) - { - uptime = _uptime; - if (_uptime < uptimeThreshold) { - client.transfer(address(this).balance); - } else if (block.timestamp >= endAt) { // solhint-disable-line not-rely-on-time - serviceProvider.transfer(address(this).balance); - } - } - -} diff --git a/examples/uptime_sla/deploy b/examples/uptime_sla/deploy deleted file mode 100755 index dd3d792569c..00000000000 --- a/examples/uptime_sla/deploy +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -./node_modules/.bin/truffle migrate --compile-all --reset diff --git a/examples/uptime_sla/get_uptime.js b/examples/uptime_sla/get_uptime.js deleted file mode 100755 index 5509a483e0e..00000000000 --- a/examples/uptime_sla/get_uptime.js +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-var-requires */ - -const Web3 = require('web3') -const contract = require('truffle-contract') -const path = require('path') -const UptimeSLAJSON = require(path.join( - __dirname, - 'build/contracts/UptimeSLA.json', -)) - -const provider = new Web3.providers.HttpProvider('http://localhost:18545') -const UptimeSLA = contract(UptimeSLAJSON) -UptimeSLA.setProvider(provider) - -UptimeSLA.deployed() - .then(function(instance) { - return instance.uptime.call() - }) - .then(console.log, console.log) diff --git a/examples/uptime_sla/migrations/1_initial_migration.js b/examples/uptime_sla/migrations/1_initial_migration.js deleted file mode 100644 index a7a4fdbc824..00000000000 --- a/examples/uptime_sla/migrations/1_initial_migration.js +++ /dev/null @@ -1,5 +0,0 @@ -const Migrations = artifacts.require('Migrations') - -module.exports = function(deployer) { - deployer.deploy(Migrations) -} diff --git a/examples/uptime_sla/migrations/2_link_token.js b/examples/uptime_sla/migrations/2_link_token.js deleted file mode 100644 index d36a9916a92..00000000000 --- a/examples/uptime_sla/migrations/2_link_token.js +++ /dev/null @@ -1,5 +0,0 @@ -const LINK = artifacts.require('LinkToken') - -module.exports = function(deployer) { - deployer.deploy(LINK) -} diff --git a/examples/uptime_sla/migrations/3_oracle.js b/examples/uptime_sla/migrations/3_oracle.js deleted file mode 100644 index ed309bc6aa1..00000000000 --- a/examples/uptime_sla/migrations/3_oracle.js +++ /dev/null @@ -1,6 +0,0 @@ -const LINK = artifacts.require('LinkToken') -const Oracle = artifacts.require('Oracle') - -module.exports = function(deployer) { - deployer.deploy(Oracle, LINK.address) -} diff --git a/examples/uptime_sla/migrations/4_uptime_sla.js b/examples/uptime_sla/migrations/4_uptime_sla.js deleted file mode 100644 index a229710fa8b..00000000000 --- a/examples/uptime_sla/migrations/4_uptime_sla.js +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -const clmigration = require('../clmigration.js') -const request = require('request-promise').defaults({ jar: true }) -const UptimeSLA = artifacts.require('UptimeSLA') -const Oracle = artifacts.require('Oracle') -const LINK = artifacts.require('LinkToken') - -const sessionsUrl = 'http://localhost:6688/sessions' -const specsUrl = 'http://localhost:6688/v2/specs' -const credentials = { email: 'notreal@fakeemail.ch', password: 'twochains' } -const job = { - _comment: - 'GETs a number from JSON, multiplies by 10,000, and reports uint256', - initiators: [{ type: 'runlog' }], - tasks: [ - { type: 'httpGet' }, - { type: 'jsonParse' }, - { type: 'multiply', params: { times: 10000 } }, - { type: 'ethuint256' }, - { type: 'ethtx' }, - ], -} - -module.exports = clmigration(async function(truffleDeployer) { - const client = '0x542B68aE7029b7212A5223ec2867c6a94703BeE3' - const serviceProvider = '0xB16E8460cCd76aEC437ca74891D3D358EA7d1d88' - - await request.post(sessionsUrl, { json: credentials }) - const body = await request.post(specsUrl, { json: job }) - console.log(`Deploying UptimeSLA:`) - console.log(`\tjob: ${body.data.id}`) - console.log(`\tclient: ${client}`) - console.log(`\tservice provider: ${serviceProvider}`) - - await truffleDeployer.deploy( - UptimeSLA, - client, - serviceProvider, - LINK.address, - Oracle.address, - body.id, - { value: 1000000000 }, - ) -}) diff --git a/examples/uptime_sla/package.json b/examples/uptime_sla/package.json deleted file mode 100644 index 9b09a95b278..00000000000 --- a/examples/uptime_sla/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@chainlink/example-uptime-sla", - "version": "0.6.0", - "license": "MIT", - "private": true, - "scripts": { - "depcheck": "echo \"@chainlink/example-uptime-sla\" && depcheck --ignore-dirs=contracts || true", - "eslint": "eslint --ext .js,.ts .", - "solhint": "solhint ./contracts/**/*.sol", - "lint": "yarn eslint && yarn solhint", - "format": "prettier --write \"**/*\"", - "setup": "echo \"No setup required for @chainlink/example-uptime-sla\"", - "test": "truffle test" - }, - "devDependencies": { - "@chainlink/eslint-config": "0.0.1", - "@chainlink/prettier-config": "0.0.1", - "bignumber.js": "^9.0.0", - "bn.js": "^4.11.8", - "cbor": "^4.0.0", - "chainlink": "^0.6.5", - "depcheck": "^0.8.3", - "eslint": "^6.3.0", - "ethereumjs-abi": "^0.6.5", - "ethereumjs-util": "^5.1.5", - "fs": "^0.0.1-security", - "link_token": "^1.0.6", - "prettier": "^1.18.2", - "request-promise": "^4.2.2", - "solc": "0.4.24", - "solhint": "^2.1.0", - "truffle": "^5.0.25", - "truffle-contract": "^4.0.31", - "web3": "^1.0.0-beta.35" - }, - "prettier": "@chainlink/prettier-config" -} diff --git a/examples/uptime_sla/send_sla_transaction.js b/examples/uptime_sla/send_sla_transaction.js deleted file mode 100755 index 7d3682f10a2..00000000000 --- a/examples/uptime_sla/send_sla_transaction.js +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-var-requires */ - -const Web3 = require('web3') -const contract = require('truffle-contract') -const path = require('path') -const UptimeSLAJSON = require(path.join( - __dirname, - 'build/contracts/UptimeSLA.json', -)) - -const provider = new Web3.providers.HttpProvider('http://localhost:18545') -const UptimeSLA = contract(UptimeSLAJSON) -UptimeSLA.setProvider(provider) -const devnetAddress = '0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f' - -UptimeSLA.deployed() - .then(function(instance) { - return instance.updateUptime('0', { from: devnetAddress }) - }) - .then(console.log, console.log) diff --git a/examples/uptime_sla/test/UptimeSLA_test.js b/examples/uptime_sla/test/UptimeSLA_test.js deleted file mode 100644 index 249bc22c6af..00000000000 --- a/examples/uptime_sla/test/UptimeSLA_test.js +++ /dev/null @@ -1,151 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -const h = require('./support/helpers') -const LinkToken = artifacts.require('LinkToken') -const Oracle = artifacts.require('Oracle') -const SLA = artifacts.require('UptimeSLA') - -contract('UptimeSLA', accounts => { - const specId = '0x4c7b7ffb66b344fbaa64995af81e355a' - const deposit = 1000000000 - const oracleNode = accounts[1] - let link, oc, sla, client, serviceProvider, startAt - - beforeEach(async () => { - client = '0xf000000000000000000000000000000000000001' - serviceProvider = '0xf000000000000000000000000000000000000002' - link = await LinkToken.new() - oc = await Oracle.new(link.address, { from: oracleNode }) - sla = await SLA.new( - client, - serviceProvider, - link.address, - oc.address, - specId, - { - value: deposit, - }, - ) - link.transfer(sla.address, web3.utils.toWei('1', 'ether')) - startAt = await h.getLatestTimestamp() - }) - - describe('before updates', () => { - it('does not release money to anyone', async () => { - assert.equal(await web3.eth.getBalance(sla.address), deposit) - assert.equal(await web3.eth.getBalance(client), 0) - assert.equal(await web3.eth.getBalance(serviceProvider), 0) - }) - }) - - describe('#updateUptime', () => { - it('triggers a log event in the Oracle contract', async () => { - await sla.updateUptime('0') - - const events = await oc.getPastEvents('allEvents') - assert.equal(1, events.length) - assert.equal( - events[0].args.specId, - specId + '00000000000000000000000000000000', - ) - - const decoded = await h.decodeDietCBOR(events[0].args.data) - assert.deepEqual(decoded, { - get: 'https://status.heroku.com/api/ui/availabilities', - path: ['data', '0', 'attributes', 'calculation'], - }) - }) - }) - - describe('#fulfillOracleRequest', () => { - const response = - '0x00000000000000000000000000000000000000000000000000000000000f8c4c' - let request - - beforeEach(async () => { - const tx = await sla.updateUptime('0') - request = h.decodeRunRequest(tx.receipt.rawLogs[3]) - }) - - context('when the value is below 9999', async () => { - const response = - '0x000000000000000000000000000000000000000000000000000000000000270e' - - it('sends the deposit to the client', async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: oracleNode, - }) - - assert.equal(await web3.eth.getBalance(sla.address), 0) - assert.equal(await web3.eth.getBalance(client), deposit) - assert.equal(await web3.eth.getBalance(serviceProvider), 0) - }) - }) - - context('when the value is 9999 or above', () => { - const response = - '0x000000000000000000000000000000000000000000000000000000000000270f' - let originalClientBalance - - beforeEach(async () => { - originalClientBalance = await web3.eth.getBalance(client) - }) - - it('does not move the money', async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: oracleNode, - }) - - h.assertBigNum(await web3.eth.getBalance(sla.address), deposit) - h.assertBigNum(await web3.eth.getBalance(client), originalClientBalance) - h.assertBigNum(await web3.eth.getBalance(serviceProvider), 0) - }) - - context('and a month has passed', () => { - beforeEach(async () => { - await h.fastForwardTo(startAt + h.days(30)) - }) - - it('gives the money back to the service provider', async () => { - await h.fulfillOracleRequest(oc, request, response, { - from: oracleNode, - }) - - h.assertBigNum(await web3.eth.getBalance(sla.address), 0) - h.assertBigNum( - await web3.eth.getBalance(client), - originalClientBalance, - ) - h.assertBigNum(await web3.eth.getBalance(serviceProvider), deposit) - }) - }) - }) - - context('when the consumer does not recognize the request ID', () => { - beforeEach(async () => { - const fid = h.functionSelector('report(uint256,bytes32)') - const args = h.requestDataBytes(specId, sla.address, fid, 'xid', 'foo') - const tx = await link.transferAndCall(oc.address, 0, args) - request = h.decodeRunRequest(tx.receipt.rawLogs[2]) - }) - - it('does not accept the data provided', async () => { - const originalUptime = await sla.uptime() - await h.fulfillOracleRequest(oc, request, response, { - from: oracleNode, - }) - const newUptime = await sla.uptime() - - h.assertBigNum(originalUptime, newUptime) - }) - }) - - context('when called by anyone other than the oracle contract', () => { - it('does not accept the data provided', async () => { - await h.assertActionThrows(async () => { - await sla.report(request.id, response, { from: oracleNode }) - }) - }) - }) - }) -}) diff --git a/examples/uptime_sla/test/support/helpers.js b/examples/uptime_sla/test/support/helpers.js deleted file mode 100644 index 41085a277e6..00000000000 --- a/examples/uptime_sla/test/support/helpers.js +++ /dev/null @@ -1,198 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -const bigNum = number => web3.utils.toBN(number) -const cbor = require('cbor') -const abi = require('ethereumjs-abi') -const util = require('ethereumjs-util') -const BN = require('bn.js') -const ethJSUtils = require('ethereumjs-util') - -web3.providers.HttpProvider.prototype.sendAsync = - web3.providers.HttpProvider.prototype.send - -const exps = {} - -const sendEth = (method, params) => - new Promise((resolve, reject) => { - web3.currentProvider.sendAsync( - { - jsonrpc: '2.0', - method: method, - params: params || [], - id: new Date().getTime(), - }, - (error, response) => (error ? reject(error) : resolve(response.result)), - () => {}, - () => {}, - ) - }) - -exps.assertBigNum = (a, b, failureMessage) => - assert( - bigNum(a).eq(bigNum(b)), - `BigNum ${a} is not ${b}` + (failureMessage ? ': ' + failureMessage : ''), - ) - -exps.getLatestTimestamp = async () => { - const latestBlock = await web3.eth.getBlock('latest', false) - return web3.utils.toDecimal(latestBlock.timestamp) -} - -exps.fastForwardTo = async target => { - const now = await exps.getLatestTimestamp() - assert.isAbove(target, now, 'Cannot fast forward to the past') - const difference = target - now - await sendEth('evm_increaseTime', [difference]) - await sendEth('evm_mine') -} - -const minutes = number => number * 60 -const hours = number => number * minutes(60) - -exps.days = number => number * hours(24) - -const abiEncode = (types, values) => { - return abi.rawEncode(types, values).toString('hex') -} - -const startMapBuffer = Buffer.from([0xbf]) -const endMapBuffer = Buffer.from([0xff]) - -const autoAddMapDelimiters = data => { - let buffer = data - - if (buffer[0] >> 5 !== 5) { - buffer = Buffer.concat( - [startMapBuffer, buffer, endMapBuffer], - buffer.length + 2, - ) - } - - return buffer -} - -const zeroX = value => (value.slice(0, 2) !== '0x' ? `0x${value}` : value) - -const toHexWithoutPrefix = arg => { - if (arg instanceof Buffer || arg instanceof BN) { - return arg.toString('hex') - } else if (arg instanceof Uint8Array) { - return Array.prototype.reduce.call( - arg, - (a, v) => a + v.toString('16').padStart(2, '0'), - '', - ) - } else { - return Buffer.from(arg, 'ascii').toString('hex') - } -} - -const toHex = value => { - return zeroX(toHexWithoutPrefix(value)) -} - -exps.assertActionThrows = action => - Promise.resolve() - .then(action) - .catch(error => { - assert(error, 'Expected an error to be raised') - assert(error.message, 'Expected an error to be raised') - return error.message - }) - .then(errorMessage => { - assert(errorMessage, 'Expected an error to be raised') - const invalidOpcode = errorMessage.includes('invalid opcode') - const reverted = errorMessage.includes( - 'VM Exception while processing transaction: revert', - ) - assert( - invalidOpcode || reverted, - 'expected following error message to include "invalid JUMP" or ' + - `"revert": "${errorMessage}"`, - ) - // see https://github.com/ethereumjs/testrpc/issues/39 - // for why the "invalid JUMP" is the throw related error when using TestRPC - }) - -exps.decodeDietCBOR = data => { - return cbor.decodeFirst(autoAddMapDelimiters(ethJSUtils.toBuffer(data))) -} - -exps.decodeRunRequest = log => { - const runABI = util.toBuffer(log.data) - const types = [ - 'address', - 'bytes32', - 'uint256', - 'address', - 'bytes4', - 'uint256', - 'uint256', - 'bytes', - ] - const [ - requester, - requestId, - payment, - callbackAddress, - callbackFunc, - expiration, - version, - data, - ] = abi.rawDecode(types, runABI) - - return { - topic: log.topics[0], - jobId: log.topics[1], - requester: zeroX(requester), - id: toHex(requestId), - payment: toHex(payment), - callbackAddr: zeroX(callbackAddress), - callbackFunc: toHex(callbackFunc), - expiration: toHex(expiration), - dataVersion: version, - data: autoAddMapDelimiters(data), - } -} - -exps.functionSelector = signature => - '0x' + - web3.utils - .sha3(signature) - .slice(2) - .slice(0, 8) - -exps.fulfillOracleRequest = async (oracle, request, response, options) => { - if (!options) options = {} - - return oracle.fulfillOracleRequest( - request.id, - request.payment, - request.callbackAddr, - request.callbackFunc, - request.expiration, - response, - options, - ) -} - -exps.requestDataBytes = (specId, to, fHash, nonce, data) => { - const types = [ - 'address', - 'uint256', - 'bytes32', - 'address', - 'bytes4', - 'uint256', - 'uint256', - 'bytes', - ] - const values = [0, 0, specId, to, fHash, nonce, 1, data] - const encoded = abiEncode(types, values) - const funcSelector = exps.functionSelector( - 'oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)', - ) - return funcSelector + encoded -} - -module.exports = exps diff --git a/examples/uptime_sla/truffle.js b/examples/uptime_sla/truffle.js deleted file mode 100644 index 6c476df1834..00000000000 --- a/examples/uptime_sla/truffle.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -module.exports = { - compilers: { - solc: { - version: '0.4.24', - }, - }, - networks: { - cldev: { - host: '127.0.0.1', - port: 18545, - network_id: '*', - gas: 4700000, - }, - }, -} diff --git a/explorer/client/package.json b/explorer/client/package.json index f28088a6410..602ba88a479 100644 --- a/explorer/client/package.json +++ b/explorer/client/package.json @@ -1,7 +1,7 @@ { "name": "@chainlink/explorer-client", "private": true, - "version": "0.0.0", + "version": "0.0.1", "description": "LINK Explorer Client", "author": "Chainlink Dev Team", "license": "MIT", @@ -21,7 +21,7 @@ }, "scripts": { "start": "cross-env PORT=3001 craco start", - "build:clean": "rm -rf build", + "build:clean": "rimraf -rf build", "prebuild": "yarn install", "build": "craco build", "lint": "eslint --ext .js,.ts,.tsx,.jsx src", @@ -72,7 +72,7 @@ "@types/redux-thunk": "^2.1.0", "brotli-webpack-plugin": "^1.1.0", "compression-webpack-plugin": "^3.0.0", - "cross-env": "^6.0.0", + "cross-env": "^6.0.3", "depcheck": "^0.8.3", "dynamic-cdn-webpack-plugin": "^4.0.0", "enzyme": "^3.10.0", @@ -81,10 +81,11 @@ "html-webpack-plugin": "4.0.0-beta.5", "module-to-cdn": "^3.1.2", "prettier": "^1.18.2", - "react-hot-loader": "^4.8.0", + "react-hot-loader": "^4.12.16", "react-scripts": "^3.1.0", "redux-devtools-extension": "^2.13.8", "redux-logger": "^3.0.6", + "rimraf": "^3.0.0", "typescript": "^3.6.3", "webpack": "4.41.1" }, diff --git a/explorer/e2e/showJobRun.test.ts b/explorer/e2e/showJobRun.test.ts deleted file mode 100644 index 2ae165a4e63..00000000000 --- a/explorer/e2e/showJobRun.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import expect from 'expect-puppeteer' -import { Server } from 'http' -import { Browser, launch, Page } from 'puppeteer' -import { getDb } from '../src/database' -import { createChainlinkNode } from '../src/entity/ChainlinkNode' -import { DEFAULT_TEST_PORT, start, stop } from '../src/support/server' -import { createJobRun } from '../src/factories' - -describe('End to end', () => { - let browser: Browser - let page: Page - let server: Server - - beforeAll(async () => { - browser = await launch({ - args: ['--no-sandbox'], - devtools: false, - headless: true, - }) - - page = await browser.newPage() - server = await start() - - page.on('console', msg => console.log('PAGE LOG:', msg.text())) - }) - - afterAll(async done => { - browser.close() - stop(server, done) - }) - - it('can search for job run', async () => { - const db = await getDb() - const [node] = await createChainlinkNode(db, 'endToEndChainlinkNode') - const jobRun = await createJobRun(db, node) - - await page.goto(`http://localhost:${DEFAULT_TEST_PORT}`) - await expect(page).toFill('form input[name=search]', jobRun.runId) - await expect(page).toClick('form button') - await page.waitForNavigation() - await expect(page).toMatch(jobRun.runId) - }) -}) diff --git a/explorer/package.json b/explorer/package.json index 9236ed9e6d5..7c9390524b7 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -1,7 +1,7 @@ { "name": "@chainlink/explorer", "private": true, - "version": "0.0.0", + "version": "0.0.1", "description": "LINK Explorer", "author": "Chainlink Dev Team", "license": "MIT", @@ -20,11 +20,8 @@ "pretest": "NODE_ENV=test yarn automigrate", "test": "yarn jest --runInBand --detectOpenHandles", "test-ci": "yarn jest src/__tests__ --runInBand --detectOpenHandles", - "test-ci:e2e": "yarn build && yarn test-ci:e2e:no-build", - "test-ci:e2e:no-build": "yarn jest e2e/ --runInBand --detectOpenHandles", "test-ci:silent": "yarn test-ci --silent", - "test-ci:e2e:silent": "yarn test-ci:e2e --silent", - "lint": "eslint --ext .js,.ts,.tsx,.jsx src e2e", + "lint": "eslint --ext .js,.ts,.tsx,.jsx src", "lint:fix": "yarn lint --fix", "format": "prettier --write \"**/*\"", "migration:run": "cross-env ts-node ./src/bin/migrator.ts migrate", @@ -37,11 +34,12 @@ "main": "src/index.ts", "dependencies": { "argon2": "^0.24.1", - "class-validator": "^0.10.0", + "class-validator": "^0.11.0", "cookie-session": "^1.3.3", "express": "^4.16.4", "express-winston": "^3.4.0", "helmet": "^3.20.0", + "jayson": "^3.1.2", "js-sha256": "^0.9.0", "jsonapi-serializer": "^3.6.4", "local-storage-fallback": "^4.1.1", @@ -51,7 +49,7 @@ "ts-node": "^8.0.3", "typeorm": "^0.2.15", "winston": "^3.2.1", - "ws": "^7.1.0", + "ws": "^7.2.0", "yargs": "^13.2.2" }, "devDependencies": { @@ -59,7 +57,6 @@ "@chainlink/prettier-config": "0.0.1", "@types/bcrypt": "^3.0.0", "@types/cookie-session": "^2.0.37", - "@types/expect-puppeteer": "^3.3.1", "@types/express": "^4.16.1", "@types/express-winston": "^3.0.1", "@types/helmet": "0.0.43", @@ -69,21 +66,18 @@ "@types/mime-types": "^2.1.0", "@types/node": "^11.11.3", "@types/node-fetch": "^2.5.0", - "@types/puppeteer": "^1.12.0", "@types/supertest": "^2.0.7", "@types/uuid": "^3.4.4", "@types/ws": "^6.0.1", - "@types/yargs": "^12.0.10", - "concurrently": "^4.1.0", - "cross-env": "^6.0.0", + "@types/yargs": "^13.0.3", + "concurrently": "^5.0.0", + "cross-env": "^6.0.3", "depcheck": "^0.8.3", "eslint": "^6.3.0", - "expect-puppeteer": "^4.1.0", "http-status-codes": "^1.3.2", "jest": "^24.7.0", "node-fetch": "^2.6.0", "prettier": "^1.18.2", - "puppeteer": "^1.20.0", "supertest": "^4.0.2", "ts-jest": "^24.0.0", "ts-node-dev": "^1.0.0-pre.40", diff --git a/explorer/src/__tests__/middleware/adminAuth.test.ts b/explorer/src/__tests__/middleware/adminAuth.test.ts index 9d56a3ecae4..63e6e69a87b 100644 --- a/explorer/src/__tests__/middleware/adminAuth.test.ts +++ b/explorer/src/__tests__/middleware/adminAuth.test.ts @@ -4,7 +4,7 @@ import http from 'http' import httpStatus from 'http-status-codes' import cookieSession from 'cookie-session' import { Connection } from 'typeorm' -import { closeDbConnection, getDb } from '../../database' +import { getDb } from '../../database' import { clearDb } from '../testdatabase' import { createAdmin } from '../../support/admin' import { stop } from '../../support/server' diff --git a/explorer/src/__tests__/realtime.test.ts b/explorer/src/__tests__/realtime.test.ts deleted file mode 100644 index 34fde3062dd..00000000000 --- a/explorer/src/__tests__/realtime.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { Server } from 'http' -import { Connection, getCustomRepository } from 'typeorm' -import WebSocket from 'ws' -import { getDb } from '../database' -import { ChainlinkNode, createChainlinkNode } from '../entity/ChainlinkNode' -import { JobRun } from '../entity/JobRun' -import { TaskRun } from '../entity/TaskRun' -import { DEFAULT_TEST_PORT, start, stop } from '../support/server' -import ethtxFixture from './fixtures/JobRun.ethtx.fixture.json' -import createFixture from './fixtures/JobRun.fixture.json' -import updateFixture from './fixtures/JobRunUpdate.fixture.json' -import { clearDb } from './testdatabase' -import { - ACCESS_KEY_HEADER, - NORMAL_CLOSE, - SECRET_HEADER, -} from '../utils/constants' -import { JobRunRepository } from '../repositories/JobRunRepository' - -const ENDPOINT = `ws://localhost:${DEFAULT_TEST_PORT}` - -const newChainlinkNode = ( - url: string, - accessKey: string, - secret: string, -): Promise => { - const ws = new WebSocket(ENDPOINT, { - headers: { - [ACCESS_KEY_HEADER]: accessKey, - [SECRET_HEADER]: secret, - }, - }) - - return new Promise((resolve: (arg0: WebSocket) => void, reject) => { - ws.on('error', (error: Error) => { - reject(error) - }) - - ws.on('open', () => resolve(ws)) - }) -} - -describe('realtime', () => { - let server: Server - let db: Connection - let chainlinkNode: ChainlinkNode - let secret: string - - beforeAll(async () => { - server = await start() - db = await getDb() - }) - - beforeEach(async () => { - clearDb() - ;[chainlinkNode, secret] = await createChainlinkNode( - db, - 'explore realtime test chainlinkNode', - ) - }) - - afterAll(done => stop(server, done)) - - it('create a job run for valid JSON', async () => { - expect.assertions(3) - - const ws = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) - - ws.send(JSON.stringify(createFixture)) - - await new Promise(resolve => { - ws.on('message', (data: WebSocket.Data) => { - const result = JSON.parse(data as string) - expect(result.status).toEqual(201) - ws.close() - resolve() - }) - }) - - const jobRunCount = await db.manager.count(JobRun) - expect(jobRunCount).toEqual(1) - - const taskRunCount = await db.manager.count(TaskRun) - expect(taskRunCount).toEqual(1) - }) - - it('can create and update a job run and task runs', async () => { - expect.assertions(6) - - const ws = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) - - ws.send(JSON.stringify(createFixture)) - - await new Promise(resolve => { - let responses = 0 - ws.on('message', (data: any) => { - responses += 1 - const result = JSON.parse(data) - - if (responses === 1) { - expect(result.status).toEqual(201) - ws.send(JSON.stringify(updateFixture)) - } - - if (responses === 2) { - expect(result.status).toEqual(201) - ws.close() - resolve() - } - }) - }) - - const jobRunCount = await db.manager.count(JobRun) - expect(jobRunCount).toEqual(1) - - const taskRunCount = await db.manager.count(TaskRun) - expect(taskRunCount).toEqual(1) - - const jr = await db.manager.findOne(JobRun) - expect(jr.status).toEqual('completed') - - const tr = jr.taskRuns[0] - expect(tr.status).toEqual('completed') - }) - - it('can create a task run with transactionHash and status', async () => { - expect.assertions(10) - - const ws = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) - - const messageReceived = new Promise(resolve => { - ws.on('message', (data: any) => { - const result = JSON.parse(data) - expect(result.status).toEqual(201) - resolve() - }) - }) - - ws.send(JSON.stringify(ethtxFixture)) - await messageReceived - - const jobRunCount = await db.manager.count(JobRun) - expect(jobRunCount).toEqual(1) - - const taskRunCount = await db.manager.count(TaskRun) - expect(taskRunCount).toEqual(4) - - const jobRunRepository = getCustomRepository(JobRunRepository, db.name) - const jr = await jobRunRepository.getFirst() - - expect(jr.status).toEqual('completed') - - const tr = jr.taskRuns[3] - expect(tr.status).toEqual('completed') - expect(tr.transactionHash).toEqual( - '0x1111111111111111111111111111111111111111111111111111111111111111', - ) - expect(tr.timestamp).toEqual(new Date('2018-01-08T18:12:01.103Z')) - expect(tr.blockHeight).toEqual('3735928559') - expect(tr.blockHash).toEqual('0xbadc0de5') - expect(tr.transactionStatus).toEqual('fulfilledRunLog') - ws.close() - }) - - it('rejects malformed json events with code 422', async (done: any) => { - expect.assertions(2) - - const ws = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) - - ws.send('{invalid json}') - - ws.on('message', async (data: any) => { - const result = JSON.parse(data) - expect(result.status).toEqual(422) - - const count = await db.manager.count(JobRun) - expect(count).toEqual(0) - - ws.close() - done() - }) - }) - - it('rejects invalid authentication', async (done: any) => { - expect.assertions(1) - - newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, 'lol-no').catch( - error => { - expect(error).toBeDefined() - done() - }, - ) - }) - - it('rejects multiple connections from single node', async done => { - expect.assertions(8) - - // eslint-disable-next-line prefer-const - let ws1: WebSocket, ws2: WebSocket, ws3: WebSocket - - // eslint-disable-next-line prefer-const - ws1 = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) - - ws1.addEventListener('close', (event: WebSocket.CloseEvent) => { - expect(ws1.readyState).toBe(WebSocket.CLOSED) - expect(ws2.readyState).toBe(WebSocket.OPEN) - expect(event.code).toBe(NORMAL_CLOSE) - expect(event.reason).toEqual('Duplicate connection opened') - }) - - ws2 = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) - - ws2.addEventListener('close', (event: WebSocket.CloseEvent) => { - expect(ws2.readyState).toBe(WebSocket.CLOSED) - expect(ws3.readyState).toBe(WebSocket.OPEN) - expect(event.code).toBe(NORMAL_CLOSE) - expect(event.reason).toEqual('Duplicate connection opened') - ws3.close() - done() - }) - - ws3 = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) - }) -}) diff --git a/explorer/src/__tests__/rpcMethods/upsertJobRun.test.ts b/explorer/src/__tests__/rpcMethods/upsertJobRun.test.ts new file mode 100644 index 00000000000..41342e6c72a --- /dev/null +++ b/explorer/src/__tests__/rpcMethods/upsertJobRun.test.ts @@ -0,0 +1,139 @@ +import { Server } from 'http' +import { Connection, getCustomRepository } from 'typeorm' +import WebSocket from 'ws' +import jayson from 'jayson' +import { getDb } from '../../database' +import { ChainlinkNode, createChainlinkNode } from '../../entity/ChainlinkNode' +import { JobRun } from '../../entity/JobRun' +import { TaskRun } from '../../entity/TaskRun' +import { start, stop } from '../../support/server' +import ethtxFixture from '../fixtures/JobRun.ethtx.fixture.json' +import createFixture from '../fixtures/JobRun.fixture.json' +import updateFixture from '../fixtures/JobRunUpdate.fixture.json' +import { clearDb } from '../testdatabase' +import { JobRunRepository } from '../../repositories/JobRunRepository' +import { + ENDPOINT, + createRPCRequest, + newChainlinkNode, + sendSingleMessage, +} from '../../support/client' + +const { INVALID_PARAMS } = jayson.Server.errors + +describe('realtime', () => { + let server: Server + let db: Connection + let chainlinkNode: ChainlinkNode + let secret: string + let ws: WebSocket + + beforeAll(async () => { + server = await start() + db = await getDb() + }) + + beforeEach(async () => { + clearDb() + ;[chainlinkNode, secret] = await createChainlinkNode( + db, + 'upsertJobRun test chainlinkNode', + ) + ws = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) + }) + + afterEach(async () => { + ws.close() + }) + + afterAll(done => stop(server, done)) + + describe('#upsertJobRun', () => { + it('can create a job run with valid JSON', async () => { + expect.assertions(3) + + const request = createRPCRequest('upsertJobRun', createFixture) + const response = await sendSingleMessage(ws, request) + expect(response.result).toEqual('success') + + const jobRunCount = await db.manager.count(JobRun) + expect(jobRunCount).toEqual(1) + + const taskRunCount = await db.manager.count(TaskRun) + expect(taskRunCount).toEqual(1) + }) + + it('can create and update a job run and task runs', async () => { + expect.assertions(6) + + const createRequest = createRPCRequest('upsertJobRun', createFixture) + const updateRequest = createRPCRequest('upsertJobRun', updateFixture) + ws.send(JSON.stringify(createRequest)) + + await new Promise(resolve => { + ws.on('message', (data: any) => { + const response = JSON.parse(data) + if (response.id === createRequest.id) { + expect(response.result).toEqual('success') + ws.send(JSON.stringify(updateRequest)) + } + if (response.id === updateRequest.id) { + expect(response.result).toEqual('success') + ws.close() + resolve() + } + }) + }) + + const jobRunCount = await db.manager.count(JobRun) + expect(jobRunCount).toEqual(1) + + const taskRunCount = await db.manager.count(TaskRun) + expect(taskRunCount).toEqual(1) + + const jr = await db.manager.findOne(JobRun) + expect(jr.status).toEqual('completed') + + const tr = jr.taskRuns[0] + expect(tr.status).toEqual('completed') + }) + + it('can create a task run with transactionHash and status', async () => { + expect.assertions(10) + + const request = createRPCRequest('upsertJobRun', ethtxFixture) + const response = await sendSingleMessage(ws, request) + expect(response.result).toEqual('success') + + const jobRunCount = await db.manager.count(JobRun) + expect(jobRunCount).toEqual(1) + + const taskRunCount = await db.manager.count(TaskRun) + expect(taskRunCount).toEqual(4) + + const jobRunRepository = getCustomRepository(JobRunRepository, db.name) + const jr = await jobRunRepository.getFirst() + + expect(jr.status).toEqual('completed') + + const tr = jr.taskRuns[3] + expect(tr.status).toEqual('completed') + expect(tr.transactionHash).toEqual( + '0x1111111111111111111111111111111111111111111111111111111111111111', + ) + expect(tr.timestamp).toEqual(new Date('2018-01-08T18:12:01.103Z')) + expect(tr.blockHeight).toEqual('3735928559') + expect(tr.blockHash).toEqual('0xbadc0de5') + expect(tr.transactionStatus).toEqual('fulfilledRunLog') + }) + + it(`rejects invalid params with code ${INVALID_PARAMS}`, async () => { + expect.assertions(2) + const request = createRPCRequest('upsertJobRun', { invalid: 'params' }) + const response = await sendSingleMessage(ws, request) + expect(response.error.code).toEqual(INVALID_PARAMS) + const count = await db.manager.count(JobRun) + expect(count).toEqual(0) + }) + }) +}) diff --git a/explorer/src/__tests__/server/legacy.test.ts b/explorer/src/__tests__/server/legacy.test.ts new file mode 100644 index 00000000000..d3888b0e466 --- /dev/null +++ b/explorer/src/__tests__/server/legacy.test.ts @@ -0,0 +1,136 @@ +import { Server } from 'http' +import { Connection, getCustomRepository } from 'typeorm' +import WebSocket from 'ws' +import { getDb } from '../../database' +import { ChainlinkNode, createChainlinkNode } from '../../entity/ChainlinkNode' +import { JobRun } from '../../entity/JobRun' +import { TaskRun } from '../../entity/TaskRun' +import { start, stop } from '../../support/server' +import ethtxFixture from '../fixtures/JobRun.ethtx.fixture.json' +import createFixture from '../fixtures/JobRun.fixture.json' +import updateFixture from '../fixtures/JobRunUpdate.fixture.json' +import { clearDb } from '../testdatabase' +import { JobRunRepository } from '../../repositories/JobRunRepository' +import { + ENDPOINT, + newChainlinkNode, + sendSingleMessage, +} from '../../support/client' + +describe('realtime', () => { + let server: Server + let db: Connection + let chainlinkNode: ChainlinkNode + let secret: string + let ws: WebSocket + + beforeAll(async () => { + server = await start() + db = await getDb() + }) + + beforeEach(async () => { + clearDb() + ;[chainlinkNode, secret] = await createChainlinkNode( + db, + 'legacy test chainlinkNode', + ) + ws = await newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) + }) + + afterEach(async () => { + ws.close() + }) + + afterAll(done => stop(server, done)) + + describe('when sending messages in legacy format', () => { + it('can create a job run with valid JSON', async () => { + expect.assertions(3) + + const response = await sendSingleMessage(ws, createFixture) + expect(response.status).toEqual(201) + + const jobRunCount = await db.manager.count(JobRun) + expect(jobRunCount).toEqual(1) + + const taskRunCount = await db.manager.count(TaskRun) + expect(taskRunCount).toEqual(1) + }) + + it('can create and update a job run and task runs', async () => { + expect.assertions(6) + + ws.send(JSON.stringify(createFixture)) + + await new Promise(resolve => { + let responses = 0 + ws.on('message', (data: any) => { + responses += 1 + const response = JSON.parse(data) + + if (responses === 1) { + expect(response.status).toEqual(201) + ws.send(JSON.stringify(updateFixture)) + } + + if (responses === 2) { + expect(response.status).toEqual(201) + ws.close() + resolve() + } + }) + }) + + const jobRunCount = await db.manager.count(JobRun) + expect(jobRunCount).toEqual(1) + + const taskRunCount = await db.manager.count(TaskRun) + expect(taskRunCount).toEqual(1) + + const jr = await db.manager.findOne(JobRun) + expect(jr.status).toEqual('completed') + + const tr = jr.taskRuns[0] + expect(tr.status).toEqual('completed') + }) + + it('can create a task run with transactionHash and status', async () => { + expect.assertions(10) + + const response = await sendSingleMessage(ws, ethtxFixture) + expect(response.status).toEqual(201) + + const jobRunCount = await db.manager.count(JobRun) + expect(jobRunCount).toEqual(1) + + const taskRunCount = await db.manager.count(TaskRun) + expect(taskRunCount).toEqual(4) + + const jobRunRepository = getCustomRepository(JobRunRepository, db.name) + const jr = await jobRunRepository.getFirst() + + expect(jr.status).toEqual('completed') + + const tr = jr.taskRuns[3] + expect(tr.status).toEqual('completed') + expect(tr.transactionHash).toEqual( + '0x1111111111111111111111111111111111111111111111111111111111111111', + ) + expect(tr.timestamp).toEqual(new Date('2018-01-08T18:12:01.103Z')) + expect(tr.blockHeight).toEqual('3735928559') + expect(tr.blockHash).toEqual('0xbadc0de5') + expect(tr.transactionStatus).toEqual('fulfilledRunLog') + ws.close() + }) + + it('rejects malformed json events with code 422', async () => { + expect.assertions(2) + const request = '{invalid json}' + const response = await sendSingleMessage(ws, request) + expect(response.status).toEqual(422) + const count = await db.manager.count(JobRun) + expect(count).toEqual(0) + }) + }) +}) diff --git a/explorer/src/__tests__/server/realtime.test.ts b/explorer/src/__tests__/server/realtime.test.ts new file mode 100644 index 00000000000..457475661e1 --- /dev/null +++ b/explorer/src/__tests__/server/realtime.test.ts @@ -0,0 +1,123 @@ +import { Server } from 'http' +import { Connection } from 'typeorm' +import WebSocket from 'ws' +import jayson from 'jayson' +import { getDb } from '../../database' +import { ChainlinkNode, createChainlinkNode } from '../../entity/ChainlinkNode' +import { start, stop } from '../../support/server' +import { clearDb } from '../testdatabase' +import { NORMAL_CLOSE } from '../../utils/constants' +import { + ENDPOINT, + createRPCRequest, + newChainlinkNode, + sendSingleMessage, +} from '../../support/client' + +const { PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND } = jayson.Server.errors + +describe('realtime', () => { + let server: Server + let db: Connection + let chainlinkNode: ChainlinkNode + let secret: string + + const newAuthenticatedNode = async () => + newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, secret) + + beforeAll(async () => { + server = await start() + db = await getDb() + }) + + beforeEach(async () => { + clearDb() + ;[chainlinkNode, secret] = await createChainlinkNode( + db, + 'realtime test chainlinkNode', + ) + }) + + afterAll(done => stop(server, done)) + + describe('when sending messages in JSON-RPC format', () => { + let ws: WebSocket + + beforeEach(async () => { + ws = await newAuthenticatedNode() + }) + + afterEach(async () => { + ws.close() + }) + + it(`rejects non-existing methods with code ${METHOD_NOT_FOUND}`, async () => { + expect.assertions(1) + const request = createRPCRequest('doesNotExist') + const response = await sendSingleMessage(ws, request) + expect(response.error.code).toEqual(METHOD_NOT_FOUND) + }) + + // this test depends on the presence of "jsonrpc" in the message + // otherwise, the server will attempt to process the message as a + // legacy message and will respond with { status: 422 }. + // This test will be more appropriate once the legacy format is removed. + it(`rejects malformed json with code ${PARSE_ERROR}`, async () => { + expect.assertions(1) + const request = 'jsonrpc invalid' + const response = await sendSingleMessage(ws, request) + expect(response.error.code).toEqual(PARSE_ERROR) + }) + + it(`rejects invalid rpc requests with code ${INVALID_REQUEST}`, async () => { + expect.assertions(1) + const request = { + jsonrpc: '2.0', + function: 'foo', + id: 1, + } + const response = await sendSingleMessage(ws, request) + expect(response.error.code).toEqual(INVALID_REQUEST) + }) + }) + + it('rejects invalid authentication', async done => { + expect.assertions(1) + newChainlinkNode(ENDPOINT, chainlinkNode.accessKey, 'lol-no').catch( + error => { + expect(error).toBeDefined() + done() + }, + ) + }) + + it('rejects multiple connections from single node', async done => { + expect.assertions(8) + + // eslint-disable-next-line prefer-const + let ws1: WebSocket, ws2: WebSocket, ws3: WebSocket + + // eslint-disable-next-line prefer-const + ws1 = await newAuthenticatedNode() + + ws1.addEventListener('close', (event: WebSocket.CloseEvent) => { + expect(ws1.readyState).toBe(WebSocket.CLOSED) + expect(ws2.readyState).toBe(WebSocket.OPEN) + expect(event.code).toBe(NORMAL_CLOSE) + expect(event.reason).toEqual('Duplicate connection opened') + }) + + ws2 = await newAuthenticatedNode() + + ws2.addEventListener('close', (event: WebSocket.CloseEvent) => { + expect(ws2.readyState).toBe(WebSocket.CLOSED) + expect(ws3.readyState).toBe(WebSocket.OPEN) + expect(event.code).toBe(NORMAL_CLOSE) + expect(event.reason).toEqual('Duplicate connection opened') + ws3.close() + done() + }) + + ws3 = await newAuthenticatedNode() + }) +}) diff --git a/explorer/src/entity/JobRun.ts b/explorer/src/entity/JobRun.ts index a433b8ad7eb..906a1e12df5 100644 --- a/explorer/src/entity/JobRun.ts +++ b/explorer/src/entity/JobRun.ts @@ -14,7 +14,7 @@ export class JobRun { @PrimaryGeneratedColumn('uuid') id: string - @Column({ nullable: true }) + @Column() chainlinkNodeId: number @Column() @@ -59,8 +59,7 @@ export class JobRun { chainlinkNode: ChainlinkNode } -export const fromString = (str: string): JobRun => { - const json = JSON.parse(str) +export const fromJSONObject = (json: any): JobRun => { const jr = new JobRun() jr.runId = json.runId jr.jobId = json.jobId @@ -98,6 +97,11 @@ export const fromString = (str: string): JobRun => { return jr } +export const fromString = (str: string): JobRun => { + const json = JSON.parse(str) + return fromJSONObject(json) +} + export const saveJobRunTree = async (db: Connection, jobRun: JobRun) => { await db.manager.transaction(async manager => { let builder = manager.createQueryBuilder() diff --git a/explorer/src/server.ts b/explorer/src/server.ts index 5fb9d352fe5..57d0b274ee8 100644 --- a/explorer/src/server.ts +++ b/explorer/src/server.ts @@ -6,7 +6,7 @@ import cookieSession from 'cookie-session' import adminAuth from './middleware/adminAuth' import * as controllers from './controllers' import { addRequestLogging, logger } from './logging' -import { bootstrapRealtime } from './realtime' +import { bootstrapRealtime } from './server/realtime' import seed from './seed' export const DEFAULT_PORT = parseInt(process.env.SERVER_PORT, 10) || 8080 diff --git a/explorer/src/server/handleMessage.ts b/explorer/src/server/handleMessage.ts new file mode 100644 index 00000000000..1aa6a6529b8 --- /dev/null +++ b/explorer/src/server/handleMessage.ts @@ -0,0 +1,52 @@ +import rpcServer from './rpcServer' +import { logger } from '../logging' +import { getDb } from '../database' +import { fromString, saveJobRunTree } from '../entity/JobRun' +import jayson from 'jayson' + +export interface ServerContext { + chainlinkNodeId: number +} + +// legacy server response synonymous with upsertJobRun RPC method +const handleLegacy = async (json: string, context: ServerContext) => { + try { + const db = await getDb() + const jobRun = fromString(json) + jobRun.chainlinkNodeId = context.chainlinkNodeId + await saveJobRunTree(db, jobRun) + return { status: 201 } + } catch (e) { + logger.error(e) + return { status: 422 } + } +} + +const handleJSONRCP = (request: string, context: ServerContext) => { + return new Promise(resolve => { + rpcServer.call( + request, + context, + (error: jayson.JSONRPCErrorLike, result: jayson.JSONRPCResultLike) => { + // resolve both errored and successful responses + if (error) { + logger.error(error.message) + resolve(error) + } else { + resolve(result) + } + }, + ) + }) +} + +export const handleMessage = async ( + message: string, + context: ServerContext, +) => { + if (message.includes('jsonrpc')) { + return await handleJSONRCP(message, context) + } else { + return await handleLegacy(message, context) + } +} diff --git a/explorer/src/realtime.ts b/explorer/src/server/realtime.ts similarity index 79% rename from explorer/src/realtime.ts rename to explorer/src/server/realtime.ts index c2dab3a9669..3c10e52a25d 100644 --- a/explorer/src/realtime.ts +++ b/explorer/src/server/realtime.ts @@ -1,33 +1,15 @@ import http from 'http' -import { fromString, saveJobRunTree } from './entity/JobRun' -import { logger } from './logging' +import { logger } from '../logging' import WebSocket from 'ws' -import { Connection } from 'typeorm' -import { getDb } from './database' -import { authenticate } from './sessions' -import { closeSession, Session } from './entity/Session' +import { getDb } from '../database' +import { authenticate } from '../sessions' +import { closeSession, Session } from '../entity/Session' +import { handleMessage } from './handleMessage' import { ACCESS_KEY_HEADER, NORMAL_CLOSE, SECRET_HEADER, -} from './utils/constants' - -const handleMessage = async ( - message: string, - chainlinkNodeId: number, - db: Connection, -) => { - try { - const jobRun = fromString(message) - jobRun.chainlinkNodeId = chainlinkNodeId - - await saveJobRunTree(db, jobRun) - return { status: 201 } - } catch (e) { - logger.error(e) - return { status: 422 } - } -} +} from '../utils/constants' export const bootstrapRealtime = async (server: http.Server) => { const db = await getDb() @@ -96,11 +78,10 @@ export const bootstrapRealtime = async (server: http.Server) => { return } - const result = await handleMessage( - message as string, - session.chainlinkNodeId, - db, - ) + const result = await handleMessage(message as string, { + chainlinkNodeId: session.chainlinkNodeId, + }) + ws.send(JSON.stringify(result)) }) diff --git a/explorer/src/server/rpcMethods/index.ts b/explorer/src/server/rpcMethods/index.ts new file mode 100644 index 00000000000..999ad566819 --- /dev/null +++ b/explorer/src/server/rpcMethods/index.ts @@ -0,0 +1 @@ +export { default as upsertJobRun } from './upsertJobRun' diff --git a/explorer/src/server/rpcMethods/upsertJobRun.ts b/explorer/src/server/rpcMethods/upsertJobRun.ts new file mode 100644 index 00000000000..0b3f896cbeb --- /dev/null +++ b/explorer/src/server/rpcMethods/upsertJobRun.ts @@ -0,0 +1,26 @@ +import { fromJSONObject, saveJobRunTree } from '../../entity/JobRun' +import { logger } from '../../logging' +import { getDb } from '../../database' +import { ServerContext } from './../handleMessage' +import jayson from 'jayson' + +// default invalid params error +const { INVALID_PARAMS } = jayson.Server.errors +const invalidParamsError = new jayson.Server().error(INVALID_PARAMS) + +export default async ( + payload: object, + context: ServerContext, + callback: jayson.JSONRPCCallbackTypePlain, +) => { + try { + const db = await getDb() + const jobRun = fromJSONObject(payload) + jobRun.chainlinkNodeId = context.chainlinkNodeId + await saveJobRunTree(db, jobRun) + callback(null, 'success') + } catch (e) { + logger.error(e) + callback(invalidParamsError) + } +} diff --git a/explorer/src/server/rpcServer.ts b/explorer/src/server/rpcServer.ts new file mode 100644 index 00000000000..33db0adc3c2 --- /dev/null +++ b/explorer/src/server/rpcServer.ts @@ -0,0 +1,8 @@ +import jayson from 'jayson' +import * as rpcMethods from './rpcMethods' + +const serverOptions = { + useContext: true, // permits passing extra data object to RPC methods as 'server context' +} + +export default new jayson.Server(rpcMethods, serverOptions) diff --git a/explorer/src/support/client.ts b/explorer/src/support/client.ts new file mode 100644 index 00000000000..83e768bb636 --- /dev/null +++ b/explorer/src/support/client.ts @@ -0,0 +1,49 @@ +import WebSocket from 'ws' +import jayson from 'jayson' +import { DEFAULT_TEST_PORT } from './server' +import { ACCESS_KEY_HEADER, SECRET_HEADER } from '../utils/constants' + +export const ENDPOINT = `ws://localhost:${DEFAULT_TEST_PORT}` + +export const newChainlinkNode = ( + url: string, + accessKey: string, + secret: string, +): Promise => { + const ws = new WebSocket(ENDPOINT, { + headers: { + [ACCESS_KEY_HEADER]: accessKey, + [SECRET_HEADER]: secret, + }, + }) + + return new Promise((resolve: (arg0: WebSocket) => void, reject) => { + ws.on('error', (error: Error) => { + reject(error) + }) + + ws.on('open', () => resolve(ws)) + }) +} + +const jsonClient = new jayson.Client(null, null) +export const createRPCRequest = ( + method: string, + params?: jayson.RequestParamsLike, +) => jsonClient.request(method, params) + +// helper function that sends a message and only resolves once the +// rsponse is received +export const sendSingleMessage = ( + ws: WebSocket, + request: string | object, +): Promise => + new Promise(resolve => { + const requestData: string = + typeof request === 'object' ? JSON.stringify(request) : request + ws.send(requestData) + ws.on('message', async (data: string) => { + const response = JSON.parse(data) + resolve(response) + }) + }) diff --git a/explorer/tsconfig.json b/explorer/tsconfig.json index 407e608672f..826c0de4d09 100644 --- a/explorer/tsconfig.json +++ b/explorer/tsconfig.json @@ -15,5 +15,5 @@ "baseUrl": ".", "typeRoots": ["node_modules/@types", "../node_modules/@types", "@types"] }, - "include": ["src/**/*", "e2e/**/*"] + "include": ["src/**/*"] } diff --git a/go.mod b/go.mod index 90ea062d4c4..66cdda3b01a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/smartcontractkit/chainlink +module chainlink go 1.12 @@ -28,7 +28,6 @@ require ( github.com/gobuffalo/packr v1.30.1 github.com/gofrs/flock v0.7.1 github.com/gofrs/uuid v3.2.0+incompatible - github.com/golang/mock v1.3.1 github.com/google/uuid v1.1.0 // indirect github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.0 @@ -37,24 +36,23 @@ require ( github.com/jpillora/backoff v0.0.0-20170918002102-8eab2debe79d github.com/manyminds/api2go v0.0.0-20171030193247-e7b693844a6f github.com/mattn/go-colorable v0.0.9 // indirect - github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/mrwonko/cron v0.0.0-20180828170130-e0ddd0f7e7db - github.com/olekukonko/tablewriter v0.0.1 - github.com/onsi/gomega v1.7.0 + github.com/olekukonko/tablewriter v0.0.2 + github.com/onsi/gomega v1.7.1 github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect github.com/pkg/errors v0.8.1 github.com/rjeczalik/notify v0.9.2 // indirect github.com/rs/cors v1.6.0 // indirect github.com/satori/go.uuid v1.2.0 github.com/spf13/afero v1.2.1 // indirect - github.com/spf13/viper v1.4.0 + github.com/spf13/viper v1.5.0 github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 // indirect github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3 // indirect - github.com/stretchr/testify v1.3.1-0.20190712000136-221dbe5ed467 + github.com/stretchr/testify v1.4.0 github.com/syndtr/goleveldb v0.0.0-20190203031304-2f17a3356c66 // indirect github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 - github.com/tidwall/gjson v1.3.2 + github.com/tidwall/gjson v1.3.4 github.com/tidwall/sjson v1.0.4 github.com/ugorji/go/codec v1.1.7 github.com/ulule/limiter v0.0.0-20190417201358-7873d115fc4e @@ -62,9 +60,9 @@ require ( github.com/urfave/cli v1.22.1 github.com/willf/pad v0.0.0-20190207183901-eccfe5d84172 go.dedis.ch/fixbuf v1.0.3 - go.dedis.ch/kyber/v3 v3.0.7 - go.uber.org/multierr v1.2.0 - go.uber.org/zap v1.11.0 + go.dedis.ch/kyber/v3 v3.0.9 + go.uber.org/multierr v1.4.0 + go.uber.org/zap v1.12.0 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 diff --git a/go.sum b/go.sum index 1058ebc32d7..2c924fc9816 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,6 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v0.0.0-20170307001533-c9c7427a2a70/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -145,6 +143,7 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s= github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -211,6 +210,8 @@ github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manyminds/api2go v0.0.0-20171030193247-e7b693844a6f h1:tVvGiZQFjOXP+9YyGqSA6jE55x1XVxmoPYudncxrZ8U= github.com/manyminds/api2go v0.0.0-20171030193247-e7b693844a6f/go.mod h1:Z60vy0EZVSu0bOugCHdcN5ZxFMKSpjRgsnh0XKPFqqk= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= @@ -239,12 +240,14 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2 h1:sq53g+DWf0J6/ceFUHpQ0nAEb6WgM++fq16MZ91cS6o= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 h1:zNBQb37RGLmJybyMcs983HfUfpkw9OTFD9tbBfAViHE= github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= @@ -304,8 +307,8 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4= +github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4= github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 h1:gIlAHnH1vJb5vwEjIp5kBj/eu99p/bl0Ay2goiPe5xE= github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw= github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3 h1:njlZPzLwU639dk2kqnCPPv+wNjq7Xb6EfUxe/oX0/NM= @@ -317,14 +320,16 @@ github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRci github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.3.1-0.20190712000136-221dbe5ed467 h1:/pva5wyh0PKqe0bnHBbndEzbqsilMKFNXI0GPbO+L8c= -github.com/stretchr/testify v1.3.1-0.20190712000136-221dbe5ed467/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v0.0.0-20190203031304-2f17a3356c66 h1:AwmkkZT+TucFotNCL+aNJ/0KCMsRtlXN9fs8uoOMSRk= github.com/syndtr/goleveldb v0.0.0-20190203031304-2f17a3356c66/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 h1:hNna6Fi0eP1f2sMBe/rJicDmaHmoXGe1Ta84FPYHLuE= github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0= -github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI= -github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/gjson v1.3.4 h1:On5waDnyKKk3SWE4EthbjjirAWXp43xx5cKCUZY1eZw= +github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= @@ -355,8 +360,8 @@ go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= go.dedis.ch/kyber/v3 v3.0.4 h1:FDuC/S3STkvwxZ0ooo3gcp56QkUKsN7Jy7cpzBxL+vQ= go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= -go.dedis.ch/kyber/v3 v3.0.7 h1:yBaDckJWBkkK2xChU+vXotLqQareeRkfwY++GSwLkOI= -go.dedis.ch/kyber/v3 v3.0.7/go.mod h1:V1z0JihG9+dUEUCKLI9j9tjnlIflBw3wx8UOg0g3Pnk= +go.dedis.ch/kyber/v3 v3.0.9 h1:i0ZbOQocHUjfFasBiUql5zVeC7u/vahFd96DFA8UOWk= +go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= go.dedis.ch/protobuf v1.0.5 h1:EbF1czEKICxf5KY8Tm7wMF28hcOQbB6yk4IybIFWTYE= go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= go.dedis.ch/protobuf v1.0.7 h1:wRUEiq3u0/vBhLjcw9CmAVrol+BnDyq2M0XLukdphyI= @@ -365,14 +370,19 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.2.0 h1:6I+W7f5VwC5SV9dNrZ3qXrDB9mD0dyGOi/ZJmYw03T4= -go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.11.0 h1:gSmpCfs+R47a4yQPAI4xJ0IPDLTRGXskm6UelqNXpqE= -go.uber.org/zap v1.11.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.12.0 h1:dySoUQPFBGj6xwjmBzageVL8jGi8uxc6bEmJQjA06bw= +go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -380,6 +390,7 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -388,6 +399,9 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20170324220409-6c2325251549/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -404,6 +418,8 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -444,8 +460,12 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -485,6 +505,10 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/integration-scripts/compiler.json b/integration-scripts/compiler.json new file mode 100644 index 00000000000..d5b3579a0a1 --- /dev/null +++ b/integration-scripts/compiler.json @@ -0,0 +1,7 @@ +{ + "contractsDir": "contracts", + "artifactsDir": "dist/artifacts", + "contracts": "*", + "solcVersion": "0.4.24", + "useDockerisedSolc": false +} diff --git a/integration-scripts/contracts/Oracle.sol b/integration-scripts/contracts/Oracle.sol deleted file mode 100644 index b86c03710d4..00000000000 --- a/integration-scripts/contracts/Oracle.sol +++ /dev/null @@ -1,3 +0,0 @@ -pragma solidity 0.4.24; - -import "../../evm/contracts/Oracle.sol"; diff --git a/integration-scripts/contracts/RunLog.sol b/integration-scripts/contracts/RunLog.sol index 5d6aa0b1407..95d5ecc54e0 100644 --- a/integration-scripts/contracts/RunLog.sol +++ b/integration-scripts/contracts/RunLog.sol @@ -1,6 +1,6 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; -import "../../evm/contracts/Chainlinked.sol"; +import "chainlink/contracts/Chainlinked.sol"; contract RunLog is Chainlinked { uint256 constant private ORACLE_PAYMENT = 1 * LINK; // solium-disable-line zeppelin/no-arithmetic-operations diff --git a/integration-scripts/package.json b/integration-scripts/package.json index ad356ef238d..df77e8edced 100644 --- a/integration-scripts/package.json +++ b/integration-scripts/package.json @@ -7,11 +7,9 @@ "license": "MIT", "private": true, "scripts": { - "generate-typings": "echo \"\u001b[1;33mWARN: Please import the generated contract factory classes for LinkTokenV05, Coordinator and MeanAggregator directly from the chainlinkv0.5 package when exported.\u001b[0m\" && typechain --target ethers --outDir src/generated \"{build/contracts/*,../evm/v0.5/test/support/LinkTokenV05.json,../evm/v0.5/build/contracts/Coordinator.json,../evm/v0.5/build/contracts/MeanAggregator.json}\"", - "build:contracts": "truffle compile", - "postbuild:contracts": "yarn generate-typings", - "prebuild": "yarn build:contracts", - "build": "tsc", + "generate-typings": "typechain --target ethers --outDir src/generated \"dist/artifacts/*\"", + "postgenerate-typings": "yarn workspace chainlink export-typings ../integration-scripts/src/generated ../integration-scripts/dist/src/generated", + "build": "sol-compiler && yarn generate-typings && tsc", "setup": "yarn build", "format": "prettier --write \"src/**/*\"", "lint": "eslint --ext .ts src", @@ -25,33 +23,34 @@ "initiate-service-agreement": "node ./dist/initiateServiceAgreement", "start-echo-server": "node ./dist/echoServer" }, - "devDependencies": { - "@chainlink/eslint-config": "0.0.1", - "@chainlink/prettier-config": "^0.0.1", - "@types/body-parser": "^1.17.1", - "@types/express": "^4.17.1", - "@types/shelljs": "^0.8.5", - "debug": "4.1.1", - "eslint": "6.3.0", - "typechain": "1.0.1", - "typechain-target-ethers": "^1.0.0-beta.1", - "typescript": "^3.6.3" - }, "dependencies": { "@0x/sol-compiler": "^3.1.15", "@0x/sol-trace": "^2.0.20", "@chainlink/eslint-config": "0.0.1", "@chainlink/prettier-config": "^0.0.1", "body-parser": "^1.18.3", - "chainlink": "0.7.7", + "chainlink": "0.7.8", + "chainlinkv0.5": "0.0.2", "chalk": "^2.4.2", "ethers": "^4.0.37", "express": "^4.16.4", "link_token": "^1.0.6", "request-promise": "4.2.4", "shelljs": "^0.8.3", - "source-map-support": "^0.5.13", - "truffle": "^5.0.39" + "source-map-support": "^0.5.13" + }, + "devDependencies": { + "@chainlink/eslint-config": "0.0.1", + "@chainlink/prettier-config": "^0.0.1", + "@types/body-parser": "^1.17.1", + "@types/express": "^4.17.1", + "@types/shelljs": "^0.8.5", + "debug": "4.1.1", + "eslint": "6.3.0", + "typechain": "1.0.3", + "typechain-target-ethers": "^1.0.1", + "typescript": "^3.6.3", + "@0x/sol-compiler": "^3.1.15" }, "prettier": "@chainlink/prettier-config" } diff --git a/integration-scripts/src/deployContracts.ts b/integration-scripts/src/deployContracts.ts index 98ad3ffc688..31935ddc005 100644 --- a/integration-scripts/src/deployContracts.ts +++ b/integration-scripts/src/deployContracts.ts @@ -1,4 +1,4 @@ -import { OracleFactory } from './generated/OracleFactory' +import { generated as chainlink } from 'chainlink' import { createProvider, DEVNET_ADDRESS, @@ -6,9 +6,8 @@ import { getArgs, deployContract, } from './common' -import { EthLogFactory } from './generated/EthLogFactory' -import { RunLogFactory } from './generated/RunLogFactory' import { deployLinkTokenContract } from './deployLinkTokenContract' +import { EthLogFactory, RunLogFactory } from './generated' async function main() { registerPromiseHandler() @@ -28,7 +27,7 @@ async function deployContracts({ chainlinkNodeAddress }: Args) { const linkToken = await deployLinkTokenContract() const oracle = await deployContract( - { Factory: OracleFactory, name: 'Oracle', signer }, + { Factory: chainlink.OracleFactory, name: 'Oracle', signer }, linkToken.address, ) await oracle.setFulfillmentPermission(chainlinkNodeAddress, true) diff --git a/integration-scripts/src/deployLinkTokenContract.ts b/integration-scripts/src/deployLinkTokenContract.ts index 00d07a35dc2..1aa549b35a9 100644 --- a/integration-scripts/src/deployLinkTokenContract.ts +++ b/integration-scripts/src/deployLinkTokenContract.ts @@ -1,9 +1,9 @@ import { createProvider, DEVNET_ADDRESS, deployContract } from './common' -import { LinkTokenFactory } from 'chainlink/dist/src/generated/LinkTokenFactory' -import { Instance } from 'chainlink/dist/src/contract' +import { contract, generated as chainlink } from 'chainlink' +const { LinkTokenFactory } = chainlink export async function deployLinkTokenContract(): Promise< - Instance + contract.Instance > { const provider = createProvider() const signer = provider.getSigner(DEVNET_ADDRESS) diff --git a/integration-scripts/src/deployV0.5Contracts.ts b/integration-scripts/src/deployV0.5Contracts.ts index 1b80c0dbd2d..a1e87e364e0 100644 --- a/integration-scripts/src/deployV0.5Contracts.ts +++ b/integration-scripts/src/deployV0.5Contracts.ts @@ -1,5 +1,4 @@ -import { CoordinatorFactory } from './generated/CoordinatorFactory' -import { MeanAggregatorFactory } from './generated/MeanAggregatorFactory' +import { generated as chainlink } from 'chainlinkv0.5' import { registerPromiseHandler, DEVNET_ADDRESS, @@ -7,6 +6,7 @@ import { deployContract, } from './common' import { deployLinkTokenContract } from './deployLinkTokenContract' +const { CoordinatorFactory, MeanAggregatorFactory } = chainlink async function main() { registerPromiseHandler() diff --git a/integration-scripts/src/initiateServiceAgreement.ts b/integration-scripts/src/initiateServiceAgreement.ts index 39092817c32..0d08c2c2ae7 100644 --- a/integration-scripts/src/initiateServiceAgreement.ts +++ b/integration-scripts/src/initiateServiceAgreement.ts @@ -1,3 +1,5 @@ +import { generated as chainlink } from 'chainlinkv0.5' +import { contract } from 'chainlink' import { helpers } from 'chainlink' import { getArgs, @@ -5,11 +7,12 @@ import { DEVNET_ADDRESS, createProvider, } from './common' -import { CoordinatorFactory } from './generated/CoordinatorFactory' import { ethers } from 'ethers' -import { Coordinator } from './generated/Coordinator' +const { CoordinatorFactory } = chainlink -type CoordinatorParams = Parameters +type CoordinatorParams = Parameters< + contract.Instance['initiateServiceAgreement'] +> type ServiceAgreement = CoordinatorParams[0] type OracleSignatures = CoordinatorParams[1] diff --git a/integration-scripts/src/sendEthlogTransaction.ts b/integration-scripts/src/sendEthlogTransaction.ts index 1bbf12ec4c9..41ebab60362 100755 --- a/integration-scripts/src/sendEthlogTransaction.ts +++ b/integration-scripts/src/sendEthlogTransaction.ts @@ -1,5 +1,5 @@ import url from 'url' -import { EthLogFactory } from './generated/EthLogFactory' +import { EthLogFactory } from './generated' import { createProvider, getArgs, diff --git a/integration-scripts/src/sendRunlogTransaction.ts b/integration-scripts/src/sendRunlogTransaction.ts index 085da71de85..d3e4369632d 100755 --- a/integration-scripts/src/sendRunlogTransaction.ts +++ b/integration-scripts/src/sendRunlogTransaction.ts @@ -1,5 +1,5 @@ -import { RunLogFactory } from './generated/RunLogFactory' -import { LinkTokenFactory } from 'chainlink/dist/src/generated/LinkTokenFactory' +import { RunLogFactory } from './generated' +import { generated as chainlink } from 'chainlink' import { RunLog } from './generated/RunLog' import { ethers } from 'ethers' import url from 'url' @@ -46,7 +46,7 @@ async function sendRunlogTransaction({ const signer = provider.getSigner(DEVNET_ADDRESS) const runLogFactory = new RunLogFactory(signer) - const linkTokenFactory = new LinkTokenFactory(signer) + const linkTokenFactory = new chainlink.LinkTokenFactory(signer) const runLog = runLogFactory.attach(runLogAddress) const linkToken = linkTokenFactory.attach(linkTokenAddress) diff --git a/integration-scripts/truffle.js b/integration-scripts/truffle.js deleted file mode 100644 index 43f375d8d7b..00000000000 --- a/integration-scripts/truffle.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -module.exports = { - compilers: { - solc: { - version: '0.4.24', - }, - }, - networks: { - test: { - host: '127.0.0.1', - port: process.env.ETH_HTTP_PORT || 18545, - network_id: '*', - gas: 4700000, - gasPrice: 5e9, - }, - }, -} diff --git a/integration/common b/integration/common index 40338a8e7d4..7f28c10e5e5 100644 --- a/integration/common +++ b/integration/common @@ -43,7 +43,7 @@ launch_gethnet() { printf -- "[\033[31mstopped\033[0m]\n" title "Starting geth..." - $SRCROOT/tools/bin/gethnet &>$SRCROOT/integration/gethnet.log & + $SRCROOT/tools/bin/gethnet &>$SRCROOT/integration/logs/gethnet.log & waitForResponse $ETH_HTTP_URL title "Geth is running." } @@ -57,7 +57,7 @@ launch_parity() { printf -- "[\033[31mstopped\033[0m]\n" title "Starting parity..." - $SRCROOT/tools/bin/devnet &>$SRCROOT/integration/devnet.log & + $SRCROOT/tools/bin/devnet &>$SRCROOT/integration/logs/devnet.log & waitForResponse $ETH_HTTP_URL title "Parity is running." } @@ -68,7 +68,7 @@ install_chainlink() { fi title "Making chainlink..." - make install &>$SRCROOT/integration/make.log + make install &>$SRCROOT/integration/logs/make.log } launch_chainlink() { @@ -88,15 +88,15 @@ launch_chainlink() { trap "rm -rf $clroot" EXIT HUP TERM INT cp $SRCROOT/tools/clroot/{password.txt,apicredentials} $clroot/ echo running chainlink from ${clroot} - chainlink node start -d -p $clroot/password.txt -a $clroot/apicredentials &>$SRCROOT/integration/chainlink.log & + chainlink node start -d -p $clroot/password.txt -a $clroot/apicredentials &>$SRCROOT/integration/logs/chainlink.log & waitForResponse $chainlink_url title "Chainlink is running." - waitFor "grep 'Unlocked account' '$SRCROOT/integration/chainlink.log'" 10 - export CHAINLINK_NODE_ADDRESS=`cat $SRCROOT/integration/chainlink.log | grep 'Unlocked account' | awk '{print$5}'` + waitFor "grep 'Unlocked account' '$SRCROOT/integration/logs/chainlink.log'" 10 + export CHAINLINK_NODE_ADDRESS=`cat $SRCROOT/integration/logs/chainlink.log | grep 'Unlocked account' | awk '{print$5}'` - yarn workspace @chainlink/integration-scripts fund-address > $SRCROOT/integration/fund_address.log + yarn workspace @chainlink/integration-scripts fund-address > $SRCROOT/integration/logs/fund_address.log } explorer_url="http://127.0.0.1:8080" @@ -110,8 +110,8 @@ launch_explorer() { printf -- "[\033[31mstopped\033[0m]\n" title "Starting explorer..." - yarn workspace @chainlink/explorer run build &>$SRCROOT/integration/explorer-yarn.log - yarn workspace @chainlink/explorer run prod &>$SRCROOT/integration/explorer.log & waitForResponse $explorer_url + yarn workspace @chainlink/explorer run build &>$SRCROOT/integration/logs/explorer-yarn.log + yarn workspace @chainlink/explorer run prod &>$SRCROOT/integration/logs/explorer.log & waitForResponse $explorer_url title "Explorer is running." } @@ -128,7 +128,7 @@ add_clnode_to_explorer() { } setup_scripts() { - yarn --no-progress install &>$SRCROOT/integration/yarn.log + yarn --no-progress install &>$SRCROOT/integration/logs/yarn.log yarn workspace chainlinkv0.5 build yarn workspace chainlink build yarn workspace @chainlink/integration-scripts setup @@ -139,18 +139,18 @@ deploy_contracts() { pushd integration >/dev/null # run migrations - yarn workspace @chainlink/integration-scripts deploy-contracts | tee $SRCROOT/integration/deploy.log - export LINK_TOKEN_ADDRESS=`cat $SRCROOT/integration/deploy.log | grep 'Deployed LinkToken at:' | awk '{print$4}'` - export ORACLE_CONTRACT_ADDRESS=`cat $SRCROOT/integration/deploy.log | grep 'Deployed Oracle at:' | awk '{print$4}'` - export ETH_LOG_ADDRESS=`cat $SRCROOT/integration/deploy.log | grep 'Deployed EthLog at:' | awk '{print$4}'` - export RUN_LOG_ADDRESS=`cat $SRCROOT/integration/deploy.log | grep 'Deployed RunLog at:' | awk '{print$4}'` + yarn workspace @chainlink/integration-scripts deploy-contracts | tee $SRCROOT/integration/logs/deploy.log + export LINK_TOKEN_ADDRESS=`cat $SRCROOT/integration/logs/deploy.log | grep 'Deployed LinkToken at:' | awk '{print$4}'` + export ORACLE_CONTRACT_ADDRESS=`cat $SRCROOT/integration/logs/deploy.log | grep 'Deployed Oracle at:' | awk '{print$4}'` + export ETH_LOG_ADDRESS=`cat $SRCROOT/integration/logs/deploy.log | grep 'Deployed EthLog at:' | awk '{print$4}'` + export RUN_LOG_ADDRESS=`cat $SRCROOT/integration/logs/deploy.log | grep 'Deployed RunLog at:' | awk '{print$4}'` echo "RunLog address: $RUN_LOG_ADDRESS" popd >/dev/null title "Migration complete." } deploy_v05_contracts() { - log_path=$SRCROOT/integration/initiate-service-agreement.log + log_path=$SRCROOT/integration/logs/initiate-service-agreement.log yarn workspace @chainlink/integration-scripts deploy-v0.5-contracts | tee $log_path export LINK_TOKEN_ADDRESS=`cat $log_path | grep 'Deployed LinkToken at:' | awk '{print$4}'` export COORDINATOR_ADDRESS=`cat $log_path | grep 'Deployed Coordinator at:' | awk '{print$4}'` @@ -167,7 +167,7 @@ launch_echo_server() { title "Starting echo server..." pushd integration >/dev/null - yarn workspace @chainlink/integration-scripts start-echo-server "$ECHO_SERVER_PORT" &>$SRCROOT/integration/echo-server.log & + yarn workspace @chainlink/integration-scripts start-echo-server "$ECHO_SERVER_PORT" &>$SRCROOT/integration/logs/echo-server.log & waitForResponse $ECHO_SERVER_URL popd >/dev/null diff --git a/integration/cypress/integration/createAndRunJob.spec.ts b/integration/cypress/integration/createAndRunJob.spec.ts index 906ab7480d7..46c01fef92b 100644 --- a/integration/cypress/integration/createAndRunJob.spec.ts +++ b/integration/cypress/integration/createAndRunJob.spec.ts @@ -38,6 +38,7 @@ context('End to end', function() { cy.clickButton('Search') cy.get('@runId').then(runId => { cy.clickLink(runId) + cy.contains(runId).should('exist') }) cy.contains('h5', 'Complete').should('exist') }) diff --git a/integration/ethlog_test b/integration/ethlog_test index a2e0e476eff..4a8a48dde8e 100755 --- a/integration/ethlog_test +++ b/integration/ethlog_test @@ -26,4 +26,4 @@ assert "Jobs count" "chainlink -j jobs list | jq length" $expected_job_count assert "EthLog Runs count" "chainlink -j runs list --jobid $jid | jq length" 1 # Check that the run completed -assert "Run completed" 'chainlink -j runs list --jobid $jid | jq ".[].result.status" | sed s/\"//g' completed +assert "Run completed" 'chainlink -j runs list --jobid $jid | jq ".[].status" | sed s/\"//g' completed diff --git a/integration/forks/docker-compose.yaml b/integration/forks/docker-compose.yaml index c8c7a201010..c406762298a 100644 --- a/integration/forks/docker-compose.yaml +++ b/integration/forks/docker-compose.yaml @@ -3,7 +3,7 @@ # If you modify this, make sure to make corresponding modifications to the scripts in forks/scripts. # Geth is bad about letting you know when it's getting inconsistent inputs, so be careful. -version: '3.7' +version: "3.7" services: geth: @@ -26,6 +26,17 @@ services: - password - "0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f" + # imports the 0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f key into the CL db + chainlink_key_import: + image: smartcontract/chainlink + command: local import /run/secrets/0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f + volumes: + - ./tmp/clroot/:/clroot/ + environment: + - ROOT=/clroot + secrets: + - "0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f" + chainlink: image: smartcontract/chainlink container_name: forks_chainlink @@ -41,9 +52,12 @@ services: - MIN_OUTGOING_CONFIRMATIONS=2 - MINIMUM_CONTRACT_PAYMENT=1000000000000 - CHAINLINK_DEV=true + - CHAINLINK_TLS_PORT=0 networks: gethnet: ipv4_address: 172.16.1.102 + depends_on: + - chainlink_key_import expose: - 6688 secrets: diff --git a/integration/forks/test b/integration/forks/test index a858d89c58b..ece6532cf0b 100755 --- a/integration/forks/test +++ b/integration/forks/test @@ -1,5 +1,12 @@ #!/bin/bash +# save logs if script errors and exits early +chain_number=1 +function on_exit { + save_logs $chain_number +} +trap on_exit EXIT + # set this directory as working directory cd "$(dirname "$0")" # import test helpers @@ -9,7 +16,7 @@ source ./test_helpers initial_setup mkdir -p logs -# Runs the first chain, where the ethlog contract is deployed +# run the first chain, where the ethlog contract is deployed printf "\nSTARTING CHAIN 1\n" # make sure chainlink has actually started receiving blocks from geth search_chainlink_logs 'Received new head' @@ -17,31 +24,32 @@ search_chainlink_logs 'Received new head' create_contract # wait for chainlink to get notified about transaction search_chainlink_logs 'New run triggered by ethlog' -search_chainlink_logs 'Run cannot continue because it lacks sufficient confirmations' -# save log -docker-compose logs chainlink > logs/chain_1.log -docker-compose logs geth > logs/geth.log -# tear down network before sufficient confirmations can be reached -docker-compose down +search_chainlink_logs 'Pausing run pending confirmations' +# stop mining before sufficient confirmations can be reached +docker-compose stop # assert that nothing has been uncled yet assert_not_in_chainlink_logs 'presumably has been uncled' +# tear down +save_logs 1 +docker-compose down # create 2nd chain that is longer than first chain. Job should be uncled, not run printf "\nSTARTING CHAIN 2\n" +chain_number=2 start_network # 2nd chain should be younger than first, and so chainlink won't immediately save new heads search_chainlink_logs 'Cannot save new head confirmation' # when 2nd chain gets longer, chainlink resumes saving heads -search_chainlink_logs 'New head resuming run' +search_chainlink_logs 'New connection resuming run' # will wait for head # to be 10 more than block # with contract creation -search_chainlink_logs 'Run cannot continue because it lacks sufficient confirmations services' +search_chainlink_logs 'Cannot save new head confirmation' # should eventually abort running running job search_chainlink_logs 'presumably has been uncled' -# save log -docker-compose logs chainlink > logs/chain_2.log -docker-compose logs geth >> logs/geth.log +# assert job was never run +docker-compose stop +assert_not_in_chainlink_logs 'All tasks complete for run' # tear down +save_logs 2 docker-compose down -assert_not_in_chainlink_logs 'All tasks complete for run' echo "test passed!" diff --git a/integration/forks/test_helpers b/integration/forks/test_helpers index e656d06cb2a..a3b0a231b75 100644 --- a/integration/forks/test_helpers +++ b/integration/forks/test_helpers @@ -30,7 +30,7 @@ create_job() { create_contract() { CONTRACT_DATA=`cat fixtures/create_contract.json` - curl \ + docker run --network forks_gethnet --ip 172.16.1.199 --rm pstauffer/curl:latest curl \ -X POST \ -H "Content-Type: application/json" \ --data "$CONTRACT_DATA" \ @@ -41,11 +41,11 @@ initial_setup() { tear_down build_images start_network - waitForResponse "172.16.1.102:6688" + waitFor "docker run --network forks_gethnet --ip 172.16.1.199 --rm pstauffer/curl:latest curl -s 172.16.1.102:6688 2>/dev/null" + assert_testing_correct_artifact create_job } - assert_not_in_chainlink_logs() { echo "asserting \"$1\" not present ..." num_found=`docker-compose logs chainlink | grep -c "$1"` @@ -56,6 +56,12 @@ assert_not_in_chainlink_logs() { fi; } +assert_testing_correct_artifact() { + version="`cat ../../VERSION`" + sha="`git rev-parse HEAD`" + search_chainlink_logs "Starting Chainlink Node ${version} at commit ${sha}" +} + search_chainlink_logs() { echo "searching for \"$1\" ... " check_count=0; @@ -70,3 +76,9 @@ search_chainlink_logs() { sleep 1; done } + +save_logs() { + echo "saving logs for chain $1" + docker-compose logs chainlink > "logs/chain_$1.log" + docker-compose logs geth >> "logs/geth_$1.log" +} diff --git a/examples/uptime_sla/.node-xmlhttprequest-sync-78767 b/integration/logs/.gitkeep similarity index 100% rename from examples/uptime_sla/.node-xmlhttprequest-sync-78767 rename to integration/logs/.gitkeep diff --git a/integration/package.json b/integration/package.json index e04fc273b87..881ca491ffb 100644 --- a/integration/package.json +++ b/integration/package.json @@ -14,30 +14,20 @@ "test:cypress": "cross-env NODE_ENV=test cypress run", "test:forks": "./forks/test" }, - "dependencies": { - "chainlink": "0.6.1" - }, + "dependencies": {}, "devDependencies": { "@chainlink/eslint-config": "0.0.1", "@chainlink/prettier-config": "0.0.1", "@cypress/webpack-preprocessor": "^4.1.0", - "@types/node": "^12.7.5", - "babel-jest": "^24.1.0", - "command-line-args": "^5.1.1", - "cross-env": "^6.0.0", + "cross-env": "^6.0.3", "cypress": "^3.4.1", "depcheck": "^0.8.3", "eslint": "^6.3.0", - "ethers": "^4.0.36", "prettier": "^1.18.2", - "request-promise": "^4.2.4", - "solc": "0.4.24", - "truffle": "^5.0.25", "ts-loader": "^6.2.1", "ts-node": "^8.4.1", "typescript": "^3.6.3", - "webpack": "^4.41.1", - "webpack-cli": "^3.3.9" + "webpack": "^4.41.1" }, "prettier": "@chainlink/prettier-config" } diff --git a/integration/runlog_test b/integration/runlog_test index 87e1b339c81..b1fc0df6cba 100755 --- a/integration/runlog_test +++ b/integration/runlog_test @@ -12,7 +12,7 @@ fi expected_echo_count=$(expr $(curl -sS "$ECHO_SERVER_URL") + 1) expected_job_count=$(expr $(chainlink -j jobs list | jq length) + 1) -yarn workspace @chainlink/integration-scripts send-runlog-transaction | tee $SRCROOT/integration/send_runlog_transaction.log +yarn workspace @chainlink/integration-scripts send-runlog-transaction | tee $SRCROOT/integration/logs/send_runlog_transaction.log # Check echo count assert "Echo count" "curl -sS $ECHO_SERVER_URL" $expected_echo_count @@ -35,7 +35,7 @@ tx_receiver=$(chainlink -j runs list --jobid $jid | jq '.[].result.data.address' echo "Test sent TX to: $tx_receiver" # Check for the Fullfillment event -yarn workspace @chainlink/integration-scripts count-transaction-events | tee $SRCROOT/integration/send_runlog_transaction.log -tx_event_count=`cat $SRCROOT/integration/send_runlog_transaction.log | grep "Events from $RUN_LOG_ADDRESS in $txid:" | awk '{print$6}'` +yarn workspace @chainlink/integration-scripts count-transaction-events | tee $SRCROOT/integration/logs/send_runlog_transaction.log +tx_event_count=`cat $SRCROOT/integration/logs/send_runlog_transaction.log | grep "Events from $RUN_LOG_ADDRESS in $txid:" | awk '{print$6}'` assert "Transaction Events" "echo $tx_event_count" 2 diff --git a/operator_ui/__tests__/actions.test.ts b/operator_ui/__tests__/actions.test.ts index 49abf51042c..046a1a3afdd 100644 --- a/operator_ui/__tests__/actions.test.ts +++ b/operator_ui/__tests__/actions.test.ts @@ -4,6 +4,7 @@ import jsonApiJobSpecRunFactory from 'factories/jsonApiJobSpecRun' import configureStore from 'redux-mock-store' import thunk from 'redux-thunk' import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' +import globPath from 'test-helpers/globPath' const middlewares = [thunk] const mockStore = configureStore(middlewares) @@ -28,7 +29,7 @@ describe('fetchJob', () => { tasks: [expectedTask], }) - global.fetch.getOnce(`/v2/specs/${jobSpecId}`, jobSpecResponse) + global.fetch.getOnce(globPath(`/v2/specs/${jobSpecId}`), jobSpecResponse) const store = mockStore({}) return store.dispatch(actions.fetchJob(jobSpecId)).then(() => { @@ -60,7 +61,7 @@ describe('fetchJobRun', () => { }) const id = runResponse.data.id - global.fetch.getOnce(`/v2/runs/${id}`, runResponse) + global.fetch.getOnce(globPath(`/v2/runs/${id}`), runResponse) const store = mockStore({}) return store.dispatch(actions.fetchJobRun(id)).then(() => { diff --git a/operator_ui/__tests__/connectors/reducers/authentication.test.js b/operator_ui/__tests__/connectors/reducers/authentication.test.js index 43fb6ff6a53..86f1880effb 100644 --- a/operator_ui/__tests__/connectors/reducers/authentication.test.js +++ b/operator_ui/__tests__/connectors/reducers/authentication.test.js @@ -1,5 +1,5 @@ import reducer from 'connectors/redux/reducers' -import { get as getAuthenticationStorage } from 'utils/authenticationStorage' +import { getAuthentication } from 'utils/storage' import { REQUEST_SIGNIN, RECEIVE_SIGNIN_SUCCESS, @@ -46,7 +46,7 @@ describe('connectors/reducers/authentication', () => { const action = { type: RECEIVE_SIGNIN_SUCCESS, authenticated: true } reducer(undefined, action) - expect(getAuthenticationStorage()).toEqual({ allowed: true }) + expect(getAuthentication()).toEqual({ allowed: true }) }) }) @@ -69,7 +69,7 @@ describe('connectors/reducers/authentication', () => { const action = { type: RECEIVE_SIGNIN_FAIL } reducer(undefined, action) - expect(getAuthenticationStorage()).toEqual({ allowed: false }) + expect(getAuthentication()).toEqual({ allowed: false }) }) }) @@ -92,7 +92,7 @@ describe('connectors/reducers/authentication', () => { const action = { type: RECEIVE_SIGNIN_ERROR } reducer(undefined, action) - expect(getAuthenticationStorage()).toEqual({ allowed: false }) + expect(getAuthentication()).toEqual({ allowed: false }) }) }) @@ -122,7 +122,7 @@ describe('connectors/reducers/authentication', () => { const action = { type: RECEIVE_SIGNOUT_SUCCESS, authenticated: false } reducer(undefined, action) - expect(getAuthenticationStorage()).toEqual({ allowed: false }) + expect(getAuthentication()).toEqual({ allowed: false }) }) }) @@ -145,7 +145,7 @@ describe('connectors/reducers/authentication', () => { const action = { type: RECEIVE_SIGNOUT_ERROR } reducer(undefined, action) - expect(getAuthenticationStorage()).toEqual({ allowed: false }) + expect(getAuthentication()).toEqual({ allowed: false }) }) }) }) diff --git a/operator_ui/__tests__/containers/Bridges/Index.test.js b/operator_ui/__tests__/containers/Bridges/Index.test.js index 1514b5418fb..8cb66a2c4a7 100644 --- a/operator_ui/__tests__/containers/Bridges/Index.test.js +++ b/operator_ui/__tests__/containers/Bridges/Index.test.js @@ -9,6 +9,7 @@ import { MemoryRouter } from 'react-router-dom' import clickNextPage from 'test-helpers/clickNextPage' import clickPreviousPage from 'test-helpers/clickPreviousPage' import syncFetch from 'test-helpers/syncFetch' +import globPath from 'test-helpers/globPath' const classes = {} const mountIndex = (opts = {}) => @@ -30,7 +31,7 @@ describe('containers/Bridges/Index', () => { url: 'butbobistho.com', }, ]) - global.fetch.getOnce('begin:/v2/bridge_types', bridgesResponse) + global.fetch.getOnce(globPath('/v2/bridge_types'), bridgesResponse) const wrapper = mountIndex() @@ -46,7 +47,7 @@ describe('containers/Bridges/Index', () => { [{ name: 'ID-ON-FIRST-PAGE', url: 'bridge.com' }], 2, ) - global.fetch.getOnce('begin:/v2/bridge_types', pageOneResponse) + global.fetch.getOnce(globPath('/v2/bridge_types'), pageOneResponse) const wrapper = mountIndex({ pageSize: 1 }) @@ -58,14 +59,14 @@ describe('containers/Bridges/Index', () => { [{ name: 'ID-ON-SECOND-PAGE', url: 'bridge.com' }], 2, ) - global.fetch.getOnce('begin:/v2/bridge_types', pageTwoResponse) + global.fetch.getOnce(globPath('/v2/bridge_types'), pageTwoResponse) clickNextPage(wrapper) await syncFetch(wrapper) expect(wrapper.text()).not.toContain('ID-ON-FIRST-PAGE') expect(wrapper.text()).toContain('ID-ON-SECOND-PAGE') - global.fetch.getOnce('begin:/v2/bridge_types', pageOneResponse) + global.fetch.getOnce(globPath('/v2/bridge_types'), pageOneResponse) clickPreviousPage(wrapper) await syncFetch(wrapper) diff --git a/operator_ui/__tests__/containers/Bridges/Show.test.js b/operator_ui/__tests__/containers/Bridges/Show.test.js index c5ed30d8280..26173f0ba07 100644 --- a/operator_ui/__tests__/containers/Bridges/Show.test.js +++ b/operator_ui/__tests__/containers/Bridges/Show.test.js @@ -5,6 +5,7 @@ import { mount } from 'enzyme' import { Provider } from 'react-redux' import { MemoryRouter } from 'react-router-dom' import { ConnectedShow as Show } from 'containers/Bridges/Show' +import globPath from 'test-helpers/globPath' const classes = {} const mountShow = props => @@ -32,7 +33,7 @@ describe('containers/Bridges/Show', () => { }, } - global.fetch.getOnce(`/v2/bridge_types/tallbridge`, response) + global.fetch.getOnce(globPath(`/v2/bridge_types/tallbridge`), response) const props = { match: { params: { bridgeId: 'tallbridge' } } } const wrapper = mountShow(props) diff --git a/operator_ui/__tests__/containers/Configuration.test.js b/operator_ui/__tests__/containers/Configuration.test.js index 8ecdc77bbd5..05f2eb806cc 100644 --- a/operator_ui/__tests__/containers/Configuration.test.js +++ b/operator_ui/__tests__/containers/Configuration.test.js @@ -4,6 +4,7 @@ import configurationFactory from 'factories/configuration' import React from 'react' import mountWithinStoreAndRouter from 'test-helpers/mountWithinStoreAndRouter' import syncFetch from 'test-helpers/syncFetch' +import globPath from 'test-helpers/globPath' const classes = {} const mount = props => { @@ -20,7 +21,7 @@ describe('containers/Configuration', () => { band: 'Major Lazer', singer: 'Bob Marley', }) - global.fetch.getOnce('/v2/config', configurationResponse) + global.fetch.getOnce(globPath('/v2/config'), configurationResponse) const wrapper = mount() diff --git a/operator_ui/__tests__/containers/Dashboards/Index.test.js b/operator_ui/__tests__/containers/Dashboards/Index.test.js index 85fced5c5a0..71fb0bda2ea 100644 --- a/operator_ui/__tests__/containers/Dashboards/Index.test.js +++ b/operator_ui/__tests__/containers/Dashboards/Index.test.js @@ -4,6 +4,7 @@ import accountBalanceFactory from 'factories/accountBalance' import React from 'react' import mountWithTheme from 'test-helpers/mountWithTheme' import syncFetch from 'test-helpers/syncFetch' +import globPath from 'test-helpers/globPath' const classes = {} const mountIndex = () => mountWithTheme() @@ -35,7 +36,7 @@ describe('containers/Dashboards/Index', () => { ], meta: { count: 2 }, } - global.fetch.getOnce('begin:/v2/runs', recentJobRuns) + global.fetch.getOnce(globPath('/v2/runs'), recentJobRuns) const recentlyCreatedJobsResponse = { data: [ @@ -57,13 +58,13 @@ describe('containers/Dashboards/Index', () => { }, ], } - global.fetch.getOnce('begin:/v2/specs', recentlyCreatedJobsResponse) + global.fetch.getOnce(globPath('/v2/specs'), recentlyCreatedJobsResponse) const accountBalanceResponse = accountBalanceFactory( '10123456000000000000000', '7467870000000000000000', ) - global.fetch.getOnce('/v2/user/balances', accountBalanceResponse) + global.fetch.getOnce(globPath('/v2/user/balances'), accountBalanceResponse) const wrapper = mountIndex() diff --git a/operator_ui/__tests__/containers/JobRuns/Index.test.js b/operator_ui/__tests__/containers/JobRuns/Index.test.js index 9d66f0b97fa..a7ea2713ab4 100644 --- a/operator_ui/__tests__/containers/JobRuns/Index.test.js +++ b/operator_ui/__tests__/containers/JobRuns/Index.test.js @@ -10,6 +10,7 @@ import clickNextPage from 'test-helpers/clickNextPage' import clickPreviousPage from 'test-helpers/clickPreviousPage' import mountWithTheme from 'test-helpers/mountWithTheme' import syncFetch from 'test-helpers/syncFetch' +import globPath from 'test-helpers/globPath' const classes = {} const mountIndex = props => @@ -32,7 +33,7 @@ describe('containers/JobRuns/Index', () => { expect.assertions(2) const runsResponse = jsonApiJobSpecRunFactory([{ jobId: jobSpecId }]) - global.fetch.getOnce(`begin:/v2/runs`, runsResponse) + global.fetch.getOnce(globPath('/v2/runs'), runsResponse) const props = { match: { params: { jobSpecId: jobSpecId } } } const wrapper = mountIndex(props) @@ -49,7 +50,7 @@ describe('containers/JobRuns/Index', () => { [{ id: 'ID-ON-FIRST-PAGE', jobId: jobSpecId }], 3, ) - global.fetch.getOnce(`begin:/v2/runs`, pageOneResponse) + global.fetch.getOnce(globPath('/v2/runs'), pageOneResponse) const props = { match: { params: { jobSpecId: jobSpecId } }, pageSize: 1 } const wrapper = mountIndex(props) @@ -62,14 +63,14 @@ describe('containers/JobRuns/Index', () => { [{ id: 'ID-ON-SECOND-PAGE', jobId: jobSpecId }], 3, ) - global.fetch.getOnce(`begin:/v2/runs`, pageTwoResponse) + global.fetch.getOnce(globPath('/v2/runs'), pageTwoResponse) clickNextPage(wrapper) await syncFetch(wrapper) expect(wrapper.text()).not.toContain('ID-ON-FIRST-PAGE') expect(wrapper.text()).toContain('ID-ON-SECOND-PAGE') - global.fetch.getOnce(`begin:/v2/runs`, pageOneResponse) + global.fetch.getOnce(globPath('/v2/runs'), pageOneResponse) clickPreviousPage(wrapper) await syncFetch(wrapper) @@ -80,7 +81,7 @@ describe('containers/JobRuns/Index', () => { [{ id: 'ID-ON-THIRD-PAGE', jobId: jobSpecId }], 3, ) - global.fetch.getOnce(`begin:/v2/runs`, pageThreeResponse) + global.fetch.getOnce(globPath('/v2/runs'), pageThreeResponse) clickLastPage(wrapper) await syncFetch(wrapper) @@ -88,7 +89,7 @@ describe('containers/JobRuns/Index', () => { expect(wrapper.text()).not.toContain('ID-ON-FIRST-PAGE') expect(wrapper.text()).not.toContain('ID-ON-SECOND-PAGE') - global.fetch.getOnce(`begin:/v2/runs`, pageOneResponse) + global.fetch.getOnce(globPath('/v2/runs'), pageOneResponse) clickFirstPage(wrapper) await syncFetch(wrapper) @@ -101,7 +102,7 @@ describe('containers/JobRuns/Index', () => { expect.assertions(1) const runsResponse = jsonApiJobSpecRunFactory([]) - global.fetch.getOnce(`begin:/v2/runs`, runsResponse) + await global.fetch.getOnce(globPath('/v2/runs'), runsResponse) const props = { match: { params: { jobSpecId: jobSpecId } } } const wrapper = mountIndex(props) diff --git a/operator_ui/__tests__/containers/JobRuns/Show/Overview.test.js b/operator_ui/__tests__/containers/JobRuns/Show/Overview.test.js index 5b663e09af8..786343305b5 100644 --- a/operator_ui/__tests__/containers/JobRuns/Show/Overview.test.js +++ b/operator_ui/__tests__/containers/JobRuns/Show/Overview.test.js @@ -7,6 +7,7 @@ import { MemoryRouter } from 'react-router-dom' import { ConnectedShow as Show } from 'containers/JobRuns/Show/Overview' import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' import mountWithTheme from 'test-helpers/mountWithTheme' +import globPath from 'test-helpers/globPath' const classes = {} const mountShow = props => @@ -48,7 +49,7 @@ describe('containers/JobRuns/Show/Overview', () => { }, }, }) - global.fetch.getOnce(`/v2/runs/${jobRunId}`, jobRunResponse) + global.fetch.getOnce(globPath(`/v2/runs/${jobRunId}`), jobRunResponse) const props = { match: { params: { jobSpecId: jobSpecId, jobRunId: jobRunId } }, diff --git a/operator_ui/__tests__/containers/Jobs/Index.test.js b/operator_ui/__tests__/containers/Jobs/Index.test.js index 5962cac2f2e..0bf6cb391c2 100644 --- a/operator_ui/__tests__/containers/Jobs/Index.test.js +++ b/operator_ui/__tests__/containers/Jobs/Index.test.js @@ -9,6 +9,7 @@ import { MemoryRouter } from 'react-router-dom' import clickNextPage from 'test-helpers/clickNextPage' import clickPreviousPage from 'test-helpers/clickPreviousPage' import syncFetch from 'test-helpers/syncFetch' +import globPath from 'test-helpers/globPath' const classes = {} const mountIndex = (opts = {}) => @@ -31,7 +32,7 @@ describe('containers/Jobs/Index', () => { createdAt: new Date().toISOString(), }, ]) - global.fetch.getOnce(`begin:/v2/specs`, jobSpecsResponse) + global.fetch.getOnce(globPath('/v2/specs'), jobSpecsResponse) const wrapper = mountIndex() @@ -48,7 +49,7 @@ describe('containers/Jobs/Index', () => { [{ id: 'ID-ON-FIRST-PAGE' }], 2, ) - global.fetch.getOnce(`begin:/v2/specs`, pageOneResponse) + global.fetch.getOnce(globPath('/v2/specs'), pageOneResponse) const wrapper = mountIndex({ pageSize: 1 }) @@ -60,14 +61,14 @@ describe('containers/Jobs/Index', () => { [{ id: 'ID-ON-SECOND-PAGE' }], 2, ) - global.fetch.getOnce(`begin:/v2/specs`, pageTwoResponse) + global.fetch.getOnce(globPath('/v2/specs'), pageTwoResponse) clickNextPage(wrapper) await syncFetch(wrapper) expect(wrapper.text()).not.toContain('ID-ON-FIRST-PAGE') expect(wrapper.text()).toContain('ID-ON-SECOND-PAGE') - global.fetch.getOnce(`begin:/v2/specs`, pageOneResponse) + global.fetch.getOnce(globPath('/v2/specs'), pageOneResponse) clickPreviousPage(wrapper) await syncFetch(wrapper) diff --git a/operator_ui/__tests__/containers/Jobs/Show.test.js b/operator_ui/__tests__/containers/Jobs/Show.test.js index af2c22022bc..5a1790fa9b1 100644 --- a/operator_ui/__tests__/containers/Jobs/Show.test.js +++ b/operator_ui/__tests__/containers/Jobs/Show.test.js @@ -8,6 +8,7 @@ import { MemoryRouter } from 'react-router-dom' import isoDate, { MINUTE_MS } from 'test-helpers/isoDate' import mountWithTheme from 'test-helpers/mountWithTheme' import syncFetch from 'test-helpers/syncFetch' +import globPath from 'test-helpers/globPath' import { GWEI_PER_TOKEN, WEI_PER_TOKEN } from '../../../src/utils/constants' const mountShow = props => @@ -33,7 +34,7 @@ describe('containers/Jobs/Show', () => { earnings: GWEI_PER_TOKEN, minPayment: 100 * WEI_PER_TOKEN, }) - global.fetch.getOnce(`/v2/specs/${jobSpecId}`, jobSpecResponse) + global.fetch.getOnce(globPath(`/v2/specs/${jobSpecId}`), jobSpecResponse) const jobRunResponse = jsonApiJobSpecRunsFactory([ { @@ -42,7 +43,7 @@ describe('containers/Jobs/Show', () => { status: 'pending', }, ]) - global.fetch.getOnce(`begin:/v2/runs`, jobRunResponse) + global.fetch.getOnce(globPath('/v2/runs'), jobRunResponse) const props = { match: { params: { jobSpecId: jobSpecId } } } const wrapper = mountShow(props) @@ -71,8 +72,8 @@ describe('containers/Jobs/Show', () => { }) const jobRunsResponse = jsonApiJobSpecRunsFactory(runs) - global.fetch.getOnce(`begin:/v2/specs/${jobSpecId}`, jobSpecResponse) - global.fetch.getOnce(`begin:/v2/runs`, jobRunsResponse) + global.fetch.getOnce(globPath(`/v2/specs/${jobSpecId}`), jobSpecResponse) + global.fetch.getOnce(globPath('/v2/runs'), jobRunsResponse) const props = { match: { params: { jobSpecId: jobSpecId } } } const wrapper = mountShow(props) diff --git a/operator_ui/__tests__/containers/SignIn.test.js b/operator_ui/__tests__/containers/SignIn.test.js index 30a0ea4a798..0958cf1b59f 100644 --- a/operator_ui/__tests__/containers/SignIn.test.js +++ b/operator_ui/__tests__/containers/SignIn.test.js @@ -7,6 +7,7 @@ import { Switch, Route } from 'react-router-dom' import { MemoryRouter } from 'react-router' import SignIn from 'containers/SignIn' import fillIn from 'test-helpers/fillIn' +import globPath from 'test-helpers/globPath' const RedirectApp = () =>
Behind authentication
const mountSignIn = store => @@ -39,7 +40,7 @@ const AUTHENTICATED_RESPONSE = { describe('containers/SignIn', () => { it('unauthenticated user can input credentials and sign in', async () => { const store = createStore() - global.fetch.postOnce(`/sessions`, AUTHENTICATED_RESPONSE) + global.fetch.postOnce(globPath('/sessions'), AUTHENTICATED_RESPONSE) const wrapper = mountSignIn(store) submitForm(wrapper) @@ -47,13 +48,15 @@ describe('containers/SignIn', () => { await syncFetch(wrapper) const newState = store.getState() expect(newState.authentication.allowed).toEqual(true) + + await syncFetch(wrapper) expect(wrapper.text()).toContain('Behind authentication') }) it('unauthenticated user inputs wrong credentials', async () => { const store = createStore() global.fetch.postOnce( - '/sessions', + globPath('/sessions'), { authenticated: false, errors: [{ detail: 'Invalid email' }] }, { response: { status: 401 } }, ) diff --git a/operator_ui/__tests__/utils/authenticationStorage.test.js b/operator_ui/__tests__/utils/authenticationStorage.test.js deleted file mode 100644 index 285e081d928..00000000000 --- a/operator_ui/__tests__/utils/authenticationStorage.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import { get, set } from 'utils/authenticationStorage' - -describe('utils/authenticationStorage', () => { - beforeEach(() => { - global.localStorage.clear() - }) - - describe('get', () => { - it('returns a JS object for JSON stored as "chainlink.authentication" in localStorage', () => { - global.localStorage.setItem('chainlink.authentication', '{"foo":"FOO"}') - expect(get()).toEqual({ foo: 'FOO' }) - }) - }) - - describe('set', () => { - it('saves the JS object as JSON under the key "chainlink.authentication" in localStorage', () => { - set({ foo: 'FOO' }) - expect(global.localStorage.getItem('chainlink.authentication')).toEqual( - '{"foo":"FOO"}', - ) - }) - }) -}) diff --git a/operator_ui/__tests__/utils/formatRequestURI.test.js b/operator_ui/__tests__/utils/formatRequestURI.test.js deleted file mode 100644 index f8c2e8fa4da..00000000000 --- a/operator_ui/__tests__/utils/formatRequestURI.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import formatRequestURI from 'utils/formatRequestURI' - -describe('formatRequestURI', () => { - describe('port specified', () => { - it('returns host and port in URI', () => { - expect( - formatRequestURI('/api', {}, { hostname: 'localhost', port: 6689 }), - ).toEqual('localhost:6689/api') - }) - }) - - describe('no port specified', () => { - it('returns just the path in the URI', () => { - expect(formatRequestURI('/api')).toEqual('/api') - }) - }) -}) diff --git a/operator_ui/__tests__/utils/storage.test.js b/operator_ui/__tests__/utils/storage.test.js deleted file mode 100644 index 4fb3242323d..00000000000 --- a/operator_ui/__tests__/utils/storage.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import { get, set } from 'utils/storage' - -describe('utils/storage', () => { - beforeEach(() => { - global.localStorage.clear() - }) - - describe('get', () => { - it('returns a JS object for JSON keyed under "chainlink." in localStorage', () => { - global.localStorage.setItem('chainlink.foo', '{"foo":"FOO"}') - expect(get('foo')).toEqual({ foo: 'FOO' }) - }) - - it('returns an empty JS object when not valid JSON', () => { - global.localStorage.setItem('chainlink.foo', '{"foo"}') - expect(get('foo')).toEqual({}) - }) - - it('returns an empty JS object when the key does not exist', () => { - expect(get('foo')).toEqual({}) - }) - }) - - describe('set', () => { - it('saves the JS object as JSON keyed under "chainlink." in localStorage', () => { - set('foo', { foo: 'FOO' }) - expect(global.localStorage.getItem('chainlink.foo')).toEqual( - '{"foo":"FOO"}', - ) - }) - }) -}) diff --git a/operator_ui/__tests__/utils/storage.test.ts b/operator_ui/__tests__/utils/storage.test.ts new file mode 100644 index 00000000000..748b38fc333 --- /dev/null +++ b/operator_ui/__tests__/utils/storage.test.ts @@ -0,0 +1,26 @@ +import { getAuthentication, setAuthentication } from '../../src/utils/storage' + +describe('utils/storage', () => { + beforeEach(() => { + global.localStorage.clear() + }) + + describe('getAuthentication', () => { + it('returns a JS object for JSON stored as "chainlink.authentication" in localStorage', () => { + global.localStorage.setItem( + 'chainlink.authentication', + '{"allowed":true}', + ) + expect(getAuthentication()).toEqual({ allowed: true }) + }) + }) + + describe('setAuthentication', () => { + it('saves the JS object as JSON under the key "chainlink.authentication" in localStorage', () => { + setAuthentication({ allowed: true }) + expect(global.localStorage.getItem('chainlink.authentication')).toEqual( + '{"allowed":true}', + ) + }) + }) +}) diff --git a/operator_ui/package.json b/operator_ui/package.json index 78007de457a..31e5beb5aad 100644 --- a/operator_ui/package.json +++ b/operator_ui/package.json @@ -22,13 +22,14 @@ }, "dependencies": { "@babel/polyfill": "^7.2.5", - "@chainlink/styleguide": "0.0.0", + "@chainlink/local-storage": "0.0.1", + "@chainlink/styleguide": "0.0.1", "@material-ui/core": "^3.9.2", "@material-ui/icons": "^3.0.1", "bignumber.js": "^9.0.0", "change-case": "^3.0.2", "classnames": "^2.2.6", - "core-js": "^3.2.1", + "core-js": "^3.4.0", "formik": "^1.0.3", "humps": "^2.0.1", "isomorphic-unfetch": "^3.0.0", @@ -38,7 +39,7 @@ "local-storage-fallback": "^4.1.1", "lodash": "^4.17.13", "moment": "^2.24.0", - "normalize-url": "^4.3.0", + "normalize-url": "^4.5.0", "numeral": "^2.0.6", "path-to-regexp": "^3.0.0", "promise.prototype.finally": "^3.1.0", @@ -61,7 +62,7 @@ "uuid": "^3.3.2" }, "devDependencies": { - "@babel/core": "^7.2.2", + "@babel/core": "^7.6.4", "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.1.0", @@ -79,8 +80,8 @@ "@types/react-dom": "^16.7.0", "@types/react-redux": "~6.0.0", "@types/react-resize-detector": "^4.0.1", - "@types/react-router": "^5.1.1", - "@types/react-router-dom": "^4.3.4", + "@types/react-router": "^5.1.2", + "@types/react-router-dom": "^5.1.2", "@types/redux-mock-store": "^1.0.1", "depcheck": "^0.8.3", "enzyme": "^3.10.0", @@ -91,7 +92,7 @@ "jest-silent-reporter": "^0.1.2", "mock-local-storage": "^1.0.5", "prettier": "^1.18.2", - "react-hot-loader": "^4.6.3", + "react-hot-loader": "^4.12.16", "react-static": "^6.3.8", "react-static-plugin-jss": "^6.3.0", "react-static-plugin-react-router": "^6.3.4", diff --git a/operator_ui/src/App.tsx b/operator_ui/src/App.tsx index 4f6c00a7362..9647fe77cf1 100644 --- a/operator_ui/src/App.tsx +++ b/operator_ui/src/App.tsx @@ -3,14 +3,16 @@ import { Provider } from 'react-redux' import createStore from './connectors/redux' import './index.css' import Layout from './Layout' -import { set } from './utils/storage' +import { setPersistUrl } from './utils/storage' + +const SIGNIN_PATH = '/signin' const store = createStore() store.subscribe(() => { const prevURL = store.getState().notifications.currentUrl - if (prevURL !== '/signin') { - set('persistURL', prevURL) + if (prevURL !== SIGNIN_PATH) { + setPersistUrl(prevURL) } }) diff --git a/operator_ui/src/actions.ts b/operator_ui/src/actions.ts index 41a2b7d2c1b..2458e996170 100644 --- a/operator_ui/src/actions.ts +++ b/operator_ui/src/actions.ts @@ -1,9 +1,10 @@ +import * as jsonapi from '@chainlink/json-api-client' +import * as api from './api' import * as models from 'core/store/models' import * as presenters from 'core/store/presenters' import normalize from 'json-api-normalizer' import { Action, Dispatch } from 'redux' import { ThunkAction } from 'redux-thunk' -import * as api from './api' import { AppState } from './connectors/redux/reducers' export type GetNormalizedData = ReturnType< @@ -13,10 +14,10 @@ export type GetNormalizedData = ReturnType< : never type Errors = - | api.errors.AuthenticationError - | api.errors.BadRequestError - | api.errors.ServerError - | api.errors.UnknownResponseError + | jsonapi.AuthenticationError + | jsonapi.BadRequestError + | jsonapi.ServerError + | jsonapi.UnknownResponseError const createAction = (type: string) => ({ type: type }) @@ -38,7 +39,7 @@ const redirectToSignOut = () => ({ const curryErrorHandler = (dispatch: Dispatch, type: string) => ( error: Error, ) => { - if (error instanceof api.errors.AuthenticationError) { + if (error instanceof jsonapi.AuthenticationError) { dispatch(redirectToSignOut()) } else { dispatch(createErrorAction(error, type)) @@ -120,7 +121,7 @@ function sendSignIn(data: Parameter) { .createSession(data) .then(doc => dispatch(signInSuccessAction(doc))) .catch((error: Errors) => { - if (error instanceof api.errors.AuthenticationError) { + if (error instanceof jsonapi.AuthenticationError) { dispatch(signInFailAction()) } else { dispatch(createErrorAction(error, RECEIVE_SIGNIN_ERROR)) @@ -283,7 +284,7 @@ export const updateBridge = ( // // The calls above will be converted gradually. const handleError = (dispatch: Dispatch) => (error: Error) => { - if (error instanceof api.errors.AuthenticationError) { + if (error instanceof jsonapi.AuthenticationError) { dispatch(redirectToSignOut()) } else { dispatch(notifyError(({ msg }: any) => msg, error)) diff --git a/operator_ui/src/api/index.ts b/operator_ui/src/api/index.ts index e4b1d639c33..6dd45e55dab 100644 --- a/operator_ui/src/api/index.ts +++ b/operator_ui/src/api/index.ts @@ -1,5 +1,4 @@ -import * as errors from './errors' import * as v2 from './v2' export * from './sessions' -export { v2, errors } +export { v2 } diff --git a/operator_ui/src/api/sessions.ts b/operator_ui/src/api/sessions.ts index 6e23c41eb10..15a8dfe35b5 100644 --- a/operator_ui/src/api/sessions.ts +++ b/operator_ui/src/api/sessions.ts @@ -1,4 +1,4 @@ -import * as jsonapi from 'api/transport/json' +import * as jsonapi from '@chainlink/json-api-client' import * as models from 'core/store/models' import * as sessionsController from 'core/web/sessions_controller' diff --git a/operator_ui/src/api/v2/bridgeTypes.ts b/operator_ui/src/api/v2/bridgeTypes.ts index fba22c9d282..0ec63529fc0 100644 --- a/operator_ui/src/api/v2/bridgeTypes.ts +++ b/operator_ui/src/api/v2/bridgeTypes.ts @@ -1,4 +1,4 @@ -import * as jsonapi from 'api/transport/json' +import * as jsonapi from '@chainlink/json-api-client' import * as models from 'core/store/models' // Create adds the BridgeType to the given context. diff --git a/operator_ui/src/api/v2/bulkDeleteRuns.ts b/operator_ui/src/api/v2/bulkDeleteRuns.ts index 4782f38876b..7bc03f0ca11 100644 --- a/operator_ui/src/api/v2/bulkDeleteRuns.ts +++ b/operator_ui/src/api/v2/bulkDeleteRuns.ts @@ -1,4 +1,4 @@ -import * as jsonapi from 'api/transport/json' +import * as jsonapi from '@chainlink/json-api-client' import * as models from 'core/store/models' /** diff --git a/operator_ui/src/api/v2/config.ts b/operator_ui/src/api/v2/config.ts index 9def73abcd8..98e94cb93bd 100644 --- a/operator_ui/src/api/v2/config.ts +++ b/operator_ui/src/api/v2/config.ts @@ -1,5 +1,6 @@ -import * as jsonapi from 'api/transport/json' +import * as jsonapi from '@chainlink/json-api-client' import * as presenters from 'core/store/presenters' + /** * Show returns the whitelist of config variables * diff --git a/operator_ui/src/api/v2/runs.ts b/operator_ui/src/api/v2/runs.ts index cdbacdaea4f..a28cfc3c165 100644 --- a/operator_ui/src/api/v2/runs.ts +++ b/operator_ui/src/api/v2/runs.ts @@ -1,4 +1,4 @@ -import * as jsonapi from 'api/transport/json' +import * as jsonapi from '@chainlink/json-api-client' import * as models from 'core/store/models' import * as presenters from 'core/store/presenters' /** diff --git a/operator_ui/src/api/v2/specs.ts b/operator_ui/src/api/v2/specs.ts index 6f3e7829ae5..fdbc9ce4cee 100644 --- a/operator_ui/src/api/v2/specs.ts +++ b/operator_ui/src/api/v2/specs.ts @@ -1,4 +1,4 @@ -import * as jsonapi from 'api/transport/json' +import * as jsonapi from '@chainlink/json-api-client' import * as models from 'core/store/models' import * as presenters from 'core/store/presenters' /** diff --git a/operator_ui/src/api/v2/transactions.ts b/operator_ui/src/api/v2/transactions.ts index ea4ab6d34d8..30219c0096d 100644 --- a/operator_ui/src/api/v2/transactions.ts +++ b/operator_ui/src/api/v2/transactions.ts @@ -1,4 +1,4 @@ -import * as jsonapi from 'api/transport/json' +import * as jsonapi from '@chainlink/json-api-client' import * as presenters from 'core/store/presenters' /** diff --git a/operator_ui/src/api/v2/user/balances.ts b/operator_ui/src/api/v2/user/balances.ts index 962caed5a74..db05e82286f 100644 --- a/operator_ui/src/api/v2/user/balances.ts +++ b/operator_ui/src/api/v2/user/balances.ts @@ -1,4 +1,4 @@ -import * as jsonapi from 'api/transport/json' +import * as jsonapi from '@chainlink/json-api-client' import * as presenters from 'core/store/presenters' /** diff --git a/operator_ui/src/components/AvatarMenu.js b/operator_ui/src/components/AvatarMenu.js index b36444c49db..276828519f0 100644 --- a/operator_ui/src/components/AvatarMenu.js +++ b/operator_ui/src/components/AvatarMenu.js @@ -55,7 +55,10 @@ const AvatarMenu = useHooks(({ classes, submitSignOut }) => { if (anchorEl.current.contains(event.target)) { return } + setOpenState(false) + } + const handleLogOut = () => { submitSignOut() setOpenState(false) } @@ -89,7 +92,10 @@ const AvatarMenu = useHooks(({ classes, submitSignOut }) => { - + Log out diff --git a/operator_ui/src/components/Bridges/Form.tsx b/operator_ui/src/components/Bridges/Form.tsx index 1e8376d1737..80e60effe8b 100644 --- a/operator_ui/src/components/Bridges/Form.tsx +++ b/operator_ui/src/components/Bridges/Form.tsx @@ -5,12 +5,14 @@ import { withStyles, WithStyles, } from '@material-ui/core/styles' -import Button from 'components/Button' +import * as storage from '@chainlink/local-storage' import { withFormik, FormikProps, Form as FormikForm } from 'formik' import normalizeUrl from 'normalize-url' import React from 'react' import { Prompt } from 'react-router-dom' -import { get, set } from 'utils/storage' +import isEmpty from 'lodash/isEmpty' +import isEqual from 'lodash/isEqual' +import Button from 'components/Button' const styles = (theme: Theme) => createStyles({ @@ -30,25 +32,9 @@ const styles = (theme: Theme) => }, }) -const isDirty = ({ values, submitCount }: Props) => { - return ( - (values.name !== '' || - values.url !== '' || - (values.minimumContractPayment !== '0' && values.confirmations !== 0)) && - submitCount === 0 - ) -} - -// CHECKME -interface OwnProps extends Partial, WithStyles { - actionText: string - nameDisabled?: boolean - onSubmit: any - onSuccess: any - onError: any -} +const SUBMITTING_TIMEOUT_MS = 1000 +const UNSAVED_BRIDGE = 'persistBridge' -// CHECKME interface FormValues { name: string minimumContractPayment: string @@ -56,8 +42,56 @@ interface FormValues { url: string } +interface OwnProps extends Partial, WithStyles { + actionText: string + nameDisabled?: boolean + onSubmit: ( + values: FormValues, + onSuccesss: Function, + onError: Function, + ) => void + onSuccess: Function + onError: Function +} + type Props = FormikProps & OwnProps +function submitSuccess(callback: Function) { + return (response: object) => { + storage.remove(UNSAVED_BRIDGE) + return callback(response) + } +} + +function submitError(callback: Function, values: FormValues) { + return (error: object) => { + storage.setJson(UNSAVED_BRIDGE, values) + return callback(error) + } +} + +function initialValues({ + name, + url, + minimumContractPayment, + confirmations, +}: OwnProps): FormValues { + const unsavedBridge = storage.getJson(UNSAVED_BRIDGE) + const propValues = { + name: name || '', + url: url || '', + minimumContractPayment: minimumContractPayment || '0', + confirmations: confirmations || 0, + } + + return isEmpty(unsavedBridge) ? propValues : unsavedBridge +} + +function isDirty(props: Props): boolean { + const initial = initialValues(props) + return !isEqual(props.values, initial) && props.submitCount === 0 +} + const Form: React.SFC = props => ( <> ({ - mapPropsToValues({ name, url, minimumContractPayment, confirmations }) { - const shouldPersist = Object.keys(get('persistBridge')).length !== 0 - const persistedJSON = shouldPersist && get('persistBridge') - if (shouldPersist) set('persistBridge', {}) - const json: FormValues = { - name: name || '', - url: url || '', - minimumContractPayment: minimumContractPayment || '0', - confirmations: confirmations || 0, - } - return (shouldPersist && persistedJSON) || json + mapPropsToValues(ownProps) { + return initialValues(ownProps) }, handleSubmit(values, { props, setSubmitting }) { @@ -157,11 +182,14 @@ const WithFormikForm = withFormik({ } catch { values.url = '' } - props.onSubmit(values, props.onSuccess, props.onError) - set('persistBridge', values) + props.onSubmit( + values, + submitSuccess(props.onSuccess), + submitError(props.onError, values), + ) setTimeout(() => { setSubmitting(false) - }, 1000) + }, SUBMITTING_TIMEOUT_MS) }, })(Form) diff --git a/operator_ui/src/components/Jobs/Form.js b/operator_ui/src/components/Jobs/Form.js deleted file mode 100644 index 8df1abcad38..00000000000 --- a/operator_ui/src/components/Jobs/Form.js +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import * as formik from 'formik' -import { withStyles } from '@material-ui/core/styles' -import Button from 'components/Button' -import { TextField, Grid } from '@material-ui/core' -import { Prompt } from 'react-router-dom' -import { set, get } from 'utils/storage' - -const styles = theme => ({ - card: { - paddingBottom: theme.spacing.unit * 2, - }, - flash: { - textAlign: 'center', - paddingTop: theme.spacing.unit, - paddingBottom: theme.spacing.unit, - }, - button: { - marginTop: theme.spacing.unit * 2, - }, -}) - -const Form = ({ - actionText, - isSubmitting, - classes, - handleChange, - values, - touched, - errors, - submitCount, -}) => { - return ( - - - - - - - - - - - - - - ) -} - -Form.propTypes = { - actionText: PropTypes.string.isRequired, - onSubmit: PropTypes.func.isRequired, -} - -const formikOpts = { - mapPropsToValues({ definition }) { - const shouldPersist = Object.keys(get('persistSpec')).length !== 0 - const persistedJSON = shouldPersist && get('persistSpec') - if (shouldPersist) set('persistSpec', {}) - const json = - JSON.stringify(definition, null, '\t') || - (shouldPersist && persistedJSON) || - '' - return { json } - }, - - validate(values) { - const errors = {} - - try { - JSON.parse(values.json, null, '\t') - } catch (e) { - errors.json = 'Invalid JSON' - } - - return errors - }, - - handleSubmit(values, { props, setSubmitting }) { - const definition = JSON.parse(values.json) - set('persistSpec', values.json) - props.onSubmit(definition, props.onSuccess, props.onError) - setTimeout(() => { - setSubmitting(false) - }, 1000) - }, -} - -const FormikForm = formik.withFormik(formikOpts)(Form) - -export default withStyles(styles)(FormikForm) diff --git a/operator_ui/src/components/Jobs/Form.tsx b/operator_ui/src/components/Jobs/Form.tsx new file mode 100644 index 00000000000..f0e2b28dcbf --- /dev/null +++ b/operator_ui/src/components/Jobs/Form.tsx @@ -0,0 +1,153 @@ +import React from 'react' +import { + createStyles, + Theme, + withStyles, + WithStyles, +} from '@material-ui/core/styles' +import { TextField, Grid } from '@material-ui/core' +import { withFormik, FormikProps, Form as FormikForm } from 'formik' +import * as storage from '@chainlink/local-storage' +import { Prompt } from 'react-router-dom' +import isEqual from 'lodash/isEqual' +import Button from 'components/Button' + +const styles = (theme: Theme) => + createStyles({ + card: { + paddingBottom: theme.spacing.unit * 2, + }, + flash: { + textAlign: 'center', + paddingTop: theme.spacing.unit, + paddingBottom: theme.spacing.unit, + }, + button: { + marginTop: theme.spacing.unit * 2, + }, + }) + +const SUBMITTING_TIMEOUT_MS = 1000 +const UNSAVED_JOB_SPEC = 'persistSpec' + +interface FormValues { + json: string +} + +interface OwnProps extends Partial, WithStyles { + definition: string + actionText: string + isSubmitting: boolean + handleChange: Function + errors: any + onSubmit: ( + values: FormValues, + onSuccesss: Function, + onError: Function, + ) => void + onSuccess: Function + onError: Function +} + +type Props = FormikProps & OwnProps + +function submitSuccess(callback: Function) { + return (response: object) => { + storage.remove(UNSAVED_JOB_SPEC) + return callback(response) + } +} + +function submitError(callback: Function, values: FormValues) { + return (error: object) => { + storage.set(UNSAVED_JOB_SPEC, values.json) + return callback(error) + } +} + +function initialValues({ json }: OwnProps): FormValues { + const unsavedJobSpec = storage.get(UNSAVED_JOB_SPEC) + return unsavedJobSpec ? { json: unsavedJobSpec } : { json: json || '' } +} + +function isDirty(props: Props): boolean { + const initial = initialValues(props) + return !isEqual(props.values, initial) && props.submitCount === 0 +} + +const Form: React.FC = props => { + return ( + <> + + + + + + + + + + + + + ) +} + +const WithFormikForm = withFormik({ + mapPropsToValues({ definition }) { + const json = + JSON.stringify(definition, null, '\t') || + storage.get(UNSAVED_JOB_SPEC) || + '' + return { json } + }, + + validate(values) { + try { + JSON.parse(values.json) + return {} + } catch { + return { json: 'Invalid JSON' } + } + }, + + handleSubmit(values, { props, setSubmitting }) { + const definition = JSON.parse(values.json) + props.onSubmit( + definition, + submitSuccess(props.onSuccess), + submitError(props.onError, values), + ) + setTimeout(() => { + setSubmitting(false) + }, SUBMITTING_TIMEOUT_MS) + }, +})(Form) + +export default withStyles(styles)(WithFormikForm) diff --git a/operator_ui/src/components/Notifications/DefaultError.js b/operator_ui/src/components/Notifications/DefaultError.js deleted file mode 100644 index 33eb04abafe..00000000000 --- a/operator_ui/src/components/Notifications/DefaultError.js +++ /dev/null @@ -1 +0,0 @@ -export default ({ msg }) => msg diff --git a/operator_ui/src/components/Notifications/DefaultError.tsx b/operator_ui/src/components/Notifications/DefaultError.tsx new file mode 100644 index 00000000000..17cdda22eda --- /dev/null +++ b/operator_ui/src/components/Notifications/DefaultError.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +interface Props { + msg: string +} + +export default function DefaultError({ msg }: Props) { + return msg +} diff --git a/operator_ui/src/connectors/redux/reducers/authentication.ts b/operator_ui/src/connectors/redux/reducers/authentication.ts index 8b0e2bd4aac..27762e6f31d 100644 --- a/operator_ui/src/connectors/redux/reducers/authentication.ts +++ b/operator_ui/src/connectors/redux/reducers/authentication.ts @@ -1,4 +1,4 @@ -import * as authenticationStorage from 'utils/authenticationStorage' +import * as storage from 'utils/storage' const defaultState = { allowed: false, @@ -9,7 +9,7 @@ const defaultState = { const initialState = Object.assign( {}, defaultState, - authenticationStorage.get(), + storage.getAuthentication(), ) export type AuthenticationAction = @@ -53,7 +53,8 @@ export default (state = initialState, action: AuthenticationAction) => { case AuthenticationActionType.RECEIVE_SIGNOUT_SUCCESS: case AuthenticationActionType.RECEIVE_SIGNIN_SUCCESS: { const allowed = { allowed: action.authenticated } - authenticationStorage.set(allowed) + storage.setAuthentication(allowed) + return Object.assign({}, state, allowed, { errors: [], networkError: false, @@ -61,13 +62,15 @@ export default (state = initialState, action: AuthenticationAction) => { } case AuthenticationActionType.RECEIVE_SIGNIN_FAIL: { const allowed = { allowed: false } - authenticationStorage.set(allowed) + storage.setAuthentication(allowed) + return Object.assign({}, state, allowed, { errors: [] }) } case AuthenticationActionType.RECEIVE_SIGNIN_ERROR: case AuthenticationActionType.RECEIVE_SIGNOUT_ERROR: { const allowed = { allowed: false } - authenticationStorage.set(allowed) + storage.setAuthentication(allowed) + return Object.assign({}, state, allowed, { errors: action.errors || [], networkError: action.networkError, diff --git a/operator_ui/src/connectors/redux/reducers/notifications.js b/operator_ui/src/connectors/redux/reducers/notifications.js index d6be33439b1..4b311c7a998 100644 --- a/operator_ui/src/connectors/redux/reducers/notifications.js +++ b/operator_ui/src/connectors/redux/reducers/notifications.js @@ -4,8 +4,8 @@ import { NOTIFY_SUCCESS, NOTIFY_ERROR, } from 'actions' -import { set } from 'utils/storage' -import { BadRequestError } from '../../../api/errors' +import * as storage from '@chainlink/local-storage' +import { BadRequestError } from '@chainlink/json-api-client' const initialState = { errors: [], @@ -38,9 +38,12 @@ export default (state = initialState, action = {}) => { component: action.component, props: action.props, } - if (success.props.data && success.props.data.type === 'specs') - set('persistSpec', {}) - else if (typeof success.props.url === 'string') set('persistBridge', {}) + if (success.props.data && success.props.data.type === 'specs') { + storage.setJson('persistSpec', {}) + } else if (typeof success.props.url === 'string') { + storage.setJson('persistBridge', {}) + } + return Object.assign({}, state, { successes: [success], errors: [], @@ -65,7 +68,9 @@ export default (state = initialState, action = {}) => { } else { errorNotifications = [error] } - if (error instanceof BadRequestError) set('persistBridge', {}) + if (error instanceof BadRequestError) { + storage.setJson('persistBridge', {}) + } return Object.assign({}, state, { successes: [], diff --git a/operator_ui/src/containers/SignIn.js b/operator_ui/src/containers/SignIn.js index be2d67cfe22..8b1e4e51be8 100644 --- a/operator_ui/src/containers/SignIn.js +++ b/operator_ui/src/containers/SignIn.js @@ -13,7 +13,7 @@ import { hot } from 'react-hot-loader' import { submitSignIn } from 'actions' import HexagonLogo from 'components/Logos/Hexagon' import matchRouteAndMapDispatchToProps from 'utils/matchRouteAndMapDispatchToProps' -import { get } from 'utils/storage' +import { getPersistUrl } from '../utils/storage' const styles = theme => ({ container: { @@ -53,9 +53,10 @@ export const SignIn = useHooks(props => { } const { classes, fetching, authenticated, errors } = props - const hasPrevState = Object.keys(get('persistURL')).length !== 0 - if (authenticated) - return + if (authenticated) { + return + } + return ( storage.get('authentication') - -// CHECK ME -export const set = (obj: any) => storage.set('authentication', obj) diff --git a/operator_ui/src/utils/formatRequestURI.js b/operator_ui/src/utils/formatRequestURI.js deleted file mode 100644 index 6eaf9cb5a73..00000000000 --- a/operator_ui/src/utils/formatRequestURI.js +++ /dev/null @@ -1,14 +0,0 @@ -import url from 'url' - -export default (path, query = {}, options = {}) => { - const formatOptions = { - pathname: path, - query: query, - } - - if (options.port) { - formatOptions['port'] = options.port - formatOptions['hostname'] = options.hostname - } - return url.format(formatOptions) -} diff --git a/operator_ui/src/utils/storage.js b/operator_ui/src/utils/storage.js deleted file mode 100644 index 856cf449bc3..00000000000 --- a/operator_ui/src/utils/storage.js +++ /dev/null @@ -1,20 +0,0 @@ -import storage from 'local-storage-fallback' - -export const get = key => { - const localStorageItem = storage.getItem(`chainlink.${key}`) - const obj = {} - - if (localStorageItem) { - try { - return JSON.parse(localStorageItem) - } catch (e) { - // continue regardless of error - } - } - - return obj -} - -export const set = (key, obj) => { - storage.setItem(`chainlink.${key}`, JSON.stringify(obj)) -} diff --git a/operator_ui/src/utils/storage.ts b/operator_ui/src/utils/storage.ts new file mode 100644 index 00000000000..a7616e954f6 --- /dev/null +++ b/operator_ui/src/utils/storage.ts @@ -0,0 +1,23 @@ +import * as storage from '@chainlink/local-storage' + +const PERSIST_URL = 'persistURL' + +export function getPersistUrl(): string { + return storage.get(PERSIST_URL) || '' +} + +export function setPersistUrl(url: string): void { + storage.set(PERSIST_URL, url) +} + +export interface Auth { + allowed?: boolean +} + +export function getAuthentication(): Auth { + return storage.getJson('authentication') +} + +export function setAuthentication(auth: Auth): void { + storage.setJson('authentication', auth) +} diff --git a/operator_ui/static.config.js b/operator_ui/static.config.js index 1b943b3e342..1089958527a 100644 --- a/operator_ui/static.config.js +++ b/operator_ui/static.config.js @@ -10,6 +10,10 @@ const generateClassName = createGenerateClassName() export default { maxThreads: MAX_EXPORT_HTML_THREADS || CORES, + devServer: { + port: 3000, + host: '127.0.0.1', + }, getSiteData: () => ({ title: 'Chainlink', }), diff --git a/operator_ui/support/test-helpers/globPath.js b/operator_ui/support/test-helpers/globPath.js new file mode 100644 index 00000000000..67adcb76e42 --- /dev/null +++ b/operator_ui/support/test-helpers/globPath.js @@ -0,0 +1,3 @@ +export default path => { + return `glob:${process.env.CHAINLINK_PORT || ''}${path}*` +} diff --git a/operator_ui/tsconfig.json b/operator_ui/tsconfig.json index 6748324fa83..f84dc48e7c5 100644 --- a/operator_ui/tsconfig.json +++ b/operator_ui/tsconfig.json @@ -22,5 +22,9 @@ }, "include": ["src", "./@types"], - "references": [{ "path": "../styleguide" }] + "references": [ + { "path": "../styleguide" }, + { "path": "../tools/json-api-client" }, + { "path": "../tools/local-storage" } + ] } diff --git a/package.json b/package.json index cc7427d23f6..1b5380e1af7 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,10 @@ "evm/box", "evm/v0.5", "tools", - "tools/prettier-config", "tools/eslint-config", + "tools/local-storage", + "tools/json-api-client", + "tools/prettier-config", "operator_ui", "integration", "styleguide", diff --git a/styleguide/package.json b/styleguide/package.json index 09109d0f734..7f9a8e9d253 100644 --- a/styleguide/package.json +++ b/styleguide/package.json @@ -1,13 +1,13 @@ { "name": "@chainlink/styleguide", "private": true, - "version": "0.0.0", + "version": "0.0.1", "main": "./dist/src", "types": "./dist/src", "scripts": { "start": "start-storybook -p 9001", "build-storybook": "build-storybook", - "build": "rm -rf dist && tsc", + "build": "rimraf -rf dist && tsc", "eslint": "eslint --ext .js,.jsx,.ts,.tsx src stories @types", "lint": "tsc --noEmit && yarn eslint", "format": "prettier --write \"**/*\"", @@ -17,7 +17,7 @@ "dependencies": { "@material-ui/core": "^3.9.2", "change-case": "^3.0.2", - "core-js": "^3.2.1", + "core-js": "^3.4.0", "javascript-time-ago": "^2.0.1", "moment": "^2.24.0", "prop-types": "^15.6.2", @@ -28,7 +28,7 @@ "typescript": "^3.6.3" }, "devDependencies": { - "@babel/core": "^7.2.2", + "@babel/core": "^7.6.4", "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.1.0", @@ -36,7 +36,7 @@ "@chainlink/prettier-config": "0.0.1", "@storybook/addon-actions": "^5.1.10", "@storybook/addon-info": "^5.2.4", - "@storybook/addon-links": "^5.2.4", + "@storybook/addon-links": "^5.2.5", "@storybook/addons": "^5.2.1", "@storybook/react": "^5.1.10", "@types/react": "^16.9.5", @@ -48,6 +48,7 @@ "eslint": "^6.3.0", "prettier": "^1.18.2", "react-docgen-typescript-loader": "^3.1.0", + "rimraf": "^3.0.0", "webpack": "^4.41.1" }, "prettier": "@chainlink/prettier-config" diff --git a/tools/bin/echo_server b/tools/bin/echo_server deleted file mode 100755 index 3b2be113e4d..00000000000 --- a/tools/bin/echo_server +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -yarn workspace @chainlink/example-echo-server run start diff --git a/tools/bin/ldflags b/tools/bin/ldflags index 041b9610938..c9b45b9f75d 100755 --- a/tools/bin/ldflags +++ b/tools/bin/ldflags @@ -5,5 +5,4 @@ cd "$(dirname "$0")" COMMIT_SHA=${COMMIT_SHA:-$(git rev-parse HEAD)} VERSION=${VERSION:-$(cat "../../VERSION")} -echo "-X github.com/smartcontractkit/chainlink/core/store.Version=$VERSION" \ - "-X github.com/smartcontractkit/chainlink/core/store.Sha=$COMMIT_SHA" +echo "-X chainlink/core/store.Version=$VERSION -X chainlink/core/store.Sha=$COMMIT_SHA" diff --git a/tools/ci/appveyor.yml b/tools/ci/appveyor.yml deleted file mode 100644 index a4392c1963b..00000000000 --- a/tools/ci/appveyor.yml +++ /dev/null @@ -1,43 +0,0 @@ -build: false - -clone_folder: c:\chainlink - -deploy: false - -environment: - CC: gcc.exe - CGO_ENABLED: 1 - GOPATH: c:\gopath - GOVERSION: 1.12.6 - MSYS2_ARCH: x86_64 - MSYS2_BITS: 64 - MSYSTEM: MINGW64 - PATH: C:\msys64\mingw64\bin\;C:\Program Files (x86)\NSIS\;%PATH% - nodejs_version: "10" - PYTHON: "3.7" - -stack: python %PYTHON% - -init: - - git config --global core.autocrlf input - -install: - # Install the configured nodejs version - - ps: Install-Product node $env:nodejs_version - # Generate solidity contract artifacts - - yarn install - - yarn workspace chainlink run setup - - # Install the specific Go version. - - rmdir c:\go /s /q - - appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi - - msiexec /i go%GOVERSION%.windows-amd64.msi /q - - set Path=c:\go\bin;c:\gopath\bin;C:\msys64\mingw64\bin\;%Path% - # Check tools exist. - - gcc --version - - go version - - go env - - go mod download - -test_script: - - go test -parallel 2 -p 1 ./... diff --git a/tools/ci/ethereum_test b/tools/ci/ethereum_test index ce8670af771..df5235de331 100755 --- a/tools/ci/ethereum_test +++ b/tools/ci/ethereum_test @@ -60,7 +60,7 @@ title 'End to end tests.' launch_cypress_job_server set -o pipefail -yarn workspace @chainlink/integration test:cypress | tee $SRCROOT/integration/e2e.tests.log +yarn workspace @chainlink/integration test:cypress | tee $SRCROOT/integration/logs/e2e.tests.log set +o pipefail title 'All tests passed.' diff --git a/tools/ci/forks_test b/tools/ci/forks_test index c625e8364fb..c1c06569aa7 100755 --- a/tools/ci/forks_test +++ b/tools/ci/forks_test @@ -6,7 +6,7 @@ source ./integration/common heading 'Forks Test' -yarn workspace @chainlink/integration test:forks +./integration/forks/test title 'All tests passed.' diff --git a/tools/ci/go_test b/tools/ci/go_test index 9302fab728a..3073c63f02b 100755 --- a/tools/ci/go_test +++ b/tools/ci/go_test @@ -9,8 +9,8 @@ yarn workspace chainlink setup # Run golang coverage go get -u github.com/smartcontractkit/goverage -goverage -parallel 2 -coverprofile=c.out ./... +goverage -v -parallel 2 -coverprofile=c.out ./... if [ -n "$CC_TEST_REPORTER_ID" ]; then - cc-test-reporter format-coverage --output "coverage/codeclimate.go.json" --prefix "github.com/smartcontractkit/chainlink" + cc-test-reporter format-coverage --output "coverage/codeclimate.go.json" --prefix "chainlink" gsutil cp "coverage/codeclimate.go.json" gs://codeclimate-aggregation/$CIRCLE_WORKFLOW_ID/ fi diff --git a/tools/ci/gorace_test b/tools/ci/gorace_test index 69e5d5940d9..6e7df9b1a17 100755 --- a/tools/ci/gorace_test +++ b/tools/ci/gorace_test @@ -1,4 +1,4 @@ #!/bin/bash set -e -GORACE="halt_on_error=1" go test -v -race -parallel 2 -p 1 github.com/smartcontractkit/chainlink/core/internal github.com/smartcontractkit/chainlink/core/services +GORACE="halt_on_error=1" go test -v -race -parallel 2 -p 1 chainlink/core/internal chainlink/core/services diff --git a/tools/ci/prepublish_npm_test b/tools/ci/prepublish_npm_test new file mode 100755 index 00000000000..f1decf8cdd5 --- /dev/null +++ b/tools/ci/prepublish_npm_test @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +yarn workspace chainlink prepublishOnly +yarn workspace chainlinkv0.5 prepublishOnly diff --git a/tools/ci/truffle_test b/tools/ci/truffle_test index a42d0f4ae9c..3f48dfda4af 100755 --- a/tools/ci/truffle_test +++ b/tools/ci/truffle_test @@ -8,9 +8,9 @@ yarn workspaces run lint yarn workspace chainlink run slither yarn workspace chainlinkv0.5 run slither +yarn workspace chainlinkv0.5 build + # These should be merged into a global test command yarn workspace chainlink test yarn workspace chainlinkv0.5 test -yarn workspace @chainlink/example-uptime-sla test -yarn workspace @chainlink/example-echo-server test yarn workspace @chainlink/box test \ No newline at end of file diff --git a/tools/eslint-config/package.json b/tools/eslint-config/package.json index 5ed2e07c1eb..b846541abc0 100644 --- a/tools/eslint-config/package.json +++ b/tools/eslint-config/package.json @@ -13,14 +13,9 @@ "eslint:config:react": "eslint -c ./react.js --print-config ./base", "setup": "echo \"No setup required for @chainlink/eslint-config\"" }, - "peerDependencies": { - "@chainlink/prettier-config": "0.0.1", - "prettier": "^1.18.2", - "typescript": "^3.6.3" - }, "dependencies": { "@typescript-eslint/eslint-plugin": "^2.1.0", - "@typescript-eslint/parser": "^2.5.0", + "@typescript-eslint/parser": "^2.6.1", "eslint-config-prettier": "^6.2.0", "eslint-plugin-prettier": "^3.1.0", "eslint-plugin-react": "^7.14.3", diff --git a/tools/json-api-client/.eslintignore b/tools/json-api-client/.eslintignore new file mode 100644 index 00000000000..6ca845cd079 --- /dev/null +++ b/tools/json-api-client/.eslintignore @@ -0,0 +1,3 @@ +public/ +node_modules/ +dist/ diff --git a/tools/json-api-client/.eslintrc.js b/tools/json-api-client/.eslintrc.js new file mode 100644 index 00000000000..e81b54ce712 --- /dev/null +++ b/tools/json-api-client/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['@chainlink/eslint-config/react'], +} diff --git a/tools/json-api-client/.gitignore b/tools/json-api-client/.gitignore new file mode 100644 index 00000000000..1521c8b7652 --- /dev/null +++ b/tools/json-api-client/.gitignore @@ -0,0 +1 @@ +dist diff --git a/tools/json-api-client/.prettierignore b/tools/json-api-client/.prettierignore new file mode 100644 index 00000000000..7cb6210d43f --- /dev/null +++ b/tools/json-api-client/.prettierignore @@ -0,0 +1,2 @@ +.prettierignore +.gitignore diff --git a/tools/json-api-client/@types/global.d.ts b/tools/json-api-client/@types/global.d.ts new file mode 100644 index 00000000000..34dd032a93b --- /dev/null +++ b/tools/json-api-client/@types/global.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface Global { + fetch: any + } +} diff --git a/tools/json-api-client/@types/json-api-normalizer.d.ts b/tools/json-api-client/@types/json-api-normalizer.d.ts new file mode 100644 index 00000000000..c31065eebcc --- /dev/null +++ b/tools/json-api-client/@types/json-api-normalizer.d.ts @@ -0,0 +1,209 @@ +/* eslint @typescript-eslint/class-name-casing: 'off' */ + +declare module 'json-api-normalizer' { + export interface JsonApiResponse< + TData extends + | ResourceObject[] + | ResourceObject + | null = + | ResourceObject[] + | ResourceObject, + TError extends ErrorsObject[] = ErrorsObject[], + TIncluded extends (ResourceObject[]) | never = never, + TMeta extends Record | never = never, + TLinks extends LinksObject | never = never + > { + data: TData + included?: TIncluded + links: TLinks + errors?: TError + meta: TMeta + } + + /** + * Where specified, a links member can be used to represent links. The value of each links + * member MUST be an object (a “links object”). + * + * Each member of a links object is a “link”. A link MUST be represented as either: + * + * - a string containing the link’s URL. + * - an object (“link object”) which can contain the following members: + * - href: a string containing the link’s URL. + * - meta: a meta object containing non-standard meta-information about the link. + */ + type LinksObject = Partial> + export interface LinkObject< + TLinkMeta extends Record = Record + > { + href: string + meta: TLinkMeta + } + + export interface ErrorsObject< + TMeta extends Record = Record, + TLinks extends LinksObject = LinksObject + > { + id?: string + links?: TLinks + status?: string + code?: string + title?: string + detail?: string + source?: { + pointer?: string + parameter?: string + [propName: string]: any + } + meta?: TMeta + } + + /** + * Resource Identifier Objects + * A “resource identifier object” is an object that identifies an individual resource. + * + * A “resource identifier object” MUST contain type and id members. + * + * A “resource identifier object” MAY also include a meta member, whose value is a meta object + * that contains non-standard meta-information. + */ + export interface ResourceIdentifierObject< + TMeta extends Record = Record + > { + type: string + id: string + meta?: TMeta + } + + /** + * Resource Linkage + * + * Resource linkage in a compound document allows a client to + * link together all of the included resource objects without + * having to GET any URLs via links. + * + * Resource linkage MUST be represented as one of the following: + * + * - null for empty to-one relationships. + * - an empty array ([]) for empty to-many relationships. + * - a single resource identifier object for non-empty to-one relationships. + * - an array of resource identifier objects for non-empty to-many relationships. + */ + export type ResourceLinkage = + | null + | [] + | ResourceIdentifierObject + | ResourceIdentifierObject[] + + /** + * The value of the relationships key MUST be an object (a “relationships object”). Members of + * the relationships object (“relationships”) represent references from the resource object in + * which it’s defined to other resource objects. + * + * Relationships may be to-one or to-many. + * + * A “relationship object” MUST contain at least one of the following: + * + * - links: a links object containing at least one of the following: + * - self: a link for the relationship itself (a “relationship link”). + * This link allows the client to directly manipulate the relationship. + * For example, removing an author through an article’s relationship URL would + * disconnect the person from the article without deleting the people resource itself. + * When fetched successfully, this link returns the linkage for the related resources + * as its primary data. (See Fetching Relationships.) + * - related: a related resource link + *- data: resource linkage + *- meta: a meta object that contains non-standard meta-information about the relationship. + * + * A relationship object that represents a to-many relationship MAY also contain pagination + * links under the links member, as described below. + * Any pagination links in a relationship object MUST paginate the relationship data, + * not the related resources. + */ + export type Relationship< + TMeta extends Record = Record, + TLinks extends LinksObject = LinksObject + > = _Relationship + export interface _Relationship< + TMeta extends Record, + TLinks extends LinksObject + > { + data?: JsonApiResponse | JsonApiResponse[] + links?: TLinks + meta?: TMeta + } + + /** + * The value of the attributes key MUST be an object (an “attributes object”). Members of the + * attributes object (“attributes”) represent information about the resource object in which + * it’s defined. + * + * Attributes may contain any valid JSON value. + * + * Complex data structures involving JSON objects and arrays are allowed as attribute values. + * However, any object that constitutes or is contained in an attribute MUST NOT contain a + * relationships or links member, as those members are reserved by this specification for + * future use. + * + * Although has-one foreign keys (e.g. author_id) are often stored internally alongside other + * information to be represented in a resource object, these keys SHOULD NOT appear as + * attributes. + */ + interface AttributesObject extends Record {} + + /** + * Resource Objects + * + * “Resource objects” appear in a JSON:API document to represent resources. + * + * A resource object MUST contain at least the following top-level members: + * - id + * - type + * + * Exception: The id member is not required when the resource object originates at the client + * and represents a new resource to be created on the server. + * + * In addition, a resource object MAY contain any of these top-level members: + * - attributes: an attributes object representing some of the resource’s data. + * - relationships: a relationships object describing relationships between the resource and other JSON:API resources. + * - links: a links object containing links related to the resource. + * - meta: a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship. + */ + export interface ResourceObject< + TAttributes extends AttributesObject | never = never, + TRelationships extends + | Record>> + | never = never, + TMeta extends Record | never = never, + TLinks extends LinksObject | never = never + > { + id: string + type: string + attributes: TAttributes + relationships: TRelationships + links: TLinks + meta: TMeta + } + + // we cant infer TNormalized from the arguments + // because of the transformations it does on the supplied data + // unfortunately, this means that the user will have to supply + // their own typing for TNormalized + + /** + * Normalize JSON:API spec compliant JSON + * + * @param json The JSON:API spec compliant JSON to normalize + * @param opts Options for normalizing + */ + export default function normalize( + json: JsonApiResponse, + opts?: Opts, + ): TNormalized + + interface Opts { + camelizeKeys?: boolean + camelizeTypeValues?: boolean + endpoint?: string + filterEndpoint?: boolean + } +} diff --git a/operator_ui/__tests__/utils/fetchWithTimeout.test.ts b/tools/json-api-client/__tests__/fetchWithTimeout.test.ts similarity index 91% rename from operator_ui/__tests__/utils/fetchWithTimeout.test.ts rename to tools/json-api-client/__tests__/fetchWithTimeout.test.ts index b64bda6beb6..09294dacbc7 100644 --- a/operator_ui/__tests__/utils/fetchWithTimeout.test.ts +++ b/tools/json-api-client/__tests__/fetchWithTimeout.test.ts @@ -1,15 +1,17 @@ -import fetchWithTimeout from 'utils/fetchWithTimeout' +import fetchWithTimeout from '../src/fetchWithTimeout' describe('fetchWithTimeout', () => { it('rejects fetch requests after timeout period', () => { const timeoutResponse = new Promise(res => setTimeout(() => res(200), 100)) global.fetch.getOnce('/test', timeoutResponse) + return expect(fetchWithTimeout('/test', {}, 1)).rejects.toThrow('timeout') }) it('resolves fetch requests before timeout period', () => { const timeoutResponse = new Promise(res => setTimeout(() => res(200), 1)) global.fetch.getOnce('/test', timeoutResponse) + return expect(fetchWithTimeout('/test', {}, 100)).resolves.toMatchObject({ status: 200, }) diff --git a/tools/json-api-client/__tests__/formatRequestURI.test.ts b/tools/json-api-client/__tests__/formatRequestURI.test.ts new file mode 100644 index 00000000000..cb8cd265d19 --- /dev/null +++ b/tools/json-api-client/__tests__/formatRequestURI.test.ts @@ -0,0 +1,23 @@ +import formatRequestURI from '../src/formatRequestURI' + +describe('formatRequestURI', () => { + describe('port specified', () => { + it('returns host and port in URI', () => { + const uri = formatRequestURI( + '/api', + {}, + { hostname: 'localhost', port: '6689' }, + ) + + expect(uri).toEqual('localhost:6689/api') + }) + }) + + describe('no port specified', () => { + it('returns just the path in the URI', () => { + const uri = formatRequestURI('/api') + + expect(uri).toEqual('/api') + }) + }) +}) diff --git a/tools/json-api-client/jest.config.js b/tools/json-api-client/jest.config.js new file mode 100644 index 00000000000..957032f3d22 --- /dev/null +++ b/tools/json-api-client/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + setupFilesAfterEnv: ['/jest.setup.js'], + testPathIgnorePatterns: ['/node_modules/'], +} diff --git a/tools/json-api-client/jest.setup.js b/tools/json-api-client/jest.setup.js new file mode 100644 index 00000000000..4f37aed1a13 --- /dev/null +++ b/tools/json-api-client/jest.setup.js @@ -0,0 +1,2 @@ +global.fetch = require('fetch-mock').sandbox() +global.fetch.config.overwriteRoutes = true diff --git a/tools/json-api-client/package.json b/tools/json-api-client/package.json new file mode 100644 index 00000000000..1d9ac3d898f --- /dev/null +++ b/tools/json-api-client/package.json @@ -0,0 +1,35 @@ +{ + "name": "@chainlink/json-api-client", + "private": true, + "version": "0.0.1", + "main": "./dist/src", + "types": "./dist/src", + "scripts": { + "build": "rimraf -rf dist && tsc", + "lint": "eslint --ext .ts .", + "format": "prettier --write \"*.ts\"", + "setup": "yarn build", + "test": "jest", + "test:ci": "yarn test --coverage --reporters jest-silent-reporter --maxWorkers=50%" + }, + "peerDependencies": {}, + "dependencies": { + "isomorphic-unfetch": "^3.0.0", + "json-api-normalizer": "^0.4.13", + "path-to-regexp": "^3.0.0", + "typescript": "^3.6.3", + "url": "^0.11.0" + }, + "devDependencies": { + "@chainlink/eslint-config": "0.0.1", + "@chainlink/prettier-config": "0.0.1", + "@types/fetch-mock": "^7.3.1", + "@types/jest": "^24.0.18", + "@types/path-to-regexp": "^1.7.0", + "eslint": "^6.3.0", + "fetch-mock": "^7.3.1", + "jest": "^24.9.0", + "rimraf": "^3.0.0", + "ts-jest": "^24.0.0" + } +} diff --git a/operator_ui/src/api/errors.ts b/tools/json-api-client/src/errors.ts similarity index 71% rename from operator_ui/src/api/errors.ts rename to tools/json-api-client/src/errors.ts index 4126f185efa..fe77950f4d9 100644 --- a/operator_ui/src/api/errors.ts +++ b/tools/json-api-client/src/errors.ts @@ -1,6 +1,6 @@ import { JsonApiResponse } from 'json-api-normalizer' -interface Error { +export interface ErrorItem { status: number detail: any } @@ -10,7 +10,7 @@ export interface DocumentWithErrors { } export class AuthenticationError extends Error { - errors: Error[] + errors: ErrorItem[] constructor(response: Response) { super(`AuthenticationError(${response.statusText})`) @@ -32,22 +32,26 @@ export class BadRequestError extends Error { } } +export class UnprocessableEntityError extends Error { + errors: ErrorItem[] + + constructor(errors: ErrorItem[]) { + super('UnprocessableEntityError') + this.errors = errors + } +} + export class ServerError extends Error { - errors: Error[] + errors: ErrorItem[] - constructor(response: Response) { - super(`ServerError(${response.statusText})`) - this.errors = [ - { - status: response.status, - detail: response.statusText, - }, - ] + constructor(errors: ErrorItem[]) { + super('ServerError') + this.errors = errors } } export class UnknownResponseError extends Error { - errors: Error[] + errors: ErrorItem[] constructor(response: Response) { super(`UnknownResponseError(${response.statusText})`) diff --git a/operator_ui/src/utils/fetchWithTimeout.ts b/tools/json-api-client/src/fetchWithTimeout.ts similarity index 100% rename from operator_ui/src/utils/fetchWithTimeout.ts rename to tools/json-api-client/src/fetchWithTimeout.ts diff --git a/tools/json-api-client/src/formatRequestURI.ts b/tools/json-api-client/src/formatRequestURI.ts new file mode 100644 index 00000000000..9deef8486ac --- /dev/null +++ b/tools/json-api-client/src/formatRequestURI.ts @@ -0,0 +1,21 @@ +import * as url from 'url' + +export interface Options { + port?: string + hostname?: string +} + +export default function( + pathname: string, + query: Record = {}, + options: Options = {}, +) { + const formatOptions: url.UrlObject = { pathname, query } + + if (options.port) { + formatOptions.port = options.port + formatOptions.hostname = options.hostname + } + + return url.format(formatOptions) +} diff --git a/tools/json-api-client/src/index.ts b/tools/json-api-client/src/index.ts new file mode 100644 index 00000000000..1f145d31b82 --- /dev/null +++ b/tools/json-api-client/src/index.ts @@ -0,0 +1,3 @@ +export * from './errors' +export * from './transport/http' +export * from './transport/json' diff --git a/operator_ui/src/api/transport/http.ts b/tools/json-api-client/src/transport/http.ts similarity index 88% rename from operator_ui/src/api/transport/http.ts rename to tools/json-api-client/src/transport/http.ts index 8de25b9a423..d74cad70af8 100644 --- a/operator_ui/src/api/transport/http.ts +++ b/tools/json-api-client/src/transport/http.ts @@ -1,5 +1,5 @@ import 'isomorphic-unfetch' -import formatRequestURI from 'utils/formatRequestURI' +import formatRequestURI from '../formatRequestURI' export enum Method { GET = 'GET', @@ -25,7 +25,7 @@ export function getOptions(method: Method): (val?: any) => FetchOptions { return CUDOptionFactory(method) } -export function formatURI(path: string, query: object = {}) { +export function formatURI(path: string, query: Record = {}) { return formatRequestURI(path, query, { hostname: location.hostname, port: process.env.CHAINLINK_PORT, diff --git a/operator_ui/src/api/transport/json.ts b/tools/json-api-client/src/transport/json.ts similarity index 85% rename from operator_ui/src/api/transport/json.ts rename to tools/json-api-client/src/transport/json.ts index 09b9a1c07eb..fbea3bc2f14 100644 --- a/operator_ui/src/api/transport/json.ts +++ b/tools/json-api-client/src/transport/json.ts @@ -5,12 +5,14 @@ import { ResourceObject, } from 'json-api-normalizer' import pathToRegexp from 'path-to-regexp' -import fetchWithTimeout from 'utils/fetchWithTimeout' +import fetchWithTimeout from '../fetchWithTimeout' import { AuthenticationError, BadRequestError, + UnprocessableEntityError, ServerError, UnknownResponseError, + ErrorItem, } from '../errors' import * as http from './http' @@ -77,10 +79,9 @@ function methodFactory(method: http.Method) { return (params, namedPathParams) => { // if required, compile our path with its named path parameters const path = namedPathParams ? toPath(namedPathParams) : url - const uri = http.formatURI( - path, - method === http.Method.GET ? params : undefined, // add query string options if its a GET method - ) + // add query string options if its a GET method + const query = method === http.Method.GET ? params : {} + const uri = http.formatURI(path, query) const options = http.getOptions(method) const fetch = fetchWithTimeout(uri, options(params)) @@ -125,9 +126,9 @@ type ResponseType = TParams extends PaginatedRequestParams ? PaginatedApiResponse : ApiResponse -function parseResponse(response: Response): Promise { +async function parseResponse(response: Response): Promise { if (response.status === 204) { - return new Promise(resolve => resolve({} as T)) + return {} as T } else if (response.status >= 200 && response.status < 300) { return response.json() } else if (response.status === 400) { @@ -136,9 +137,31 @@ function parseResponse(response: Response): Promise { }) } else if (response.status === 401) { throw new AuthenticationError(response) + } else if (response.status === 422) { + const errors = await errorItems(response) + throw new UnprocessableEntityError(errors) } else if (response.status >= 500) { - throw new ServerError(response) + const errors = await errorItems(response) + throw new ServerError(errors) } else { throw new UnknownResponseError(response) } } + +async function errorItems(response: Response): Promise { + const json = await response.json() + + if (json.errors) { + return json.errors.map((e: ErrorsObject) => ({ + status: response.status, + detail: e.detail, + })) + } + + return [ + { + status: response.status, + detail: response.statusText, + }, + ] +} diff --git a/tools/json-api-client/tsconfig.json b/tools/json-api-client/tsconfig.json new file mode 100644 index 00000000000..38b42d2702d --- /dev/null +++ b/tools/json-api-client/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "strict": true, + "noEmitOnError": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "target": "es2017", + "module": "es2015", + "lib": ["es2018"], + "outDir": "./dist", + "jsx": "preserve", + "sourceMap": true, + "removeComments": false + }, + + "include": ["src", "@types"] +} diff --git a/tools/local-storage/.eslintignore b/tools/local-storage/.eslintignore new file mode 100644 index 00000000000..6ca845cd079 --- /dev/null +++ b/tools/local-storage/.eslintignore @@ -0,0 +1,3 @@ +public/ +node_modules/ +dist/ diff --git a/tools/local-storage/.eslintrc.js b/tools/local-storage/.eslintrc.js new file mode 100644 index 00000000000..e81b54ce712 --- /dev/null +++ b/tools/local-storage/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['@chainlink/eslint-config/react'], +} diff --git a/tools/local-storage/.gitignore b/tools/local-storage/.gitignore new file mode 100644 index 00000000000..1521c8b7652 --- /dev/null +++ b/tools/local-storage/.gitignore @@ -0,0 +1 @@ +dist diff --git a/tools/local-storage/.prettierignore b/tools/local-storage/.prettierignore new file mode 100644 index 00000000000..7cb6210d43f --- /dev/null +++ b/tools/local-storage/.prettierignore @@ -0,0 +1,2 @@ +.prettierignore +.gitignore diff --git a/tools/local-storage/__tests__/.gitkeep b/tools/local-storage/__tests__/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/local-storage/__tests__/storage.test.ts b/tools/local-storage/__tests__/storage.test.ts new file mode 100644 index 00000000000..74b82b825a3 --- /dev/null +++ b/tools/local-storage/__tests__/storage.test.ts @@ -0,0 +1,61 @@ +import storage from 'local-storage-fallback' +import { get, set, remove, getJson, setJson } from '../src/storage' + +beforeEach(() => { + storage.clear() +}) + +describe('get', () => { + it('returns a string keyed under "chainlink." in localStorage', () => { + storage.setItem('chainlink.foo', 'FOO') + expect(get('foo')).toEqual('FOO') + }) + + it('returns null when the key does not exist', () => { + expect(get('foo')).toEqual(null) + }) +}) + +describe('set', () => { + it('saves the string keyed under "chainlink." in localStorage', () => { + set('foo', 'FOO') + + const stored = storage.getItem('chainlink.foo') + expect(stored).toEqual('FOO') + }) +}) + +describe('remove', () => { + it('deletes the chainlink key', () => { + storage.setItem('chainlink.foo', 'FOO') + expect(storage.getItem('chainlink.foo')).toEqual('FOO') + + remove('foo') + expect(storage.getItem('chainlink.foo')).toEqual(null) + }) +}) + +describe('getJson', () => { + it('returns a JS object for JSON keyed under "chainlink." in localStorage', () => { + storage.setItem('chainlink.foo', '{"foo":"FOO"}') + expect(getJson('foo')).toEqual({ foo: 'FOO' }) + }) + + it('returns an empty JS object when it retrieves invalid JSON from storage', () => { + storage.setItem('chainlink.foo', '{"foo"}') + expect(getJson('foo')).toEqual({}) + }) + + it('returns an empty JS object when the key does not exist', () => { + expect(getJson('foo')).toEqual({}) + }) +}) + +describe('setJson', () => { + it('saves the JS object as JSON keyed under "chainlink." in localStorage', () => { + setJson('foo', { foo: 'FOO' }) + + const stored = storage.getItem('chainlink.foo') + expect(stored).toEqual('{"foo":"FOO"}') + }) +}) diff --git a/tools/local-storage/jest.config.js b/tools/local-storage/jest.config.js new file mode 100644 index 00000000000..957032f3d22 --- /dev/null +++ b/tools/local-storage/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + setupFilesAfterEnv: ['/jest.setup.js'], + testPathIgnorePatterns: ['/node_modules/'], +} diff --git a/tools/local-storage/jest.setup.js b/tools/local-storage/jest.setup.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/local-storage/package.json b/tools/local-storage/package.json new file mode 100644 index 00000000000..3047aac36f3 --- /dev/null +++ b/tools/local-storage/package.json @@ -0,0 +1,29 @@ +{ + "name": "@chainlink/local-storage", + "private": true, + "version": "0.0.1", + "main": "./dist/src", + "types": "./dist/src", + "scripts": { + "build": "rimraf -rf dist && tsc", + "lint": "eslint --ext .ts .", + "format": "prettier --write \"*.ts\"", + "setup": "yarn build", + "test": "jest", + "test:ci": "yarn test --coverage --reporters jest-silent-reporter --maxWorkers=50%" + }, + "peerDependencies": {}, + "dependencies": { + "local-storage-fallback": "^4.1.1", + "typescript": "^3.6.3" + }, + "devDependencies": { + "@chainlink/eslint-config": "0.0.1", + "@chainlink/prettier-config": "0.0.1", + "@types/jest": "^24.0.18", + "eslint": "^6.3.0", + "jest": "^24.9.0", + "rimraf": "^3.0.0", + "ts-jest": "^24.0.0" + } +} diff --git a/tools/local-storage/src/index.ts b/tools/local-storage/src/index.ts new file mode 100644 index 00000000000..69b61ecc471 --- /dev/null +++ b/tools/local-storage/src/index.ts @@ -0,0 +1 @@ +export * from './storage' diff --git a/tools/local-storage/src/storage.ts b/tools/local-storage/src/storage.ts new file mode 100644 index 00000000000..577efbd7895 --- /dev/null +++ b/tools/local-storage/src/storage.ts @@ -0,0 +1,33 @@ +import storage from 'local-storage-fallback' + +export function get(key: string): string | null { + return storage.getItem(`chainlink.${key}`) +} + +export function set(key: string, val: string): void { + storage.setItem(`chainlink.${key}`, val) +} + +export function remove(key: string): void { + storage.removeItem(`chainlink.${key}`) +} + +export function getJson(key: string): any { + const stored = get(key) + const obj = {} + + if (stored) { + try { + return JSON.parse(stored) + } catch (e) { + // continue regardless of error + } + } + + return obj +} + +export function setJson(key: string, obj: any): void { + const json = JSON.stringify(obj) + set(key, json) +} diff --git a/tools/local-storage/tsconfig.json b/tools/local-storage/tsconfig.json new file mode 100644 index 00000000000..eafa53e4f03 --- /dev/null +++ b/tools/local-storage/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "strict": true, + "noEmitOnError": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "target": "es2017", + "module": "es2015", + "lib": [ + "es2018", + "dom" + ], + "outDir": "./dist", + "jsx": "preserve", + "sourceMap": true, + "removeComments": false + }, + + "include": ["src", "@types"] +} diff --git a/tools/package.json b/tools/package.json index 692fb8efee6..f036f043b07 100644 --- a/tools/package.json +++ b/tools/package.json @@ -9,7 +9,7 @@ "setup": "echo \"No setup required for @chainlink/tools\"" }, "dependencies": { - "axios": "^0.18.1" + "axios": "^0.19.0" }, "devDependencies": { "@chainlink/eslint-config": "0.0.1", diff --git a/yarn.lock b/yarn.lock index e522e1b8af7..a8a1c4402ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,7 +40,7 @@ "@0x/sol-compiler@^3.1.15": version "3.1.15" - resolved "https://registry.yarnpkg.com/@0x/sol-compiler/-/sol-compiler-3.1.15.tgz#aaaad55008dddd69ad1e3226aa4a2832e0dd13b3" + resolved "https://registry.npmjs.org/@0x/sol-compiler/-/sol-compiler-3.1.15.tgz#aaaad55008dddd69ad1e3226aa4a2832e0dd13b3" integrity sha512-IobhcQ/whFRL942/ykKc0fV6/YstHhvnQJ0noUZ9GabMDtaBlW6k5vAerSkXZU/YyOd8sD9nw7QSm295D6HoTA== dependencies: "@0x/assert" "^2.1.6" @@ -228,7 +228,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@7.5.5", "@babel/core@^7.4.5": +"@babel/core@7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg== @@ -248,38 +248,18 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.2.2", "@babel/core@^7.5.5": - version "7.6.0" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.6.0.tgz#9b00f73554edd67bebc86df8303ef678be3d7b48" - integrity sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw== - dependencies: - "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.6.0" - "@babel/helpers" "^7.6.0" - "@babel/parser" "^7.6.0" - "@babel/template" "^7.6.0" - "@babel/traverse" "^7.6.0" - "@babel/types" "^7.6.0" - convert-source-map "^1.1.0" - debug "^4.1.0" - json5 "^2.1.0" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/core@^7.0.1": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.2.tgz#069a776e8d5e9eefff76236bc8845566bd31dd91" - integrity sha512-l8zto/fuoZIbncm+01p8zPSDZu/VuuJhAfA7d/AbzM09WR7iVhavvfNDYCNpo1VvLk6E6xgAoP9P+/EMJHuRkQ== +"@babel/core@^7.0.0", "@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.2.2", "@babel/core@^7.4.5", "@babel/core@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.4.tgz#6ebd9fe00925f6c3e177bb726a188b5f578088ff" + integrity sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.6.2" + "@babel/generator" "^7.6.4" "@babel/helpers" "^7.6.2" - "@babel/parser" "^7.6.2" + "@babel/parser" "^7.6.4" "@babel/template" "^7.6.0" - "@babel/traverse" "^7.6.2" - "@babel/types" "^7.6.0" + "@babel/traverse" "^7.6.3" + "@babel/types" "^7.6.3" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -288,23 +268,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.4.0", "@babel/generator@^7.5.5", "@babel/generator@^7.6.0": - version "7.6.0" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz#e2c21efbfd3293ad819a2359b448f002bfdfda56" - integrity sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA== - dependencies: - "@babel/types" "^7.6.0" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - trim-right "^1.0.1" - -"@babel/generator@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.2.tgz#dac8a3c2df118334c2a29ff3446da1636a8f8c03" - integrity sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ== +"@babel/generator@^7.0.0", "@babel/generator@^7.4.0", "@babel/generator@^7.5.5", "@babel/generator@^7.6.3", "@babel/generator@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" + integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w== dependencies: - "@babel/types" "^7.6.0" + "@babel/types" "^7.6.3" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" @@ -484,16 +453,7 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.5.5", "@babel/helpers@^7.6.0": - version "7.6.0" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.6.0.tgz#21961d16c6a3c3ab597325c34c465c0887d31c6e" - integrity sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ== - dependencies: - "@babel/template" "^7.6.0" - "@babel/traverse" "^7.6.0" - "@babel/types" "^7.6.0" - -"@babel/helpers@^7.6.2": +"@babel/helpers@^7.5.5", "@babel/helpers@^7.6.2": version "7.6.2" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.2.tgz#681ffe489ea4dcc55f23ce469e58e59c1c045153" integrity sha512-3/bAUL8zZxYs1cdX2ilEE0WobqbCmKWr/889lf2SS0PpDcpEIY8pb1CCyz0pEcX3pEb+MCbks1jIokz2xLtGTA== @@ -511,20 +471,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.3.1", "@babel/parser@^7.4.3", "@babel/parser@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" - integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== - -"@babel/parser@^7.6.0": - version "7.6.0" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b" - integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ== - -"@babel/parser@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1" - integrity sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.3.1", "@babel/parser@^7.4.3", "@babel/parser@^7.5.5", "@babel/parser@^7.6.0", "@babel/parser@^7.6.3", "@babel/parser@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81" + integrity sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A== "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -1289,10 +1239,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-typescript" "^7.6.0" -"@babel/register@^7.0.0": - version "7.6.0" - resolved "https://registry.npmjs.org/@babel/register/-/register-7.6.0.tgz#76b6f466714680f4becafd45beeb2a7b87431abf" - integrity sha512-78BomdN8el+x/nkup9KwtjJXuptW5oXMFmP11WoM2VJBjxrKv4grC3qjpLL8RGGUYUGsm57xnjYFM2uom+jWUQ== +"@babel/register@^7.0.0", "@babel/register@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.6.2.tgz#25765a922202cb06f8bdac5a3b1e70cd6bf3dd45" + integrity sha512-xgZk2LRZvt6i2SAUWxc7ellk4+OYRgS3Zpsnr13nMS1Qo25w21Uu8o6vTOAqNaxiqrnv30KTYzh9YWY2k21CeQ== dependencies: find-cache-dir "^2.0.0" lodash "^4.17.13" @@ -1337,55 +1287,25 @@ "@babel/parser" "^7.6.0" "@babel/types" "^7.6.0" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5", "@babel/traverse@^7.6.0": - version "7.6.0" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz#389391d510f79be7ce2ddd6717be66d3fed4b516" - integrity sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ== - dependencies: - "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.6.0" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.6.0" - "@babel/types" "^7.6.0" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.2.3": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" - integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ== - dependencies: - "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.5.5" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.5.5" - "@babel/types" "^7.5.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.2.tgz#b0e2bfd401d339ce0e6c05690206d1e11502ce2c" - integrity sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ== +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.2.3", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5", "@babel/traverse@^7.6.2", "@babel/traverse@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.3.tgz#66d7dba146b086703c0fb10dd588b7364cec47f9" + integrity sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.6.2" + "@babel/generator" "^7.6.3" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.6.2" - "@babel/types" "^7.6.0" + "@babel/parser" "^7.6.3" + "@babel/types" "^7.6.3" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.6.0": - version "7.6.1" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648" - integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g== +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.6.0", "@babel/types@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09" + integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA== dependencies: esutils "^2.0.2" lodash "^4.17.13" @@ -1762,48 +1682,48 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@ledgerhq/devices@^4.72.2": - version "4.72.2" - resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-4.72.2.tgz#9f91371c6a08cd4d93be0d6fe1c5d53a9ccd4a14" - integrity sha512-iX6ZfxFrKxlO3A7rDypjsqbu1Ta2IB+OsOk/7jnd3fzcvwlSEQwflAHb81CsjOPhdVG2aPIFU/VxzMioDFz24g== +"@ledgerhq/devices@^4.73.4": + version "4.73.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-4.73.4.tgz#0a1866fd1f10e2afc57c5f002f60d52ad5e6bb99" + integrity sha512-oEhd1UlbkdRFe5ZQCr8aXZ4Z/uFvrTsIhXv6bDlqA0sR2VYZRqQPR4C0xLdcDo3NG88fP+6Xfi6wq3wBbT5n3A== dependencies: - "@ledgerhq/errors" "^4.72.2" + "@ledgerhq/errors" "^4.73.4" "@ledgerhq/logs" "^4.72.0" rxjs "^6.5.3" -"@ledgerhq/errors@^4.72.2": - version "4.72.2" - resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-4.72.2.tgz#62a87a249c3c05e7295f66c3d49d78e7d46d611f" - integrity sha512-Q9SnjsoIiNhoTujUWfNgGqvYHgaPGRLzuYAC0Rx2WjEKp7swO42BMukYtSgXyyrz4yfHhMBMYerEF2sonBVzXw== +"@ledgerhq/errors@^4.73.4": + version "4.73.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-4.73.4.tgz#df4705d11350f252c0f0c202e88a32723f409484" + integrity sha512-5Wz48jm0NozGkipPcYRAAFcwm50xLhCWSpmP96Fs7v8BtM72E1AtqOud5SCyTKS6OP10mPuDB1459b7FGFoBcw== "@ledgerhq/hw-app-eth@^4.3.0": - version "4.72.2" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.72.2.tgz#64ec0377efa462bc8092cba88b700d11ed51c13f" - integrity sha512-pW2vOX29M5UvjqvEHVQsd3pDskJBcRPtQCSXtknLj4ZlSuCCuZtOhtD4qO3/nEiiHZL0DejTlRB9gl2Qug50lw== + version "4.73.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.73.4.tgz#3696df17c819dea213da56c4841341db7fb571aa" + integrity sha512-MJZlzVj6s/ZRQBikNo6Z9n5N/RQLc78xF+Esxo8C9RnFj0K0mTW6J0fqwqRwyfSJrJblAQE3ucTNAoXI4vJDPg== dependencies: - "@ledgerhq/errors" "^4.72.2" - "@ledgerhq/hw-transport" "^4.72.2" + "@ledgerhq/errors" "^4.73.4" + "@ledgerhq/hw-transport" "^4.73.4" -"@ledgerhq/hw-transport-node-hid-noevents@^4.72.2": - version "4.72.2" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-4.72.2.tgz#3991614bd6793a1bf66a27c9fe50ec23ba2aa5ec" - integrity sha512-yzsN5dDAeZlC3FzYQbCN8yeyWvHzNNRrYQFwtWFDs43ZD2k0p9j1xr5rozAIP9NTzVlAyAow0VCRTQEzUb9QuA== +"@ledgerhq/hw-transport-node-hid-noevents@^4.73.4": + version "4.73.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-4.73.4.tgz#964d3699a3110a48663247ba861278baeaffcf79" + integrity sha512-UQ8ntXxftN6PeASD9UpWs2rKanEY66BCwPSMeMJ6PpQC1IRO/HYJ0bdfaLrIWiAr+zJ4BjDRPui1jX5uDmMs3A== dependencies: - "@ledgerhq/devices" "^4.72.2" - "@ledgerhq/errors" "^4.72.2" - "@ledgerhq/hw-transport" "^4.72.2" + "@ledgerhq/devices" "^4.73.4" + "@ledgerhq/errors" "^4.73.4" + "@ledgerhq/hw-transport" "^4.73.4" "@ledgerhq/logs" "^4.72.0" node-hid "^0.7.9" "@ledgerhq/hw-transport-node-hid@^4.3.0": - version "4.72.2" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-4.72.2.tgz#921b6d5757f31fff6209dba5148a4b74b10c7a4a" - integrity sha512-KGFt/GqJUO8gDTA0HzXckvDcGgvGRA6jFrsRLaRBPmflaVMOjJ7UkeTL9kSLkJCyxFp1YuJ0eZaGIO25dACn0g== - dependencies: - "@ledgerhq/devices" "^4.72.2" - "@ledgerhq/errors" "^4.72.2" - "@ledgerhq/hw-transport" "^4.72.2" - "@ledgerhq/hw-transport-node-hid-noevents" "^4.72.2" + version "4.73.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-4.73.4.tgz#ef9efdcfbcb08146a2087beccc47865e99966c78" + integrity sha512-rSzOjnjRzVBkH6hrbKG6Y979LR/oGeihyTE2Bt7UHeY8QepJGgkJavJkmKHq71QK6RbT5ntBfhErbNvrno5B1Q== + dependencies: + "@ledgerhq/devices" "^4.73.4" + "@ledgerhq/errors" "^4.73.4" + "@ledgerhq/hw-transport" "^4.73.4" + "@ledgerhq/hw-transport-node-hid-noevents" "^4.73.4" "@ledgerhq/logs" "^4.72.0" lodash "^4.17.15" node-hid "^0.7.9" @@ -1817,13 +1737,13 @@ "@ledgerhq/hw-transport" "^4.24.0" u2f-api "0.2.7" -"@ledgerhq/hw-transport@^4.24.0", "@ledgerhq/hw-transport@^4.72.2": - version "4.72.2" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-4.72.2.tgz#b13d1797ba7530b9b94ebee71f3d0d2185153988" - integrity sha512-HYe3kTNoA9YHGCsktAzMURpXqZ1QvMUreI24GhMKfec6AIuB0d05avn2j7z8QeOpAUh5L6/OaPPWi9t4KpqG5A== +"@ledgerhq/hw-transport@^4.24.0", "@ledgerhq/hw-transport@^4.73.4": + version "4.73.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-4.73.4.tgz#e3b58eebbc99b003406838660b7d17611e02ebc7" + integrity sha512-UAw7v+le8pyEz+q2sA0TVS9WGmifKqGxuwcYz+hN1quJkfTmivvcHCuSF5t7BDCuuuRNoCT36YjnIzf363Tf2w== dependencies: - "@ledgerhq/devices" "^4.72.2" - "@ledgerhq/errors" "^4.72.2" + "@ledgerhq/devices" "^4.73.4" + "@ledgerhq/errors" "^4.73.4" events "^3.0.0" "@ledgerhq/logs@^4.72.0": @@ -1911,82 +1831,6 @@ resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== -"@nomiclabs/buidler@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@nomiclabs/buidler/-/buidler-1.0.1.tgz#4ac100fa96cf4326a551bf7099922b4c61956a50" - integrity sha512-MpjKvp7iYbTCT8gf4/RTYl5LK98owGYkibjUsWZ+B8COtlNQgpt7Eoxrh77lls06oGP8GmseWtVFF+qt9oZczw== - dependencies: - "@nomiclabs/eth-sig-util" "^2.4.4" - "@nomiclabs/ethereumjs-vm" "^4.1.0" - "@types/bn.js" "^4.11.5" - "@types/lru-cache" "^5.1.0" - abort-controller "^3.0.0" - bip32 "^2.0.3" - bip39 "^3.0.2" - chalk "^2.4.2" - ci-info "^2.0.0" - debug "^4.1.1" - deepmerge "^2.1.0" - download "^7.1.0" - enquirer "^2.3.0" - ethereumjs-abi "^0.6.8" - ethereumjs-account "^3.0.0" - ethereumjs-block "^2.2.0" - ethereumjs-common "^1.3.2" - ethereumjs-tx "^2.1.1" - ethereumjs-util "^6.1.0" - find-up "^2.1.0" - fp-ts "1.19.3" - fs-extra "^7.0.1" - glob "^7.1.3" - io-ts "1.10.4" - is-installed-globally "^0.2.0" - lodash "^4.17.11" - merkle-patricia-tree "^3.0.0" - mocha "^5.2.0" - node-fetch "^2.6.0" - qs "^6.7.0" - semver "^6.3.0" - solc "0.5.11" - solidity-parser-antlr "^0.4.2" - source-map-support "^0.5.13" - ts-essentials "^2.0.7" - tsort "0.0.1" - uuid "^3.3.2" - -"@nomiclabs/eth-sig-util@^2.4.4": - version "2.4.4" - resolved "https://registry.npmjs.org/@nomiclabs/eth-sig-util/-/eth-sig-util-2.4.4.tgz#e3518a90027a7952af24204a81713c21b500d546" - integrity sha512-t6vqztYHLq9t0CTiULheO+zrNqp1c10EjFgfKNxQGSV+A8hoztJDgU8FRKpczWN6MgQv2qFFgGMRfymadjY4pA== - dependencies: - buffer "^5.2.1" - elliptic "^6.4.0" - ethereumjs-abi "0.6.5" - ethereumjs-util "^5.1.1" - tweetnacl "^1.0.0" - tweetnacl-util "^0.15.0" - -"@nomiclabs/ethereumjs-vm@^4.1.0": - version "4.1.0" - resolved "https://registry.npmjs.org/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.1.0.tgz#2767e5e779c9c1db4ce50adb789d87a683622710" - integrity sha512-2eordoieZBmNEMkspyOpn3X4jqUtIzKarahGlPUd3WRVPvOSfbf68wNgKN+IbcQ9xND6nvb64fpW3kgckXdclQ== - dependencies: - async "^2.1.2" - async-eventemitter "^0.2.2" - core-js-pure "^3.0.1" - ethereumjs-account "^3.0.0" - ethereumjs-block "~2.2.0" - ethereumjs-blockchain "^4.0.1" - ethereumjs-common "^1.3.2" - ethereumjs-tx "^2.1.1" - ethereumjs-util "^6.1.0" - fake-merkle-patricia-tree "^1.0.1" - functional-red-black-tree "^1.0.1" - merkle-patricia-tree "^2.3.2" - rustbn.js "~0.2.0" - safe-buffer "^5.1.1" - util.promisify "^1.0.0" - "@phc/format@^0.5.0": version "0.5.0" resolved "https://registry.npmjs.org/@phc/format/-/format-0.5.0.tgz#a99d27a83d78b3100a191412adda04315e2e3aba" @@ -2010,11 +1854,6 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== -"@sindresorhus/is@^0.7.0": - version "0.7.0" - resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" - integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== - "@storybook/addon-actions@^5.1.10": version "5.1.11" resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-5.1.11.tgz#ebc299b9dfe476b5c65eb5d148c4b064f682ca08" @@ -2057,14 +1896,14 @@ react-lifecycles-compat "^3.0.4" util-deprecate "^1.0.2" -"@storybook/addon-links@^5.2.4": - version "5.2.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-5.2.4.tgz#5a5d631dbd66bf5800d4be7660b79568085a210d" - integrity sha512-MG+Qne4gUWGYx2qQuLQXNcl7oOBF4PbIcR0oboZNrkZ+D+6f3nHwyb53CTtzVTc+SF45CFFYLHvFdGZvv5fcAw== +"@storybook/addon-links@^5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-5.2.5.tgz#4e700688a5826b47a82adee5f4cb4d96130499e8" + integrity sha512-QuXOcZlDSRWEIwmHJZ9uAsjtNysVUsofX5yABX+x5Nkm4BCqT1NyAuu8Xq9IlyLF1ngiOF61dy530p4lcntmHA== dependencies: - "@storybook/addons" "5.2.4" - "@storybook/core-events" "5.2.4" - "@storybook/router" "5.2.4" + "@storybook/addons" "5.2.5" + "@storybook/core-events" "5.2.5" + "@storybook/router" "5.2.5" common-tags "^1.8.0" core-js "^3.0.1" global "^4.3.2" @@ -2083,7 +1922,7 @@ global "^4.3.2" util-deprecate "^1.0.2" -"@storybook/addons@5.2.4", "@storybook/addons@^5.2.1": +"@storybook/addons@5.2.4": version "5.2.4" resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.2.4.tgz#5c4f031e403c90a517cd6d208ec51d7e2455683a" integrity sha512-Q+bnVlBA308qnELxnh18hBDRSUgltR9KbV537285dUL/okv/NC6n51mxJwIaG+ksBW2wU+5e6tqSayaKF3uHLw== @@ -2096,6 +1935,19 @@ global "^4.3.2" util-deprecate "^1.0.2" +"@storybook/addons@5.2.5", "@storybook/addons@^5.2.1": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.2.5.tgz#e3e23d5ea6eb221df31e1a5d125be47454e9a0e8" + integrity sha512-CvMj7Bs3go9tv5rZuAvFwuwe8p/16LDCHS7+5nVFosvcL8nuN339V3rzakw8nLy/S6XKeZ1ACu4t3vYkreRE3w== + dependencies: + "@storybook/api" "5.2.5" + "@storybook/channels" "5.2.5" + "@storybook/client-logger" "5.2.5" + "@storybook/core-events" "5.2.5" + core-js "^3.0.1" + global "^4.3.2" + util-deprecate "^1.0.2" + "@storybook/api@5.1.11": version "5.1.11" resolved "https://registry.yarnpkg.com/@storybook/api/-/api-5.1.11.tgz#71ef00285cd8602aad24cdb26c60c5d3c76631e5" @@ -2142,6 +1994,29 @@ telejson "^3.0.2" util-deprecate "^1.0.2" +"@storybook/api@5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-5.2.5.tgz#dcc68c873820485372a47c095a8fc5e4fb53a34c" + integrity sha512-JvLafqFVgA3dIWpLMoGNk4sRuogE5imhD6/g0d8DOwnCID9xowj5xIptSrCTKvGGGxuN3wWRGn6I2lEbY6969g== + dependencies: + "@storybook/channels" "5.2.5" + "@storybook/client-logger" "5.2.5" + "@storybook/core-events" "5.2.5" + "@storybook/router" "5.2.5" + "@storybook/theming" "5.2.5" + core-js "^3.0.1" + fast-deep-equal "^2.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + prop-types "^15.6.2" + react "^16.8.3" + semver "^6.0.0" + shallow-equal "^1.1.0" + store2 "^2.7.1" + telejson "^3.0.2" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@5.1.11": version "5.1.11" resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-5.1.11.tgz#e75ab7d59ba19476eb631cdb69ee713c3b956c2b" @@ -2167,6 +2042,13 @@ dependencies: core-js "^3.0.1" +"@storybook/channels@5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-5.2.5.tgz#d6ca2b490281dacb272096563fe760ccb353c4bb" + integrity sha512-I+zB3ym5ozBcNBqyzZbvB6gRIG/ZKKkqy5k6LwKd5NMx7NU7zU74+LQUBBOcSIrigj8kCArZz7rlgb0tlSKXxQ== + dependencies: + core-js "^3.0.1" + "@storybook/client-api@5.1.11": version "5.1.11" resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-5.1.11.tgz#30d82c09c6c40aa70d932e77b1d1e65526bddc0c" @@ -2199,6 +2081,13 @@ dependencies: core-js "^3.0.1" +"@storybook/client-logger@5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.2.5.tgz#6f386ac6f81b4a783c57d54bb328281abbea1bab" + integrity sha512-6DyYUrMgAvF+th0foH7UNz+2JJpRdvNbpvYKtvi/+hlvRIaI6AqANgLkPUgMibaif5TLzjCr0bLdAYcjeJz03w== + dependencies: + core-js "^3.0.1" + "@storybook/components@5.1.11": version "5.1.11" resolved "https://registry.npmjs.org/@storybook/components/-/components-5.1.11.tgz#da253af0a8cb1b063c5c2e8016c4540c983f717d" @@ -2261,6 +2150,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-5.2.5.tgz#62881164a4a01aa99ff0691e70eaed2dd58e229e" + integrity sha512-O5GM8XEBbYNbM6Z7a4H1bbnbO2cxQrXMhEwansC7a7YinQdkTPiuGxke3NiyK+7pLDh778kpQyjoCjXq6UfAoQ== + dependencies: + core-js "^3.0.1" + "@storybook/core@5.1.11": version "5.1.11" resolved "https://registry.npmjs.org/@storybook/core/-/core-5.1.11.tgz#d7c4b14b02f74c183ab5baffe9b3e5ec8289b320" @@ -2395,6 +2291,19 @@ memoizerific "^1.11.3" qs "^6.6.0" +"@storybook/router@5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-5.2.5.tgz#a005332bc6aa1e7849503187ad50c41b3f3bef92" + integrity sha512-e6ElDAWSoEW1KSnsTbVwbpzaZ8CNWYw0Ok3b5AHfY2fuSH5L4l6s6k/bP7QSYqvWUeTvkFQYux7A2rOFCriAgA== + dependencies: + "@reach/router" "^1.2.1" + "@types/reach__router" "^1.2.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + "@storybook/theming@5.1.11": version "5.1.11" resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.1.11.tgz#0d1af46535f2e601293c999a314905069a93ec3b" @@ -2431,6 +2340,24 @@ prop-types "^15.7.2" resolve-from "^5.0.0" +"@storybook/theming@5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.2.5.tgz#9579e7944f61ded637d1d79be5fb859a617620f5" + integrity sha512-PGZNYrRgAhXFJKnktFpyyKlaDXEhtTi5XPq5ASVJrsPW6l963Mk2EMKSm4TCTxIJhs0Kx4cv2MnNZFDqHf47eg== + dependencies: + "@emotion/core" "^10.0.14" + "@emotion/styled" "^10.0.14" + "@storybook/client-logger" "5.2.5" + common-tags "^1.8.0" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.14" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.3.1" + prop-types "^15.7.2" + resolve-from "^5.0.0" + "@storybook/ui@5.1.11": version "5.1.11" resolved "https://registry.npmjs.org/@storybook/ui/-/ui-5.1.11.tgz#02246f7656f644a36908430de12abbdf4e2a8a72" @@ -2638,7 +2565,7 @@ resolved "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.0.tgz#851489a9065a067cb7f3c9cbe4ce9bed8bba0876" integrity sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ== -"@types/bn.js@*", "@types/bn.js@^4.11.0", "@types/bn.js@^4.11.5": +"@types/bn.js@*", "@types/bn.js@^4.11.0": version "4.11.5" resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.5.tgz#40e36197433f78f807524ec623afcf0169ac81dc" integrity sha512-AEAZcIZga0JgVMHNtl1CprA/hXX7/wPt79AgR4XqaDt7jyj3QWYw6LPoOiznPtugDmlubUnAahMs2PFxGcQrng== @@ -2662,7 +2589,7 @@ "@types/chai@^4.2.4": version "4.2.4" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.4.tgz#8936cffad3c96ec470a2dc26a38c3ba8b9b6f619" + resolved "https://registry.npmjs.org/@types/chai/-/chai-4.2.4.tgz#8936cffad3c96ec470a2dc26a38c3ba8b9b6f619" integrity sha512-7qvf9F9tMTzo0akeswHPGqgUx/gIaJqrOEET/FCD8CFRkSUHlygQiM5yB6OvjrtdxBVLSyw7COJubsFYs0683g== "@types/cheerio@*": @@ -2677,7 +2604,7 @@ resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b" integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A== -"@types/connect@*": +"@types/connect@*", "@types/connect@^3.4.32": version "3.4.32" resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg== @@ -2741,14 +2668,6 @@ resolved "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== -"@types/expect-puppeteer@^3.3.1": - version "3.3.1" - resolved "https://registry.npmjs.org/@types/expect-puppeteer/-/expect-puppeteer-3.3.1.tgz#46e5944bf425b86ea13a563c7c8b86901414988d" - integrity sha512-3raSnf28NelDtv0ksvQPZs410taJZ4d70vA8sVzmbRPV04fpmQm9/BOxUCloETD/ZI1EXRpv0pzOQKhPTbm4jg== - dependencies: - "@types/jest" "*" - "@types/puppeteer" "*" - "@types/express-serve-static-core@*": version "4.16.9" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz#69e00643b0819b024bdede95ced3ff239bb54558" @@ -2757,6 +2676,14 @@ "@types/node" "*" "@types/range-parser" "*" +"@types/express-serve-static-core@^4.16.9": + version "4.16.10" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.10.tgz#3c1313c6e6b75594561b473a286f016a9abf2132" + integrity sha512-gM6evDj0OvTILTRKilh9T5dTaGpv1oYiFcJAfgSejuMJgGJUsD9hKEU2lB4aiTNy4WwChxRnjfYFuBQsULzsJw== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + "@types/express-winston@^3.0.1": version "3.0.4" resolved "https://registry.npmjs.org/@types/express-winston/-/express-winston-3.0.4.tgz#0818399a094374f16c39bcba24ebcea3891791c6" @@ -2780,6 +2707,13 @@ resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-7.3.1.tgz#df7421e8bcb351b430bfbfa5c52bb353826ac94f" integrity sha512-2U4vZWHNbsbK7TRmizgr/pbKe0FKopcxu+hNDtIBDiM1wvrKRItybaYj7VQ6w/hZJStU/JxRiNi5ww4YDEvKbA== +"@types/ganache-core@^2.7.0": + version "2.7.0" + resolved "https://registry.npmjs.org/@types/ganache-core/-/ganache-core-2.7.0.tgz#867ecc7f3f34a9b7bfac45567db8fab1d051c1ab" + integrity sha512-kNnP+XBHmyEMYGLFzA+5PDt6P1TazTjALtZoJXExVisVT9buU8Al1xY24f3V+ROWshghQCL1cqofi8YsmacIew== + dependencies: + ganache-core "*" + "@types/glob@*", "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -2846,7 +2780,7 @@ resolved "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== -"@types/jest@*", "@types/jest@^24.0.0", "@types/jest@^24.0.18": +"@types/jest@^24.0.0", "@types/jest@^24.0.18": version "24.0.18" resolved "https://registry.npmjs.org/@types/jest/-/jest-24.0.18.tgz#9c7858d450c59e2164a8a9df0905fc5091944498" integrity sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ== @@ -2881,16 +2815,16 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.138.tgz#34f52640d7358230308344e579c15b378d91989e" integrity sha512-A4uJgHz4hakwNBdHNPdxOTkYmXNgmUAKLbXZ7PKGslgeV0Mb8P3BlbYfPovExek1qnod4pDfRbxuzcVs3dlFLg== +"@types/lodash@^4.14.139": + version "4.14.144" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.144.tgz#12e57fc99064bce45e5ab3c8bc4783feb75eab8e" + integrity sha512-ogI4g9W5qIQQUhXAclq6zhqgqNUr7UlFaqDHbch7WLSLeeM/7d3CRaw7GLajxvyFvhJqw4Rpcz5bhoaYtIx6Tg== + "@types/logform@*": version "1.2.0" resolved "https://registry.npmjs.org/@types/logform/-/logform-1.2.0.tgz#4ead916c7eb1ee99d726bfa849b6a2ee5ea50e3e" integrity sha512-E8cLzW+PmqHI//4HLR+ZvE3cyzqVjsncYBx1O14P07oVJj3ezhmiL/azlgkXKLFyCWAeKsPQdjHNg/NEhBF5Ow== -"@types/lru-cache@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" - integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== - "@types/material-ui@^0.21.6": version "0.21.7" resolved "https://registry.npmjs.org/@types/material-ui/-/material-ui-0.21.7.tgz#2a4ab77a56a16adef044ba607edde5214151a5d8" @@ -2928,20 +2862,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^12.7.5": - version "12.7.5" - resolved "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz#e19436e7f8e9b4601005d73673b6dc4784ffcc2f" - integrity sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w== - -"@types/node@10.12.18": - version "10.12.18" - resolved "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" - integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== - -"@types/node@11.11.6": - version "11.11.6" - resolved "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a" - integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ== +"@types/node@*": + version "12.11.5" + resolved "https://registry.npmjs.org/@types/node/-/node-12.11.5.tgz#6c3c8dc84988aff11fd2a63d7b5fbf39eaaab7b1" + integrity sha512-LC8ALj/24PhByn39nr5jnTvpE7MujK8y7LQmV74kHYF5iQ0odCPkMH4IZNZw+cobKfSXqaC8GgegcbIsQpffdA== "@types/node@^10.3.2": version "10.14.4" @@ -2953,6 +2877,16 @@ resolved "https://registry.npmjs.org/@types/node/-/node-11.13.20.tgz#da42fe93d6599f80b35ffeb5006f4c31f40d89ea" integrity sha512-JE0UpLWZTV1sGcaj0hN+Q0760OEjpgyFJ06DOMVW6qKBducKdJQaIw0TGL6ccj7VXRduIOHLWQi+tHwulZJHVQ== +"@types/node@^12.7.5": + version "12.7.5" + resolved "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz#e19436e7f8e9b4601005d73673b6dc4784ffcc2f" + integrity sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w== + +"@types/node@^12.7.7": + version "12.12.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.3.tgz#ebfe83507ac506bc3486314a8aa395be66af8d23" + integrity sha512-opgSsy+cEF9N8MgaVPnWVtdJ3o4mV2aMHvDq7thkQUFt0EuOHJon4rQpJfhjmNHB+ikl0Cd6WhWIErOyQ+f7tw== + "@types/numeral@^0.0.25": version "0.0.25" resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.25.tgz#b6f55062827a4787fe4ab151cf3412a468e65271" @@ -2966,22 +2900,15 @@ path-to-regexp "*" "@types/prettier@^1.13.2": - version "1.18.2" - resolved "https://registry.npmjs.org/@types/prettier/-/prettier-1.18.2.tgz#069e7d132024d436fd1f5771f6932426a695f230" - integrity sha512-2JBasa5Qaj81Qsp/dxX2Njy+MdKC767WytHUDsRM7TYEfQvKPxsnGpnCBlBS1i2Aiv1YwCpmKSbQ6O6v8TpiKg== + version "1.18.3" + resolved "https://registry.npmjs.org/@types/prettier/-/prettier-1.18.3.tgz#64ff53329ce16139f17c3db9d3e0487199972cd8" + integrity sha512-48rnerQdcZ26odp+HOvDGX8IcUkYOCuMc2BodWYTe956MqkHlOGAG4oFQ83cjZ0a4GAgj7mb4GUClxYd2Hlodg== "@types/prop-types@*": version "15.7.2" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.2.tgz#0e58ae66773d7fd7c372a493aff740878ec9ceaa" integrity sha512-f8JzJNWVhKtc9dg/dyDNfliTKNOJSLa7Oht/ElZdF/UbMUmAH3rLmAk3ODNjw0mZajDEgatA03tRjB4+Dp/tzA== -"@types/puppeteer@*", "@types/puppeteer@^1.12.0": - version "1.20.0" - resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-1.20.0.tgz#8a09062a744ca1e2f0607468d810b149ab7cf97f" - integrity sha512-V3nWu/ENNW7LzHumrgf1D+HnIEQHE+nHThq9MTPmGjQC75SVlWsalCp1OaZFaDeuUgWOmQPIBhSLz0cz+Hlz8w== - dependencies: - "@types/node" "*" - "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -3054,19 +2981,19 @@ dependencies: "@types/react" "*" -"@types/react-router-dom@^4.3.4": - version "4.3.5" - resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.5.tgz#72f229967690c890d00f96e6b85e9ee5780db31f" - integrity sha512-eFajSUASYbPHg2BDM1G8Btx+YqGgvROPIg6sBhl3O4kbDdYXdFdfrgQFf/pcBuQVObjfT9AL/dd15jilR5DIEA== +"@types/react-router-dom@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.2.tgz#853f229f1f297513c0be84f7c914a08b778cfdf5" + integrity sha512-kRx8hoBflE4Dp7uus+j/0uMHR5uGTAvQtc4A3vOTWKS+epe0leCuxEx7HNT7XGUd1lH53/moWM51MV2YUyhzAg== dependencies: "@types/history" "*" "@types/react" "*" "@types/react-router" "*" -"@types/react-router@*", "@types/react-router@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.1.tgz#e0b827556abc70da3473d05daf074c839d6852aa" - integrity sha512-S7SlFAPb7ZKr6HHMW0kLHGcz8pyJSL0UdM+JtlWthDqKUWwr7E6oPXuHgkofDI8dKCm16slg8K8VCf5pZJquaA== +"@types/react-router@*", "@types/react-router@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.2.tgz#41e5e6aa333a7b9a2bfdac753c04e1ca4b3e0d21" + integrity sha512-euC3SiwDg3NcjFdNmFL8uVuAFTpZJm0WMFUw+4eXMUnxa7M9RGFEG0szt0z+/Zgk4G2k9JBFhaEnY64RBiFmuw== dependencies: "@types/history" "*" "@types/react" "*" @@ -3085,10 +3012,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.9.5": - version "16.9.5" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.5.tgz#079dabd918b19b32118c25fd00a786bb6d0d5e51" - integrity sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA== +"@types/react@*", "@types/react@^15.0.0 || ^16.0.0", "@types/react@^16.9.5": + version "16.9.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.11.tgz#70e0b7ad79058a7842f25ccf2999807076ada120" + integrity sha512-UBT4GZ3PokTXSWmdgC/GeCGEJXE5ofWyibCcecRLUVN2ZBpXQGVgQGtG2foS7CrTKFKlQVVswLvf7Js6XA/CVQ== dependencies: "@types/prop-types" "*" csstype "^2.2.0" @@ -3137,6 +3064,14 @@ "@types/glob" "*" "@types/node" "*" +"@types/shelljs@^0.8.6": + version "0.8.6" + resolved "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.6.tgz#45193a51df99e0f00513c39a2152832399783221" + integrity sha512-svx2eQS268awlppL/P8wgDLBrsDXdKznABHJcuqXyWpSKJgE1s2clXlBvAwbO/lehTmG06NtEWJRkAk4tAgenA== + dependencies: + "@types/glob" "*" + "@types/node" "*" + "@types/solidity-parser-antlr@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@types/solidity-parser-antlr/-/solidity-parser-antlr-0.2.3.tgz#bb2d9c6511bf483afe4fc3e2714da8a924e59e3f" @@ -3199,10 +3134,10 @@ dependencies: "@types/node" "*" -"@types/validator@10.11.2": - version "10.11.2" - resolved "https://registry.npmjs.org/@types/validator/-/validator-10.11.2.tgz#48b60ca2cca927081f37a1ad1de3e25d04abc9f0" - integrity sha512-k/ju1RsdP5ACFUWebqsyEy0avP5uNJCs2p3pmTHzOZdd4gMSAJTq7iUEHFY3tt3emBrPTm6oGvfZ4SzcqOgLPQ== +"@types/validator@10.11.3": + version "10.11.3" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-10.11.3.tgz#945799bef24a953c5bc02011ca8ad79331a3ef25" + integrity sha512-GKF2VnEkMmEeEGvoo03ocrP9ySMuX1ypKazIYMlsjfslfBMhOAtC5dmEWKdJioW4lJN7MZRS88kalTsVClyQ9w== "@types/web3-provider-engine@^14.0.0": version "14.0.0" @@ -3211,7 +3146,7 @@ dependencies: "@types/ethereum-protocol" "*" -"@types/web3@^1.0.0", "@types/web3@^1.0.19": +"@types/web3@^1.0.19": version "1.0.19" resolved "https://registry.npmjs.org/@types/web3/-/web3-1.0.19.tgz#46b85d91d398ded9ab7c85a5dd57cb33ac558924" integrity sha512-fhZ9DyvDYDwHZUp5/STa9XW2re0E8GxoioYJ4pEUZ13YHpApSagixj7IAdoYH5uAK+UalGq6Ml8LYzmgRA/q+A== @@ -3241,15 +3176,10 @@ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-11.1.3.tgz#33c8ebf05f78f1edeb249c1cde1a42ae57f5664e" integrity sha512-moBUF6X8JsK5MbLZGP3vCfG/TVHZHsaePj3EimlLKp8+ESUjGjpXalxyn90a2L9fTM2ZGtW4swb6Am1DvVRNGA== -"@types/yargs@^12.0.10": - version "12.0.12" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916" - integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw== - -"@types/yargs@^13.0.0": - version "13.0.2" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.2.tgz#a64674fc0149574ecd90ba746e932b5a5f7b3653" - integrity sha512-lwwgizwk/bIIU+3ELORkyuOgDjCh7zuWDFqRtPPhhVgq9N1F7CvLNKg1TX4f2duwtKQ0p044Au9r1PLIXHrIzQ== +"@types/yargs@^13.0.0", "@types/yargs@^13.0.3": + version "13.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.3.tgz#76482af3981d4412d65371a318f992d33464a380" + integrity sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ== dependencies: "@types/yargs-parser" "*" @@ -3293,13 +3223,13 @@ "@typescript-eslint/typescript-estree" "2.1.0" eslint-scope "^4.0.0" -"@typescript-eslint/experimental-utils@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.5.0.tgz#383a97ded9a7940e5053449f6d73995e782b8fb1" - integrity sha512-UgcQGE0GKJVChyRuN1CWqDW8Pnu7+mVst0aWrhiyuUD1J9c+h8woBdT4XddCvhcXDodTDVIfE3DzGHVjp7tUeQ== +"@typescript-eslint/experimental-utils@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.6.1.tgz#eddaca17a399ebf93a8628923233b4f93793acfd" + integrity sha512-EVrrUhl5yBt7fC7c62lWmriq4MIc49zpN3JmrKqfiFXPXCM5ErfEcZYfKOhZXkW6MBjFcJ5kGZqu1b+lyyExUw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.5.0" + "@typescript-eslint/typescript-estree" "2.6.1" eslint-scope "^5.0.0" "@typescript-eslint/parser@1.13.0": @@ -3312,14 +3242,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-visitor-keys "^1.0.0" -"@typescript-eslint/parser@^2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.5.0.tgz#858030ddd808fbbe88e03f42e5971efaccb8218a" - integrity sha512-9UBMiAwIDWSl79UyogaBdj3hidzv6exjKUx60OuZuFnJf56tq/UMpdPcX09YmGqE8f4AnAueYtBxV8IcAT3jdQ== +"@typescript-eslint/parser@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.6.1.tgz#3c00116baa0d696bc334ca18ac5286b34793993c" + integrity sha512-PDPkUkZ4c7yA+FWqigjwf3ngPUgoLaGjMlFh6TRtbjhqxFBnkElDfckSjm98q9cMr4xRzZ15VrS/xKm6QHYf0w== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.5.0" - "@typescript-eslint/typescript-estree" "2.5.0" + "@typescript-eslint/experimental-utils" "2.6.1" + "@typescript-eslint/typescript-estree" "2.6.1" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -3340,16 +3270,17 @@ lodash.unescape "4.0.1" semver "^6.2.0" -"@typescript-eslint/typescript-estree@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.5.0.tgz#40ada624d6217ef092a3a79ed30d947ad4f212ce" - integrity sha512-AXURyF8NcA3IsnbjNX1v9qbwa0dDoY9YPcKYR2utvMHoUcu3636zrz0gRWtVAyxbPCkhyKuGg6WZIyi2Fc79CA== +"@typescript-eslint/typescript-estree@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.6.1.tgz#fb363dd4ca23384745c5ea4b7f4c867432b00d31" + integrity sha512-+sTnssW6bcbDZKE8Ce7VV6LdzkQz2Bxk7jzk1J8H1rovoTxnm6iXvYIyncvNsaB/kBCOM63j/LNJfm27bNdUoA== dependencies: debug "^4.1.1" glob "^7.1.4" is-glob "^4.0.1" lodash.unescape "4.0.1" semver "^6.3.0" + tsutils "^3.17.1" "@usulpro/color-picker@^1.1.3": version "1.1.3" @@ -3532,6 +3463,14 @@ resolved "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz#004e8e553b4cd53d538bd38eac7bcbf58a867fe3" integrity sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg== +JSONStream@^1.3.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + abab@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -3552,17 +3491,10 @@ abbrev@1.0.x: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - abortcontroller-polyfill@^1.1.9: - version "1.3.0" - resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.3.0.tgz#de69af32ae926c210b7efbcc29bf644ee4838b00" - integrity sha512-lbWQgf+eRvku3va8poBlDBO12FigTQr9Zb7NIjXrePrhxWVKdCP2wbDl1tLDaYa18PWTom3UEWwdH13S46I+yA== + version "1.4.0" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4" + integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA== abstract-leveldown@3.0.0: version "3.0.0" @@ -3638,16 +3570,11 @@ acorn-globals@^4.3.0: acorn "^6.0.1" acorn-walk "^6.0.1" -acorn-jsx@^5.0.0: +acorn-jsx@^5.0.0, acorn-jsx@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f" integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw== -acorn-jsx@^5.0.2: - version "5.1.0" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" - integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== - acorn-walk@^6.0.1, acorn-walk@^6.1.1: version "6.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" @@ -3714,13 +3641,6 @@ after@0.8.2: resolved "https://registry.npmjs.org/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= -agent-base@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" - integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== - dependencies: - es6-promisify "^5.0.0" - "airbnb-js-shims@^1 || ^2": version "2.2.0" resolved "https://registry.npmjs.org/airbnb-js-shims/-/airbnb-js-shims-2.2.0.tgz#46e1d9d9516f704ef736de76a3b6d484df9a96d8" @@ -3830,7 +3750,7 @@ ansi-colors@^1.0.1: dependencies: ansi-wrap "^0.1.0" -ansi-colors@^3.0.0, ansi-colors@^3.2.1, ansi-colors@^3.2.3: +ansi-colors@^3.0.0, ansi-colors@^3.2.3: version "3.2.4" resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== @@ -3947,13 +3867,6 @@ arch@2.1.1, arch@^2.1.0: resolved "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== -archive-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz#f92e72233056dfc6969472749c267bdb046b1d70" - integrity sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA= - dependencies: - file-type "^4.2.0" - archy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" @@ -4056,11 +3969,6 @@ array-back@^2.0.0: dependencies: typical "^2.6.1" -array-back@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" - integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== - array-each@^1.0.0, array-each@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" @@ -4201,7 +4109,7 @@ arrify@^1.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -asap@^2.0.0, asap@~2.0.3, asap@~2.0.6: +asap@~2.0.3, asap@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= @@ -4370,7 +4278,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== -axios@^0.18.0, axios@^0.18.1: +axios@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3" integrity sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g== @@ -4378,6 +4286,14 @@ axios@^0.18.0, axios@^0.18.1: follow-redirects "1.5.10" is-buffer "^2.0.2" +axios@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== + dependencies: + follow-redirects "1.5.10" + is-buffer "^2.0.2" + axobject-query@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -4605,7 +4521,7 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-jest@^24.1.0, babel-jest@^24.8.0, babel-jest@^24.9.0: +babel-jest@^24.8.0, babel-jest@^24.9.0: version "24.9.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== @@ -5433,26 +5349,13 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -bindings@^1.2.1, bindings@^1.3.0, bindings@^1.3.1, bindings@^1.4.0, bindings@^1.5.0: +bindings@^1.2.1, bindings@^1.3.1, bindings@^1.4.0, bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== dependencies: file-uri-to-path "1.0.0" -bip32@^2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/bip32/-/bip32-2.0.4.tgz#b662bd28710d4676fb351ba8a13be45cb4d85d01" - integrity sha512-ioPytarPDIrWckWMuK4RNUtvwhvWEc2fvuhnO0WEwu732k5OLjUXv4rXi2c/KJHw9ZMNQMkYRJrBw81RujShGQ== - dependencies: - "@types/node" "10.12.18" - bs58check "^2.1.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - tiny-secp256k1 "^1.1.0" - typeforce "^1.11.5" - wif "^2.0.6" - bip39@2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/bip39/-/bip39-2.5.0.tgz#51cbd5179460504a63ea3c000db3f787ca051235" @@ -5475,16 +5378,6 @@ bip39@^2.5.0: safe-buffer "^5.0.1" unorm "^1.3.3" -bip39@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz#2baf42ff3071fc9ddd5103de92e8f80d9257ee32" - integrity sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ== - dependencies: - "@types/node" "11.11.6" - create-hash "^1.1.0" - pbkdf2 "^3.0.9" - randombytes "^2.0.1" - bip66@^1.1.3: version "1.1.5" resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" @@ -5857,7 +5750,7 @@ bs58@^4.0.0: dependencies: base-x "^3.0.2" -bs58check@<3.0.0, bs58check@^2.1.1, bs58check@^2.1.2: +bs58check@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== @@ -5891,11 +5784,6 @@ buffer-crc32@~0.2.3: resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - buffer-equal@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" @@ -5933,7 +5821,7 @@ buffer-xor@^1.0.3: buffer@5.4.0, buffer@^5.1.0: version "5.4.0" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.4.0.tgz#33294f5c1f26e08461e528b69fa06de3c45cbd8c" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.0.tgz#33294f5c1f26e08461e528b69fa06de3c45cbd8c" integrity sha512-Xpgy0IwHK2N01ncykXTy6FpCWuM+CJSHoPVBLyNqyrWxsedpLvwsYUhf0ME3WRFNUhos0dMamz9cOS/xRDtU5g== dependencies: base64-js "^1.0.2" @@ -6047,19 +5935,6 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -cacheable-request@^2.1.1: - version "2.1.4" - resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" - integrity sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0= - dependencies: - clone-response "1.0.2" - get-stream "3.0.0" - http-cache-semantics "3.8.1" - keyv "3.0.0" - lowercase-keys "1.0.0" - normalize-url "2.0.1" - responselike "1.0.2" - cacheable-request@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" @@ -6225,7 +6100,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -caw@^2.0.0, caw@^2.0.1: +caw@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz#6c3ca071fc194720883c2dc5da9b074bfc7e9e95" integrity sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA== @@ -6235,7 +6110,15 @@ caw@^2.0.0, caw@^2.0.1: tunnel-agent "^0.6.0" url-to-options "^1.0.1" -cbor@>=4.0.0, cbor@^4.0.0, cbor@^4.1.1: +cbor@>=4.0.0, cbor@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/cbor/-/cbor-5.0.1.tgz#243eea46b19c6e54ffb18fb07fa52c1c627a6f05" + integrity sha512-l4ghwqioCyuAaD3LvY4ONwv8NMuERz62xjbMHGdWBqERJPygVmoFER1b4+VS6iW0rXwoVGuKZPPPTofwWOg3YQ== + dependencies: + bignumber.js "^9.0.0" + nofilter "^1.0.3" + +cbor@^4.1.1: version "4.1.5" resolved "https://registry.yarnpkg.com/cbor/-/cbor-4.1.5.tgz#c8ef3702b2f8710bde1425903c0c16a50419d369" integrity sha512-WqpISHl7/kk1u1uoaqctGyGiET3+wGN4A6pPsFCzEBztJJlCVxnMB4JwcuuNSrMiV4V6UBlxMkhNzJrUxDWO1A== @@ -6270,19 +6153,9 @@ chai@^4.0.1, chai@^4.2.0: pathval "^1.1.0" type-detect "^4.0.5" -chainlink@0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/chainlink/-/chainlink-0.6.1.tgz#f658b44525d788dbbbdbe40b770eefdbac6b66a1" - integrity sha512-rTfd22GpYkTbNSuwoNi7T+aE9wsE3e0Qh5NG4zXit6OPi5FzYjL/ib15/HrNsK5STd0enfHzCK/FnxhGMtH8GA== - dependencies: - "@ensdomains/ens" "^0.1.1" - link_token "^1.0.3" - openzeppelin-solidity "^1.12.0" - solidity-cborutils "^1.0.2" - chainlink@0.7.5: version "0.7.5" - resolved "https://registry.yarnpkg.com/chainlink/-/chainlink-0.7.5.tgz#82043e99b8477dac8478a1707d217d0ef716b28d" + resolved "https://registry.npmjs.org/chainlink/-/chainlink-0.7.5.tgz#82043e99b8477dac8478a1707d217d0ef716b28d" integrity sha512-UFwFuF5EdxVIBj6lSUmDsEUjnM+hOefelKTZ5vfjPEfUJCq4KAw3Ew1JJYsssmceaOYWcg4iL3A5Q40MnTpmIg== dependencies: "@ensdomains/ens" "^0.1.1" @@ -6298,15 +6171,16 @@ chainlink@0.7.5: truffle-contract "^4.0.31" web3 "^1.2.0" -chainlink@^0.6.5: - version "0.6.5" - resolved "https://registry.npmjs.org/chainlink/-/chainlink-0.6.5.tgz#35c8fef8529f4c48a996b6a1d2546cfcdb38af59" - integrity sha512-V6Z1Z4BYEwpicc5jVw66oyQeVe+WT90ZlDKTsbO04eASSJup2eCUxSp9nEDywA2gva/1D1F1q+AHDkMDWHpoPA== +chainlink@0.7.8: + version "0.7.8" + resolved "https://registry.yarnpkg.com/chainlink/-/chainlink-0.7.8.tgz#4b7f93d629bb35d7201155e30bb587b56579f848" + integrity sha512-S8oCmasY95irJouUKBA02RhX4bSaE4hJ81HMeiF7Cg/GYAkoUg1L2umRc1WLc+3pDg7udMt2pRDmEmx3tpCIpQ== dependencies: - "@ensdomains/ens" "^0.1.1" - link_token "^1.0.3" + cbor "^4.1.1" + chainlinkv0.5 "0.0.2" + ethers "^4.0.37" + link_token "^1.0.6" openzeppelin-solidity "^1.12.0" - solidity-cborutils "^1.0.4" chalk@2.4.1: version "2.4.1" @@ -6459,9 +6333,9 @@ chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.6, chokidar@^2.1.8: fsevents "^1.2.7" chokidar@^3.0.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.2.2.tgz#a433973350021e09f2b853a2287781022c0dc935" - integrity sha512-bw3pm7kZ2Wa6+jQWYP/c7bAZy3i4GwiIiMO2EeRjrE48l8vBqC/WvFhSF0xyM8fQiPEGvwMY/5bqDG7sSEOuhg== + version "3.2.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.2.3.tgz#b9270a565d14f02f6bfdd537a6a2bbf5549b8c8c" + integrity sha512-GtrxGuRf6bzHQmXWRepvsGnXpkQkVU+D2/9a7dAe4a7v1NhrfZOZ2oKf76M3nOs46fFYL8D+Q8JYA4GYeJ8Cjw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -6523,14 +6397,14 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -class-validator@^0.10.0: - version "0.10.0" - resolved "https://registry.npmjs.org/class-validator/-/class-validator-0.10.0.tgz#5e5f8108fa200d5ce1906cd5537af102c072dbcd" - integrity sha512-RvjxRlvoCvM/ojUq11j78ISpReGdBoMErdmDk1e27aQZK6ppSXq751UE6jB9JI7ayEnL6Nnmllzn/HXVSu3dmg== +class-validator@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.11.0.tgz#5fca8b8a957c738a6749391e03ee81fad375dc4a" + integrity sha512-niAmmSPFku9xsnpYYrddy8NZRrCX3yyoZ/rgPKOilE5BG0Ma1eVCIxpR4X0LasL/6BzbYzsutG+mSbAXlh4zNw== dependencies: - "@types/validator" "10.11.2" + "@types/validator" "10.11.3" google-libphonenumber "^3.1.6" - validator "11.1.0" + validator "12.0.0" classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" @@ -6685,7 +6559,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clone-response@1.0.2, clone-response@^1.0.2: +clone-response@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -6845,16 +6719,6 @@ command-line-args@^4.0.7: find-replace "^1.0.3" typical "^2.6.1" -command-line-args@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.1.1.tgz#88e793e5bb3ceb30754a86863f0401ac92fd369a" - integrity sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg== - dependencies: - array-back "^3.0.1" - find-replace "^3.0.0" - lodash.camelcase "^4.3.0" - typical "^4.0.0" - commander@2.15.1: version "2.15.1" resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" @@ -6870,12 +6734,7 @@ commander@2.18.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== -commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.8.1, commander@~2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== - -commander@^2.5.0: +commander@^2.11.0, commander@^2.12.2, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.5.0, commander@^2.8.1, commander@~2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -7021,13 +6880,13 @@ concat-stream@1.6.2, concat-stream@^1.5.0, concat-stream@^1.5.1, concat-stream@^ readable-stream "^2.2.2" typedarray "^0.0.6" -concurrently@^4.1.0: - version "4.1.2" - resolved "https://registry.npmjs.org/concurrently/-/concurrently-4.1.2.tgz#1a683b2b5c41e9ed324c9002b9f6e4c6e1f3b6d7" - integrity sha512-Kim9SFrNr2jd8/0yNYqDTFALzUX1tvimmwFWxmp/D4mRI+kbqIIwE2RkBDrxS2ic25O1UgQMI5AtBqdtX3ynYg== +concurrently@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.0.0.tgz#99c7567d009411fbdc98299d553c4b99a978612c" + integrity sha512-1yDvK8mduTIdxIxV9C60KoiOySUl/lfekpdbI+U5GXaPrgdffEavFa9QZB3vh68oWOpbCC+TuvxXV9YRPMvUrA== dependencies: chalk "^2.4.2" - date-fns "^1.30.1" + date-fns "^2.0.1" lodash "^4.17.15" read-pkg "^4.0.1" rxjs "^6.5.2" @@ -7089,7 +6948,7 @@ content-disposition@0.5.2: resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= -content-disposition@0.5.3, content-disposition@^0.5.2: +content-disposition@0.5.3: version "0.5.3" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== @@ -7237,10 +7096,10 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.5, core-js@^2.6.9: resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.1, core-js@^3.0.4, core-js@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" - integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== +core-js@^3.0.1, core-js@^3.0.4, core-js@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.4.0.tgz#29ea478601789c72f2978e9bb98f43546f89d3aa" + integrity sha512-lQxb4HScV71YugF/X28LtePZj9AB7WqOpcB+YztYxusvhrgZiQXPmCYfPC5LHsw/+ScEtDbXU3xbqH3CjBRmYA== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -7298,7 +7157,7 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" -create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2, create-hash@^1.2.0: +create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2: version "1.2.0" resolved "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -7309,7 +7168,7 @@ create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: version "1.1.7" resolved "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -7337,10 +7196,10 @@ create-react-context@^0.3.0: gud "^1.0.0" warning "^4.0.3" -cross-env@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/cross-env/-/cross-env-6.0.0.tgz#3c8e71440ea20aa6faaf5aec541235efc565dac6" - integrity sha512-G/B6gtkjgthT8AP/xN1wdj5Xe18fVyk58JepK8GxpUbqcz3hyWxegocMbvnZK+KoTslwd0ACZ3woi/DVUdVjyQ== +cross-env@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941" + integrity sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag== dependencies: cross-spawn "^7.0.0" @@ -7744,11 +7603,16 @@ data-urls@^1.0.0, data-urls@^1.1.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@^1.27.2, date-fns@^1.30.1: +date-fns@^1.27.2: version "1.30.1" resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== +date-fns@^2.0.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.7.0.tgz#8271d943cc4636a1f27698f1b8d6a9f1ceb74026" + integrity sha512-wxYp2PGoUDN5ZEACc61aOtYFvSsJUylIvCjpjDOqM1UDaKIIuMJ9fAnMYFHV3TQaDpfTVxhwNK/GiCaHKuemTA== + date-now@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -7870,7 +7734,7 @@ decompress-unzip@^4.0.1: pify "^2.3.0" yauzl "^2.4.2" -decompress@^4.0.0, decompress@^4.2.0: +decompress@^4.0.0: version "4.2.0" resolved "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz#7aedd85427e5a92dacfe55674a7c505e96d01f9d" integrity sha1-eu3YVCflqS2s/lVnSnxQXpbQH50= @@ -7916,7 +7780,7 @@ deep-object-diff@^1.1.0: resolved "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== -deepmerge@^2.1.0, deepmerge@^2.1.1: +deepmerge@^2.1.1: version "2.2.1" resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== @@ -8076,11 +7940,6 @@ depd@~1.1.2: resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -deprecate@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/deprecate/-/deprecate-1.0.0.tgz#661490ed2428916a6c8883d8834e5646f4e4a4a8" - integrity sha1-ZhSQ7SQokWpsiIPYg05WRvTkpKg= - deprecate@^1.0.0: version "1.1.1" resolved "https://registry.npmjs.org/deprecate/-/deprecate-1.1.1.tgz#4632e981fc815eeaf00be945a40359c0f8bf9913" @@ -8420,24 +8279,6 @@ download@^5.0.3: mkdirp "^0.5.1" pify "^2.3.0" -download@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/download/-/download-7.1.0.tgz#9059aa9d70b503ee76a132897be6dec8e5587233" - integrity sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ== - dependencies: - archive-type "^4.0.0" - caw "^2.0.1" - content-disposition "^0.5.2" - decompress "^4.2.0" - ext-name "^5.0.0" - file-type "^8.1.0" - filenamify "^2.0.0" - get-stream "^3.0.0" - got "^8.3.1" - make-dir "^1.2.0" - p-event "^2.1.0" - pify "^3.0.0" - drbg.js@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b" @@ -8499,13 +8340,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - editorconfig@^0.15.3: version "0.15.3" resolved "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" @@ -8689,15 +8523,6 @@ engine.io@~3.3.1: engine.io-parser "~2.1.0" ws "~6.1.0" -enhanced-resolve@4.1.0, enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" - integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - tapable "^1.0.0" - enhanced-resolve@^3.4.0: version "3.4.1" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" @@ -8708,12 +8533,14 @@ enhanced-resolve@^3.4.0: object-assign "^4.0.1" tapable "^0.2.7" -enquirer@^2.3.0: - version "2.3.2" - resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.2.tgz#1c30284907cadff5ed2404bd8396036dd3da070e" - integrity sha512-PLhTMPUXlnaIv9D3Cq3/Zr1xb7soeDDgunobyCmYLUG19n24dvC8i+ZZgm2DekGpDnx7JvFSHV7lxfM58PMtbA== +enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" + integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== dependencies: - ansi-colors "^3.2.1" + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + tapable "^1.0.0" entities@^1.1.1, entities@~1.1.1: version "1.1.2" @@ -9437,6 +9264,13 @@ ethashjs@~0.0.7: ethereumjs-util "^4.0.1" miller-rabin "^4.0.0" +ethereum-bloom-filters@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.6.tgz#9cdebb3ec20de96ec4a434c6bad6ea5a513037aa" + integrity sha512-dE9CGNzgOOsdh7msZirvv8qjHtnHpvBlKe2647kM8v+yeF71IRso55jpojemvHV+jMjr48irPWxMRaHuOWzAFA== + dependencies: + js-sha3 "^0.8.0" + ethereum-common@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.2.0.tgz#13bf966131cce1eeade62a1b434249bb4cb120ca" @@ -9463,7 +9297,7 @@ ethereumjs-abi@0.6.5: bn.js "^4.10.0" ethereumjs-util "^4.3.0" -ethereumjs-abi@0.6.7, ethereumjs-abi@^0.6.5: +ethereumjs-abi@0.6.7: version "0.6.7" resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.7.tgz#d1d1c5cdb8d910a7d97645ba9e93be5d153bba2e" integrity sha512-EMLOA8ICO5yAaXDhjVEfYjsJIXYutY8ufTE93eEKwsVtp2usQreKwsDTJ9zvam3omYqNuffr8IONIqb2uUslGQ== @@ -9486,7 +9320,7 @@ ethereumjs-abi@^0.6.7, ethereumjs-abi@^0.6.8: bn.js "^4.11.8" ethereumjs-util "^6.0.0" -ethereumjs-account@3.0.0, ethereumjs-account@^3.0.0: +ethereumjs-account@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/ethereumjs-account/-/ethereumjs-account-3.0.0.tgz#728f060c8e0c6e87f1e987f751d3da25422570a9" integrity sha512-WP6BdscjiiPkQfF9PVfMcwx/rDvfZTjFKY0Uwc09zSQr9JfIVH87dYIJu0gNhBhpmovV4yq295fdllS925fnBA== @@ -9504,7 +9338,7 @@ ethereumjs-account@^2.0.3: rlp "^2.0.0" safe-buffer "^5.1.1" -ethereumjs-block@2.2.0, ethereumjs-block@^2.2.0, ethereumjs-block@~2.2.0: +ethereumjs-block@2.2.0, ethereumjs-block@~2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-2.2.0.tgz#8c6c3ab4a5eff0a16d9785fbeedbe643f4dbcbef" integrity sha512-Ye+uG/L2wrp364Zihdlr/GfC3ft+zG8PdHcRtsBFNNH1CkOhxOwdB8friBU85n89uRZ9eIMAywCq0F4CwT1wAw== @@ -9542,23 +9376,7 @@ ethereumjs-blockchain@^3.4.0: safe-buffer "^5.1.2" semaphore "^1.1.0" -ethereumjs-blockchain@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/ethereumjs-blockchain/-/ethereumjs-blockchain-4.0.1.tgz#db113dfed4fcc5197d223391f10adbc5a1b3536b" - integrity sha512-twf2yeyzeBXzCgclLyF9wZEyCKbCweM2KZdZkTsnlqGgaffgnSgY44+z+9BHUIVoWY+gxMj+XsTlTgVcbha8rg== - dependencies: - async "^2.6.1" - ethashjs "~0.0.7" - ethereumjs-block "~2.2.0" - ethereumjs-common "^1.1.0" - ethereumjs-util "~6.1.0" - flow-stoplight "^1.0.0" - level-mem "^3.0.1" - lru-cache "^5.1.1" - rlp "^2.2.2" - semaphore "^1.1.0" - -ethereumjs-common@^1.1.0, ethereumjs-common@^1.3.1, ethereumjs-common@^1.3.2: +ethereumjs-common@^1.1.0: version "1.3.2" resolved "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.3.2.tgz#5a20831e52199a31ff4b68ef361e34c05c976ed0" integrity sha512-GkltYRIqBLzaZLmF/K3E+g9lZ4O4FL+TtpisAlD3N+UVlR+mrtoG+TvxavqVa6PwOY4nKIEMe5pl6MrTio3Lww== @@ -9578,15 +9396,7 @@ ethereumjs-tx@1.3.7, ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^ ethereum-common "^0.0.18" ethereumjs-util "^5.0.0" -ethereumjs-tx@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-2.1.1.tgz#7d204e2b319156c9bc6cec67e9529424a26e8ccc" - integrity sha512-QtVriNqowCFA19X9BCRPMgdVNJ0/gMBS91TQb1DfrhsbR748g4STwxZptFAwfqehMyrF8rDwB23w87PQwru0wA== - dependencies: - ethereumjs-common "^1.3.1" - ethereumjs-util "^6.0.0" - -ethereumjs-util@6.1.0, ethereumjs-util@^6.0.0, ethereumjs-util@^6.1.0, ethereumjs-util@~6.1.0: +ethereumjs-util@6.1.0, ethereumjs-util@^6.0.0, ethereumjs-util@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz#e9c51e5549e8ebd757a339cc00f5380507e799c8" integrity sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q== @@ -9610,7 +9420,7 @@ ethereumjs-util@^4.0.1, ethereumjs-util@^4.3.0: rlp "^2.0.0" secp256k1 "^3.0.1" -ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.1.3, ethereumjs-util@^5.1.5, ethereumjs-util@^5.2.0: +ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.1.3, ethereumjs-util@^5.1.5: version "5.2.0" resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz#3e0c0d1741471acf1036052d048623dee54ad642" integrity sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA== @@ -9718,7 +9528,7 @@ ethers@4.0.0-beta.3: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@^4.0.0-beta.1, ethers@^4.0.32, ethers@^4.0.36, ethers@^4.0.37: +ethers@^4.0.0-beta.1, ethers@^4.0.32, ethers@^4.0.37: version "4.0.37" resolved "https://registry.npmjs.org/ethers/-/ethers-4.0.37.tgz#dfa70d59498663878c5e4a977d14356660ca5b90" integrity sha512-B7bDdyQ45A5lPr6k2HOkEKMtYOuqlfy+nNf8glnRvWidkDQnToKw1bv7UyrwlbsIgY2mE03UxTVtouXcT6Vvcw== @@ -9735,9 +9545,9 @@ ethers@^4.0.0-beta.1, ethers@^4.0.32, ethers@^4.0.36, ethers@^4.0.37: xmlhttprequest "1.8.0" ethers@~4.0.4: - version "4.0.38" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.38.tgz#3fabafb4f79c435205b143b66b4a1af035043e37" - integrity sha512-l7l7RIfk2/rIFgRRVLFY3H06S9dhXXPUdMlYm6SCelB6oG+ABmoRig7xSVOLcHLayBfSwssjAAYLKxf1jWhbuQ== + version "4.0.39" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.39.tgz#5ce9dfffedb03936415743f63b37d96280886a47" + integrity sha512-QVtC8TTUgTrnlQjQvdFJ7fkSWKwp8HVTbKRmrdbVryrPzJHMTf3WSeRNvLF2enGyAFtyHJyFNnjN0fSshcEr9w== dependencies: "@types/node" "^10.3.2" aes-js "3.0.0" @@ -9783,11 +9593,6 @@ event-emitter@~0.3.5: d "1" es5-ext "~0.10.14" -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - eventemitter3@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.1.1.tgz#47786bdaa087caf7b1b75e73abc5c7d540158cd0" @@ -9889,7 +9694,7 @@ execa@^1.0.0: execa@^3.2.0: version "3.2.0" - resolved "https://registry.npmjs.org/execa/-/execa-3.2.0.tgz#18326b79c7ab7fbd6610fd900c1b9e95fa48f90a" + resolved "https://registry.yarnpkg.com/execa/-/execa-3.2.0.tgz#18326b79c7ab7fbd6610fd900c1b9e95fa48f90a" integrity sha512-kJJfVbI/lZE1PZYDI5VPxp8zXPO9rtxOkhpZ0jMKha56AI9y2gGVC6bkukStQf0ka5Rh15BA5m7cCCH4jmHqkw== dependencies: cross-spawn "^7.0.0" @@ -9969,11 +9774,6 @@ expect-ct@0.2.0: resolved "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz#3a54741b6ed34cc7a93305c605f63cd268a54a62" integrity sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g== -expect-puppeteer@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-4.3.0.tgz#732a3c94ab44af0c7d947040ad3e3637a0359bf3" - integrity sha512-p8N/KSVPG9PAOJlftK5f1n3JrULJ6Qq1EQ8r/n9xzkX2NmXbK8PcnJnkSAEzEHrMycELKGnlJV7M5nkgm+wEWA== - expect@^24.9.0: version "24.9.0" resolved "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" @@ -10030,21 +9830,6 @@ express@^4.14.0, express@^4.15.2, express@^4.16.2, express@^4.16.3, express@^4.1 utils-merge "1.0.1" vary "~1.1.2" -ext-list@^2.0.0: - version "2.2.2" - resolved "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" - integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== - dependencies: - mime-db "^1.28.0" - -ext-name@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" - integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== - dependencies: - ext-list "^2.0.0" - sort-keys-length "^1.0.0" - extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -10106,7 +9891,7 @@ extract-css-chunks-webpack-plugin@^3.2.1: schema-utils "^1.0.0" webpack-sources "^1.1.0" -extract-zip@1.6.7, extract-zip@^1.6.6: +extract-zip@1.6.7: version "1.6.7" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= @@ -10126,6 +9911,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +eyes@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= + fake-merkle-patricia-tree@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3" @@ -10343,11 +10133,6 @@ file-type@^3.8.0: resolved "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= -file-type@^4.2.0: - version "4.4.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz#1b600e5fca1fbdc6e80c0a70c71c8dba5f7906c5" - integrity sha1-G2AOX8ofvcboDApwxxyNul95BsU= - file-type@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" @@ -10358,11 +10143,6 @@ file-type@^6.1.0: resolved "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== -file-type@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz#244f3b7ef641bbe0cca196c7276e4b332399f68c" - integrity sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ== - file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -10480,13 +10260,6 @@ find-replace@^1.0.3: array-back "^1.0.4" test-value "^2.1.0" -find-replace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" - integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== - dependencies: - array-back "^3.0.1" - find-root@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -10522,16 +10295,6 @@ find-up@^4.0.0: locate-path "^5.0.0" path-exists "^4.0.0" -findup-sync@3.0.0, findup-sync@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - findup-sync@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" @@ -10542,6 +10305,16 @@ findup-sync@^2.0.0: micromatch "^3.0.4" resolve-dir "^1.0.1" +findup-sync@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + fined@^1.0.1: version "1.2.0" resolved "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" @@ -10707,16 +10480,6 @@ forwarded@~0.1.2: resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= -fp-ts@1.19.3: - version "1.19.3" - resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz#261a60d1088fbff01f91256f91d21d0caaaaa96f" - integrity sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg== - -fp-ts@^1.0.0: - version "1.19.5" - resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.5.tgz#3da865e585dfa1fdfd51785417357ac50afc520a" - integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A== - fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -10734,7 +10497,7 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -from2@^2.1.0, from2@^2.1.1: +from2@^2.1.0: version "2.3.0" resolved "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= @@ -10847,11 +10610,6 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fs@^0.0.1-security: - version "0.0.1-security" - resolved "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" - integrity sha1-invTcYa23d84E/I4WLV+yq9eQdQ= - fsevents@2.0.7: version "2.0.7" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a" @@ -10919,7 +10677,7 @@ ganache-cli@^6.1.0: source-map-support "0.5.9" yargs "11.1.0" -ganache-core@^2.6.0, ganache-core@^2.8.0: +ganache-core@*, ganache-core@^2.6.0: version "2.8.0" resolved "https://registry.npmjs.org/ganache-core/-/ganache-core-2.8.0.tgz#eeadc7f7fc3a0c20d99f8f62021fb80b5a05490c" integrity sha512-hfXqZGJx700jJqwDHNXrU2BnPYuETn1ekm36oRHuXY3oOuJLFs5C+cFdUFaBlgUxcau1dOgZUUwKqTAy0gTA9Q== @@ -11003,11 +10761,6 @@ get-stdin@^6.0.0: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== -get-stream@3.0.0, get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - get-stream@^2.2.0: version "2.3.1" resolved "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" @@ -11016,6 +10769,11 @@ get-stream@^2.2.0: object-assign "^4.0.1" pinkie-promise "^2.0.0" +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -11158,7 +10916,7 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.4: +glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@~7.1.4: version "7.1.4" resolved "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== @@ -11182,7 +10940,19 @@ glob@^7.0.3, glob@^7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^0.1.0, global-dirs@^0.1.1: +glob@^7.1.2: + version "7.1.5" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0" + integrity sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= @@ -11375,29 +11145,6 @@ got@^6.3.0: unzip-response "^2.0.1" url-parse-lax "^1.0.0" -got@^8.3.1: - version "8.3.2" - resolved "https://registry.npmjs.org/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" - integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== - dependencies: - "@sindresorhus/is" "^0.7.0" - cacheable-request "^2.1.1" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - into-stream "^3.1.0" - is-retry-allowed "^1.1.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - mimic-response "^1.0.0" - p-cancelable "^0.4.0" - p-timeout "^2.0.1" - pify "^3.0.0" - safe-buffer "^5.1.1" - timed-out "^4.0.1" - url-parse-lax "^3.0.0" - url-to-options "^1.0.1" - graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" @@ -11498,9 +11245,9 @@ handle-thing@^2.0.0: integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== handlebars@^4.0.1: - version "4.4.3" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.4.3.tgz#180bae52c1d0e9ec0c15d7e82a4362d662762f6e" - integrity sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw== + version "4.5.1" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.1.tgz#8a01c382c180272260d07f2d1aa3ae745715c7ba" + integrity sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -11964,11 +11711,6 @@ htmlparser2@^3.3.0, htmlparser2@^3.9.1: inherits "^2.0.1" readable-stream "^3.1.1" -http-cache-semantics@3.8.1: - version "3.8.1" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" - integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== - http-cache-semantics@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#495704773277eeef6e43f9ab2c2c7d259dda25c5" @@ -12069,17 +11811,9 @@ https-browserify@^1.0.0: resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" - integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ== - dependencies: - agent-base "^4.1.0" - debug "^3.1.0" - human-signals@^1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== humps@^2.0.1: @@ -12232,7 +11966,7 @@ import-from@^2.1.0: dependencies: resolve-from "^3.0.0" -import-local@2.0.0, import-local@^2.0.0: +import-local@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== @@ -12286,7 +12020,7 @@ inflected@^1.1.6: inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" @@ -12414,7 +12148,7 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -interpret@1.2.0, interpret@^1.0.0, interpret@^1.1.0, interpret@^1.2.0: +interpret@^1.0.0, interpret@^1.1.0, interpret@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== @@ -12424,14 +12158,6 @@ intersection-observer@^0.5.1: resolved "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.5.1.tgz#e340fc56ce74290fe2b2394d1ce88c4353ac6dfa" integrity sha512-Zd7Plneq82kiXFixs7bX62YnuZ0BMRci9br7io88LwDyF3V43cQMI+G5IiTlTNTt+LsDUppl19J/M2Fp9UkH6g== -into-stream@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" - integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY= - dependencies: - from2 "^2.1.1" - p-is-promise "^1.1.0" - invariant@2.2.4, invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -12449,13 +12175,6 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== -io-ts@1.10.4: - version "1.10.4" - resolved "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz#cd5401b138de88e4f920adbcb7026e2d1967e6e2" - integrity sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g== - dependencies: - fp-ts "^1.0.0" - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -12763,14 +12482,6 @@ is-installed-globally@0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" -is-installed-globally@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.2.0.tgz#8cde07ade508458b51f14bcda315ffaf4898de30" - integrity sha512-g3TzWCnR/eO4Q3abCwgFjOFw7uVOfxG4m8hMr/39Jcf2YvE5mHrFKqpyuraWV4zwx9XhjnVO4nY0ZI4llzl0Pg== - dependencies: - global-dirs "^0.1.1" - is-path-inside "^2.1.0" - is-lower-case@^1.1.0: version "1.1.3" resolved "https://registry.npmjs.org/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" @@ -12933,11 +12644,6 @@ is-retry-allowed@^1.0.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ= -is-retry-allowed@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - is-root@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/is-root/-/is-root-2.0.0.tgz#838d1e82318144e5a6f77819d90207645acc7019" @@ -12955,7 +12661,7 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: is-stream@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== is-string@^1.0.4: @@ -13187,6 +12893,23 @@ javascript-time-ago@^2.0.1, javascript-time-ago@^2.0.2: dependencies: relative-time-format "^0.1.3" +jayson@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.1.2.tgz#1497ef3ebba53bbdb4737ed7a2456079024a846f" + integrity sha512-EYGNAuffmuwSsoL+/wqaILH+2hhUJCdH/XIc+STshrIsS8yjFqYpBCn8qPHogtGl2Hq9/tJ4VoQ8V/tdZdaLDw== + dependencies: + "@types/connect" "^3.4.32" + "@types/express-serve-static-core" "^4.16.9" + "@types/lodash" "^4.14.139" + "@types/node" "^12.7.7" + JSONStream "^1.3.1" + commander "^2.12.2" + es6-promisify "^5.0.0" + eyes "^0.1.8" + json-stringify-safe "^5.0.1" + lodash "^4.17.15" + uuid "^3.2.1" + jest-changed-files@^24.9.0: version "24.9.0" resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" @@ -13642,7 +13365,7 @@ js-sha3@0.5.7, js-sha3@^0.5.7: resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7" integrity sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc= -js-sha3@0.8.0: +js-sha3@0.8.0, js-sha3@^0.8.0: version "0.8.0" resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== @@ -13875,7 +13598,7 @@ json-stringify-pretty-compact@^1.0.1: resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-1.2.0.tgz#0bc316b5e6831c07041fc35612487fb4e9ab98b8" integrity sha512-/11Pj1OyX814QMKO7K8l85SHPTr/KsFxHp8GE2zVa0BtJgGimDjXHfM3FhC7keQdWDea7+nXf+f1de7ATZcZkQ== -json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= @@ -13938,27 +13661,16 @@ jsonify@~0.0.0: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + jsonschema@^1.2.0: version "1.2.4" resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.4.tgz#a46bac5d3506a254465bc548876e267c6d0d6464" integrity sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw== -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -14089,23 +13801,6 @@ just-debounce@^1.0.0: resolved "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" integrity sha1-h/zPrv/AtozRnVX2cilD+SnqNeo= -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - keccak@^1.0.2: version "1.4.0" resolved "https://registry.yarnpkg.com/keccak/-/keccak-1.4.0.tgz#572f8a6dbee8e7b3aa421550f9e6408ca2186f80" @@ -14129,13 +13824,6 @@ keygrip@~1.0.3: resolved "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g== -keyv@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" - integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== - dependencies: - json-buffer "3.0.0" - keyv@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -14379,15 +14067,6 @@ level-ws@0.0.0: readable-stream "~1.0.15" xtend "~2.1.1" -level-ws@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/level-ws/-/level-ws-1.0.0.tgz#19a22d2d4ac57b18cc7c6ecc4bd23d899d8f603b" - integrity sha512-RXEfCmkd6WWFlArh3X8ONvQPm8jNpfA0s/36M4QzLqrLEIt1iJE9WBHLZ5vZJK6haMjJPJGJCQWfjMNnRcq/9Q== - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.8" - xtend "^4.0.1" - levelup@3.1.1, levelup@^3.0.0: version "3.1.1" resolved "https://registry.npmjs.org/levelup/-/levelup-3.1.1.tgz#c2c0b3be2b4dc316647c53b42e2f559e232d2189" @@ -14438,7 +14117,7 @@ liftoff@^3.1.0: rechoir "^0.6.2" resolve "^1.1.7" -link_token@^1.0.3, link_token@^1.0.6: +link_token@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/link_token/-/link_token-1.0.6.tgz#98c5a1f53d4e22d7b0d5f80c5051702e7dc3b436" integrity sha512-WI6n2Ri9kWQFsFYPTnLvU+Y3DT87he55uqLLX/sAwCVkGTXI/wionGEHAoWU33y8RepWlh46y+RD+DAz5Wmejg== @@ -14611,11 +14290,6 @@ lodash.assign@^4.0.3, lodash.assign@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -14631,41 +14305,16 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -14676,7 +14325,7 @@ lodash.mergewith@^4.6.2: resolved "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash.once@^4.0.0, lodash.once@^4.1.1: +lodash.once@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= @@ -14824,11 +14473,6 @@ lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: resolved "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= -lowercase-keys@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" - integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= - lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -14879,7 +14523,7 @@ ltgt@~2.1.1: resolved "https://registry.npmjs.org/ltgt/-/ltgt-2.1.3.tgz#10851a06d9964b971178441c23c9e52698eece34" integrity sha1-EIUaBtmWS5cReEQcI8nlJpjuzjQ= -make-dir@^1.0.0, make-dir@^1.2.0, make-dir@^1.3.0: +make-dir@^1.0.0, make-dir@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== @@ -15136,19 +14780,6 @@ merkle-patricia-tree@2.3.2, merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2 rlp "^2.0.0" semaphore ">=1.0.1" -merkle-patricia-tree@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-3.0.0.tgz#448d85415565df72febc33ca362b8b614f5a58f8" - integrity sha512-soRaMuNf/ILmw3KWbybaCjhx86EYeBbD8ph0edQCTed0JN/rxDt1EBN52Ajre3VyGo+91f8+/rfPIRQnnGMqmQ== - dependencies: - async "^2.6.1" - ethereumjs-util "^5.2.0" - level-mem "^3.0.1" - level-ws "^1.0.0" - readable-stream "^3.0.6" - rlp "^2.0.0" - semaphore ">=1.0.1" - methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -15228,11 +14859,6 @@ mime-db@1.40.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.41.0.tgz#9110408e1f6aa1b34aef51f2c9df3caddf46b6a0" integrity sha512-B5gxBI+2K431XW8C2rcc/lhppbuji67nf9v39eH8pkWoZDxnAL0PxdpH32KYRScniF8qDHBDlI+ipgg5WrCUYw== -mime-db@^1.28.0: - version "1.42.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" - integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== - mime-db@~1.33.0: version "1.33.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" @@ -15281,7 +14907,7 @@ mimic-fn@^2.0.0: mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== mimic-response@^1.0.0, mimic-response@^1.0.1: @@ -15413,7 +15039,7 @@ mkdirp@*, mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5. dependencies: minimist "0.0.8" -mocha@5.2.0, mocha@^5.2.0: +mocha@5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== @@ -15779,10 +15405,10 @@ node-sass-tilde-importer@^1.0.2: dependencies: find-parent-dir "^0.3.0" -nofilter@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.2.tgz#d2887b9e76531fa251f20038c3843b993f1b3e26" - integrity sha512-d38SORxm9UNoDsnPXajV9nBEebKX4/paXAlyRGnSjZuFbLLZDFUO4objr+tbybqsbqGXDWllb6gQoKUDc9q3Cg== +nofilter@^1.0.1, nofilter@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.3.tgz#34e54b4cc9757de0cad38cc0d19462489b1b7f5d" + integrity sha512-FlUlqwRK6reQCaFLAhMcF+6VkVG2caYjKQY3YsRDTl4/SEch595Qb3oLjJRDr8dkHAAOVj2pOx3VknfnSgkE5g== noop-logger@^0.1.1: version "0.1.1" @@ -15846,24 +15472,15 @@ normalize-url@1.9.1: query-string "^4.1.0" sort-keys "^1.0.0" -normalize-url@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" - integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== - dependencies: - prepend-http "^2.0.0" - query-string "^5.0.1" - sort-keys "^2.0.0" - normalize-url@^3.0.0, normalize-url@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== -normalize-url@^4.1.0, normalize-url@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee" - integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ== +normalize-url@^4.1.0, normalize-url@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== now-and-later@^2.0.0: version "2.0.1" @@ -15902,7 +15519,7 @@ npm-run-path@^2.0.0: npm-run-path@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.0.tgz#d644ec1bd0569187d2a52909971023a0a58e8438" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.0.tgz#d644ec1bd0569187d2a52909971023a0a58e8438" integrity sha512-8eyAOAH+bYXFPSnNnKr3J+yoybe8O87Is5rtAQ8qRczJz1ajcsjg8l2oZqP+Ppx15Ii3S1vUTjQN2h4YO2tWWQ== dependencies: path-key "^3.0.0" @@ -16174,7 +15791,7 @@ onetime@^2.0.0: onetime@^5.1.0: version "5.1.0" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== dependencies: mimic-fn "^2.1.0" @@ -16305,7 +15922,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-locale@^3.0.0, os-locale@^3.1.0: +os-locale@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -16341,11 +15958,6 @@ p-cancelable@^0.3.0: resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" integrity sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw== -p-cancelable@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" - integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== - p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -16363,13 +15975,6 @@ p-each-series@^1.0.0: dependencies: p-reduce "^1.0.0" -p-event@^2.1.0: - version "2.3.1" - resolved "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz#596279ef169ab2c3e0cae88c1cfbb08079993ef6" - integrity sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA== - dependencies: - p-timeout "^2.0.1" - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -16377,14 +15982,9 @@ p-finally@^1.0.0: p-finally@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== -p-is-promise@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" - integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= - p-is-promise@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5" @@ -16454,13 +16054,6 @@ p-timeout@^1.1.1: dependencies: p-finally "^1.0.0" -p-timeout@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" - integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== - dependencies: - p-finally "^1.0.0" - p-try@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -16851,7 +16444,12 @@ pgpass@1.x: dependencies: split "^1.0.0" -picomatch@^2.0.4, picomatch@^2.0.5: +picomatch@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.1.0.tgz#0fd042f568d08b1ad9ff2d3ec0f0bfb3cb80e177" + integrity sha512-uhnEDzAbrcJ8R3g2fANnSuXZMBtkpSjxTTgn2LeSiQlfmq72enQJWdQllXW24MBLYnA1SBD2vfvx2o0Zw3Ielw== + +picomatch@^2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== @@ -16963,11 +16561,6 @@ polished@^3.3.1: dependencies: "@babel/runtime" "^7.4.5" -pop-iterate@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/pop-iterate/-/pop-iterate-1.0.1.tgz#ceacfdab4abf353d7a0f2aaa2c1fc7b3f9413ba3" - integrity sha1-zqz9q0q/NT16DyqqLB/Hs/lBO6M= - popper.js@1.14.3: version "1.14.3" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095" @@ -17928,11 +17521,6 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.0" -proxy-from-env@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" - integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= - prr@~1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -18061,29 +17649,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -puppeteer@^1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.20.0.tgz#e3d267786f74e1d87cf2d15acc59177f471bbe38" - integrity sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ== - dependencies: - debug "^4.1.0" - extract-zip "^1.6.6" - https-proxy-agent "^2.2.1" - mime "^2.0.3" - progress "^2.0.1" - proxy-from-env "^1.0.0" - rimraf "^2.6.1" - ws "^6.1.0" - -q@2.0.x: - version "2.0.3" - resolved "https://registry.npmjs.org/q/-/q-2.0.3.tgz#75b8db0255a1a5af82f58c3f3aaa1efec7d0d134" - integrity sha1-dbjbAlWhpa+C9Yw/Oqoe/sfQ0TQ= - dependencies: - asap "^2.0.0" - pop-iterate "^1.0.1" - weak-map "^1.0.5" - q@^1.1.2: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -18104,7 +17669,7 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.1, qs@^6.6.0, qs@^6.7.0: +qs@^6.5.1, qs@^6.6.0: version "6.9.0" resolved "https://registry.npmjs.org/qs/-/qs-6.9.0.tgz#d1297e2a049c53119cb49cca366adbbacc80b409" integrity sha512-27RP4UotQORTpmNQDX8BHPukOnBP3p1uUJY5UnDhaJB+rMt9iMsok724XL+UHU23bEFOHRMQ2ZhI99qOWUMGFA== @@ -18185,7 +17750,7 @@ randomatic@^3.0.0: kind-of "^6.0.0" math-random "^1.0.1" -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.0.6: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.0.6, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -18478,11 +18043,12 @@ react-helmet@^5.2.0: react-fast-compare "^2.0.2" react-side-effect "^1.1.0" -react-hot-loader@^4.3.6, react-hot-loader@^4.6.1, react-hot-loader@^4.6.3, react-hot-loader@^4.8.0: - version "4.12.15" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.15.tgz#6bf3984e52edbdf02ea8952777f53da1b3c68c95" - integrity sha512-sgkN6g+tgPE6xZzD0Ysqll7KUFYJbMX0DrczT5OxD6S7hZlSnmqSC3ceudwCkiDd65ZTtm+Ayk4Y9k5xxCvpOw== +react-hot-loader@^4.12.16, react-hot-loader@^4.3.6, react-hot-loader@^4.6.1: + version "4.12.16" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.16.tgz#353bd07fbb08f791b5720535f86b0a8f9b651317" + integrity sha512-KC33uTBacgdunMtfpZFP2pgPpyvKIcCwuh0XmWESbeFrkWLqUtCFN91zyaTdU5OiAM982+E8xh1gjE5EINumaw== dependencies: + "@types/react" "^15.0.0 || ^16.0.0" fast-levenshtein "^2.0.6" global "^4.3.0" hoist-non-react-statics "^3.3.0" @@ -18954,7 +18520,7 @@ read-pkg@^4.0.1: parse-json "^4.0.0" pify "^3.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.8, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -19405,7 +18971,7 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request-promise@4.2.4, request-promise@^4.2.2, request-promise@^4.2.4: +request-promise@4.2.4: version "4.2.4" resolved "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz#1c5ed0d71441e38ad58c7ce4ea4ea5b06d54b310" integrity sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg== @@ -19415,7 +18981,7 @@ request-promise@4.2.4, request-promise@^4.2.2, request-promise@^4.2.4: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@2.88.0, request@^2.67.0, request@^2.79.0, request@^2.83.0, request@^2.85.0, request@^2.87.0, request@^2.88.0: +request@2.88.0, request@^2.67.0, request@^2.79.0, request@^2.85.0, request@^2.87.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -19561,20 +19127,13 @@ resolve@1.1.7, resolve@1.1.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.12.0, resolve@1.x, resolve@^1.0.0, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: +resolve@1.12.0, resolve@1.x, resolve@^1.0.0, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.8.1: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== dependencies: path-parse "^1.0.6" -resolve@^1.5.0: - version "1.10.0" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" - integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== - dependencies: - path-parse "^1.0.6" - resolve@~1.11.1: version "1.11.1" resolved "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" @@ -19582,7 +19141,7 @@ resolve@~1.11.1: dependencies: path-parse "^1.0.6" -responselike@1.0.2, responselike@^1.0.2: +responselike@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= @@ -19666,6 +19225,13 @@ rimraf@2.6.3: dependencies: glob "^7.1.3" +rimraf@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" + integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== + dependencies: + glob "^7.1.3" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -19674,7 +19240,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2: +rlp@^2.0.0, rlp@^2.2.1: version "2.2.3" resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.3.tgz#7f94aef86cec412df87d5ea1d8cb116a47d45f0e" integrity sha512-l6YVrI7+d2vpW6D6rS05x2Xrmq8oW7v3pieZOJKBEdjuTF4Kz/iwk55Zyh1Zaz+KOB2kC8+2jZlp2u9L4tTzCQ== @@ -19682,11 +19248,6 @@ rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2: bn.js "^4.11.1" safe-buffer "^5.1.1" -rootpath@0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/rootpath/-/rootpath-0.1.2.tgz#5b379a87dca906e9b91d690a599439bef267ea6b" - integrity sha1-Wzeah9ypBum5HWkKWZQ5vvJn6ms= - rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" @@ -19726,27 +19287,13 @@ rxjs@^5.0.0-beta.11: dependencies: symbol-observable "1.0.1" -rxjs@^6.1.0: - version "6.4.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504" - integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw== - dependencies: - tslib "^1.9.0" - -rxjs@^6.4.0, rxjs@^6.5.3: +rxjs@^6.1.0, rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.3: version "6.5.3" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== dependencies: tslib "^1.9.0" -rxjs@^6.5.2: - version "6.5.2" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" - integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== - dependencies: - tslib "^1.9.0" - safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -19851,11 +19398,6 @@ schema-utils@^2.0.0, schema-utils@^2.0.1: ajv "^6.10.2" ajv-keywords "^3.4.1" -scmp@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/scmp/-/scmp-2.0.0.tgz#247110ef22ccf897b13a3f0abddb52782393cd6a" - integrity sha1-JHEQ7yLM+JexOj8KvdtSeCOTzWo= - scroll-into-view-if-needed@^2.2.16: version "2.2.20" resolved "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.20.tgz#3a46847a72233a3af9770e55df450f2a7f2e2a0e" @@ -20252,7 +19794,7 @@ shell-quote@1.6.1: shelljs@^0.8.3: version "0.8.3" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" + resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== dependencies: glob "^7.0.0" @@ -20501,20 +20043,6 @@ solc@0.5.0: semver "^5.5.0" yargs "^11.0.0" -solc@0.5.11: - version "0.5.11" - resolved "https://registry.npmjs.org/solc/-/solc-0.5.11.tgz#5905261191d01befd78ef52610a006820022ee8f" - integrity sha512-F8avCCVDYnJzvIm/ITsU11GFNdFI4HaNsME+zw9lK5a3ojD3LZN2Op2cIfWg7w1HeRYRiMOU1dM77saX6jUIKw== - dependencies: - command-exists "^1.2.8" - fs-extra "^0.30.0" - js-sha3 "0.8.0" - memorystream "^0.3.1" - require-from-string "^2.0.0" - semver "^5.5.0" - tmp "0.0.33" - yargs "^13.2.0" - solc@^0.4.20: version "0.4.25" resolved "https://registry.yarnpkg.com/solc/-/solc-0.4.25.tgz#06b8321f7112d95b4b903639b1138a4d292f5faa" @@ -20559,7 +20087,7 @@ solhint@^2.1.0: optionalDependencies: prettier "^1.14.3" -solidity-cborutils@^1.0.2, solidity-cborutils@^1.0.4: +solidity-cborutils@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/solidity-cborutils/-/solidity-cborutils-1.0.5.tgz#25a2875d942c18d4b61e889c0b56604de1f41063" integrity sha512-SHwp1Un9s6Ki8rXMod18w/6VAqzuBor61SqPS6uy7s1fPDx1rUwnhcx3zWK8CfA0Pm7UY31Apy9NOBu/Vo4ZMg== @@ -20572,13 +20100,6 @@ solidity-parser-antlr@^0.4.2: resolved "https://registry.npmjs.org/solidity-parser-antlr/-/solidity-parser-antlr-0.4.11.tgz#af43e1f13b3b88309a875455f5d6e565b05ee5f1" integrity sha512-4jtxasNGmyC0midtjH/lTFPZYvTTUMy6agYcF+HoMnzW8+cqo3piFrINb4ZCzpPW+7tTVFCGa5ubP34zOzeuMg== -sort-keys-length@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" - integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg= - dependencies: - sort-keys "^1.0.0" - sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -20586,13 +20107,6 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" -sort-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" - integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= - dependencies: - is-plain-obj "^1.0.0" - source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -20632,7 +20146,15 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map-support@^0.5.0, source-map-support@^0.5.12, source-map-support@^0.5.13, source-map-support@^0.5.6, source-map-support@^0.5.9, source-map-support@~0.5.12: +source-map-support@^0.5.0: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@^0.5.12, source-map-support@^0.5.13, source-map-support@^0.5.6, source-map-support@^0.5.9, source-map-support@~0.5.12: version "0.5.13" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== @@ -21075,7 +20597,7 @@ strip-eof@^1.0.0: strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-hex-prefix@1.0.0: @@ -21172,13 +20694,6 @@ supports-color@5.5.0, supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@6.1.0, supports-color@^6.0.0, supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -21198,6 +20713,13 @@ supports-color@^4.2.1, supports-color@^4.5.0: dependencies: has-flag "^2.0.0" +supports-color@^6.0.0, supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + sver-compat@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" @@ -21608,7 +21130,7 @@ through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, through@^2.3.6, through@^2.3.8, through@~2.3.4, through@~2.3.8: +through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3.4, through@~2.3.8: version "2.3.8" resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -21650,17 +21172,6 @@ tiny-invariant@^1.0.2: resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g== -tiny-secp256k1@^1.1.0: - version "1.1.3" - resolved "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.3.tgz#e93b1e1bf62e9bd1ad3ab24af27ff6127ce0e077" - integrity sha512-ZpobrhOtHP98VYEN51IYQH1YcrbFpnxFhI6ceWa3OEbJn7eHvSd8YFjGPxbedGCy7PNYU1v/+BRsdvyr5uRd4g== - dependencies: - bindings "^1.3.0" - bn.js "^4.11.8" - create-hmac "^1.1.7" - elliptic "^6.4.0" - nan "^2.13.2" - tiny-warning@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28" @@ -21890,15 +21401,6 @@ truffle@^5.0.25: mocha "5.2.0" original-require "1.0.1" -truffle@^5.0.39: - version "5.0.39" - resolved "https://registry.yarnpkg.com/truffle/-/truffle-5.0.39.tgz#5710ba8f60a7184d9eb51d632308f2af0a2e8aff" - integrity sha512-2a17t4o6r0rNMpeQXBc51nXigtIaP9/sU8N2zflaazvzYgDgLMZfqh/dir2mTfyybOsrR47NL310p+6+c8u8VA== - dependencies: - app-module-path "^2.2.0" - mocha "5.2.0" - original-require "1.0.1" - tryer@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" @@ -21909,11 +21411,6 @@ ts-essentials@^1.0.0: resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-1.0.4.tgz#ce3b5dade5f5d97cf69889c11bf7d2da8555b15a" integrity sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ== -ts-essentials@^2.0.7: - version "2.0.12" - resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745" - integrity sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w== - ts-generator@^0.0.8: version "0.0.8" resolved "https://registry.npmjs.org/ts-generator/-/ts-generator-0.0.8.tgz#7bd48ca064db026d9520bcb682b69efc20971d6a" @@ -22026,12 +21523,7 @@ tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tsort@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" - integrity sha1-4igPXoF/i/QnVlf9D5rr1E9aJ4Y= - -tsutils@^3.14.0, tsutils@^3.7.0: +tsutils@^3.14.0, tsutils@^3.17.1, tsutils@^3.7.0: version "3.17.1" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== @@ -22065,22 +21557,6 @@ tweetnacl@^1.0.0: resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz#2594d42da73cd036bd0d2a54683dd35a6b55ca17" integrity sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A== -twilio@^3.34.0: - version "3.34.0" - resolved "https://registry.yarnpkg.com/twilio/-/twilio-3.34.0.tgz#3d3721c6019395f9b0ef6f922c1a457e03a987d2" - integrity sha512-LBOOyT2NZJXe3gnC2kGmtqopKRiKDjGQjDK93IJ0+5ePUIh77huulaWHkdwaoD1ejL3gKs+K+P3MVIkFHeAqUQ== - dependencies: - "@types/express" "^4.17.1" - deprecate "1.0.0" - jsonwebtoken "^8.5.1" - lodash "^4.17.15" - moment "^2.24.0" - q "2.0.x" - request "^2.88.0" - rootpath "0.1.2" - scmp "2.0.0" - xmlbuilder "9.0.1" - type-check@~0.3.2: version "0.3.2" resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -22119,21 +21595,22 @@ type@^1.0.1: resolved "https://registry.npmjs.org/type/-/type-1.0.3.tgz#16f5d39f27a2d28d86e48f8981859e9d3296c179" integrity sha512-51IMtNfVcee8+9GJvj0spSuFcZHe9vSib6Xtgsny1Km9ugyz2mbS08I3rsUIRYgJohFRFU1160sgRodYz378Hg== -typechain-target-ethers@^1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.npmjs.org/typechain-target-ethers/-/typechain-target-ethers-1.0.0-beta.1.tgz#3c97f28e66fedb37153e152bc8a4f1f5fe2c62a1" - integrity sha512-n8IoNCcbQSvX8hvl+Q8pjh13n9ezRzvKpk0ZYIE+WEdfR76EUYN0/6k/lzSgkpJUM33QF62dwjo7ZZsXlOV6pw== +typechain-target-ethers@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/typechain-target-ethers/-/typechain-target-ethers-1.0.1.tgz#b4a1467a115d8bc46dbc6a2531d9da278bc358e5" + integrity sha512-4rsWiKxZc20D4ldF0Pviqo2z07yrW3xIbf6+GdifEZP6zmc0Z2yXOHxEVkqhjLpBp/4z5yRgpW5G2/I32TsI/w== dependencies: lodash "^4.17.15" -typechain@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typechain/-/typechain-1.0.1.tgz#e0dcf63bb0ae8feb74a56ff3153abfd706473efe" - integrity sha512-mB/8UbcziE+CJi3ph8hHhBRILAnFjIpAELIhkNWYs08xg2UPFIWVEfK5v0Eb+G7B/XEhOMFbd0EJwGaeWp54Xg== +typechain@1.0.3, typechain@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/typechain/-/typechain-1.0.3.tgz#c4465cdc27526989c735774ad96fadbb908bfe01" + integrity sha512-GG/isuyXy0vtm4fwfboMqnApU3hUF+XdrpQMMVUIhlHgm1kXxxf+w8+v8shn4gfhRoiATCZGDOt9fbE+/hMhjQ== dependencies: command-line-args "^4.0.7" debug "^3.0.1" fs-extra "^7.0.0" + js-sha3 "^0.8.0" lodash "^4.17.15" ts-generator "^0.0.8" @@ -22154,11 +21631,6 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typeforce@^1.11.5: - version "1.18.0" - resolved "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" - integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== - typeorm@^0.2.15: version "0.2.18" resolved "https://registry.npmjs.org/typeorm/-/typeorm-0.2.18.tgz#8ae1d21104117724af41ddc11035c40a705e1de8" @@ -22191,10 +21663,10 @@ typescript@^3.6.3: resolved "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== -typescript@^3.7.0-beta: - version "3.7.0-beta" - resolved "https://registry.npmjs.org/typescript/-/typescript-3.7.0-beta.tgz#4ad556e0eee14b90ecc39261001690e16e5eeba9" - integrity sha512-4jyCX+IQamrPJxgkABPq9xf+hUN+GWHVxoj+oey1TadCPa4snQl1RKwUba+1dyzYCamwlCxKvZQ3TjyWLhMGBA== +typescript@^3.7.0, typescript@^3.7.0-beta: + version "3.7.2" + resolved "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" + integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== typewise-core@^1.2, typewise-core@^1.2.0: version "1.2.0" @@ -22218,11 +21690,6 @@ typical@^2.6.0, typical@^2.6.1: resolved "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0= -typical@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" - integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== - u2f-api@0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/u2f-api/-/u2f-api-0.2.7.tgz#17bf196b242f6bf72353d9858e6a7566cc192720" @@ -22603,16 +22070,11 @@ uuid@3.3.2, uuid@^3.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.1.0, uuid@^3.3.2: +uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: version "3.3.3" resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== -v8-compile-cache@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" - integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== - v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" @@ -22638,10 +22100,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -validator@11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/validator/-/validator-11.1.0.tgz#ac18cac42e0aa5902b603d7a5d9b7827e2346ac4" - integrity sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg== +validator@12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-12.0.0.tgz#fb33221f5320abe2422cda2f517dc3838064e813" + integrity sha512-r5zA1cQBEOgYlesRmSEwc9LkbfNLTtji+vWyaHzRZUxCTHdsX3bd+sdHfs5tGZ2W6ILGGsxWxCNwT/h3IY/3ng== value-equal@^0.4.0: version "0.4.0" @@ -22791,11 +22253,6 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -weak-map@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/weak-map/-/weak-map-1.0.5.tgz#79691584d98607f5070bd3b70a40e6bb22e401eb" - integrity sha1-eWkVhNmGB/UHC9O3CkDmuyLkAes= - web3-bzz@1.0.0-beta.37: version "1.0.0-beta.37" resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.0.0-beta.37.tgz#59e3e4f5a9d732731008fe9165c3ec8bf85d502f" @@ -22939,7 +22396,7 @@ web3-eth-abi@1.0.0-beta.37: underscore "1.8.3" web3-utils "1.0.0-beta.37" -web3-eth-abi@1.2.1, web3-eth-abi@^1.0.0-beta.24: +web3-eth-abi@1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.2.1.tgz#9b915b1c9ebf82f70cca631147035d5419064689" integrity sha512-jI/KhU2a/DQPZXHjo2GW0myEljzfiKOn+h1qxK1+Y9OQfTcBMxrQJyH5AP89O6l6NZ1QvNdq99ThAxBFoy5L+g== @@ -22948,6 +22405,15 @@ web3-eth-abi@1.2.1, web3-eth-abi@^1.0.0-beta.24: underscore "1.9.1" web3-utils "1.2.1" +web3-eth-abi@^1.0.0-beta.24: + version "1.2.2" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.2.2.tgz#d5616d88a90020f894763423a9769f2da11fe37a" + integrity sha512-Yn/ZMgoOLxhTVxIYtPJ0eS6pnAnkTAaJgUJh1JhZS4ekzgswMfEYXOwpMaD5eiqPJLpuxmZFnXnBZlnQ1JMXsw== + dependencies: + ethers "4.0.0-beta.3" + underscore "1.9.1" + web3-utils "1.2.2" + web3-eth-accounts@1.0.0-beta.37: version "1.0.0-beta.37" resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.0.0-beta.37.tgz#0a5a9f14a6c3bd285e001c15eb3bb38ffa4b5204" @@ -23282,6 +22748,20 @@ web3-utils@1.2.1, web3-utils@^1.0.0-beta.31, web3-utils@^1.2.0: underscore "1.9.1" utf8 "3.0.0" +web3-utils@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.2.2.tgz#b53a08c40d2c3f31d3c4a28e7d749405df99c8c0" + integrity sha512-joF+s3243TY5cL7Z7y4h1JsJpUCf/kmFmj+eJar7Y2yNIGVcW961VyrAms75tjUysSuHaUQ3eQXjBEUJueT52A== + dependencies: + bn.js "4.11.8" + eth-lib "0.2.7" + ethereum-bloom-filters "^1.0.6" + ethjs-unit "0.1.6" + number-to-bn "1.7.0" + randombytes "^2.1.0" + underscore "1.9.1" + utf8 "3.0.0" + web3@1.0.0-beta.37: version "1.0.0-beta.37" resolved "https://registry.yarnpkg.com/web3/-/web3-1.0.0-beta.37.tgz#b42c30e67195f816cd19d048fda872f70eca7083" @@ -23295,7 +22775,7 @@ web3@1.0.0-beta.37: web3-shh "1.0.0-beta.37" web3-utils "1.0.0-beta.37" -web3@1.2.1, web3@^1.0.0-beta.35, web3@^1.0.0-beta.55, web3@^1.2.0: +web3@1.2.1, web3@^1.2.0: version "1.2.1" resolved "https://registry.npmjs.org/web3/-/web3-1.2.1.tgz#5d8158bcca47838ab8c2b784a2dee4c3ceb4179b" integrity sha512-nNMzeCK0agb5i/oTWNdQ1aGtwYfXzHottFP2Dz0oGIzavPMGSKyVlr8ibVb1yK5sJBjrWVnTdGaOC2zKDFuFRw== @@ -23337,23 +22817,6 @@ webpack-bundle-analyzer@^3.0.3: opener "^1.5.1" ws "^6.0.0" -webpack-cli@^3.3.9: - version "3.3.9" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.9.tgz#79c27e71f94b7fe324d594ab64a8e396b9daa91a" - integrity sha512-xwnSxWl8nZtBl/AFJCOn9pG7s5CYUYdZxmmukv+fAHLcBIHM36dImfpQg3WfShZXeArkWlf6QRw24Klcsv8a5A== - dependencies: - chalk "2.4.2" - cross-spawn "6.0.5" - enhanced-resolve "4.1.0" - findup-sync "3.0.0" - global-modules "2.0.0" - import-local "2.0.0" - interpret "1.2.0" - loader-utils "1.2.3" - supports-color "6.1.0" - v8-compile-cache "2.0.3" - yargs "13.2.4" - webpack-dev-middleware@^3.5.1: version "3.6.1" resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.6.1.tgz#91f2531218a633a99189f7de36045a331a4b9cd4" @@ -23731,13 +23194,6 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" -wif@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" - integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ= - dependencies: - bs58check "<3.0.0" - window-size@0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" @@ -23989,17 +23445,17 @@ ws@^5.1.1, ws@^5.2.0: dependencies: async-limiter "~1.0.0" -ws@^6.0.0, ws@^6.1.0, ws@^6.1.2, ws@^6.2.1: +ws@^6.0.0, ws@^6.1.2, ws@^6.2.1: version "6.2.1" resolved "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== dependencies: async-limiter "~1.0.0" -ws@^7.1.0: - version "7.1.2" - resolved "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73" - integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg== +ws@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.0.tgz#422eda8c02a4b5dba7744ba66eebbd84bcef0ec7" + integrity sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg== dependencies: async-limiter "^1.0.0" @@ -24070,11 +23526,6 @@ xml2js@^0.4.17: sax ">=0.6.0" xmlbuilder "~9.0.1" -xmlbuilder@9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.1.tgz#91cd70897755363eba57c12ddeeab4a341a61f65" - integrity sha1-kc1wiXdVNj66V8Et3uq0o0GmH2U= - xmlbuilder@~9.0.1: version "9.0.7" resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" @@ -24166,7 +23617,7 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.1.0, yargs-parser@^13.1.1: +yargs-parser@^13.1.1: version "13.1.1" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== @@ -24264,23 +23715,6 @@ yargs@12.0.5, yargs@^12.0.5: y18n "^3.2.1 || ^4.0.0" yargs-parser "^11.1.1" -yargs@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" - yargs@^10.0.3: version "10.1.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5"