├── .dockerignore ├── .github └── workflows │ ├── continuous-integration-workflow.yml │ ├── docker.yml │ └── static.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── blockchain ├── agoric.go ├── agoric_test.go ├── binance-smart-chain.go ├── binance-smart-chain_test.go ├── bsn-irita.go ├── bsn-irita_test.go ├── cfx.go ├── cfx_test.go ├── common.go ├── common_test.go ├── eth.go ├── eth_test.go ├── evm.go ├── evm_test.go ├── hmy.go ├── hmy_test.go ├── iotex.go ├── iotex_test.go ├── keeper.go ├── keeper_test.go ├── klaytn.go ├── klaytn_test.go ├── near.go ├── near_test.go ├── ont.go ├── ont_test.go ├── substrate.go ├── substrate_test.go ├── testdata │ ├── near_test_oracle_get_all_requests.json │ ├── tezos_test_block_operations_sc_initiated.json │ └── tezos_test_block_operations_user_initiated.json ├── tezos.go └── tezos_test.go ├── chainlink ├── node.go └── node_test.go ├── client ├── client.go ├── client_test.go ├── config.go ├── config_test.go ├── service.go ├── service_test.go ├── web.go └── web_test.go ├── eitest └── eitest.go ├── go.mod ├── go.sum ├── integration ├── .gitignore ├── Dockerfile ├── chainlink.env ├── common ├── docker-compose.yml ├── docker-init-scripts │ ├── chainlink │ │ └── import-keystore.sh │ └── postgres │ │ └── create-multiple-databases.sh ├── mock-client │ ├── Dockerfile │ ├── README.md │ ├── blockchain │ │ ├── binance-smart-chain.go │ │ ├── binance-smart-chain_test.go │ │ ├── bsn-irita.go │ │ ├── bsn-irita_test.go │ │ ├── canned-responses.go │ │ ├── cfx.go │ │ ├── cfx_test.go │ │ ├── common.go │ │ ├── eth.go │ │ ├── eth_test.go │ │ ├── harmony.go │ │ ├── harmony_test.go │ │ ├── iotex.go │ │ ├── iotex_test.go │ │ ├── keeper.go │ │ ├── keeper_test.go │ │ ├── klaytn.go │ │ ├── klaytn_test.go │ │ ├── near.go │ │ ├── near_test.go │ │ ├── ont.go │ │ ├── ont_test.go │ │ ├── static │ │ │ ├── binance-smart-chain.json │ │ │ ├── birita.json │ │ │ ├── cfx.json │ │ │ ├── eth.json │ │ │ ├── file.go │ │ │ ├── hmy.json │ │ │ ├── keeper.json │ │ │ ├── klaytn.json │ │ │ ├── near.json │ │ │ ├── ont.json │ │ │ ├── substrate.json │ │ │ └── xtz.json │ │ ├── substrate_test.go │ │ ├── xtz.go │ │ └── xtz_test.go │ ├── grpc │ │ └── grpc.go │ ├── main.go │ └── web │ │ └── client.go ├── node_modules │ └── .yarn-integrity ├── run_test ├── scripts │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── addExternalInitiator.ts │ │ ├── asserts.ts │ │ ├── chainlinkNode.ts │ │ ├── common.ts │ │ ├── runTest.ts │ │ └── tests │ │ │ ├── birita.ts │ │ │ ├── bsc.ts │ │ │ ├── cfx.ts │ │ │ ├── eth.ts │ │ │ ├── hmy.ts │ │ │ ├── index.ts │ │ │ ├── iotx.ts │ │ │ ├── keeper.ts │ │ │ ├── klaytn.ts │ │ │ ├── near.ts │ │ │ ├── ont.ts │ │ │ ├── substrate.ts │ │ │ └── xtz.ts │ ├── tsconfig.json │ └── yarn.lock ├── secrets │ ├── 0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f.json │ ├── apicredentials │ └── password.txt ├── setup ├── stop_docker └── yarn.lock ├── main.go ├── store ├── database.go ├── database_test.go ├── migrations │ ├── migrate.go │ ├── migration0 │ │ └── migrate.go │ ├── migration1576509489 │ │ └── migrate.go │ ├── migration1576783801 │ │ └── migrate.go │ ├── migration1582671289 │ │ └── migrate.go │ ├── migration1587897988 │ │ └── migrate.go │ ├── migration1592829052 │ │ └── migrate.go │ ├── migration1594317706 │ │ └── migrate.go │ ├── migration1599849837 │ │ └── migrate.go │ ├── migration1603803454 │ │ └── migrate.go │ ├── migration1605288480 │ │ └── migrate.go │ ├── migration1608026935 │ │ └── migrate.go │ ├── migration1610281978 │ │ └── migrate.go │ ├── migration1611169747 │ │ └── migrate.go │ └── migration1613356332 │ │ └── migrate.go └── models.go └── subscriber ├── rpc.go ├── rpc_test.go ├── subscriber.go ├── subscriber_test.go ├── ws.go └── ws_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # This is required when building mock-client, and will be removed manually 2 | # in external-initiator Dockerfile (not needed in that context). 3 | # ----------- 4 | # integration 5 | README.md 6 | .circleci 7 | .git 8 | .github 9 | 10 | # mock-client build context is set to this folder, so it applies .dockerignore from here 11 | integration/external_initiator.env 12 | integration/tmp/ 13 | integration/cl_login.txt 14 | integration/scripts 15 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration Workflow 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | env: 15 | DATABASE_URL: postgres://ei@localhost:5432/ei_test?sslmode=disable 16 | services: 17 | postgres: 18 | image: postgres 19 | ports: 20 | - 5432:5432 21 | env: 22 | POSTGRES_USER: postgres 23 | POSTGRES_HOST_AUTH_METHOD: trust 24 | # Set health checks to wait until postgres has started 25 | options: >- 26 | --health-cmd pg_isready 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | steps: 31 | - name: Set up Postgres user 32 | uses: docker://postgres 33 | with: 34 | args: psql -v ON_ERROR_STOP=1 --username postgres -h postgres -c "CREATE USER ei NOSUPERUSER CREATEDB;" 35 | - name: Checkout the repo 36 | uses: actions/checkout@v2 37 | - name: Setup Go 38 | uses: actions/setup-go@v2 39 | with: 40 | go-version: 1.15 41 | - name: Fetch EI dependencies 42 | run: go mod download 43 | - name: Test EI 44 | run: go test -v ./... 45 | - name: Fetch Mock client dependencies 46 | run: cd ./integration/mock-client && go mod download 47 | - name: Test Mock client 48 | run: cd ./integration/mock-client && go test -v ./... 49 | 50 | integration: 51 | name: Integration tests 52 | runs-on: ubuntu-latest 53 | needs: build 54 | steps: 55 | - name: Checkout the repo 56 | uses: actions/checkout@v2 57 | - name: Use Node.js 58 | uses: actions/setup-node@v1 59 | with: 60 | node-version: "12.x" 61 | - name: Run the setup 62 | run: ./integration/setup 63 | - name: Run the tests 64 | run: ./integration/run_test 65 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker image 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - v* 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout the repo 15 | uses: actions/checkout@v2 16 | - name: Publish to Registry 17 | uses: elgohr/Publish-Docker-Github-Action@2.18 18 | with: 19 | name: smartcontract/external-initiator 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | tag_names: true 23 | tag_semver: true 24 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: golang/static 2 | on: pull_request 3 | jobs: 4 | vet: 5 | name: vet 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-go@v2 10 | with: 11 | go-version: 1.15 12 | - run: go mod download 13 | - run: go vet ./... 14 | shadow: 15 | name: Shadow 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.15 22 | - run: go get -v golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow 23 | - run: go mod download 24 | - run: go vet -vettool=$HOME/go/bin/shadow ./... 25 | imports: 26 | name: imports 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: actions/setup-go@v2 31 | with: 32 | go-version: 1.15 33 | - run: go get -v golang.org/x/tools/cmd/goimports 34 | - run: d="$($HOME/go/bin/goimports -d ./)" && if [ -n "$d" ]; then echo "goimports generated output:" ; echo "$d"; exit 1; fi 35 | staticcheck: 36 | name: staticheck 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-go@v2 41 | with: 42 | go-version: 1.15 43 | - run: go install honnef.co/go/tools/cmd/staticcheck 44 | - run: $HOME/go/bin/staticcheck ./... 45 | errcheck: 46 | name: errcheck 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | - uses: actions/setup-go@v2 51 | with: 52 | go-version: 1.15 53 | - run: go get -v github.com/kisielk/errcheck 54 | - run: set +e ; d="$($HOME/go/bin/errcheck -ignoretests -asserts -ignoregenerated ./... | grep -v internal)"; if [ -n "$d" ]; then echo "errcheck output:" ; echo "$d"; exit 1; else exit 0 ; fi ; set -e 55 | sec: 56 | name: sec 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v2 60 | - uses: actions/setup-go@v2 61 | with: 62 | go-version: 1.15 63 | - run: go get -v github.com/securego/gosec/cmd/gosec 64 | - run: GO111MODULE=on $HOME/go/bin/gosec -exclude=G101,G104,G204,G304 ./... 65 | lint: 66 | name: lint 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: actions/checkout@v2 70 | - uses: actions/setup-go@v2 71 | with: 72 | go-version: 1.15 73 | - run: go get -v golang.org/x/lint/golint 74 | - run: set +e ; d="$($HOME/go/bin/golint -min_confidence 1 ./... | grep -v comment)" ; if [ -z "$d" ]; then exit 0 ; else echo "golint check output:" ; echo "$d" ; exit 1 ; fi ; set -e 75 | exportloopref: 76 | name: exportloopref 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v2 80 | - uses: actions/setup-go@v2 81 | with: 82 | go-version: 1.15 83 | - run: go get -v github.com/kyoh86/exportloopref/cmd/exportloopref 84 | - run: $HOME/go/bin/exportloopref ./... 85 | exhaustive: 86 | name: exhaustive 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v2 90 | - uses: actions/setup-go@v2 91 | with: 92 | go-version: 1.15 93 | - run: go get -v github.com/nishanths/exhaustive/... 94 | - run: $HOME/go/bin/exhaustive -default-signifies-exhaustive ./... 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db/ 2 | .env 3 | external-initiator 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as build-env 2 | 3 | RUN apk add build-base linux-headers 4 | RUN apk --update add ca-certificates 5 | 6 | RUN mkdir /external-initiator 7 | WORKDIR /external-initiator 8 | COPY go.mod go.sum ./ 9 | RUN go mod download 10 | COPY . . 11 | # Delete ./integration folder that is not needed in the context of external-initiator, 12 | # but is required in the context of mock-client build. 13 | RUN rm -rf ./integration 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o /go/bin/external-initiator 15 | 16 | FROM scratch 17 | 18 | COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 19 | COPY --from=build-env /go/bin/external-initiator /go/bin/external-initiator 20 | 21 | EXPOSE 8080 22 | 23 | ENTRYPOINT ["/go/bin/external-initiator"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2020 SmartContract Chainlink Limited SEZC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /blockchain/agoric.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/smartcontractkit/chainlink/core/logger" 9 | "github.com/smartcontractkit/external-initiator/store" 10 | "github.com/smartcontractkit/external-initiator/subscriber" 11 | ) 12 | 13 | // Agoric is the identifier of this 14 | // blockchain integration. 15 | const Agoric = "agoric" 16 | 17 | type agoricFilter struct { 18 | JobID string 19 | } 20 | 21 | type agoricManager struct { 22 | endpointName string 23 | filter agoricFilter 24 | } 25 | 26 | func createAgoricManager(t subscriber.Type, conf store.Subscription) (*agoricManager, error) { 27 | if t != subscriber.WS { 28 | return nil, errors.New("only WS connections are allowed for Agoric") 29 | } 30 | 31 | return &agoricManager{ 32 | endpointName: conf.EndpointName, 33 | filter: agoricFilter{ 34 | JobID: conf.Job, 35 | }, 36 | }, nil 37 | } 38 | 39 | func (sm agoricManager) GetTriggerJson() []byte { 40 | return nil 41 | } 42 | 43 | type agoricEvent struct { 44 | Type string `json:"type"` 45 | Data json.RawMessage `json:"data"` 46 | } 47 | 48 | type agoricOnQueryData struct { 49 | QueryID string `json:"queryId"` 50 | Query json.RawMessage `json:"query"` 51 | Fee string `json:"fee"` 52 | } 53 | 54 | type chainlinkQuery struct { 55 | JobID string `json:"jobId"` 56 | Params map[string]interface{} `json:"params"` 57 | } 58 | 59 | func (sm *agoricManager) ParseResponse(data []byte) ([]subscriber.Event, bool) { 60 | promLastSourcePing.With(prometheus.Labels{"endpoint": sm.endpointName, "jobid": string(sm.filter.JobID)}).SetToCurrentTime() 61 | 62 | var agEvent agoricEvent 63 | err := json.Unmarshal(data, &agEvent) 64 | if err != nil { 65 | logger.Error("Failed parsing agoricEvent:", err) 66 | return nil, false 67 | } 68 | 69 | var subEvents []subscriber.Event 70 | 71 | switch agEvent.Type { 72 | case "oracleServer/onQuery": 73 | // Do this below. 74 | break 75 | case "oracleServer/onError": 76 | case "oracleServer/onReply": 77 | return nil, false 78 | default: 79 | // We don't need something so noisy. 80 | // logger.Error("Unimplemented message type:", agEvent.Type) 81 | return nil, false 82 | } 83 | 84 | var onQueryData agoricOnQueryData 85 | err = json.Unmarshal(agEvent.Data, &onQueryData) 86 | if err != nil { 87 | logger.Error("Failed parsing queryData:", err) 88 | return nil, false 89 | } 90 | 91 | var query chainlinkQuery 92 | err = json.Unmarshal(onQueryData.Query, &query) 93 | if err != nil { 94 | logger.Error("Failed parsing chainlink query:", err) 95 | return nil, false 96 | } 97 | 98 | // Check that the job ID matches. 99 | if query.JobID != sm.filter.JobID { 100 | return subEvents, true 101 | } 102 | 103 | var requestParams map[string]interface{} 104 | if query.Params == nil { 105 | requestParams = make(map[string]interface{}) 106 | } else { 107 | requestParams = query.Params 108 | } 109 | requestParams["request_id"] = onQueryData.QueryID 110 | requestParams["payment"] = onQueryData.Fee 111 | 112 | event, err := json.Marshal(requestParams) 113 | if err != nil { 114 | logger.Error(err) 115 | return nil, false 116 | } 117 | subEvents = append(subEvents, event) 118 | 119 | return subEvents, true 120 | } 121 | 122 | func (sm *agoricManager) GetTestJson() []byte { 123 | return nil 124 | } 125 | 126 | func (sm *agoricManager) ParseTestResponse(data []byte) error { 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /blockchain/agoric_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/smartcontractkit/external-initiator/store" 9 | "github.com/smartcontractkit/external-initiator/subscriber" 10 | ) 11 | 12 | func TestCreateAgoricFilterMessage(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | args store.AgoricSubscription 16 | p subscriber.Type 17 | want []byte 18 | err error 19 | }{ 20 | { 21 | "empty", 22 | store.AgoricSubscription{}, 23 | subscriber.WS, 24 | nil, 25 | nil, 26 | }, 27 | { 28 | "address only", 29 | store.AgoricSubscription{}, 30 | subscriber.WS, 31 | nil, 32 | nil, 33 | }, 34 | { 35 | "empty RPC", 36 | store.AgoricSubscription{}, 37 | subscriber.RPC, 38 | nil, 39 | errors.New("only WS connections are allowed for Agoric"), 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | mgr, err := createAgoricManager(tt.p, store.Subscription{Agoric: tt.args}) 45 | if !reflect.DeepEqual(err, tt.err) { 46 | t.Errorf("createAgoricManager.err = %s, want %s", err, tt.err) 47 | } 48 | if err == nil { 49 | if got := mgr.GetTriggerJson(); !reflect.DeepEqual(got, tt.want) { 50 | t.Errorf("GetTriggerJson() = %s, want %s", got, tt.want) 51 | } 52 | } 53 | }) 54 | } 55 | 56 | t.Run("has invalid filter query", func(t *testing.T) { 57 | got := agoricManager{filter: agoricFilter{JobID: "1919"}}.GetTriggerJson() 58 | if got != nil { 59 | t.Errorf("GetTriggerJson() = %s, want nil", got) 60 | } 61 | }) 62 | } 63 | 64 | func TestAgoricManager_GetTestJson(t *testing.T) { 65 | type fields struct { 66 | filter agoricFilter 67 | p subscriber.Type 68 | } 69 | tests := []struct { 70 | name string 71 | fields fields 72 | want []byte 73 | }{ 74 | { 75 | "returns empty when using RPC", 76 | fields{ 77 | p: subscriber.RPC, 78 | }, 79 | nil, 80 | }, 81 | { 82 | "returns empty when using WS", 83 | fields{ 84 | p: subscriber.WS, 85 | }, 86 | nil, 87 | }, 88 | } 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | e := agoricManager{ 92 | filter: tt.fields.filter, 93 | } 94 | if got := e.GetTestJson(); !reflect.DeepEqual(got, tt.want) { 95 | t.Errorf("GetTestJson() = %v, want %v", got, tt.want) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func TestAgoricManager_ParseTestResponse(t *testing.T) { 102 | type fields struct { 103 | f agoricFilter 104 | p subscriber.Type 105 | } 106 | type args struct { 107 | data []byte 108 | } 109 | tests := []struct { 110 | name string 111 | fields fields 112 | args args 113 | wantErr bool 114 | }{ 115 | { 116 | "does nothing for WS", 117 | fields{f: agoricFilter{}, p: subscriber.WS}, 118 | args{}, 119 | false, 120 | }, 121 | { 122 | "parses RPC responses", 123 | fields{f: agoricFilter{}, p: subscriber.RPC}, 124 | args{[]byte(`{"jsonrpc":"2.0","id":1,"result":"0x1"}`)}, 125 | false, 126 | }, 127 | { 128 | "fails unmarshal payload", 129 | fields{f: agoricFilter{}, p: subscriber.RPC}, 130 | args{[]byte(`error`)}, 131 | false, 132 | }, 133 | { 134 | "fails unmarshal result", 135 | fields{f: agoricFilter{}, p: subscriber.RPC}, 136 | args{[]byte(`{"jsonrpc":"2.0","id":1,"result":["0x1"]}`)}, 137 | false, 138 | }, 139 | } 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | e := agoricManager{ 143 | filter: tt.fields.f, 144 | } 145 | if err := e.ParseTestResponse(tt.args.data); (err != nil) != tt.wantErr { 146 | t.Errorf("ParseTestResponse() error = %v, wantErr %v", err, tt.wantErr) 147 | } 148 | }) 149 | } 150 | } 151 | 152 | func TestAgoricManager_ParseResponse(t *testing.T) { 153 | type fields struct { 154 | filter agoricFilter 155 | p subscriber.Type 156 | } 157 | type args struct { 158 | data []byte 159 | } 160 | tests := []struct { 161 | name string 162 | fields fields 163 | args args 164 | want []subscriber.Event 165 | want1 bool 166 | }{ 167 | { 168 | "fails parsing invalid payload", 169 | fields{filter: agoricFilter{}, p: subscriber.WS}, 170 | args{data: []byte(`invalid`)}, 171 | nil, 172 | false, 173 | }, 174 | { 175 | "fails parsing invalid WS body", 176 | fields{filter: agoricFilter{}, p: subscriber.WS}, 177 | args{data: []byte(`{}`)}, 178 | nil, 179 | false, 180 | }, 181 | { 182 | "fails parsing invalid WS type", 183 | fields{filter: agoricFilter{}, p: subscriber.WS}, 184 | args{data: []byte(`{"type":"oracleServer/wrongType"}`)}, 185 | nil, 186 | false, 187 | }, 188 | { 189 | "successfully parses WS Oracle request", 190 | fields{filter: agoricFilter{JobID: "9999"}, p: subscriber.WS}, 191 | args{data: []byte(`{"type":"oracleServer/onQuery","data":{"query":{"jobID":"9999","params":{"path":"foo"}},"queryId":"123","fee":"191919000000000000000"}}`)}, 192 | []subscriber.Event{[]byte(`{"path":"foo","payment":"191919000000000000000","request_id":"123"}`)}, 193 | true, 194 | }, 195 | { 196 | "skips unfiltered WS Oracle request", 197 | fields{filter: agoricFilter{JobID: "Z9999"}, p: subscriber.WS}, 198 | args{data: []byte(`{"type":"oracleServer/onQuery","data":{"query":{"jobID":"9999","params":{"path":"foo"}},"queryId":"123","fee":"191919"}}`)}, 199 | nil, 200 | true, 201 | }, 202 | } 203 | for _, tt := range tests { 204 | t.Run(tt.name, func(t *testing.T) { 205 | e := agoricManager{ 206 | filter: tt.fields.filter, 207 | } 208 | got, got1 := e.ParseResponse(tt.args.data) 209 | if !reflect.DeepEqual(got, tt.want) { 210 | t.Errorf("ParseResponse() got = %s, want %s", got, tt.want) 211 | } 212 | if got1 != tt.want1 { 213 | t.Errorf("ParseResponse() got1 = %v, want %v", got1, tt.want1) 214 | } 215 | }) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /blockchain/binance-smart-chain.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "math/big" 6 | 7 | "github.com/ethereum/go-ethereum/common/hexutil" 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/smartcontractkit/chainlink/core/logger" 10 | "github.com/smartcontractkit/chainlink/core/store/models" 11 | "github.com/smartcontractkit/external-initiator/store" 12 | "github.com/smartcontractkit/external-initiator/subscriber" 13 | ) 14 | 15 | const BSC = "binance-smart-chain" 16 | 17 | // The bscManager implements the subscriber.JsonManager interface and allows 18 | // for interacting with ETH nodes over RPC or WS. 19 | type bscManager struct { 20 | ethManager 21 | } 22 | 23 | // createBscManager creates a new instance of bscManager with the provided 24 | // connection type and store.Subscription config. 25 | func createBscManager(p subscriber.Type, config store.Subscription) bscManager { 26 | return bscManager{ 27 | ethManager{ 28 | fq: createEvmFilterQuery(config.Job, config.BinanceSmartChain.Addresses), 29 | p: p, 30 | endpointName: config.EndpointName, 31 | jobid: config.Job, 32 | }, 33 | } 34 | } 35 | 36 | // GetTriggerJson generates a JSON payload to the ETH node 37 | // using the config in bscManager. 38 | // 39 | // If bscManager is using WebSocket: 40 | // Creates a new "eth_subscribe" subscription. 41 | // 42 | // If bscManager is using RPC: 43 | // Sends a "eth_getLogs" request. 44 | func (e bscManager) GetTriggerJson() []byte { 45 | return e.ethManager.GetTriggerJson() 46 | } 47 | 48 | // GetTestJson generates a JSON payload to test 49 | // the connection to the ETH node. 50 | // 51 | // If bscManager is using WebSocket: 52 | // Returns nil. 53 | // 54 | // If bscManager is using RPC: 55 | // Sends a request to get the latest block number. 56 | func (e bscManager) GetTestJson() []byte { 57 | return e.ethManager.GetTestJson() 58 | } 59 | 60 | // ParseTestResponse parses the response from the 61 | // ETH node after sending GetTestJson(), and returns 62 | // the error from parsing, if any. 63 | // 64 | // If bscManager is using WebSocket: 65 | // Returns nil. 66 | // 67 | // If bscManager is using RPC: 68 | // Attempts to parse the block number in the response. 69 | // If successful, stores the block number in bscManager. 70 | func (e bscManager) ParseTestResponse(data []byte) error { 71 | return e.ethManager.ParseTestResponse(data) 72 | } 73 | 74 | // ParseResponse parses the response from the 75 | // ETH node, and returns a slice of subscriber.Events 76 | // and if the parsing was successful. 77 | // 78 | // If bscManager is using RPC: 79 | // If there are new events, update bscManager with 80 | // the latest block number it sees. 81 | func (e bscManager) ParseResponse(data []byte) ([]subscriber.Event, bool) { 82 | promLastSourcePing.With(prometheus.Labels{"endpoint": e.endpointName, "jobid": e.jobid}).SetToCurrentTime() 83 | logger.Debugw("Parsing Binance Smart Chain response", "ExpectsMock", ExpectsMock) 84 | 85 | var msg JsonrpcMessage 86 | if err := json.Unmarshal(data, &msg); err != nil { 87 | logger.Error("failed parsing JSON-RPC message:", msg) 88 | return nil, false 89 | } 90 | 91 | var events []subscriber.Event 92 | 93 | switch e.p { 94 | case subscriber.WS: 95 | var res ethSubscribeResponse 96 | if err := json.Unmarshal(msg.Params, &res); err != nil { 97 | logger.Error("unmarshal:", err) 98 | return nil, false 99 | } 100 | 101 | var evt models.Log 102 | if err := json.Unmarshal(res.Result, &evt); err != nil { 103 | logger.Error("unmarshal:", err) 104 | return nil, false 105 | } 106 | 107 | if evt.Removed { 108 | return nil, false 109 | } 110 | 111 | request, err := logEventToOracleRequest(evt) 112 | if err != nil { 113 | logger.Error("failed to get oracle request:", err) 114 | return nil, false 115 | } 116 | 117 | event, err := json.Marshal(request) 118 | if err != nil { 119 | logger.Error("marshal:", err) 120 | return nil, false 121 | } 122 | 123 | events = append(events, event) 124 | 125 | case subscriber.RPC: 126 | var rawEvents []models.Log 127 | if err := json.Unmarshal(msg.Result, &rawEvents); err != nil { 128 | logger.Error("unmarshal:", err) 129 | return nil, false 130 | } 131 | 132 | for _, evt := range rawEvents { 133 | if evt.Removed { 134 | continue 135 | } 136 | 137 | request, err := logEventToOracleRequest(evt) 138 | if err != nil { 139 | logger.Error("failed to get oracle request:", err) 140 | return nil, false 141 | } 142 | 143 | event, err := json.Marshal(request) 144 | if err != nil { 145 | logger.Error("failed marshaling request:", err) 146 | continue 147 | } 148 | events = append(events, event) 149 | 150 | // Check if we can update the "fromBlock" in the query, 151 | // so we only get new events from blocks we haven't queried yet 152 | // Increment the block number by 1, since we want events from *after* this block number 153 | curBlkn := &big.Int{} 154 | curBlkn = curBlkn.Add(big.NewInt(int64(evt.BlockNumber)), big.NewInt(1)) 155 | 156 | fromBlkn, err := hexutil.DecodeBig(e.fq.FromBlock) 157 | if err != nil && !(e.fq.FromBlock == "latest" || e.fq.FromBlock == "") { 158 | logger.Error("Failed to get block number from event:", err) 159 | continue 160 | } 161 | 162 | // If our query "fromBlock" is "latest", or our current "fromBlock" is in the past compared to 163 | // the last event we received, we want to update the query 164 | if e.fq.FromBlock == "latest" || e.fq.FromBlock == "" || curBlkn.Cmp(fromBlkn) > 0 { 165 | e.fq.FromBlock = hexutil.EncodeBig(curBlkn) 166 | } 167 | } 168 | 169 | default: 170 | logger.Errorw(ErrSubscriberType.Error(), "type", e.p) 171 | return nil, false 172 | } 173 | 174 | return events, true 175 | } 176 | -------------------------------------------------------------------------------- /blockchain/bsn-irita_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/smartcontractkit/external-initiator/store" 10 | ) 11 | 12 | func TestCreateBSNIritaSubscriber(t *testing.T) { 13 | t.Run("creates biritaSubscriber from subscription", 14 | func(t *testing.T) { 15 | sub := store.Subscription{ 16 | Job: "test", 17 | BSNIrita: store.BSNIritaSubscription{ 18 | Addresses: []string{"test-provider-address"}, 19 | ServiceName: "oracle", 20 | }, 21 | } 22 | biritaSubscriber, err := createBSNIritaSubscriber(sub) 23 | assert.NoError(t, err) 24 | assert.Equal(t, "oracle", biritaSubscriber.ServiceName) 25 | assert.Equal(t, []string{"test-provider-address"}, biritaSubscriber.Addresses) 26 | }) 27 | } 28 | 29 | func TestBuildTriggerEvent(t *testing.T) { 30 | providerAddr := "iaa1cq3xx80jym3jshlxmwnfwu840jxta032aa4jss" 31 | requestID := "FFB2EA8819BAF485C49DEBC08A4431E4BA5707945F8B33C8E777110BE62491240000000000000000000000000000007900000000000017F70000" 32 | 33 | tests := []struct { 34 | name string 35 | args BIritaServiceRequest 36 | wantPass bool 37 | want []byte 38 | }{ 39 | { 40 | "basic service request", 41 | BIritaServiceRequest{ 42 | ID: requestID, 43 | Input: `{"body":{"id":"test"}}`, 44 | ServiceName: "oracle", 45 | Provider: providerAddr, 46 | }, 47 | true, 48 | []byte(`{"request_id":"FFB2EA8819BAF485C49DEBC08A4431E4BA5707945F8B33C8E777110BE62491240000000000000000000000000000007900000000000017F70000","request_body":{"id":"test"}}`), 49 | }, 50 | { 51 | "missing request id", 52 | BIritaServiceRequest{ 53 | Input: `{"body":{"id":"test"}}`, 54 | ServiceName: "oracle", 55 | Provider: providerAddr, 56 | }, 57 | false, 58 | nil, 59 | }, 60 | { 61 | "missing request input", 62 | BIritaServiceRequest{ 63 | ID: requestID, 64 | Input: "", 65 | ServiceName: "oracle", 66 | Provider: providerAddr, 67 | }, 68 | false, 69 | nil, 70 | }, 71 | { 72 | "service name does not match", 73 | BIritaServiceRequest{ 74 | ID: requestID, 75 | Input: `{"body":{"id":"test"}}`, 76 | ServiceName: "incorrect-service-name", 77 | Provider: providerAddr, 78 | }, 79 | false, 80 | nil, 81 | }, 82 | { 83 | "provider address does not match", 84 | BIritaServiceRequest{ 85 | ID: requestID, 86 | Input: `{"body":{"id":"test"}}`, 87 | ServiceName: "oracle", 88 | Provider: "incorrect-provider", 89 | }, 90 | false, 91 | nil, 92 | }, 93 | } 94 | 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | sub := &biritaSubscription{ 98 | addresses: map[string]bool{providerAddr: true}, 99 | serviceName: "oracle", 100 | } 101 | 102 | event, err := sub.buildTriggerEvent(tt.args) 103 | if tt.wantPass { 104 | assert.NoError(t, err, "buildTriggerEvent not passed, wantPass %v", tt.wantPass) 105 | } else { 106 | assert.Error(t, err, "buildTriggerEvent passed, wantPass %v", tt.wantPass) 107 | } 108 | 109 | if !bytes.Equal(event, tt.want) { 110 | t.Errorf("buildTriggerEvent got = %s, want %s", event, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /blockchain/common_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/smartcontractkit/external-initiator/store" 7 | "github.com/smartcontractkit/external-initiator/subscriber" 8 | ) 9 | 10 | func Test_GetConnectionType(t *testing.T) { 11 | type args struct { 12 | rawUrl string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want subscriber.Type 18 | wantErr bool 19 | }{ 20 | { 21 | "fails on invalid type", 22 | args{rawUrl: "invalid://localhost/"}, 23 | subscriber.Unknown, 24 | true, 25 | }, 26 | { 27 | "fails on invalid URL", 28 | args{"http://a b.com/"}, 29 | subscriber.Unknown, 30 | true, 31 | }, 32 | { 33 | "returns WS on ws://", 34 | args{"ws://localhost/"}, 35 | subscriber.WS, 36 | false, 37 | }, 38 | { 39 | "returns WS on secure wss://", 40 | args{"wss://localhost/"}, 41 | subscriber.WS, 42 | false, 43 | }, 44 | { 45 | "returns RPC on http://", 46 | args{"http://localhost/"}, 47 | subscriber.RPC, 48 | false, 49 | }, 50 | { 51 | "returns RPC on secure https://", 52 | args{"https://localhost/"}, 53 | subscriber.RPC, 54 | false, 55 | }, 56 | { 57 | "returns error on unknown protocol", 58 | args{"postgres://localhost/"}, 59 | subscriber.Unknown, 60 | true, 61 | }, 62 | } 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | got, err := GetConnectionType(store.Endpoint{Url: tt.args.rawUrl}) 66 | if (err != nil) != tt.wantErr { 67 | t.Errorf("getConnectionType() error = %v, wantErr %v", err, tt.wantErr) 68 | return 69 | } 70 | if got != tt.want { 71 | t.Errorf("getConnectionType() got = %v, want %v", got, tt.want) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /blockchain/evm_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/ethereum/go-ethereum/common/hexutil" 9 | ) 10 | 11 | func Test_toFilterArg(t *testing.T) { 12 | type args struct { 13 | q filterQuery 14 | } 15 | 16 | blockHash := common.HexToHash("abc") 17 | 18 | tests := []struct { 19 | name string 20 | args args 21 | wantErr bool 22 | }{ 23 | { 24 | "cannot specify both Blockhash and FromBlock", 25 | args{filterQuery{ 26 | BlockHash: &blockHash, 27 | FromBlock: hexutil.EncodeBig(big.NewInt(3234512922)), 28 | }}, 29 | true, 30 | }, 31 | { 32 | "cannot specify both Blockhash and ToBlock", 33 | args{filterQuery{ 34 | BlockHash: &blockHash, 35 | ToBlock: hexutil.EncodeBig(big.NewInt(3234512922)), 36 | }}, 37 | true, 38 | }, 39 | { 40 | "regular query passes", 41 | args{filterQuery{ 42 | Addresses: []common.Address{}, 43 | Topics: [][]common.Hash{}, 44 | }}, 45 | false, 46 | }, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | _, err := tt.args.q.toMapInterface() 51 | if (err != nil) != tt.wantErr { 52 | t.Errorf("toFilterArg() error = %v, wantErr %v", err, tt.wantErr) 53 | return 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /blockchain/ont.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "time" 7 | 8 | ontology_go_sdk "github.com/ontio/ontology-go-sdk" 9 | "github.com/ontio/ontology-go-sdk/common" 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/smartcontractkit/chainlink/core/logger" 12 | "github.com/smartcontractkit/chainlink/core/store/models" 13 | "github.com/smartcontractkit/external-initiator/store" 14 | "github.com/smartcontractkit/external-initiator/subscriber" 15 | ) 16 | 17 | const ( 18 | ONT = "ontology" 19 | scanInterval = 5 * time.Second 20 | ) 21 | 22 | func createOntSubscriber(sub store.Subscription) *ontSubscriber { 23 | sdk := ontology_go_sdk.NewOntologySdk() 24 | sdk.NewRpcClient().SetAddress(sub.Endpoint.Url) 25 | return &ontSubscriber{ 26 | Sdk: sdk, 27 | Addresses: sub.Ontology.Addresses, 28 | JobId: sub.Job, 29 | EndpointName: sub.EndpointName, 30 | } 31 | } 32 | 33 | type ontSubscriber struct { 34 | Sdk *ontology_go_sdk.OntologySdk 35 | Addresses []string 36 | JobId string 37 | EndpointName string 38 | } 39 | 40 | type ontSubscription struct { 41 | sdk *ontology_go_sdk.OntologySdk 42 | events chan<- subscriber.Event 43 | addresses map[string]bool 44 | jobId string 45 | endpointName string 46 | height uint32 47 | isDone bool 48 | } 49 | 50 | func (ot *ontSubscriber) SubscribeToEvents(channel chan<- subscriber.Event, _ store.RuntimeConfig) (subscriber.ISubscription, error) { 51 | logger.Infof("Using Ontology RPC endpoint: Listening for events on addresses: %v\n", ot.Addresses) 52 | addresses := make(map[string]bool) 53 | for _, a := range ot.Addresses { 54 | addresses[a] = true 55 | } 56 | ontSubscription := &ontSubscription{ 57 | sdk: ot.Sdk, 58 | events: channel, 59 | addresses: addresses, 60 | jobId: ot.JobId, 61 | endpointName: ot.EndpointName, 62 | } 63 | 64 | go ontSubscription.scanWithRetry() 65 | 66 | return ontSubscription, nil 67 | } 68 | 69 | func (ot *ontSubscriber) Test() error { 70 | _, err := ot.Sdk.GetCurrentBlockHeight() 71 | if err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | 77 | func (ots *ontSubscription) scanWithRetry() { 78 | for { 79 | ots.scan() 80 | if !ots.isDone { 81 | time.Sleep(scanInterval) 82 | continue 83 | } 84 | return 85 | } 86 | } 87 | 88 | func (ots *ontSubscription) scan() { 89 | currentHeight, err := ots.sdk.GetCurrentBlockHeight() 90 | if err != nil { 91 | logger.Error("ont scan, get current block height error:", err) 92 | return 93 | } 94 | promLastSourcePing.With(prometheus.Labels{"endpoint": ots.endpointName, "jobid": ots.jobId}).SetToCurrentTime() 95 | if ots.height == 0 { 96 | ots.height = currentHeight 97 | } 98 | for h := ots.height; h < currentHeight+1; h++ { 99 | err := ots.parseOntEvent(h) 100 | if err != nil { 101 | logger.Error("ont scan, parse ont event error:", err) 102 | return 103 | } 104 | } 105 | ots.height = currentHeight + 1 106 | } 107 | 108 | func (ots *ontSubscription) parseOntEvent(height uint32) error { 109 | ontEvents, err := ots.sdk.GetSmartContractEventByBlock(height) 110 | logger.Debugf("parseOntEvent, start to parse ont block %d", height) 111 | if err != nil { 112 | return fmt.Errorf("parseOntEvent, get smartcontract event by block error:%s", err) 113 | } 114 | 115 | for _, e := range ontEvents { 116 | for _, notify := range e.Notify { 117 | event, ok := ots.notifyTrigger(notify) 118 | if ok { 119 | ots.events <- event 120 | } 121 | } 122 | } 123 | return nil 124 | } 125 | 126 | func (ots *ontSubscription) Unsubscribe() { 127 | logger.Info("Unsubscribing from Ontology endpoint") 128 | ots.isDone = true 129 | } 130 | 131 | func (ots *ontSubscription) notifyTrigger(notify *common.NotifyEventInfo) ([]byte, bool) { 132 | states, ok := notify.States.([]interface{}) 133 | if !ok { 134 | return nil, false 135 | } 136 | _, ok = ots.addresses[notify.ContractAddress] 137 | if !ok { 138 | return nil, false 139 | } 140 | if len(states) < 11 { 141 | return nil, false 142 | } 143 | name := fmt.Sprint(states[0]) 144 | if name == hex.EncodeToString([]byte("oracleRequest")) { 145 | jobId := fmt.Sprint(states[1]) 146 | // Check if our jobID matches 147 | if !matchesJobID(ots.jobId, jobId) { 148 | return nil, false 149 | } 150 | logger.Debugf("parseOntEvent, found tracked job: %s", jobId) 151 | 152 | requestID := fmt.Sprint(states[3]) 153 | p := fmt.Sprint(states[4]) 154 | callbackAddress := fmt.Sprint(states[5]) 155 | function := fmt.Sprint(states[6]) 156 | expiration := fmt.Sprint(states[7]) 157 | data := fmt.Sprint(states[9]) 158 | dataBytes, err := hex.DecodeString(data) 159 | if err != nil { 160 | logger.Error("parseOntEvent, date from hex to bytes error:", err) 161 | return nil, false 162 | } 163 | js, err := models.ParseCBOR(dataBytes) 164 | if err != nil { 165 | logger.Error("parseOntEvent, date from bytes to JSON error:", err) 166 | return nil, false 167 | } 168 | js, err = js.Add("address", notify.ContractAddress) 169 | if err != nil { 170 | logger.Error("parseOntEvent, date JSON add address error:", err) 171 | return nil, false 172 | } 173 | js, err = js.Add("requestID", requestID) 174 | if err != nil { 175 | logger.Error("parseOntEvent, date JSON add requestID error:", err) 176 | return nil, false 177 | } 178 | js, err = js.Add("payment", p) 179 | if err != nil { 180 | logger.Error("parseOntEvent, date JSON add payment error:", err) 181 | return nil, false 182 | } 183 | js, err = js.Add("callbackAddress", callbackAddress) 184 | if err != nil { 185 | logger.Error("parseOntEvent, date JSON add callbackAddress error:", err) 186 | return nil, false 187 | } 188 | js, err = js.Add("callbackFunction", function) 189 | if err != nil { 190 | logger.Error("parseOntEvent, date JSON add callbackFunction error:", err) 191 | return nil, false 192 | } 193 | js, err = js.Add("expiration", expiration) 194 | if err != nil { 195 | logger.Error("parseOntEvent, date JSON add expiration error:", err) 196 | return nil, false 197 | } 198 | event, _ := js.MarshalJSON() 199 | return event, true 200 | } 201 | return nil, false 202 | } 203 | -------------------------------------------------------------------------------- /blockchain/tezos_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "github.com/smartcontractkit/external-initiator/eitest" 10 | "github.com/smartcontractkit/external-initiator/store" 11 | "github.com/smartcontractkit/external-initiator/subscriber" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | "github.com/tidwall/gjson" 15 | ) 16 | 17 | func TestCreateTezosSubscriber(t *testing.T) { 18 | t.Run("creates tezosSubscriber from subscription", 19 | func(t *testing.T) { 20 | sub := store.Subscription{ 21 | Endpoint: store.Endpoint{ 22 | Url: "http://example.com/api", 23 | }, 24 | Tezos: store.TezosSubscription{ 25 | Addresses: []string{"foobar", "baz"}, 26 | }, 27 | } 28 | tezosSubscriber := createTezosSubscriber(sub) 29 | assert.Equal(t, "http://example.com/api", tezosSubscriber.Endpoint) 30 | assert.Equal(t, []string{"foobar", "baz"}, tezosSubscriber.Addresses) 31 | }) 32 | t.Run("trims trailing slash from endpoint", 33 | func(t *testing.T) { 34 | sub := store.Subscription{ 35 | Endpoint: store.Endpoint{ 36 | Url: "https://example.com/api/", 37 | }, 38 | } 39 | tezosSubscriber := createTezosSubscriber(sub) 40 | assert.Equal(t, "https://example.com/api", tezosSubscriber.Endpoint) 41 | }) 42 | } 43 | 44 | func Test_extractBlockIDFromHeaderJSON(t *testing.T) { 45 | t.Run("extracts block ID from valid header JSON", 46 | func(t *testing.T) { 47 | json := []byte(`{"hash":"theBlockID","level":136875,"proto":1,"predecessor":"BLjyuxQa8QGEpXAJ5kdfYuqqL49jRs4bUPDq1Ye2PA27C4zdyGM","timestamp":"2019-12-16T20:55:42Z","validation_pass":4,"operations_hash":"LLoaRmpaxjeV1QsrczSVuLK5ddDfaSZ7xZt1BJMZMzPoS591TsXwu","fitness":["01","00000000000216aa"],"context":"CoUrZrMSmff6NYSSSg9xHqDvwKbCMQMmaVBQ8N7Bc1xXiu9MSh1K","protocol_data":"0000e11a790239180200002143b97eee6f034c1f06e4ddb0833799ad5820da57bfae68987c90e3bd61579e0733173a429c89b7415f11f8822ee715254e23a789c52a858ac52337252eef0f"}`) 48 | 49 | blockID, err := extractBlockIDFromHeaderJSON(json) 50 | assert.Nil(t, err) 51 | assert.Equal(t, "theBlockID", blockID) 52 | }) 53 | t.Run("returns error when header JSON is invalid", 54 | func(t *testing.T) { 55 | json := []byte(`{`) 56 | 57 | blockID, err := extractBlockIDFromHeaderJSON(json) 58 | assert.NotNil(t, err) 59 | assert.Equal(t, "", blockID) 60 | }) 61 | t.Run("returns error when header JSON is in an unexpected format", 62 | func(t *testing.T) { 63 | json := []byte(`{"foo":42}`) 64 | 65 | blockID, err := extractBlockIDFromHeaderJSON(json) 66 | assert.NotNil(t, err) 67 | assert.Equal(t, "", blockID) 68 | }) 69 | } 70 | 71 | func Test_extractEventsFromBlock(t *testing.T) { 72 | addresses := []string{"KT1Address", "KT2Address"} 73 | wd, _ := os.Getwd() 74 | ui := path.Join(wd, "testdata/tezos_test_block_operations_user_initiated.json") 75 | userInitiatedSampleFile, err := os.Open(ui) 76 | require.NoError(t, err) 77 | defer eitest.MustClose(userInitiatedSampleFile) 78 | 79 | sci := path.Join(wd, "testdata/tezos_test_block_operations_sc_initiated.json") 80 | scInitiatedSampleFile, err := os.Open(sci) 81 | require.NoError(t, err) 82 | defer eitest.MustClose(scInitiatedSampleFile) 83 | 84 | userInitiatedSampleJSON, err := ioutil.ReadAll(userInitiatedSampleFile) 85 | require.NoError(t, err) 86 | scInitiatedSampleJSON, err := ioutil.ReadAll(scInitiatedSampleFile) 87 | require.NoError(t, err) 88 | 89 | t.Run("returns error if json is invalid", 90 | func(t *testing.T) { 91 | json := []byte("{") 92 | 93 | _, err := extractEventsFromBlock(json, addresses, "test123") 94 | assert.NotNil(t, err) 95 | 96 | }) 97 | t.Run("returns error if json is in unexpected shape", 98 | func(t *testing.T) { 99 | json := []byte("[[]]") 100 | 101 | _, err := extractEventsFromBlock(json, addresses, "test123") 102 | assert.NotNil(t, err) 103 | 104 | }) 105 | t.Run("returns no events if the address doesn't match", 106 | func(t *testing.T) { 107 | json := userInitiatedSampleJSON 108 | 109 | events, err := extractEventsFromBlock(json, []string{"notAnAddress"}, "test123") 110 | assert.Nil(t, err) 111 | assert.Len(t, events, 0) 112 | }) 113 | t.Run("extracts SC-initiated calls to matching addresses", 114 | func(t *testing.T) { 115 | js := scInitiatedSampleJSON 116 | 117 | events, err := extractEventsFromBlock(js, addresses, "test123") 118 | require.NoError(t, err) 119 | require.Len(t, events, 1) 120 | assert.IsType(t, []subscriber.Event{}, events) 121 | assert.Equal(t, "XTZ", gjson.GetBytes(events[0], "from").Str) 122 | assert.Equal(t, "USD", gjson.GetBytes(events[0], "to").Str) 123 | assert.Equal(t, "9", gjson.GetBytes(events[0], "request_id").Str) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /chainlink/node.go: -------------------------------------------------------------------------------- 1 | // Package chainlink implements functions to interact 2 | // with a Chainlink node. 3 | package chainlink 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/avast/retry-go" 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/prometheus/client_golang/prometheus/promauto" 17 | "github.com/smartcontractkit/chainlink/core/logger" 18 | ) 19 | 20 | var ( 21 | promJobrunsTotalErrors = promauto.NewCounter(prometheus.CounterOpts{ 22 | Name: "ei_jobruns_total_errored_attempts", 23 | Help: "The total number of jobrun trigger attempts that errored", 24 | }) 25 | promJobrunsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ 26 | Name: "ei_jobruns_failed", 27 | Help: "The number of failed jobruns", 28 | }, []string{"jobid"}) 29 | promJobrunsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ 30 | Name: "ei_jobruns_success", 31 | Help: "The number of successful jobruns", 32 | }, []string{"jobid"}) 33 | ) 34 | 35 | const ( 36 | externalInitiatorAccessKeyHeader = "X-Chainlink-EA-AccessKey" 37 | externalInitiatorSecretHeader = "X-Chainlink-EA-Secret" 38 | ) 39 | 40 | type RetryConfig struct { 41 | Timeout time.Duration 42 | Attempts uint 43 | Delay time.Duration 44 | } 45 | 46 | // Node encapsulates all the configuration 47 | // necessary to interact with a Chainlink node. 48 | type Node struct { 49 | AccessKey string 50 | AccessSecret string 51 | Endpoint url.URL 52 | Retry RetryConfig 53 | } 54 | 55 | // TriggerJob wil send a job run trigger for the 56 | // provided jobId. 57 | func (cl Node) TriggerJob(jobId string, data []byte) error { 58 | logger.Infof("Sending a job run trigger to %s for job %s\n", cl.Endpoint.String(), jobId) 59 | 60 | err := cl.sendJobrunTrigger(jobId, data) 61 | if err != nil { 62 | promJobrunsFailed.With(prometheus.Labels{"jobid": jobId}).Inc() 63 | return err 64 | } 65 | 66 | promJobrunsSuccess.With(prometheus.Labels{"jobid": jobId}).Inc() 67 | return nil 68 | } 69 | 70 | func (cl Node) sendJobrunTrigger(jobId string, data []byte) error { 71 | u := cl.Endpoint 72 | u.Path = fmt.Sprintf("/v2/specs/%s/runs", jobId) 73 | 74 | request, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(data)) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | request.Header.Set("Content-Type", "application/json") 80 | request.Header.Add(externalInitiatorAccessKeyHeader, cl.AccessKey) 81 | request.Header.Add(externalInitiatorSecretHeader, cl.AccessSecret) 82 | 83 | _, statusCode, err := cl.Retry.withRetry(&http.Client{}, request) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | if statusCode >= 400 { 89 | return fmt.Errorf("received faulty status code: %v", statusCode) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (config RetryConfig) withRetry(client *http.Client, request *http.Request) (responseBody []byte, statusCode int, err error) { 96 | err = retry.Do( 97 | func() error { 98 | ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) 99 | defer cancel() 100 | requestWithTimeout := request.Clone(ctx) 101 | 102 | start := time.Now() 103 | 104 | r, e := client.Do(requestWithTimeout) 105 | if e != nil { 106 | logger.Errorf("job run trigger error making request: %v", e.Error()) 107 | return e 108 | } 109 | defer logger.ErrorIfCalling(r.Body.Close) 110 | statusCode = r.StatusCode 111 | elapsed := time.Since(start) 112 | logger.Debugw(fmt.Sprintf("job run trigger got %v in %s", statusCode, elapsed), "statusCode", statusCode, "timeElapsedSeconds", elapsed) 113 | 114 | bz, e := ioutil.ReadAll(r.Body) 115 | if e != nil { 116 | logger.Errorf("job run trigger error reading body: %v", err.Error()) 117 | return e 118 | } 119 | elapsed = time.Since(start) 120 | logger.Debugw(fmt.Sprintf("job run trigger finished after %s", elapsed), "statusCode", statusCode, "timeElapsedSeconds", elapsed) 121 | 122 | responseBody = bz 123 | 124 | // Retry on 5xx since this might give a different result 125 | if 500 <= r.StatusCode && r.StatusCode < 600 { 126 | e = fmt.Errorf("remote server error: %v\nResponse body: %v", r.StatusCode, string(responseBody)) 127 | logger.Error(e) 128 | return e 129 | } 130 | 131 | return nil 132 | }, 133 | retry.Delay(config.Delay), 134 | retry.Attempts(config.Attempts), 135 | retry.OnRetry(func(n uint, err error) { 136 | promJobrunsTotalErrors.Inc() 137 | logger.Debugw("job run trigger error, will retry", "error", err.Error(), "attempt", n, "timeout", config.Timeout) 138 | }), 139 | ) 140 | if err != nil { 141 | promJobrunsTotalErrors.Inc() 142 | } 143 | 144 | return responseBody, statusCode, err 145 | } 146 | -------------------------------------------------------------------------------- /chainlink/node_test.go: -------------------------------------------------------------------------------- 1 | package chainlink 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "os" 11 | "reflect" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | var ( 17 | clMockUrl = "" 18 | accessKey = "abc" 19 | accessSecret = "def" 20 | jobId = "123" 21 | jobIdWPayload = "123payload" 22 | testPayload = []byte(`{"somekey":"somevalue"}`) 23 | ) 24 | 25 | func TestMain(m *testing.M) { 26 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | if r.Method != http.MethodPost { 28 | w.WriteHeader(http.StatusMethodNotAllowed) 29 | return 30 | } 31 | 32 | if r.Header.Get("Content-Type") != "application/json" { 33 | w.WriteHeader(http.StatusBadRequest) 34 | return 35 | } 36 | 37 | if r.Header.Get(externalInitiatorAccessKeyHeader) != accessKey { 38 | w.WriteHeader(http.StatusForbidden) 39 | return 40 | } 41 | 42 | if r.Header.Get(externalInitiatorSecretHeader) != accessSecret { 43 | w.WriteHeader(http.StatusForbidden) 44 | return 45 | } 46 | 47 | if r.URL.Path == fmt.Sprintf("/v2/specs/%s/runs", jobIdWPayload) { 48 | body, err := ioutil.ReadAll(r.Body) 49 | if err != nil { 50 | w.WriteHeader(http.StatusInternalServerError) 51 | return 52 | } 53 | 54 | if !reflect.DeepEqual(body, testPayload) { 55 | w.WriteHeader(http.StatusBadRequest) 56 | return 57 | } 58 | } else if r.URL.Path != fmt.Sprintf("/v2/specs/%s/runs", jobId) { 59 | w.WriteHeader(http.StatusNotFound) 60 | return 61 | } 62 | 63 | fmt.Println("created...") 64 | w.WriteHeader(http.StatusCreated) 65 | })) 66 | defer ts.Close() 67 | 68 | clMockUrl = ts.URL 69 | 70 | code := m.Run() 71 | os.Exit(code) 72 | } 73 | 74 | func TestNode_TriggerJob(t *testing.T) { 75 | type fields struct { 76 | AccessKey string 77 | AccessSecret string 78 | Endpoint url.URL 79 | } 80 | type args struct { 81 | jobId string 82 | payload []byte 83 | } 84 | 85 | u, err := url.Parse(clMockUrl) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | fakeU, err := url.Parse("http://fakeurl:6688/") 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | tests := []struct { 96 | name string 97 | fields fields 98 | args args 99 | wantErr bool 100 | }{ 101 | { 102 | "is missing credentials", 103 | fields{ 104 | Endpoint: *u, 105 | }, 106 | args{jobId: jobId}, 107 | true, 108 | }, 109 | { 110 | "is missing access key", 111 | fields{ 112 | AccessKey: "", 113 | AccessSecret: accessSecret, 114 | Endpoint: *u, 115 | }, 116 | args{jobId: jobId}, 117 | true, 118 | }, 119 | { 120 | "is missing access secret", 121 | fields{ 122 | AccessKey: accessKey, 123 | AccessSecret: "", 124 | Endpoint: *u, 125 | }, 126 | args{jobId: jobId}, 127 | true, 128 | }, 129 | { 130 | "is missing job id", 131 | fields{ 132 | AccessKey: accessKey, 133 | AccessSecret: accessSecret, 134 | Endpoint: *u, 135 | }, 136 | args{jobId: ""}, 137 | true, 138 | }, 139 | { 140 | "does a successful POST request", 141 | fields{ 142 | AccessKey: accessKey, 143 | AccessSecret: accessSecret, 144 | Endpoint: *u, 145 | }, 146 | args{jobId: jobId}, 147 | false, 148 | }, 149 | { 150 | "cannot reach endpoint", 151 | fields{Endpoint: *fakeU}, 152 | args{jobId: jobId}, 153 | true, 154 | }, 155 | { 156 | "does a successful POST request with payload", 157 | fields{ 158 | AccessKey: accessKey, 159 | AccessSecret: accessSecret, 160 | Endpoint: *u, 161 | }, 162 | args{jobId: jobIdWPayload, payload: testPayload}, 163 | false, 164 | }, 165 | { 166 | "does a POST request with invalid payload", 167 | fields{ 168 | AccessKey: accessKey, 169 | AccessSecret: accessSecret, 170 | Endpoint: *u, 171 | }, 172 | args{jobId: jobIdWPayload, payload: []byte(`weird payload`)}, 173 | true, 174 | }, 175 | } 176 | for _, tt := range tests { 177 | t.Run(tt.name, func(t *testing.T) { 178 | cl := Node{ 179 | AccessKey: tt.fields.AccessKey, 180 | AccessSecret: tt.fields.AccessSecret, 181 | Endpoint: tt.fields.Endpoint, 182 | Retry: RetryConfig{ 183 | Timeout: 2 * time.Second, 184 | Attempts: 3, 185 | Delay: 100 * time.Millisecond, 186 | }, 187 | } 188 | if err := cl.TriggerJob(tt.args.jobId, tt.args.payload); (err != nil) != tt.wantErr { 189 | t.Errorf("TriggerJob() error = %v, wantErr %v", err, tt.wantErr) 190 | } 191 | }) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Package client provides the core functionality 2 | // to Run an External Initiator. 3 | package client 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/smartcontractkit/chainlink/core/logger" 13 | "github.com/smartcontractkit/external-initiator/store" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | // Run enters into the cobra command to start the external initiator. 19 | func Run() { 20 | if err := generateCmd().Execute(); err != nil { 21 | logger.Error(err) 22 | } 23 | } 24 | 25 | func generateCmd() *cobra.Command { 26 | v := viper.New() 27 | newcmd := &cobra.Command{ 28 | Use: "external-initiator [endpoint configs]", 29 | Args: cobra.MinimumNArgs(0), 30 | Long: "Monitors external blockchains and relays events to Chainlink node. Supplying endpoint configs as args will delete all other stored configs. ENV variables can be set by prefixing flag with EI_: EI_ACCESSKEY", 31 | Run: func(_ *cobra.Command, args []string) { runCallback(v, args, startService) }, 32 | } 33 | 34 | newcmd.Flags().Int("port", 8080, "The port for the EI API to listen on") 35 | must(v.BindPFlag("port", newcmd.Flags().Lookup("port"))) 36 | 37 | newcmd.Flags().String("databaseurl", "postgresql://postgres:password@localhost:5432/ei?sslmode=disable", "DatabaseURL configures the URL for external initiator to connect to. This must be a properly formatted URL, with a valid scheme (postgres://).") 38 | must(v.BindPFlag("databaseurl", newcmd.Flags().Lookup("databaseurl"))) 39 | 40 | newcmd.Flags().String("chainlinkurl", "localhost:6688", "The URL of the Chainlink Core Service") 41 | must(v.BindPFlag("chainlinkurl", newcmd.Flags().Lookup("chainlinkurl"))) 42 | 43 | newcmd.Flags().String("ic_accesskey", "", "The Chainlink access key, used for traffic flowing from this Service to Chainlink") 44 | must(v.BindPFlag("ic_accesskey", newcmd.Flags().Lookup("ic_accesskey"))) 45 | 46 | newcmd.Flags().String("ic_secret", "", "The Chainlink secret, used for traffic flowing from this Service to Chainlink") 47 | must(v.BindPFlag("ic_secret", newcmd.Flags().Lookup("ic_secret"))) 48 | 49 | newcmd.Flags().String("ci_accesskey", "", "The External Initiator access key, used for traffic flowing from Chainlink to this Service") 50 | must(v.BindPFlag("ci_accesskey", newcmd.Flags().Lookup("ci_accesskey"))) 51 | 52 | newcmd.Flags().String("ci_secret", "", "The External Initiator secret, used for traffic flowing from Chainlink to this Service") 53 | must(v.BindPFlag("ci_secret", newcmd.Flags().Lookup("ci_secret"))) 54 | 55 | newcmd.Flags().Bool("mock", false, "Set to true if the External Initiator should expect mock events from the blockchains") 56 | must(v.BindPFlag("mock", newcmd.Flags().Lookup("mock"))) 57 | 58 | newcmd.Flags().Duration("cl_timeout", 5*time.Second, "The timeout for job run triggers to the Chainlink node") 59 | must(v.BindPFlag("cl_timeout", newcmd.Flags().Lookup("cl_timeout"))) 60 | 61 | newcmd.Flags().Uint("cl_retry_attempts", 3, "The maximum number of attempts that will be made for job run triggers") 62 | must(v.BindPFlag("cl_retry_attempts", newcmd.Flags().Lookup("cl_retry_attempts"))) 63 | 64 | newcmd.Flags().Duration("cl_retry_delay", 1*time.Second, "The delay between attempts for job run triggers") 65 | must(v.BindPFlag("cl_retry_delay", newcmd.Flags().Lookup("cl_retry_delay"))) 66 | 67 | newcmd.Flags().Int64("keeper_block_cooldown", 3, "Number of blocks to cool down before triggering a new run for a Keeper job") 68 | must(v.BindPFlag("keeper_block_cooldown", newcmd.Flags().Lookup("keeper_block_cooldown"))) 69 | 70 | v.SetEnvPrefix("EI") 71 | v.AutomaticEnv() 72 | 73 | return newcmd 74 | } 75 | 76 | var requiredConfig = []string{ 77 | "port", 78 | "chainlinkurl", 79 | "ic_accesskey", 80 | "ic_secret", 81 | "databaseurl", 82 | "ci_accesskey", 83 | "ci_secret", 84 | "cl_timeout", 85 | "cl_retry_attempts", 86 | "cl_retry_delay", 87 | } 88 | 89 | // runner type matches the function signature of synchronizeForever 90 | type runner = func(Config, *store.Client, []string) 91 | 92 | func runCallback(v *viper.Viper, args []string, runner runner) { 93 | err := validateParams(v, args, requiredConfig) 94 | if err != nil { 95 | logger.Error(err) 96 | } 97 | 98 | config := newConfigFromViper(v) 99 | 100 | db, err := store.ConnectToDb(config.DatabaseURL) 101 | if err != nil { 102 | logger.Error(err) 103 | return 104 | } 105 | defer logger.ErrorIfCalling(db.Close) 106 | 107 | runner(config, db, args) 108 | } 109 | 110 | func validateParams(v *viper.Viper, args []string, required []string) error { 111 | var missing []string 112 | for _, k := range required { 113 | if v.GetString(k) == "" { 114 | msg := fmt.Sprintf("%s flag or EI_%s env must be set", k, strings.ToUpper(k)) 115 | logger.Error(msg) 116 | missing = append(missing, msg) 117 | } 118 | } 119 | if len(missing) > 0 { 120 | return errors.New(strings.Join(missing, ",")) 121 | } 122 | 123 | for _, a := range args { 124 | var config store.Endpoint 125 | err := json.Unmarshal([]byte(a), &config) 126 | if err != nil { 127 | msg := fmt.Sprintf("Invalid endpoint configuration provided: %v", a) 128 | logger.Error(msg) 129 | return errors.Wrap(err, msg) 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func must(err error) { 137 | if err != nil { 138 | panic(err) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/spf13/viper" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_validateParams(t *testing.T) { 11 | t.Run("fails on missing required fields", func(t *testing.T) { 12 | v := viper.New() 13 | v.Set("required", "") 14 | err := validateParams(v, nil, []string{"required", "required2"}) 15 | assert.Error(t, err) 16 | }) 17 | 18 | t.Run("success with required fields", func(t *testing.T) { 19 | v := viper.New() 20 | v.Set("required", "value") 21 | v.Set("required2", "value") 22 | err := validateParams(v, nil, []string{"required", "required2"}) 23 | assert.NoError(t, err) 24 | }) 25 | 26 | t.Run("fails with invalid endpoint config", func(t *testing.T) { 27 | args := []string{ 28 | `{"url":"http://localhost","name":"test"}`, 29 | `{invalid}`, 30 | } 31 | err := validateParams(viper.New(), args, nil) 32 | assert.Error(t, err) 33 | }) 34 | 35 | t.Run("succeeds with valid endpoint config", func(t *testing.T) { 36 | args := []string{ 37 | `{"url":"http://localhost","name":"test"}`, 38 | `{"url":"http://localhost","name":"valid"}`, 39 | } 40 | err := validateParams(viper.New(), args, nil) 41 | assert.NoError(t, err) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // Config contains the startup configuration parameters. 10 | type Config struct { 11 | // The port for the EI API to listen on 12 | Port int 13 | // The URL of the ChainlinkURL Core Service 14 | ChainlinkURL string 15 | // InitiatorToChainlinkAccessKey is the access key to identity the node to ChainlinkURL 16 | InitiatorToChainlinkAccessKey string 17 | // InitiatorToChainlinkSecret is the secret to authenticate the node to ChainlinkURL 18 | InitiatorToChainlinkSecret string 19 | // DatabaseURL Configures the URL for chainlink to connect to. This must be 20 | // a properly formatted URL, with a valid scheme (postgres://). 21 | DatabaseURL string 22 | // The External Initiator access key, used for traffic flowing from Chainlink to this Service 23 | ChainlinkToInitiatorAccessKey string 24 | // The External Initiator secret, used for traffic flowing from Chainlink to this Service 25 | ChainlinkToInitiatorSecret string 26 | // ExpectsMock is true if the External Initiator should expect mock events from the blockchains 27 | ExpectsMock bool 28 | // ChainlinkTimeout sets the timeout for job run triggers to the Chainlink node 29 | ChainlinkTimeout time.Duration 30 | // ChainlinkRetryAttempts sets the maximum number of attempts that will be made for job run triggers 31 | ChainlinkRetryAttempts uint 32 | // ChainlinkRetryDelay sets the delay between attempts for job run triggers 33 | ChainlinkRetryDelay time.Duration 34 | // KeeperBlockCooldown sets a number of blocks to cool down before triggering a new run for a job. 35 | KeeperBlockCooldown int64 36 | } 37 | 38 | // newConfigFromViper returns a Config based on the values supplied by viper. 39 | func newConfigFromViper(v *viper.Viper) Config { 40 | return Config{ 41 | Port: v.GetInt("port"), 42 | ChainlinkURL: v.GetString("chainlinkurl"), 43 | InitiatorToChainlinkAccessKey: v.GetString("ic_accesskey"), 44 | InitiatorToChainlinkSecret: v.GetString("ic_secret"), 45 | DatabaseURL: v.GetString("databaseurl"), 46 | ChainlinkToInitiatorAccessKey: v.GetString("ci_accesskey"), 47 | ChainlinkToInitiatorSecret: v.GetString("ci_secret"), 48 | ExpectsMock: v.GetBool("mock"), 49 | ChainlinkTimeout: v.GetDuration("cl_timeout"), 50 | ChainlinkRetryAttempts: v.GetUint("cl_retry_attempts"), 51 | ChainlinkRetryDelay: v.GetDuration("cl_retry_delay"), 52 | KeeperBlockCooldown: v.GetInt64("keeper_block_cooldown"), 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/config_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/magiconair/properties/assert" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func Test_newConfigFromViper(t *testing.T) { 11 | t.Run("binds config variables", func(t *testing.T) { 12 | names := []string{"chainlinkurl", "ic_accesskey", "ic_secret", "databaseurl", "ci_accesskey", "ci_secret"} 13 | v := viper.New() 14 | for _, val := range names { 15 | v.Set(val, val) 16 | } 17 | 18 | conf := newConfigFromViper(v) 19 | assert.Equal(t, conf.ChainlinkURL, "chainlinkurl") 20 | assert.Equal(t, conf.InitiatorToChainlinkAccessKey, "ic_accesskey") 21 | assert.Equal(t, conf.InitiatorToChainlinkSecret, "ic_secret") 22 | assert.Equal(t, conf.DatabaseURL, "databaseurl") 23 | assert.Equal(t, conf.ChainlinkToInitiatorAccessKey, "ci_accesskey") 24 | assert.Equal(t, conf.ChainlinkToInitiatorSecret, "ci_secret") 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /eitest/eitest.go: -------------------------------------------------------------------------------- 1 | package eitest 2 | 3 | func Must(err error) { 4 | if err != nil { 5 | panic(err) 6 | } 7 | } 8 | 9 | type closeable interface { 10 | Close() error 11 | } 12 | 13 | func MustClose(toClose closeable) { 14 | Must(toClose.Close()) 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smartcontractkit/external-initiator 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/Conflux-Chain/go-conflux-sdk v1.0.1 7 | github.com/Depado/ginprom v1.2.1-0.20200115153638-53bbba851bd8 8 | github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect 9 | github.com/avast/retry-go v2.6.0+incompatible 10 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 11 | github.com/centrifuge/go-substrate-rpc-client v2.0.0+incompatible 12 | github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e // indirect 13 | github.com/ethereum/go-ethereum v1.9.25 14 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a 15 | github.com/gin-gonic/gin v1.6.0 16 | github.com/golang/mock v1.4.4 17 | github.com/google/uuid v1.1.2 18 | github.com/gorilla/websocket v1.4.2 19 | github.com/iotexproject/iotex-proto v0.4.3 20 | github.com/jinzhu/gorm v1.9.16 21 | github.com/klaytn/klaytn v1.6.0 22 | github.com/magiconair/properties v1.8.1 23 | github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect 24 | github.com/ontio/ontology-go-sdk v1.11.1 25 | github.com/pierrec/xxHash v0.1.5 // indirect 26 | github.com/pkg/errors v0.9.1 27 | github.com/prometheus/client_golang v1.8.0 28 | github.com/smartcontractkit/chainlink v0.9.5-0.20201214122441-66aaea171293 29 | github.com/spf13/cobra v1.1.1 30 | github.com/spf13/viper v1.7.1 31 | github.com/stretchr/objx v0.2.0 // indirect 32 | github.com/stretchr/testify v1.6.1 33 | github.com/tendermint/tendermint v0.34.0 34 | github.com/tidwall/gjson v1.6.3 35 | go.uber.org/zap v1.16.0 36 | google.golang.org/grpc v1.33.2 37 | gopkg.in/gormigrate.v1 v1.6.0 38 | launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /integration/.gitignore: -------------------------------------------------------------------------------- 1 | external_initiator.env 2 | tmp/ 3 | cl_login.txt 4 | -------------------------------------------------------------------------------- /integration/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM public.ecr.aws/chainlink/chainlink:0.10.13 3 | 4 | COPY ./docker-init-scripts/chainlink/import-keystore.sh ./ 5 | 6 | ENTRYPOINT ["./import-keystore.sh"] 7 | -------------------------------------------------------------------------------- /integration/chainlink.env: -------------------------------------------------------------------------------- 1 | ROOT=/chainlink 2 | LOG_LEVEL=debug 3 | ETH_CHAIN_ID=34055 4 | MIN_OUTGOING_CONFIRMATIONS=2 5 | LINK_CONTRACT_ADDRESS=0xacFbbEbe1736E2BeA98975220ac5a7fB37825Bc9 6 | CHAINLINK_TLS_PORT=0 7 | SECURE_COOKIES=false 8 | ALLOW_ORIGINS=* 9 | ACCOUNT_ADDRESS=0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f 10 | CHAINLINK_DEV=true 11 | ETH_DISABLED=true 12 | -------------------------------------------------------------------------------- /integration/common: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CUR_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 4 | LOG_PATH="$CUR_DIR/tmp/logs" 5 | mkdir -p "$LOG_PATH" 6 | 7 | waitFor() { 8 | [ -z "$2" ] && timeout=60 || timeout=$2 9 | sleepCount=0 10 | while [ "$sleepCount" -le "$timeout" ] && ! eval "$1" >/dev/null; do 11 | sleep 1 12 | sleepCount=$((sleepCount + 1)) 13 | done 14 | 15 | if [ "$sleepCount" -gt "$timeout" ]; then 16 | printf -- "\033[31mTimed out waiting for '%s' (waited %ss).\033[0m\n" "$1" "${timeout}" 17 | exit 1 18 | fi 19 | } 20 | 21 | waitForResponse() { 22 | title "Waiting for $1." 23 | waitFor "curl -s \"$1\"" 24 | title "Service on $1 is ready." 25 | } 26 | 27 | launch_chainlink() { 28 | waitForResponse "$CHAINLINK_URL" 29 | title "Chainlink is running." 30 | } 31 | 32 | login_chainlink() { 33 | docker exec integration_chainlink chainlink admin login -f /run/secrets/apicredentials 34 | } 35 | 36 | run_ei() { 37 | title "Running External Initiator..." 38 | 39 | pushd integration >/dev/null || exit 40 | 41 | if [ "$EI_CI_ACCESSKEY" != "" ]; then 42 | { 43 | echo "EI_CI_ACCESSKEY=$EI_CI_ACCESSKEY" 44 | echo "EI_CI_SECRET=$EI_CI_SECRET" 45 | echo "EI_IC_ACCESSKEY=$EI_IC_ACCESSKEY" 46 | echo "EI_IC_SECRET=$EI_IC_SECRET" 47 | } >external_initiator.env 48 | fi 49 | 50 | docker-compose up -d external-initiator 51 | 52 | waitForResponse "http://localhost:8080/health" 53 | 54 | popd >/dev/null || exit 55 | } 56 | 57 | add_ei() { 58 | title "Adding External Initiator to Chainlink node..." 59 | pushd integration/scripts >/dev/null || exit 60 | 61 | local log=$LOG_PATH/add_ei.log 62 | yarn add-ei | tee "$log" 63 | EI_CI_ACCESSKEY=$(grep <"$log" 'EI outgoing token:' | awk '{print$4}') 64 | EI_CI_SECRET=$(grep <"$log" 'EI outgoing secret:' | awk '{print$4}') 65 | EI_IC_ACCESSKEY=$(grep <"$log" 'EI incoming accesskey:' | awk '{print$4}') 66 | EI_IC_SECRET=$(grep <"$log" 'EI incoming secret:' | awk '{print$4}') 67 | 68 | export EI_CI_ACCESSKEY && export EI_CI_SECRET && export EI_IC_ACCESSKEY && export EI_IC_SECRET 69 | 70 | echo "EI has been added to Chainlink node" 71 | popd >/dev/null || exit 72 | title "Done adding EI." 73 | } 74 | 75 | run_tests() { 76 | title "Running the tests..." 77 | pushd integration/scripts >/dev/null || exit 78 | local log=$LOG_PATH/run_tests.log 79 | yarn run-test "$@" 80 | popd >/dev/null || exit 81 | title "Done running tests" 82 | } 83 | 84 | start_docker() { 85 | title "Starting Docker containers" 86 | 87 | pushd integration >/dev/null || exit 88 | docker-compose up -d chainlink postgres mock 89 | popd >/dev/null || exit 90 | 91 | export CHAINLINK_URL="http://localhost:6888/" 92 | export EXTERNAL_INITIATOR_URL="http://external-initiator:8080/" 93 | 94 | launch_chainlink 95 | 96 | title "Done starting Docker containers" 97 | } 98 | 99 | stop_docker() { 100 | title "Stopping Docker containers" 101 | 102 | pushd integration >/dev/null || exit 103 | docker-compose down 104 | popd >/dev/null || exit 105 | 106 | title "Done stopping Docker containers" 107 | } 108 | 109 | build_docker() { 110 | title "Building Docker images" 111 | 112 | pushd integration >/dev/null || exit 113 | docker-compose build 114 | popd >/dev/null || exit 115 | 116 | title "Done building Docker images" 117 | } 118 | 119 | reset() { 120 | title "Removing Docker volumes" 121 | 122 | docker volume rm integration_cl || : 123 | docker volume rm integration_pg || : 124 | 125 | rm ./integration/cl_login.txt || : 126 | 127 | title "Done removing Docker volumes" 128 | } 129 | 130 | print_logs() { 131 | for log in $(find "$LOG_PATH" -maxdepth 1 -type f -iname '*.log'); do 132 | heading "$log" 133 | cat "$log" 134 | done 135 | } 136 | 137 | exit_handler() { 138 | errno=$? 139 | # Print all the logs if the test fails 140 | if [ $errno -ne 0 ]; then 141 | title "ABORTING TEST" 142 | printf -- "Exited with code %s\n" "$errno" 143 | print_logs 144 | fi 145 | exit $errno 146 | } 147 | 148 | title() { 149 | printf -- "\033[34m%s\033[0m\n" "$1" 150 | } 151 | 152 | heading() { 153 | printf -- "\n--------------------------------------------------------------------------------\n" 154 | title "$1" 155 | printf -- "--------------------------------------------------------------------------------\n\n" 156 | } 157 | -------------------------------------------------------------------------------- /integration/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | chainlink: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | container_name: integration_chainlink 8 | restart: on-failure 9 | ports: 10 | - "6888:6688" 11 | depends_on: 12 | - postgres 13 | - mock 14 | secrets: 15 | - node_password 16 | - apicredentials 17 | - keystore 18 | env_file: 19 | - ./chainlink.env 20 | environment: 21 | - DATABASE_URL=postgresql://chainlink@postgres:5432/chainlink?sslmode=disable 22 | volumes: 23 | - "cl:/chainlink" 24 | networks: 25 | - integration 26 | postgres: 27 | image: postgres 28 | restart: on-failure 29 | environment: 30 | - POSTGRES_MULTIPLE_DATABASES=chainlink,ei 31 | - POSTGRES_HOST_AUTH_METHOD=trust 32 | volumes: 33 | - "./docker-init-scripts/postgres:/docker-entrypoint-initdb.d" 34 | - "pg:/var/lib/postgresql/data" 35 | networks: 36 | - integration 37 | mock: 38 | build: 39 | context: ../. 40 | dockerfile: ./integration/mock-client/Dockerfile 41 | restart: on-failure 42 | ports: 43 | - "8081:8080" 44 | - "8091:8090" 45 | networks: 46 | - integration 47 | external-initiator: 48 | build: 49 | context: ../. 50 | dockerfile: Dockerfile 51 | restart: on-failure 52 | ports: 53 | - "8080:8080" 54 | depends_on: 55 | - postgres 56 | env_file: 57 | - ./external_initiator.env 58 | environment: 59 | - EI_DATABASEURL=postgresql://ei@postgres:5432/ei?sslmode=disable 60 | - EI_CHAINLINKURL=http://chainlink:6688/ 61 | - EI_MOCK=true 62 | command: 63 | - '{"name":"eth-mock-http","type":"ethereum","url":"http://mock:8080/rpc/eth","refreshInterval":600}' 64 | - '{"name":"eth-mock-ws","type":"ethereum","url":"ws://mock:8080/ws/eth"}' 65 | - '{"name":"hmy-mock-http","type":"harmony","url":"http://mock:8080/rpc/hmy","refreshInterval":600}' 66 | - '{"name":"hmy-mock-ws","type":"harmony","url":"ws://mock:8080/ws/hmy"}' 67 | - '{"name":"xtz-mock-http","type":"tezos","url":"http://mock:8080/http/xtz"}' 68 | - '{"name":"ont-mock-http","type":"ontology","url":"http://mock:8080/rpc/ont"}' 69 | - '{"name":"substrate-mock-ws","type":"substrate","url":"ws://mock:8080/ws/substrate"}' 70 | - '{"name":"bsc-mock-http","type":"binance-smart-chain","url":"http://mock:8080/rpc/binance-smart-chain","refreshInterval":600}' 71 | - '{"name":"bsc-mock-ws","type":"binance-smart-chain","url":"ws://mock:8080/ws/binance-smart-chain"}' 72 | - '{"name":"near-mock-http","type":"near","url":"http://mock:8080/rpc/near","refreshInterval":600}' 73 | - '{"name":"iotx-mock-grpc","type":"iotex","url":"http://mock:8090","refreshInterval":600}' 74 | - '{"name":"cfx-mock-http","type":"conflux","url":"http://mock:8080/rpc/cfx","refreshInterval":600}' 75 | - '{"name":"cfx-mock-ws","type":"conflux","url":"ws://mock:8080/ws/cfx"}' 76 | - '{"name":"keeper-mock-http","type":"keeper","url":"http://mock:8080/rpc/keeper","refreshInterval":600}' 77 | - '{"name":"keeper-mock-ws","type":"keeper","url":"ws://mock:8080/ws/keeper"}' 78 | - '{"name":"birita-mock-http","type":"bsn-irita","url":"http://mock:8080","refreshInterval":600}' 79 | - '{"name":"klaytn-mock-http","type":"klaytn","url":"http://mock:8080/rpc/klaytn","refreshInterval":600}' 80 | - '{"name":"klaytn-mock-ws","type":"klaytn","url":"ws://mock:8080/ws/klaytn"}' 81 | networks: 82 | - integration 83 | volumes: 84 | pg: 85 | cl: 86 | secrets: 87 | node_password: 88 | file: ./secrets/password.txt 89 | apicredentials: 90 | file: ./secrets/apicredentials 91 | keystore: 92 | file: ./secrets/0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f.json 93 | networks: 94 | integration: 95 | name: integration 96 | -------------------------------------------------------------------------------- /integration/docker-init-scripts/chainlink/import-keystore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "** Importing default key 0x9ca9d2d5e04012c9ed24c0e513c9bfaa4a2dd77f" 4 | chainlink node import /run/secrets/keystore 5 | 6 | echo "** Running node" 7 | chainlink node start -d -p /run/secrets/node_password -a /run/secrets/apicredentials 8 | -------------------------------------------------------------------------------- /integration/docker-init-scripts/postgres/create-multiple-databases.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | function create_user_and_database() { 7 | local database=$1 8 | echo " Creating user and database '$database'" 9 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 10 | CREATE USER $database; 11 | CREATE DATABASE $database; 12 | GRANT ALL PRIVILEGES ON DATABASE $database TO $database; 13 | EOSQL 14 | } 15 | 16 | if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then 17 | echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" 18 | for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do 19 | create_user_and_database $db 20 | done 21 | echo "Multiple databases created" 22 | fi 23 | -------------------------------------------------------------------------------- /integration/mock-client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as build-env 2 | 3 | RUN apk add build-base linux-headers 4 | 5 | # First, we copy go mod files to cache Docker image layers and avoid running 6 | # 'go mod download' if dependecies did not change. 7 | RUN mkdir -p /external-initiator/integration/mock-client 8 | WORKDIR /external-initiator 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | 12 | # Second, we copy (including the /external-initiator as a local dependency) and compile the code 13 | WORKDIR /external-initiator 14 | COPY . . 15 | WORKDIR /external-initiator/integration/mock-client 16 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o /go/bin/mock-client 17 | 18 | FROM scratch 19 | COPY --from=build-env /go/bin/mock-client /go/bin/mock-client 20 | COPY --from=build-env /external-initiator/integration/mock-client/blockchain/static /blockchain/static 21 | 22 | EXPOSE 8080 23 | EXPOSE 8090 24 | 25 | ENTRYPOINT ["/go/bin/mock-client"] 26 | -------------------------------------------------------------------------------- /integration/mock-client/README.md: -------------------------------------------------------------------------------- 1 | # Mock Client 2 | 3 | External Initiator mock client used to mock blockchain networks in integration tests. 4 | 5 | ## Unit tests 6 | 7 | Run all unit tests: 8 | 9 | ```bash 10 | go test ./... 11 | ``` 12 | 13 | ## Linter 14 | 15 | Run `golint` on all files: 16 | 17 | ```bash 18 | golint ./... 19 | ``` 20 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/binance-smart-chain.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | func handleBscRequest(conn string, msg JsonrpcMessage) ([]JsonrpcMessage, error) { 12 | if conn == "ws" { 13 | switch msg.Method { 14 | case "eth_subscribe": 15 | return handleBscSubscribe(msg) 16 | } 17 | } else { 18 | switch msg.Method { 19 | case "eth_getLogs": 20 | return handleBscGetLogs(msg) 21 | } 22 | } 23 | 24 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 25 | } 26 | 27 | type bscSubscribeResponse struct { 28 | Subscription string `json:"subscription"` 29 | Result json.RawMessage `json:"result"` 30 | } 31 | 32 | func handleBscMapStringInterface(in map[string]json.RawMessage) (bscLogResponse, error) { 33 | topics, err := getBscTopicsFromMap(in) 34 | if err != nil { 35 | return bscLogResponse{}, err 36 | } 37 | 38 | var topicsStr []string 39 | if len(topics) > 0 { 40 | for _, t := range topics[0] { 41 | topicsStr = append(topicsStr, t.String()) 42 | } 43 | } 44 | 45 | addresses, err := getBscAddressesFromMap(in) 46 | if err != nil { 47 | return bscLogResponse{}, err 48 | } 49 | 50 | return bscLogResponse{ 51 | LogIndex: "0x0", 52 | BlockNumber: "0x2", 53 | BlockHash: "0xabc0000000000000000000000000000000000000000000000000000000000000", 54 | TransactionHash: "0xabc0000000000000000000000000000000000000000000000000000000000000", 55 | TransactionIndex: "0x0", 56 | Address: addresses[0].String(), 57 | Data: "0x0000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb354f99e2ac319d0d1ff8975c41c72bf347fb69a4874e2641bd19c32e09eb88b80000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb92cdaaf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005ef1cd6b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000005663676574783f68747470733a2f2f6d696e2d6170692e63727970746f636f6d706172652e636f6d2f646174612f70726963653f6673796d3d455448267473796d733d5553446470617468635553446574696d65731864", 58 | Topics: topicsStr, 59 | }, nil 60 | } 61 | 62 | func handleBscSubscribe(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 63 | var contents []json.RawMessage 64 | err := json.Unmarshal(msg.Params, &contents) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | if len(contents) != 2 { 70 | return nil, fmt.Errorf("possibly incorrect length of params array: %v", len(contents)) 71 | } 72 | 73 | var filter map[string]json.RawMessage 74 | err = json.Unmarshal(contents[1], &filter) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | log, err := handleBscMapStringInterface(filter) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | logBz, err := json.Marshal(log) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | subResp := bscSubscribeResponse{ 90 | Subscription: "test", 91 | Result: logBz, 92 | } 93 | 94 | subBz, err := json.Marshal(subResp) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return []JsonrpcMessage{ 100 | // Send a confirmation message first 101 | // This is currently ignored, so don't fill 102 | { 103 | Version: "2.0", 104 | ID: msg.ID, 105 | Method: "eth_subscribe", 106 | }, 107 | { 108 | Version: "2.0", 109 | ID: msg.ID, 110 | Method: "eth_subscribe", 111 | Params: subBz, 112 | }, 113 | }, nil 114 | } 115 | 116 | type bscLogResponse struct { 117 | LogIndex string `json:"logIndex"` 118 | BlockNumber string `json:"blockNumber"` 119 | BlockHash string `json:"blockHash"` 120 | TransactionHash string `json:"transactionHash"` 121 | TransactionIndex string `json:"transactionIndex"` 122 | Address string `json:"address"` 123 | Data string `json:"data"` 124 | Topics []string `json:"topics"` 125 | } 126 | 127 | func getBscTopicsFromMap(req map[string]json.RawMessage) ([][]common.Hash, error) { 128 | topicsInterface, ok := req["topics"] 129 | if !ok { 130 | return nil, errors.New("no topics included") 131 | } 132 | 133 | var topicsArr []*[]string 134 | err := json.Unmarshal(topicsInterface, &topicsArr) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | var finalTopics [][]common.Hash 140 | for _, t := range topicsArr { 141 | if t == nil { 142 | continue 143 | } 144 | 145 | topics := make([]common.Hash, len(*t)) 146 | for i, s := range *t { 147 | topics[i] = common.HexToHash(s) 148 | } 149 | 150 | finalTopics = append(finalTopics, topics) 151 | } 152 | 153 | return finalTopics, nil 154 | } 155 | 156 | func getBscAddressesFromMap(req map[string]json.RawMessage) ([]common.Address, error) { 157 | addressesInterface, ok := req["address"] 158 | if !ok { 159 | return nil, errors.New("no addresses included") 160 | } 161 | 162 | var addresses []common.Address 163 | err := json.Unmarshal(addressesInterface, &addresses) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | if len(addresses) < 1 { 169 | return nil, errors.New("no addresses provided") 170 | } 171 | 172 | return addresses, nil 173 | } 174 | 175 | func bscLogRequestToResponse(msg JsonrpcMessage) (bscLogResponse, error) { 176 | var reqs []map[string]json.RawMessage 177 | err := json.Unmarshal(msg.Params, &reqs) 178 | if err != nil { 179 | return bscLogResponse{}, err 180 | } 181 | 182 | if len(reqs) != 1 { 183 | return bscLogResponse{}, fmt.Errorf("expected exactly 1 filter in request, got %d", len(reqs)) 184 | } 185 | 186 | return handleBscMapStringInterface(reqs[0]) 187 | } 188 | 189 | func handleBscGetLogs(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 190 | log, err := bscLogRequestToResponse(msg) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | logs := []bscLogResponse{log} 196 | data, err := json.Marshal(logs) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | return []JsonrpcMessage{ 202 | { 203 | Version: "2.0", 204 | ID: msg.ID, 205 | Result: data, 206 | }, 207 | }, nil 208 | } 209 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/bsn-irita.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | abci "github.com/tendermint/tendermint/abci/types" 10 | tmjson "github.com/tendermint/tendermint/libs/json" 11 | tmrpc "github.com/tendermint/tendermint/rpc/jsonrpc/types" 12 | 13 | "github.com/smartcontractkit/chainlink/core/logger" 14 | ) 15 | 16 | func setBSNIritaRoutes(router *gin.Engine) { 17 | router.POST("/", handleBSNIritaRPC) 18 | } 19 | 20 | func handleBSNIritaRPC(c *gin.Context) { 21 | var req JsonrpcMessage 22 | if err := c.BindJSON(&req); err != nil { 23 | logger.Error(err) 24 | c.JSON(http.StatusBadRequest, nil) 25 | return 26 | } 27 | 28 | rsp, err := handleBSNIritaRequest(req) 29 | if len(rsp) == 0 || err != nil { 30 | var response JsonrpcMessage 31 | response.ID = req.ID 32 | response.Version = req.Version 33 | 34 | if err != nil { 35 | logger.Error(err) 36 | errintf := interface{}(tmrpc.RPCError{ 37 | Message: err.Error(), 38 | }) 39 | response.Error = &errintf 40 | } 41 | 42 | c.JSON(http.StatusBadRequest, response) 43 | return 44 | } 45 | 46 | c.JSON(http.StatusOK, rsp[0]) 47 | } 48 | 49 | func handleBSNIritaRequest(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 50 | switch msg.Method { 51 | case "status", "block_results": 52 | rsp, ok := GetCannedResponse("birita", msg) 53 | if !ok { 54 | return nil, fmt.Errorf("failed to handle BSN-IRITA request for method %s", msg.Method) 55 | } 56 | 57 | return rsp, nil 58 | 59 | case "abci_query": 60 | return handleQueryABCI(msg) 61 | 62 | default: 63 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 64 | } 65 | } 66 | 67 | func handleQueryABCI(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 68 | var params abci.RequestQuery 69 | err := tmjson.Unmarshal(msg.Params, ¶ms) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | if params.Path == "/custom/service/request" { 75 | return handleQueryServiceRequest(msg) 76 | } 77 | 78 | return []JsonrpcMessage{ 79 | { 80 | ID: msg.ID, 81 | Result: []byte{}, 82 | }, 83 | }, nil 84 | } 85 | 86 | func handleQueryServiceRequest(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 87 | msg.Method = "abci_query_service_request" 88 | 89 | rsp, ok := GetCannedResponse("birita", msg) 90 | if !ok { 91 | return nil, fmt.Errorf("failed to handle BSN-IRITA request for service request query") 92 | } 93 | 94 | return rsp, nil 95 | } 96 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/bsn-irita_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | tmjson "github.com/tendermint/tendermint/libs/json" 9 | tmrpc "github.com/tendermint/tendermint/rpc/core/types" 10 | 11 | "github.com/smartcontractkit/external-initiator/blockchain" 12 | ) 13 | 14 | func TestHandleQueryStatus(t *testing.T) { 15 | req := JsonrpcMessage{ 16 | Version: "2.0", 17 | ID: []byte("1"), 18 | Method: "status", 19 | } 20 | 21 | rsp, ok := GetCannedResponse("birita", req) 22 | assert.True(t, ok) 23 | 24 | var status tmrpc.ResultStatus 25 | err := tmjson.Unmarshal(rsp[0].Result, &status) 26 | assert.NoError(t, err) 27 | assert.Equal(t, int64(7753), status.SyncInfo.LatestBlockHeight) 28 | } 29 | 30 | func TestHandleQueryBlockResults(t *testing.T) { 31 | req := JsonrpcMessage{ 32 | Version: "2.0", 33 | ID: []byte("1"), 34 | Method: "block_results", 35 | } 36 | 37 | rsp, ok := GetCannedResponse("birita", req) 38 | assert.True(t, ok) 39 | 40 | var blockResult tmrpc.ResultBlockResults 41 | err := tmjson.Unmarshal(rsp[0].Result, &blockResult) 42 | assert.NoError(t, err) 43 | assert.Equal(t, int64(7753), blockResult.Height) 44 | } 45 | 46 | func TestHandleQueryServiceRequest(t *testing.T) { 47 | req := JsonrpcMessage{ 48 | Version: "2.0", 49 | ID: []byte("1"), 50 | Method: "abci_query", 51 | Params: []byte(`{"path":"/custom/service/request","data:":"01","height":"0","prove":false}`), 52 | } 53 | 54 | rsp, err := handleQueryABCI(req) 55 | assert.NoError(t, err) 56 | 57 | var abciResponse tmrpc.ResultABCIQuery 58 | err = tmjson.Unmarshal(rsp[0].Result, &abciResponse) 59 | assert.NoError(t, err) 60 | 61 | var request blockchain.BIritaServiceRequest 62 | err = tmjson.Unmarshal(abciResponse.Response.Value, &request) 63 | assert.NoError(t, err) 64 | assert.Equal(t, "oracle", request.ServiceName) 65 | assert.Equal(t, "iaa1l4vp69jt8ghxtyrh6jm8jp022km50sg35eqcae", request.Provider) 66 | } 67 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/canned-responses.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/smartcontractkit/chainlink/core/logger" 7 | "github.com/smartcontractkit/external-initiator/integration/mock-client/blockchain/static" 8 | ) 9 | 10 | type CannedResponses map[string][]JsonrpcMessage 11 | 12 | // GetCannedResponses returns the static responses from a file, if such exists. JSON-RPC ID not set! 13 | func GetCannedResponses(platform string) (CannedResponses, bool) { 14 | bz, err := static.Get(platform) 15 | if err != nil { 16 | logger.Debug(err) 17 | return nil, false 18 | } 19 | 20 | var responses CannedResponses 21 | err = json.Unmarshal(bz, &responses) 22 | if err != nil { 23 | logger.Error(err) 24 | return nil, false 25 | } 26 | 27 | return responses, true 28 | } 29 | 30 | // GetCannedResponse returns the static response from a file, if such exists for the JSON-RPC method. 31 | func GetCannedResponse(platform string, msg JsonrpcMessage) ([]JsonrpcMessage, bool) { 32 | responses, ok := GetCannedResponses(platform) 33 | if !ok { 34 | return nil, false 35 | } 36 | 37 | responseList, ok := responses[msg.Method] 38 | if !ok { 39 | return nil, false 40 | } 41 | 42 | return setJsonRpcId(msg.ID, responseList), true 43 | } 44 | 45 | func setJsonRpcId(id json.RawMessage, msgs []JsonrpcMessage) []JsonrpcMessage { 46 | for i := 0; i < len(msgs); i++ { 47 | msgs[i].ID = id 48 | } 49 | return msgs 50 | } 51 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/cfx.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | func handleCfxRequest(conn string, msg JsonrpcMessage) ([]JsonrpcMessage, error) { 12 | if conn == "ws" { 13 | switch msg.Method { 14 | case "cfx_subscribe": 15 | return handleCfxSubscribe(msg) 16 | } 17 | } else { 18 | switch msg.Method { 19 | case "cfx_getLogs": 20 | return handleCfxGetLogs(msg) 21 | } 22 | } 23 | 24 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 25 | } 26 | 27 | type cfxSubscribeResponse struct { 28 | Subscription string `json:"subscription"` 29 | Result json.RawMessage `json:"result"` 30 | } 31 | 32 | func handleCfxMapStringInterface(in map[string]json.RawMessage) (cfxLogResponse, error) { 33 | topics, err := getCfxTopicsFromMap(in) 34 | if err != nil { 35 | return cfxLogResponse{}, err 36 | } 37 | 38 | var topicsStr []string 39 | if len(topics) > 0 { 40 | for _, t := range topics[0] { 41 | topicsStr = append(topicsStr, t.String()) 42 | } 43 | } 44 | 45 | addresses, err := getCfxAddressesFromMap(in) 46 | if err != nil { 47 | return cfxLogResponse{}, err 48 | } 49 | 50 | return cfxLogResponse{ 51 | LogIndex: "0x0", 52 | EpochNumber: "0x2", 53 | BlockHash: "0xabc0000000000000000000000000000000000000000000000000000000000000", 54 | TransactionHash: "0xabc0000000000000000000000000000000000000000000000000000000000000", 55 | TransactionIndex: "0x0", 56 | Address: addresses[0], 57 | Data: "0x0000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb354f99e2ac319d0d1ff8975c41c72bf347fb69a4874e2641bd19c32e09eb88b80000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb92cdaaf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005ef1cd6b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000005663676574783f68747470733a2f2f6d696e2d6170692e63727970746f636f6d706172652e636f6d2f646174612f70726963653f6673796d3d455448267473796d733d5553446470617468635553446574696d65731864", 58 | Topics: topicsStr, 59 | }, nil 60 | } 61 | 62 | func handleCfxSubscribe(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 63 | var contents []json.RawMessage 64 | err := json.Unmarshal(msg.Params, &contents) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | if len(contents) != 2 { 70 | return nil, fmt.Errorf("possibly incorrect length of params array: %v", len(contents)) 71 | } 72 | 73 | var filter map[string]json.RawMessage 74 | err = json.Unmarshal(contents[1], &filter) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | log, err := handleCfxMapStringInterface(filter) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | logBz, err := json.Marshal(log) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | subResp := cfxSubscribeResponse{ 90 | Subscription: "test", 91 | Result: logBz, 92 | } 93 | 94 | subBz, err := json.Marshal(subResp) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return []JsonrpcMessage{ 100 | // Send a confirmation message first 101 | // This is currently ignored, so don't fill 102 | { 103 | Version: "2.0", 104 | ID: msg.ID, 105 | Method: "cfx_subscribe", 106 | }, 107 | { 108 | Version: "2.0", 109 | ID: msg.ID, 110 | Method: "cfx_subscribe", 111 | Params: subBz, 112 | }, 113 | }, nil 114 | } 115 | 116 | type cfxLogResponse struct { 117 | LogIndex string `json:"logIndex"` 118 | EpochNumber string `json:"epochNumber"` 119 | BlockHash string `json:"blockHash"` 120 | TransactionHash string `json:"transactionHash"` 121 | TransactionIndex string `json:"transactionIndex"` 122 | Address string `json:"address"` 123 | Data string `json:"data"` 124 | Topics []string `json:"topics"` 125 | } 126 | 127 | func getCfxTopicsFromMap(req map[string]json.RawMessage) ([][]common.Hash, error) { 128 | topicsInterface, ok := req["topics"] 129 | if !ok { 130 | return nil, errors.New("no topics included") 131 | } 132 | 133 | var topicsArr []*[]string 134 | err := json.Unmarshal(topicsInterface, &topicsArr) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | var finalTopics [][]common.Hash 140 | for _, t := range topicsArr { 141 | if t == nil { 142 | continue 143 | } 144 | 145 | topics := make([]common.Hash, len(*t)) 146 | for i, s := range *t { 147 | topics[i] = common.HexToHash(s) 148 | } 149 | 150 | finalTopics = append(finalTopics, topics) 151 | } 152 | 153 | return finalTopics, nil 154 | } 155 | 156 | func getCfxAddressesFromMap(req map[string]json.RawMessage) ([]string, error) { 157 | addressesInterface, ok := req["address"] 158 | if !ok { 159 | return nil, errors.New("no addresses included") 160 | } 161 | 162 | var addresses []string 163 | err := json.Unmarshal(addressesInterface, &addresses) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | if len(addresses) < 1 { 169 | return nil, errors.New("no addresses provided") 170 | } 171 | 172 | return addresses, nil 173 | } 174 | 175 | func cfxLogRequestToResponse(msg JsonrpcMessage) (cfxLogResponse, error) { 176 | var reqs []map[string]json.RawMessage 177 | err := json.Unmarshal(msg.Params, &reqs) 178 | if err != nil { 179 | return cfxLogResponse{}, err 180 | } 181 | 182 | if len(reqs) != 1 { 183 | return cfxLogResponse{}, fmt.Errorf("expected exactly 1 filter in request, got %d", len(reqs)) 184 | } 185 | 186 | return handleCfxMapStringInterface(reqs[0]) 187 | } 188 | 189 | func handleCfxGetLogs(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 190 | log, err := cfxLogRequestToResponse(msg) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | logs := []cfxLogResponse{log} 196 | data, err := json.Marshal(logs) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | return []JsonrpcMessage{ 202 | { 203 | Version: "2.0", 204 | ID: msg.ID, 205 | Result: data, 206 | }, 207 | }, nil 208 | } 209 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/common.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/smartcontractkit/external-initiator/blockchain" 8 | ) 9 | 10 | // JsonrpcMessage declares JSON-RPC message type 11 | type JsonrpcMessage = blockchain.JsonrpcMessage 12 | 13 | func HandleRequest(conn, platform string, msg JsonrpcMessage) ([]JsonrpcMessage, error) { 14 | cannedResponse, ok := GetCannedResponse(platform, msg) 15 | if ok { 16 | return cannedResponse, nil 17 | } 18 | 19 | switch platform { 20 | case "eth": 21 | return handleEthRequest(conn, msg) 22 | case "hmy": 23 | return handleHmyRequest(conn, msg) 24 | case "ont": 25 | return handleOntRequest(msg) 26 | case "binance-smart-chain": 27 | return handleBscRequest(conn, msg) 28 | case "near": 29 | return handleNEARRequest(conn, msg) 30 | case "cfx": 31 | return handleCfxRequest(conn, msg) 32 | case "keeper": 33 | return handleKeeperRequest(conn, msg) 34 | case "klaytn": 35 | return handleKlaytnRequest(conn, msg) 36 | default: 37 | return nil, fmt.Errorf("unexpected platform: %v", platform) 38 | } 39 | } 40 | 41 | func SetHttpRoutes(router *gin.Engine) { 42 | setXtzRoutes(router) 43 | setBSNIritaRoutes(router) 44 | } 45 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/eth.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | func handleEthRequest(conn string, msg JsonrpcMessage) ([]JsonrpcMessage, error) { 12 | if conn == "ws" { 13 | switch msg.Method { 14 | case "eth_subscribe": 15 | return handleEthSubscribe(msg) 16 | } 17 | } else { 18 | switch msg.Method { 19 | case "eth_getLogs": 20 | return handleEthGetLogs(msg) 21 | } 22 | } 23 | 24 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 25 | } 26 | 27 | type ethSubscribeResponse struct { 28 | Subscription string `json:"subscription"` 29 | Result json.RawMessage `json:"result"` 30 | } 31 | 32 | func handleMapStringInterface(in map[string]json.RawMessage) (ethLogResponse, error) { 33 | topics, err := getTopicsFromMap(in) 34 | if err != nil { 35 | return ethLogResponse{}, err 36 | } 37 | 38 | var topicsStr []string 39 | if len(topics) > 0 { 40 | for _, t := range topics[0] { 41 | topicsStr = append(topicsStr, t.String()) 42 | } 43 | } 44 | 45 | addresses, err := getAddressesFromMap(in) 46 | if err != nil { 47 | return ethLogResponse{}, err 48 | } 49 | 50 | return ethLogResponse{ 51 | LogIndex: "0x0", 52 | BlockNumber: "0x1", 53 | BlockHash: "0x0", 54 | TransactionHash: "0x0", 55 | TransactionIndex: "0x0", 56 | Address: addresses[0].String(), 57 | Data: "0x0", 58 | Topics: topicsStr, 59 | }, nil 60 | } 61 | 62 | func handleEthSubscribe(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 63 | var contents []json.RawMessage 64 | err := json.Unmarshal(msg.Params, &contents) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | if len(contents) != 2 { 70 | return nil, fmt.Errorf("possibly incorrect length of params array: %v", len(contents)) 71 | } 72 | 73 | var filter map[string]json.RawMessage 74 | err = json.Unmarshal(contents[1], &filter) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | log, err := handleMapStringInterface(filter) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | logBz, err := json.Marshal(log) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | subResp := ethSubscribeResponse{ 90 | Subscription: "test", 91 | Result: logBz, 92 | } 93 | 94 | subBz, err := json.Marshal(subResp) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return []JsonrpcMessage{ 100 | // Send a confirmation message first 101 | // This is currently ignored, so don't fill 102 | { 103 | Version: "2.0", 104 | ID: msg.ID, 105 | Method: "eth_subscribe", 106 | }, 107 | { 108 | Version: "2.0", 109 | ID: msg.ID, 110 | Method: "eth_subscribe", 111 | Params: subBz, 112 | }, 113 | }, nil 114 | } 115 | 116 | type ethLogResponse struct { 117 | LogIndex string `json:"logIndex"` 118 | BlockNumber string `json:"blockNumber"` 119 | BlockHash string `json:"blockHash"` 120 | TransactionHash string `json:"transactionHash"` 121 | TransactionIndex string `json:"transactionIndex"` 122 | Address string `json:"address"` 123 | Data string `json:"data"` 124 | Topics []string `json:"topics"` 125 | } 126 | 127 | func getTopicsFromMap(req map[string]json.RawMessage) ([][]common.Hash, error) { 128 | topicsInterface, ok := req["topics"] 129 | if !ok { 130 | return nil, errors.New("no topics included") 131 | } 132 | 133 | var topicsArr []*[]string 134 | err := json.Unmarshal(topicsInterface, &topicsArr) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | var finalTopics [][]common.Hash 140 | for _, t := range topicsArr { 141 | if t == nil { 142 | continue 143 | } 144 | 145 | topics := make([]common.Hash, len(*t)) 146 | for i, s := range *t { 147 | topics[i] = common.HexToHash(s) 148 | } 149 | 150 | finalTopics = append(finalTopics, topics) 151 | } 152 | 153 | return finalTopics, nil 154 | } 155 | 156 | func getAddressesFromMap(req map[string]json.RawMessage) ([]common.Address, error) { 157 | addressesInterface, ok := req["address"] 158 | if !ok { 159 | return nil, errors.New("no addresses included") 160 | } 161 | 162 | var addresses []common.Address 163 | err := json.Unmarshal(addressesInterface, &addresses) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | if len(addresses) < 1 { 169 | return nil, errors.New("no addresses provided") 170 | } 171 | 172 | return addresses, nil 173 | } 174 | 175 | func ethLogRequestToResponse(msg JsonrpcMessage) (ethLogResponse, error) { 176 | var reqs []map[string]json.RawMessage 177 | err := json.Unmarshal(msg.Params, &reqs) 178 | if err != nil { 179 | return ethLogResponse{}, err 180 | } 181 | 182 | if len(reqs) != 1 { 183 | return ethLogResponse{}, fmt.Errorf("expected exactly 1 filter in request, got %d", len(reqs)) 184 | } 185 | 186 | return handleMapStringInterface(reqs[0]) 187 | } 188 | 189 | func handleEthGetLogs(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 190 | log, err := ethLogRequestToResponse(msg) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | logs := []ethLogResponse{log} 196 | data, err := json.Marshal(logs) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | return []JsonrpcMessage{ 202 | { 203 | Version: "2.0", 204 | ID: msg.ID, 205 | Result: data, 206 | }, 207 | }, nil 208 | } 209 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/harmony.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | func handleHmyRequest(conn string, msg JsonrpcMessage) ([]JsonrpcMessage, error) { 12 | if conn == "ws" { 13 | switch msg.Method { 14 | case "hmy_subscribe": 15 | return handleHmySubscribe(msg) 16 | } 17 | } else { 18 | switch msg.Method { 19 | case "hmy_getLogs": 20 | return handleHmyGetLogs(msg) 21 | } 22 | } 23 | 24 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 25 | } 26 | 27 | type hmySubscribeResponse struct { 28 | Subscription string `json:"subscription"` 29 | Result json.RawMessage `json:"result"` 30 | } 31 | 32 | func handleHmyMapStringInterface(in map[string]json.RawMessage) (hmyLogResponse, error) { 33 | topics, err := getHmyTopicsFromMap(in) 34 | if err != nil { 35 | return hmyLogResponse{}, err 36 | } 37 | 38 | var topicsStr []string 39 | if len(topics) > 0 { 40 | for _, t := range topics[0] { 41 | topicsStr = append(topicsStr, t.String()) 42 | } 43 | } 44 | 45 | addresses, err := getHmyAddressesFromMap(in) 46 | if err != nil { 47 | return hmyLogResponse{}, err 48 | } 49 | 50 | return hmyLogResponse{ 51 | LogIndex: "0x0", 52 | BlockNumber: "0x2", 53 | BlockHash: "0xabc0000000000000000000000000000000000000000000000000000000000000", 54 | TransactionHash: "0xabc0000000000000000000000000000000000000000000000000000000000000", 55 | TransactionIndex: "0x0", 56 | Address: addresses[0].String(), 57 | Data: "0x0000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb354f99e2ac319d0d1ff8975c41c72bf347fb69a4874e2641bd19c32e09eb88b80000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb92cdaaf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005ef1cd6b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000005663676574783f68747470733a2f2f6d696e2d6170692e63727970746f636f6d706172652e636f6d2f646174612f70726963653f6673796d3d455448267473796d733d5553446470617468635553446574696d65731864", 58 | Topics: topicsStr, 59 | }, nil 60 | } 61 | 62 | func handleHmySubscribe(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 63 | var contents []json.RawMessage 64 | err := json.Unmarshal(msg.Params, &contents) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | if len(contents) != 2 { 70 | return nil, fmt.Errorf("possibly incorrect length of params array: %v", len(contents)) 71 | } 72 | 73 | var filter map[string]json.RawMessage 74 | err = json.Unmarshal(contents[1], &filter) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | log, err := handleHmyMapStringInterface(filter) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | logBz, err := json.Marshal(log) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | subResp := hmySubscribeResponse{ 90 | Subscription: "test", 91 | Result: logBz, 92 | } 93 | 94 | subBz, err := json.Marshal(subResp) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return []JsonrpcMessage{ 100 | // Send a confirmation message first 101 | // This is currently ignored, so don't fill 102 | { 103 | Version: "2.0", 104 | ID: msg.ID, 105 | Method: "hmy_subscribe", 106 | }, 107 | { 108 | Version: "2.0", 109 | ID: msg.ID, 110 | Method: "hmy_subscribe", 111 | Params: subBz, 112 | }, 113 | }, nil 114 | } 115 | 116 | type hmyLogResponse struct { 117 | LogIndex string `json:"logIndex"` 118 | BlockNumber string `json:"blockNumber"` 119 | BlockHash string `json:"blockHash"` 120 | TransactionHash string `json:"transactionHash"` 121 | TransactionIndex string `json:"transactionIndex"` 122 | Address string `json:"address"` 123 | Data string `json:"data"` 124 | Topics []string `json:"topics"` 125 | } 126 | 127 | func getHmyTopicsFromMap(req map[string]json.RawMessage) ([][]common.Hash, error) { 128 | topicsInterface, ok := req["topics"] 129 | if !ok { 130 | return nil, errors.New("no topics included") 131 | } 132 | 133 | var topicsArr []*[]string 134 | err := json.Unmarshal(topicsInterface, &topicsArr) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | var finalTopics [][]common.Hash 140 | for _, t := range topicsArr { 141 | if t == nil { 142 | continue 143 | } 144 | 145 | topics := make([]common.Hash, len(*t)) 146 | for i, s := range *t { 147 | topics[i] = common.HexToHash(s) 148 | } 149 | 150 | finalTopics = append(finalTopics, topics) 151 | } 152 | 153 | return finalTopics, nil 154 | } 155 | 156 | func getHmyAddressesFromMap(req map[string]json.RawMessage) ([]common.Address, error) { 157 | addressesInterface, ok := req["address"] 158 | if !ok { 159 | return nil, errors.New("no addresses included") 160 | } 161 | 162 | var addresses []common.Address 163 | err := json.Unmarshal(addressesInterface, &addresses) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | if len(addresses) < 1 { 169 | return nil, errors.New("no addresses provided") 170 | } 171 | 172 | return addresses, nil 173 | } 174 | 175 | func hmyLogRequestToResponse(msg JsonrpcMessage) (hmyLogResponse, error) { 176 | var reqs []map[string]json.RawMessage 177 | err := json.Unmarshal(msg.Params, &reqs) 178 | if err != nil { 179 | return hmyLogResponse{}, err 180 | } 181 | 182 | if len(reqs) != 1 { 183 | return hmyLogResponse{}, fmt.Errorf("expected exactly 1 filter in request, got %d", len(reqs)) 184 | } 185 | 186 | return handleHmyMapStringInterface(reqs[0]) 187 | } 188 | 189 | func handleHmyGetLogs(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 190 | log, err := hmyLogRequestToResponse(msg) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | logs := []hmyLogResponse{log} 196 | data, err := json.Marshal(logs) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | return []JsonrpcMessage{ 202 | { 203 | Version: "2.0", 204 | ID: msg.ID, 205 | Result: data, 206 | }, 207 | }, nil 208 | } 209 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/iotex.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "strings" 7 | 8 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 9 | "github.com/iotexproject/iotex-proto/golang/iotextypes" 10 | ) 11 | 12 | type MockIoTeXServer struct { 13 | iotexapi.UnimplementedAPIServiceServer 14 | } 15 | 16 | func (*MockIoTeXServer) GetChainMeta(context.Context, *iotexapi.GetChainMetaRequest) (*iotexapi.GetChainMetaResponse, error) { 17 | return &iotexapi.GetChainMetaResponse{ 18 | ChainMeta: &iotextypes.ChainMeta{ 19 | Height: 1000, 20 | }, 21 | }, nil 22 | } 23 | 24 | func (*MockIoTeXServer) GetLogs(_ context.Context, req *iotexapi.GetLogsRequest) (*iotexapi.GetLogsResponse, error) { 25 | addresses := req.GetFilter().GetAddress() 26 | var contract string 27 | if len(addresses) != 0 { 28 | contract = addresses[0] 29 | } 30 | 31 | var topic [][]byte 32 | topics := req.GetFilter().Topics 33 | if len(topics) != 0 { 34 | topic = topics[0].GetTopic() 35 | } 36 | 37 | data, err := hexToBytes("0x0000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb354f99e2ac319d0d1ff8975c41c72bf347fb69a4874e2641bd19c32e09eb88b80000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb92cdaaf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005ef1cd6b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000005663676574783f68747470733a2f2f6d696e2d6170692e63727970746f636f6d706172652e636f6d2f646174612f70726963653f6673796d3d455448267473796d733d5553446470617468635553446574696d65731864") 38 | if err != nil { 39 | return nil, err 40 | } 41 | return &iotexapi.GetLogsResponse{ 42 | Logs: []*iotextypes.Log{ 43 | { 44 | ContractAddress: contract, 45 | Topics: topic, 46 | Data: data, 47 | BlkHeight: req.GetByRange().GetFromBlock(), 48 | Index: uint32(0), 49 | }, 50 | }, 51 | }, nil 52 | } 53 | 54 | func hexToBytes(x string) ([]byte, error) { 55 | return hex.DecodeString(strings.TrimPrefix(x, "0x")) 56 | } 57 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/iotex_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestIoTeXMockServer(t *testing.T) { 13 | serv := &MockIoTeXServer{} 14 | t.Run("iotexGetChainMeta", func(t *testing.T) { 15 | ctx := context.Background() 16 | resp, err := serv.GetChainMeta(ctx, &iotexapi.GetChainMetaRequest{}) 17 | require.NoError(t, err) 18 | assert.Equal(t, uint64(1000), resp.GetChainMeta().GetHeight()) 19 | }) 20 | 21 | t.Run("iotexGetLogs", func(t *testing.T) { 22 | ctx := context.Background() 23 | contract := "io12345678" 24 | height := uint64(1000) 25 | req := &iotexapi.GetLogsRequest{ 26 | Filter: &iotexapi.LogsFilter{ 27 | Address: []string{contract}, 28 | }, 29 | Lookup: &iotexapi.GetLogsRequest_ByRange{ 30 | ByRange: &iotexapi.GetLogsByRange{ 31 | FromBlock: height, 32 | Count: 1, 33 | }, 34 | }, 35 | } 36 | resp, err := serv.GetLogs(ctx, req) 37 | require.NoError(t, err) 38 | assert.NotZero(t, len(resp.GetLogs())) 39 | log := resp.GetLogs()[0] 40 | assert.Equal(t, contract, log.GetContractAddress()) 41 | assert.Equal(t, height, log.GetBlkHeight()) 42 | assert.NotNil(t, log.GetData()) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/keeper.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | func handleKeeperRequest(_ string, msg JsonrpcMessage) ([]JsonrpcMessage, error) { 11 | switch msg.Method { 12 | case "eth_call": 13 | return handleEthCall(msg) 14 | } 15 | 16 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 17 | } 18 | 19 | type ethCallMessage struct { 20 | From string `json:"from,omitempty"` 21 | To string `json:"to"` 22 | Gas string `json:"gas,omitempty"` 23 | GasPrice string `json:"gasPrice,omitempty"` 24 | Value string `json:"value,omitempty"` 25 | Data string `json:"data,omitempty"` 26 | } 27 | 28 | func msgToEthCall(msg JsonrpcMessage) (*ethCallMessage, error) { 29 | var params []json.RawMessage 30 | err := json.Unmarshal(msg.Params, ¶ms) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if len(params) != 2 { 36 | return nil, errors.New("unexpected amount of params") 37 | } 38 | 39 | var ethCall ethCallMessage 40 | err = json.Unmarshal(params[0], ðCall) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return ðCall, nil 45 | } 46 | 47 | func handleEthCall(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 48 | data, err := msgToEthCall(msg) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | if !strings.HasPrefix(data.Data, "0xc41b813a") { // checkUpkeep(uint256,address) 54 | return nil, errors.New("unknown function selector") 55 | } 56 | 57 | return []JsonrpcMessage{ 58 | { 59 | Version: "2.0", 60 | ID: msg.ID, 61 | Result: []byte(`"0x00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000"`), 62 | }, 63 | }, nil 64 | } 65 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/keeper_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_handleEthCall(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | msg JsonrpcMessage 12 | want []JsonrpcMessage 13 | wantErr bool 14 | }{ 15 | { 16 | name: "standard eth_call", 17 | msg: JsonrpcMessage{ 18 | Version: "2.0", 19 | ID: []byte(`1`), 20 | Method: "eth_call", 21 | Params: []byte(`[{"data":"0xc41b813a"},"latest"]`), 22 | }, 23 | want: []JsonrpcMessage{ 24 | { 25 | Version: "2.0", 26 | ID: []byte(`1`), 27 | Result: []byte(`"0x00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000"`), 28 | }, 29 | }, 30 | wantErr: false, 31 | }, 32 | { 33 | name: "unknown function selector", 34 | msg: JsonrpcMessage{ 35 | Version: "2.0", 36 | ID: []byte(`1`), 37 | Method: "eth_call", 38 | Params: []byte(`[{"data":"0x1fbe2fb6"},"latest"]`), 39 | }, 40 | want: nil, 41 | wantErr: true, 42 | }, 43 | } 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | got, err := handleEthCall(tt.msg) 47 | if (err != nil) != tt.wantErr { 48 | t.Errorf("handleEthCall() error = %v, wantErr %v", err, tt.wantErr) 49 | return 50 | } 51 | if !reflect.DeepEqual(got, tt.want) { 52 | t.Errorf("handleEthCall() got = %v, want %v", got, tt.want) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func Test_msgToEthCall(t *testing.T) { 59 | tests := []struct { 60 | name string 61 | msg JsonrpcMessage 62 | want *ethCallMessage 63 | wantErr bool 64 | }{ 65 | { 66 | name: "valid message", 67 | msg: JsonrpcMessage{ 68 | Version: "2.0", 69 | ID: []byte(`1`), 70 | Method: "eth_call", 71 | Params: []byte(`[{"data":"0xb7d06888"},"latest"]`), 72 | }, 73 | want: ðCallMessage{Data: "0xb7d06888"}, 74 | wantErr: false, 75 | }, 76 | { 77 | name: "invalid params order", 78 | msg: JsonrpcMessage{ 79 | Version: "2.0", 80 | ID: []byte(`1`), 81 | Method: "eth_call", 82 | Params: []byte(`["latest",{"data":"0xb7d06888"}]`), 83 | }, 84 | want: nil, 85 | wantErr: true, 86 | }, 87 | { 88 | name: "invalid number of params", 89 | msg: JsonrpcMessage{ 90 | Version: "2.0", 91 | ID: []byte(`1`), 92 | Method: "eth_call", 93 | Params: []byte(`[{"data":"0xb7d06888"}]`), 94 | }, 95 | want: nil, 96 | wantErr: true, 97 | }, 98 | { 99 | name: "no params", 100 | msg: JsonrpcMessage{ 101 | Version: "2.0", 102 | ID: []byte(`1`), 103 | Method: "eth_call", 104 | }, 105 | want: nil, 106 | wantErr: true, 107 | }, 108 | } 109 | for _, tt := range tests { 110 | t.Run(tt.name, func(t *testing.T) { 111 | got, err := msgToEthCall(tt.msg) 112 | if (err != nil) != tt.wantErr { 113 | t.Errorf("msgToEthCall() error = %v, wantErr %v", err, tt.wantErr) 114 | return 115 | } 116 | if !reflect.DeepEqual(got, tt.want) { 117 | t.Errorf("msgToEthCall() got = %v, want %v", got, tt.want) 118 | } 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/klaytn.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // handleKlaytnRequest handles Klaytn request. 9 | // It is different from eth that it uses 'klay' instead of 'eth' in method. 10 | func handleKlaytnRequest(conn string, msg JsonrpcMessage) ([]JsonrpcMessage, error) { 11 | if conn == "ws" { 12 | switch msg.Method { 13 | case "klay_subscribe": 14 | return handleKlaytnSubscribe(msg) 15 | } 16 | } else { 17 | switch msg.Method { 18 | case "klay_getLogs": 19 | return handleKlaytnGetLogs(msg) 20 | } 21 | } 22 | 23 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 24 | } 25 | 26 | func handleKlaytnMapStringInterface(in map[string]json.RawMessage) (klaytnLogResponse, error) { 27 | topics, err := getTopicsFromMap(in) 28 | if err != nil { 29 | return klaytnLogResponse{}, err 30 | } 31 | 32 | var topicsStr []string 33 | if len(topics) > 0 { 34 | for _, t := range topics[0] { 35 | topicsStr = append(topicsStr, t.String()) 36 | } 37 | } 38 | 39 | addresses, err := getAddressesFromMap(in) 40 | if err != nil { 41 | return klaytnLogResponse{}, err 42 | } 43 | 44 | return klaytnLogResponse{ 45 | LogIndex: "0x0", 46 | BlockNumber: "0x2", 47 | BlockHash: "0xabc0000000000000000000000000000000000000000000000000000000000000", 48 | TransactionHash: "0xabc0000000000000000000000000000000000000000000000000000000000000", 49 | TransactionIndex: "0x0", 50 | Address: addresses[0].String(), 51 | Data: "0x0000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb354f99e2ac319d0d1ff8975c41c72bf347fb69a4874e2641bd19c32e09eb88b80000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000007d0965224facd7156df0c9a1adf3a94118026eeb92cdaaf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005ef1cd6b00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000005663676574783f68747470733a2f2f6d696e2d6170692e63727970746f636f6d706172652e636f6d2f646174612f70726963653f6673796d3d455448267473796d733d5553446470617468635553446574696d65731864", 52 | Topics: topicsStr, 53 | }, nil 54 | } 55 | 56 | func handleKlaytnSubscribe(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 57 | var contents []json.RawMessage 58 | err := json.Unmarshal(msg.Params, &contents) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if len(contents) != 2 { 64 | return nil, fmt.Errorf("possibly incorrect length of params array: %v", len(contents)) 65 | } 66 | 67 | var filter map[string]json.RawMessage 68 | err = json.Unmarshal(contents[1], &filter) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | log, err := handleKlaytnMapStringInterface(filter) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | logBz, err := json.Marshal(log) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | subResp := ethSubscribeResponse{ 84 | Subscription: "test", 85 | Result: logBz, 86 | } 87 | 88 | subBz, err := json.Marshal(subResp) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | return []JsonrpcMessage{ 94 | // Send a confirmation message first 95 | // This is currently ignored, so don't fill 96 | { 97 | Version: "2.0", 98 | ID: msg.ID, 99 | Method: "klay_subscribe", 100 | }, 101 | { 102 | Version: "2.0", 103 | ID: msg.ID, 104 | Method: "klay_subscribe", 105 | Params: subBz, 106 | }, 107 | }, nil 108 | } 109 | 110 | type klaytnLogResponse struct { 111 | LogIndex string `json:"logIndex"` 112 | BlockNumber string `json:"blockNumber"` 113 | BlockHash string `json:"blockHash"` 114 | TransactionHash string `json:"transactionHash"` 115 | TransactionIndex string `json:"transactionIndex"` 116 | Address string `json:"address"` 117 | Data string `json:"data"` 118 | Topics []string `json:"topics"` 119 | } 120 | 121 | func klaytnLogRequestToResponse(msg JsonrpcMessage) (klaytnLogResponse, error) { 122 | var reqs []map[string]json.RawMessage 123 | err := json.Unmarshal(msg.Params, &reqs) 124 | if err != nil { 125 | return klaytnLogResponse{}, err 126 | } 127 | 128 | if len(reqs) != 1 { 129 | return klaytnLogResponse{}, fmt.Errorf("expected exactly 1 filter in request, got %d", len(reqs)) 130 | } 131 | 132 | r, err := handleKlaytnMapStringInterface(reqs[0]) 133 | if err != nil { 134 | return klaytnLogResponse{}, err 135 | } 136 | return klaytnLogResponse(r), nil 137 | } 138 | 139 | func handleKlaytnGetLogs(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 140 | log, err := klaytnLogRequestToResponse(msg) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | logs := []klaytnLogResponse{log} 146 | data, err := json.Marshal(logs) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | return []JsonrpcMessage{ 152 | { 153 | Version: "2.0", 154 | ID: msg.ID, 155 | Result: data, 156 | }, 157 | }, nil 158 | } 159 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/near.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/smartcontractkit/external-initiator/blockchain" 8 | ) 9 | 10 | func handleNEARRequest(conn string, msg JsonrpcMessage) ([]JsonrpcMessage, error) { 11 | if conn == "rpc" { 12 | switch msg.Method { 13 | case "query": 14 | responses, ok := GetCannedResponses("near") 15 | if !ok { 16 | return nil, fmt.Errorf("failed to load canned responses for: %v", "near") 17 | } 18 | 19 | respID, err := buildResponseID(msg) 20 | if err != nil { 21 | return nil, err 22 | } 23 | responseList, ok := responses[respID] 24 | if !ok { 25 | errResp := responses["error_MethodNotFound"] 26 | return errResp, nil 27 | } 28 | 29 | return setJsonRpcId(msg.ID, responseList), nil 30 | } 31 | // TODO: https://www.pivotaltracker.com/story/show/173896260 32 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 33 | } 34 | 35 | return nil, fmt.Errorf("unexpected connection: %v", conn) 36 | } 37 | 38 | // buildResponseID builds a response ID for supplied JSON-RPC message, 39 | // that can be used to find disk stored canned respones. 40 | func buildResponseID(msg JsonrpcMessage) (string, error) { 41 | if msg.Method == "" { 42 | return "", fmt.Errorf("failed to build response ID (Method not available): %v", msg) 43 | } 44 | 45 | var params blockchain.NEARQueryCallFunction 46 | err := json.Unmarshal(msg.Params, ¶ms) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | if params.MethodName == "" { 52 | return msg.Method, nil 53 | } 54 | 55 | return fmt.Sprintf("%v_%v", msg.Method, params.MethodName), nil 56 | } 57 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/ont.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | func handleOntRequest(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 10 | switch msg.Method { 11 | case "getsmartcodeevent": 12 | return handleGetSmartCodeEvent(msg) 13 | } 14 | 15 | return nil, fmt.Errorf("unexpected method: %v", msg.Method) 16 | } 17 | 18 | type executeNotify struct { 19 | TxHash string 20 | State byte 21 | GasConsumed uint64 22 | Notify []notifyEventInfo 23 | } 24 | 25 | type notifyEventInfo struct { 26 | ContractAddress string 27 | States interface{} 28 | } 29 | 30 | func handleGetSmartCodeEvent(msg JsonrpcMessage) ([]JsonrpcMessage, error) { 31 | eInfos := make([]*executeNotify, 0) 32 | nEI := notifyEventInfo{ 33 | ContractAddress: "0x2aD9B7b9386c2f45223dDFc4A4d81C2957bAE19A", 34 | States: []interface{}{hex.EncodeToString([]byte("oracleRequest")), "mock", "01", "02", "03", "04", 35 | "05", "06", "07", "", "08"}, 36 | } 37 | eInfo := &executeNotify{ 38 | Notify: []notifyEventInfo{nEI}, 39 | } 40 | eInfos = append(eInfos, eInfo) 41 | 42 | data, err := json.Marshal(eInfos) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return []JsonrpcMessage{ 48 | { 49 | ID: msg.ID, 50 | Result: data, 51 | }, 52 | }, nil 53 | } 54 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/ont_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type smartContactEvent struct { 11 | TxHash string 12 | State byte 13 | GasConsumed uint64 14 | Notify []*notifyEventInfo 15 | } 16 | 17 | func TestHandleGetSmartCodeEvent(t *testing.T) { 18 | req := JsonrpcMessage{ 19 | Version: "2.0", 20 | ID: []byte("1"), 21 | Method: "getsmartcodeevent", 22 | } 23 | 24 | rsp, err := handleOntRequest(req) 25 | assert.NoError(t, err) 26 | events := make([]*smartContactEvent, 0) 27 | err = json.Unmarshal(rsp[0].Result, &events) 28 | assert.NoError(t, err) 29 | } 30 | 31 | func TestHandleGetBlockCount(t *testing.T) { 32 | req := JsonrpcMessage{ 33 | Version: "2.0", 34 | ID: []byte("1"), 35 | Method: "getblockcount", 36 | } 37 | 38 | rsp, ok := GetCannedResponse("ont", req) 39 | assert.True(t, ok) 40 | var count uint32 41 | err := json.Unmarshal(rsp[0].Result, &count) 42 | assert.NoError(t, err) 43 | assert.Equal(t, count, uint32(1)) 44 | } 45 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/binance-smart-chain.json: -------------------------------------------------------------------------------- 1 | { 2 | "eth_blockNumber": [ 3 | { 4 | "jsonrpc": "2.0", 5 | "result": "0x0" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/birita.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": [ 3 | { 4 | "jsonrpc": "2.0", 5 | "result": { 6 | "node_info": { 7 | "protocol_version": { 8 | "p2p": "8", 9 | "block": "11", 10 | "app": "0" 11 | }, 12 | "id": "b616b59797b46610ef3818085866a828dd7b57e6", 13 | "listen_addr": "tcp://0.0.0.0:26656", 14 | "network": "irita-hub", 15 | "version": "0.34.0", 16 | "channels": "40202122233038606100", 17 | "moniker": "node0", 18 | "other": { 19 | "tx_index": "on", 20 | "rpc_address": "tcp://0.0.0.0:26657" 21 | } 22 | }, 23 | "sync_info": { 24 | "latest_block_hash": "B1A30C2EA97A8467293C66991D398694AC28373BF11FAFB7092A1D1344262DE9", 25 | "latest_app_hash": "44FC1AD5B755E3DB0C0FE982732931EB36A97D33DDA8056B889C86170D354304", 26 | "latest_block_height": "7753", 27 | "latest_block_time": "2020-11-11T06:55:46.321105Z", 28 | "earliest_block_hash": "B3EAB5C6155DD92CF647883C42AEC651FC97B5262D277D7AB724D3FFEE941AF8", 29 | "earliest_app_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", 30 | "earliest_block_height": "1", 31 | "earliest_block_time": "2020-11-10T05:41:06.758698Z", 32 | "catching_up": false 33 | } 34 | } 35 | } 36 | ], 37 | "block_results": [ 38 | { 39 | "jsonrpc": "2.0", 40 | "result": { 41 | "height": "7753", 42 | "txs_results": null, 43 | "begin_block_events": null, 44 | "end_block_events": [ 45 | { 46 | "type": "new_batch_request_provider", 47 | "attributes": [ 48 | { 49 | "key": "c2VydmljZV9uYW1l", 50 | "value": "b3JhY2xl", 51 | "index": true 52 | }, 53 | { 54 | "key": "cHJvdmlkZXI=", 55 | "value": "aWFhMWw0dnA2OWp0OGdoeHR5cmg2am04anAwMjJrbTUwc2czNWVxY2Fl", 56 | "index": true 57 | }, 58 | { 59 | "key": "cmVxdWVzdHM=", 60 | "value": "WyJERjUzNEIxQTU5QjhDQTlGNDZDNTg4OTkyODAxQjBFMUQ3RkY1MUM3NTc4OEY4NkEyNjM0NzkyNzIzREFGQ0Y0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwN0UwMDAwMCJd", 61 | "index": true 62 | } 63 | ] 64 | } 65 | ], 66 | "validator_updates": null, 67 | "consensus_param_updates": { 68 | "block": { 69 | "max_bytes": "22020096", 70 | "max_gas": "-1" 71 | }, 72 | "evidence": { 73 | "max_age_num_blocks": "100000", 74 | "max_age_duration": "172800000000000", 75 | "max_bytes": "1048576" 76 | }, 77 | "validator": { 78 | "pub_key_types": [ 79 | "sm2" 80 | ] 81 | } 82 | } 83 | } 84 | } 85 | ], 86 | "abci_query_service_request": [ 87 | { 88 | "jsonrpc": "2.0", 89 | "result": { 90 | "response": { 91 | "value": "ewogICJpZCI6ICJFMzNFNjExNTM2MzA0QkQ5NkQ0MEEwQTExNDhCRkM0RDNCQzI5MzFBNTA1NTIzOUJEQzYwM0Q4Q0ZFRkQ5RDBCMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDA2M0UyMDAwMCIsCiAgInNlcnZpY2VfbmFtZSI6ICJvcmFjbGUiLAogICJwcm92aWRlciI6ICJpYWExbDR2cDY5anQ4Z2h4dHlyaDZqbThqcDAyMmttNTBzZzM1ZXFjYWUiLAogICJjb25zdW1lciI6ICJpYWExa3BwZmFyY3Z0NHE4MnYwbWQ2OHZuNjljNnJ0ZWprOXBjM3hnNnQiLAogICJpbnB1dCI6ICJ7XCJoZWFkZXJcIjp7fSxcImJvZHlcIjp7XCJwYWlyXCI6XCJ1c2R0LWV0aFwifX0iLAogICJzZXJ2aWNlX2ZlZSI6IFsKICAgIHsKICAgICAgImRlbm9tIjogInBvaW50IiwKICAgICAgImFtb3VudCI6ICIxIgogICAgfQogIF0sCiAgInJlcXVlc3RfaGVpZ2h0IjogIjI1NTcwIiwKICAiZXhwaXJhdGlvbl9oZWlnaHQiOiAiMjU2NzAiLAogICJyZXF1ZXN0X2NvbnRleHRfaWQiOiAiRTMzRTYxMTUzNjMwNEJEOTZENDBBMEExMTQ4QkZDNEQzQkMyOTMxQTUwNTUyMzlCREM2MDNEOENGRUZEOUQwQjAwMDAwMDAwMDAwMDAwMDAiLAogICJyZXF1ZXN0X2NvbnRleHRfYmF0Y2hfY291bnRlciI6ICIxIgp9", 92 | "proofOps": null, 93 | "height": "0" 94 | } 95 | } 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/cfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "cfx_epochNumber": [ 3 | { 4 | "jsonrpc": "2.0", 5 | "result": "0x0" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/eth.json: -------------------------------------------------------------------------------- 1 | { 2 | "eth_blockNumber": [ 3 | { 4 | "jsonrpc": "2.0", 5 | "result": "0x0" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/file.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "strings" 9 | 10 | "github.com/smartcontractkit/chainlink/core/logger" 11 | ) 12 | 13 | func Get(platform string) ([]byte, error) { 14 | wd, _ := os.Getwd() 15 | if !strings.HasSuffix(wd, "/blockchain") { 16 | wd += "/blockchain" 17 | } 18 | responsesPath := path.Join(wd, fmt.Sprintf("static/%s.json", platform)) 19 | responsesFile, err := os.Open(responsesPath) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer logger.ErrorIfCalling(responsesFile.Close) 24 | 25 | return ioutil.ReadAll(responsesFile) 26 | } 27 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/hmy.json: -------------------------------------------------------------------------------- 1 | { 2 | "hmy_blockNumber": [ 3 | { 4 | "jsonrpc": "2.0", 5 | "result": "0x0" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/keeper.json: -------------------------------------------------------------------------------- 1 | { 2 | "eth_blockNumber": [ 3 | { 4 | "jsonrpc": "2.0", 5 | "result": "0x4" 6 | } 7 | ], 8 | "eth_subscribe": [ 9 | { 10 | "jsonrpc": "2.0", 11 | "method": "eth_subscription", 12 | "result": "0x1" 13 | }, 14 | { 15 | "jsonrpc": "2.0", 16 | "method": "eth_subscription", 17 | "params": { 18 | "subscription": "0x1", 19 | "result": { 20 | "number": "0x4", 21 | "hash": "0xabc123" 22 | } 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/klaytn.json: -------------------------------------------------------------------------------- 1 | { 2 | "klay_blockNumber": [ 3 | { 4 | "jsonrpc": "2.0", 5 | "result": "0x0" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/static/ont.json: -------------------------------------------------------------------------------- 1 | { 2 | "getblockcount": [ 3 | { 4 | "result": 1 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/substrate_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/centrifuge/go-substrate-rpc-client/types" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/smartcontractkit/external-initiator/blockchain" 14 | ) 15 | 16 | const expectedStorageKey = "0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7" 17 | 18 | func TestSubstrateMock_state_getMetadata(t *testing.T) { 19 | metadata, err := getMetadata() 20 | require.NoError(t, err) 21 | require.NotNil(t, metadata) 22 | 23 | key, err := types.CreateStorageKey(metadata, "System", "Events", nil, nil) 24 | require.NoError(t, err) 25 | assert.Equal(t, expectedStorageKey, key.Hex()) 26 | } 27 | 28 | func getMetadata() (*types.Metadata, error) { 29 | req := JsonrpcMessage{ 30 | Version: "2.0", 31 | ID: json.RawMessage("1"), 32 | Method: "state_getMetadata", 33 | } 34 | 35 | resp, ok := GetCannedResponse("substrate", req) 36 | if !ok { 37 | return nil, errors.New("Request for canned response did not return ok") 38 | } 39 | 40 | var result string 41 | err := json.Unmarshal(resp[0].Result, &result) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | var metadata types.Metadata 47 | err = types.DecodeFromHexString(result, &metadata) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return &metadata, nil 53 | } 54 | 55 | type subscribeResponseParams struct { 56 | Subscription string `json:"subscription"` 57 | Result json.RawMessage `json:"result"` 58 | } 59 | 60 | func TestSubstrateMock_state_subscribeStorage(t *testing.T) { 61 | req := JsonrpcMessage{ 62 | Version: "2.0", 63 | ID: json.RawMessage("1"), 64 | Method: "state_subscribeStorage", 65 | } 66 | 67 | resp, ok := GetCannedResponse("substrate", req) 68 | require.True(t, ok) 69 | 70 | // assert 2+ responses (subscription confirmation is the first one) 71 | assert.GreaterOrEqual(t, len(resp), 2) 72 | 73 | // get the subscription id number from the first response (subscription confirmation) 74 | var subscriptionNum string 75 | err := json.Unmarshal(resp[0].Result, &subscriptionNum) 76 | require.NoError(t, err) 77 | 78 | // require metadata for decoding 79 | metadata, err := getMetadata() 80 | require.NoError(t, err) 81 | 82 | for i := 1; i < len(resp); i++ { 83 | testName := fmt.Sprintf("Test JSON-RPC response #%d", i) 84 | t.Run(testName, func(t *testing.T) { 85 | // assert that subscription id numbers are consistent across responses 86 | var params subscribeResponseParams 87 | err = json.Unmarshal(resp[i].Params, ¶ms) 88 | require.NoError(t, err) 89 | assert.Equal(t, subscriptionNum, params.Subscription) 90 | 91 | // assert that the response is for an expected StorageKey 92 | var changeSet types.StorageChangeSet 93 | err = json.Unmarshal(params.Result, &changeSet) 94 | require.NoError(t, err) 95 | assert.GreaterOrEqual(t, len(changeSet.Changes), 1) 96 | assert.True(t, includesKeyInChanges(expectedStorageKey, changeSet.Changes)) 97 | 98 | testEventRecordsDecoding(t, metadata, changeSet.Changes) 99 | }) 100 | } 101 | } 102 | 103 | func includesKeyInChanges(expectedKey string, changes []types.KeyValueOption) bool { 104 | for _, change := range changes { 105 | if change.StorageKey.Hex() == expectedKey { 106 | return true 107 | } 108 | } 109 | return false 110 | } 111 | 112 | func testEventRecordsDecoding(t *testing.T, metadata *types.Metadata, changes []types.KeyValueOption) { 113 | for _, change := range changes { 114 | testName := fmt.Sprintf("Test decoding storage change %x", change.StorageKey) 115 | t.Run(testName, func(t *testing.T) { 116 | events := blockchain.EventRecords{} 117 | err := types.EventRecordsRaw(change.StorageData).DecodeEventRecords(metadata, &events) 118 | require.NoError(t, err) 119 | assert.NotNil(t, events) 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/xtz.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/smartcontractkit/chainlink/core/logger" 10 | "github.com/smartcontractkit/external-initiator/integration/mock-client/blockchain/static" 11 | ) 12 | 13 | func setXtzRoutes(router *gin.Engine) { 14 | router.GET("/http/xtz/monitor/heads/:chain_id", handleXtzMonitorRequest) 15 | router.GET("/http/xtz/chains/main/blocks/:block_id/operations", handleXtzOperationsRequest) 16 | } 17 | 18 | type xtzResponses map[string]interface{} 19 | 20 | func getXtzResponse(method string) (interface{}, error) { 21 | bz, err := static.Get("xtz") 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | var responses xtzResponses 27 | err = json.Unmarshal(bz, &responses) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | response, ok := responses[method] 33 | if !ok { 34 | return nil, errors.New("method not found") 35 | } 36 | 37 | return response, nil 38 | } 39 | 40 | func handleXtzMonitorRequest(c *gin.Context) { 41 | resp, err := getXtzResponse("monitor") 42 | if err != nil { 43 | logger.Error(err) 44 | c.JSON(http.StatusBadRequest, resp) 45 | return 46 | } 47 | 48 | c.JSON(http.StatusOK, resp) 49 | } 50 | 51 | func handleXtzOperationsRequest(c *gin.Context) { 52 | resp, err := getXtzResponse("operations") 53 | if err != nil { 54 | logger.Error(err) 55 | c.JSON(http.StatusBadRequest, resp) 56 | return 57 | } 58 | 59 | c.JSON(http.StatusOK, resp) 60 | } 61 | -------------------------------------------------------------------------------- /integration/mock-client/blockchain/xtz_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGetXtzMonitorResponse(t *testing.T) { 11 | t.Run("creates mock XtzMonitorResponse", 12 | func(t *testing.T) { 13 | resp, err := getXtzResponse("monitor") 14 | require.NoError(t, err) 15 | monitor, ok := resp.(map[string]interface{}) 16 | require.True(t, ok) 17 | assert.Equal(t, monitor["hash"], "8BADF00D8BADF00D8BADF00D8BADF00D8BADF00D8BADF00D8BADF00D") 18 | }) 19 | } 20 | 21 | func TestGetXtzOperationsResponse(t *testing.T) { 22 | t.Run("creates an appropriately structured mock Tezos block", 23 | func(t *testing.T) { 24 | resp, err := getXtzResponse("operations") 25 | require.NoError(t, err) 26 | ops, ok := resp.([]interface{}) 27 | require.True(t, ok) 28 | 29 | // should be a 4 element array 30 | assert.Equal(t, len(ops), 4) 31 | 32 | fourth, ok := ops[3].([]interface{}) 33 | require.True(t, ok) 34 | 35 | // fourth element has transactions 36 | assert.Greater(t, len(fourth), 0) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /integration/mock-client/grpc/grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/iotexproject/iotex-proto/golang/iotexapi" 7 | "github.com/smartcontractkit/chainlink/core/logger" 8 | "github.com/smartcontractkit/external-initiator/integration/mock-client/blockchain" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | func RunServer() { 13 | /* #nosec */ 14 | lis, err := net.Listen("tcp", ":8090") 15 | if err != nil { 16 | logger.Error(err) 17 | return 18 | } 19 | grpcServer := grpc.NewServer() 20 | 21 | attachServers(grpcServer) 22 | 23 | go func() { 24 | if err := grpcServer.Serve(lis); err != nil { 25 | logger.Error(err) 26 | return 27 | } 28 | }() 29 | } 30 | 31 | func attachServers(grpcServer *grpc.Server) { 32 | // iotex 33 | iotexapi.RegisterAPIServiceServer(grpcServer, &blockchain.MockIoTeXServer{}) 34 | } 35 | -------------------------------------------------------------------------------- /integration/mock-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/smartcontractkit/chainlink/core/logger" 5 | "github.com/smartcontractkit/external-initiator/integration/mock-client/grpc" 6 | "github.com/smartcontractkit/external-initiator/integration/mock-client/web" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | func init() { 11 | logger.SetLogger(logger.CreateProductionLogger("", false, zapcore.DebugLevel, false)) 12 | } 13 | 14 | func main() { 15 | logger.Info("Starting mock blockchain client") 16 | 17 | grpc.RunServer() 18 | web.RunWebserver() 19 | } 20 | -------------------------------------------------------------------------------- /integration/mock-client/web/client.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/gorilla/websocket" 14 | "github.com/smartcontractkit/chainlink/core/logger" 15 | "github.com/smartcontractkit/external-initiator/integration/mock-client/blockchain" 16 | ) 17 | 18 | // RunWebserver starts a new web server using the access key 19 | // and secret as provided on protected routes. 20 | func RunWebserver() { 21 | srv := NewHTTPService() 22 | err := srv.Router.Run(":8080") 23 | if err != nil { 24 | logger.Error(err) 25 | } 26 | } 27 | 28 | // HttpService encapsulates router, EI service 29 | // and access credentials. 30 | type HttpService struct { 31 | Router *gin.Engine 32 | } 33 | 34 | // NewHTTPService creates a new HttpService instance 35 | // with the default router. 36 | func NewHTTPService() *HttpService { 37 | srv := HttpService{} 38 | srv.createRouter() 39 | return &srv 40 | } 41 | 42 | // ServeHTTP calls ServeHTTP on the underlying router, 43 | // which conforms to the http.Handler interface. 44 | func (srv *HttpService) ServeHTTP(w http.ResponseWriter, r *http.Request) { 45 | srv.Router.ServeHTTP(w, r) 46 | } 47 | 48 | func (srv *HttpService) createRouter() { 49 | r := gin.Default() 50 | r.Use(gin.Recovery(), loggerFunc()) 51 | 52 | blockchain.SetHttpRoutes(r) 53 | r.GET("/ws/:platform", srv.HandleWs) 54 | r.POST("/rpc/:platform", srv.HandleRpc) 55 | 56 | srv.Router = r 57 | } 58 | 59 | // CreateSubscription expects a CreateSubscriptionReq payload, 60 | // validates the request and subscribes to the job. 61 | func (srv *HttpService) HandleRpc(c *gin.Context) { 62 | var req blockchain.JsonrpcMessage 63 | if err := c.BindJSON(&req); err != nil { 64 | logger.Error(err) 65 | c.JSON(http.StatusBadRequest, nil) 66 | return 67 | } 68 | 69 | resp, err := blockchain.HandleRequest("rpc", c.Param("platform"), req) 70 | if len(resp) == 0 || err != nil { 71 | var response blockchain.JsonrpcMessage 72 | response.ID = req.ID 73 | response.Version = req.Version 74 | if err != nil { 75 | logger.Error(err) 76 | errintf := interface{}(err.Error()) 77 | response.Error = &errintf 78 | } 79 | c.JSON(http.StatusBadRequest, resp) 80 | return 81 | } 82 | 83 | c.JSON(http.StatusOK, resp[0]) 84 | } 85 | 86 | var upgrader = websocket.Upgrader{} 87 | 88 | func (srv *HttpService) HandleWs(c *gin.Context) { 89 | conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 90 | if err != nil { 91 | logger.Error(err) 92 | c.JSON(http.StatusInternalServerError, nil) 93 | return 94 | } 95 | defer logger.ErrorIfCalling(conn.Close) 96 | 97 | for { 98 | mt, message, err := conn.ReadMessage() 99 | if err != nil { 100 | logger.Error("read:", err) 101 | break 102 | } 103 | 104 | var req blockchain.JsonrpcMessage 105 | err = json.Unmarshal(message, &req) 106 | if err != nil { 107 | logger.Error("unmarshal:", err) 108 | continue 109 | } 110 | 111 | resp, err := blockchain.HandleRequest("ws", c.Param("platform"), req) 112 | if err != nil { 113 | logger.Error("handle request:", err) 114 | continue 115 | } 116 | 117 | for _, msg := range resp { 118 | bz, err := json.Marshal(msg) 119 | if err != nil { 120 | logger.Error("marshal:", err) 121 | continue 122 | } 123 | 124 | err = conn.WriteMessage(mt, bz) 125 | if err != nil { 126 | logger.Error("write:", err) 127 | break 128 | } 129 | } 130 | } 131 | } 132 | 133 | // Inspired by https://github.com/gin-gonic/gin/issues/961 134 | func loggerFunc() gin.HandlerFunc { 135 | return func(c *gin.Context) { 136 | buf, err := ioutil.ReadAll(c.Request.Body) 137 | if err != nil { 138 | logger.Error("Web request log error: ", err.Error()) 139 | // Implicitly relies on limits.RequestSizeLimiter 140 | // overriding of c.Request.Body to abort gin's Context 141 | // inside ioutil.ReadAll. 142 | // Functions as we would like, but horrible from an architecture 143 | // and design pattern perspective. 144 | if !c.IsAborted() { 145 | c.AbortWithStatus(http.StatusBadRequest) 146 | } 147 | return 148 | } 149 | rdr := bytes.NewBuffer(buf) 150 | c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buf)) 151 | 152 | start := time.Now() 153 | c.Next() 154 | end := time.Now() 155 | 156 | logger.Infow(fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path), 157 | "method", c.Request.Method, 158 | "status", c.Writer.Status(), 159 | "path", c.Request.URL.Path, 160 | "query", c.Request.URL.Query(), 161 | "body", readBody(rdr), 162 | "clientIP", c.ClientIP(), 163 | "errors", c.Errors.String(), 164 | "servedAt", end.Format("2006-01-02 15:04:05"), 165 | "latency", fmt.Sprint(end.Sub(start)), 166 | ) 167 | } 168 | } 169 | 170 | func readBody(reader io.Reader) string { 171 | buf := new(bytes.Buffer) 172 | _, err := buf.ReadFrom(reader) 173 | if err != nil { 174 | logger.Warn("unable to read from body for sanitization: ", err) 175 | return "*FAILED TO READ BODY*" 176 | } 177 | 178 | if buf.Len() == 0 { 179 | return "" 180 | } 181 | 182 | s, err := readSanitizedJSON(buf) 183 | if err != nil { 184 | logger.Warn("unable to sanitize json for logging: ", err) 185 | return "*FAILED TO READ BODY*" 186 | } 187 | return s 188 | } 189 | 190 | func readSanitizedJSON(buf *bytes.Buffer) (string, error) { 191 | var dst map[string]interface{} 192 | err := json.Unmarshal(buf.Bytes(), &dst) 193 | if err != nil { 194 | return "", err 195 | } 196 | 197 | b, err := json.Marshal(dst) 198 | if err != nil { 199 | return "", err 200 | } 201 | return string(b), err 202 | } 203 | -------------------------------------------------------------------------------- /integration/node_modules/.yarn-integrity: -------------------------------------------------------------------------------- 1 | { 2 | "systemParams": "linux-x64-64", 3 | "modulesFolders": [], 4 | "flags": [], 5 | "linkedModules": [ 6 | "aleph-js" 7 | ], 8 | "topLevelPatterns": [], 9 | "lockfileEntries": {}, 10 | "files": [], 11 | "artifacts": {} 12 | } -------------------------------------------------------------------------------- /integration/run_test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source ./integration/common 6 | 7 | run_test() { 8 | trap exit_handler EXIT 9 | 10 | mkdir -p "$LOG_PATH" 11 | 12 | title "Initiating a fresh test" 13 | 14 | # Remove old volumes so we can run a fresh test 15 | reset 16 | 17 | start_docker 18 | 19 | add_ei 20 | 21 | # Run EI after access credentials has been generated 22 | run_ei 23 | 24 | login_chainlink 25 | 26 | run_tests "$@" 27 | 28 | stop_docker 29 | 30 | title "Done running tests" 31 | } 32 | 33 | run_test "$@" 34 | -------------------------------------------------------------------------------- /integration/scripts/.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage 7 | -------------------------------------------------------------------------------- /integration/scripts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 6 | plugins: ['@typescript-eslint'], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 10 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 11 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /integration/scripts/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | src/generated/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /integration/scripts/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | printWidth: 100, 5 | endOfLine: "auto", 6 | trailingComma: "all" 7 | } 8 | -------------------------------------------------------------------------------- /integration/scripts/README.md: -------------------------------------------------------------------------------- 1 | # External Initiator Scripts 2 | -------------------------------------------------------------------------------- /integration/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@external-initiator/integration-scripts", 3 | "version": "0.0.1", 4 | "description": "Scripts for helping perform integration tests", 5 | "repository": "https://github.com/smartcontractkit/external-initiator", 6 | "license": "MIT", 7 | "private": true, 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 15 | "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", 16 | "build": "tsc", 17 | "setup": "yarn build", 18 | "add-ei": "ts-node ./src/addExternalInitiator.ts", 19 | "run-test": "ts-node ./src/runTest.ts" 20 | }, 21 | "dependencies": { 22 | "axios": "^0.21.1", 23 | "chalk": "^2.4.2", 24 | "moment": "^2.29.1", 25 | "request": "^2.88.2", 26 | "source-map-support": "^0.5.13" 27 | }, 28 | "devDependencies": { 29 | "@tsconfig/node12": "^1.0.7", 30 | "@types/node": "^14.0.13", 31 | "@types/shelljs": "^0.8.5", 32 | "@typescript-eslint/eslint-plugin": "^4.14.0", 33 | "@typescript-eslint/parser": "^4.14.0", 34 | "debug": "4.1.1", 35 | "eslint": "^7.2.0", 36 | "eslint-config-prettier": "^6.11.0", 37 | "eslint-config-standard": "^14.1.1", 38 | "eslint-plugin-import": "^2.22.0", 39 | "eslint-plugin-node": "^11.1.0", 40 | "eslint-plugin-prettier": "^3.1.4", 41 | "eslint-plugin-promise": "^4.2.1", 42 | "eslint-plugin-standard": "^4.0.1", 43 | "husky": "^4.2.5", 44 | "lint-staged": "^10.2.11", 45 | "prettier": "^2.0.5", 46 | "ts-node": "^8.10.2", 47 | "typechain": "1.0.3", 48 | "typechain-target-ethers": "^1.0.1", 49 | "typescript": "^3.9.5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /integration/scripts/src/addExternalInitiator.ts: -------------------------------------------------------------------------------- 1 | import url from 'url' 2 | import { ChainlinkNode, ExternalInitiator } from './chainlinkNode' 3 | import { fetchConfig, fetchCredentials } from './common' 4 | 5 | async function main() { 6 | const { chainlinkUrl, initiatorUrl } = fetchConfig() 7 | 8 | const credentials = fetchCredentials() 9 | const node = new ChainlinkNode(chainlinkUrl, credentials) 10 | 11 | const ei: ExternalInitiator = { 12 | name: 'mock-client', 13 | url: url.resolve(initiatorUrl, '/jobs'), 14 | } 15 | const { 16 | data: { attributes }, 17 | } = await node.createExternalInitiator(ei) 18 | console.log(`EI incoming accesskey: ${attributes.incomingAccessKey}`) 19 | console.log(`EI incoming secret: ${attributes.incomingSecret}`) 20 | console.log(`EI outgoing token: ${attributes.outgoingToken}`) 21 | console.log(`EI outgoing secret: ${attributes.outgoingSecret}`) 22 | } 23 | 24 | main().then() 25 | -------------------------------------------------------------------------------- /integration/scripts/src/asserts.ts: -------------------------------------------------------------------------------- 1 | import { Test } from './tests' 2 | 3 | const colorFail = '\x1b[31m' 4 | const colorPass = '\x1b[32m' 5 | 6 | class AssertionError extends Error { 7 | m: string 8 | got: any 9 | expect: any 10 | 11 | constructor(m: string, got: any, expect: any) { 12 | super(m) 13 | this.m = m 14 | this.got = got 15 | this.expect = expect 16 | 17 | Object.setPrototypeOf(this, AssertionError.prototype) 18 | } 19 | 20 | toString() { 21 | return `${this.m}: got ${this.got}, expected ${this.expect}` 22 | } 23 | } 24 | 25 | export interface Context { 26 | successes: number 27 | fails: number 28 | } 29 | 30 | export const context = async (func: (ctx: Context) => Promise): Promise => { 31 | const ctx: Context = { successes: 0, fails: 0 } 32 | await func(ctx) 33 | return ctx 34 | } 35 | 36 | export const newTest = async (test: Test, func: () => Promise) => { 37 | const header = ` ${test.blockchain}: ${test.name}` 38 | output(header, true) 39 | 40 | try { 41 | await func() 42 | } catch (e) { 43 | outputError(` FAILED ${test.blockchain}: ${test.name}\n`, true) 44 | return 45 | } 46 | 47 | outputPass(` Passed ${test.blockchain}: ${test.name}\n`, true) 48 | } 49 | 50 | export const it = async (name: string, ctx: Context, func: () => Promise) => { 51 | await func() 52 | .catch((e) => { 53 | ctx.fails++ 54 | outputError(` FAILED ${name}: ${e.toString()}`) 55 | throw e 56 | }) 57 | .then(() => { 58 | ctx.successes++ 59 | outputPass(` Pass: ${name}`) 60 | }) 61 | } 62 | 63 | type Assertion = (got: T, expect: T, name: string) => void 64 | 65 | type AssertionImplied = (got: T, name: string) => void 66 | 67 | export const equals: Assertion = (got, expect, name) => { 68 | if (got !== expect) { 69 | throw new AssertionError(name, got, expect) 70 | } 71 | } 72 | 73 | export const isFalse: AssertionImplied = (got, name) => { 74 | if (got) { 75 | throw new AssertionError(name, got, false) 76 | } 77 | } 78 | 79 | export const withRetry = async (assertion: () => Promise, attempts: number) => { 80 | let attempt = 0 81 | while (attempt++ < attempts) { 82 | try { 83 | await assertion() 84 | } catch (e) { 85 | if (!(e instanceof AssertionError) || attempt >= attempts) throw e 86 | const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)) 87 | await delay(1000) 88 | continue 89 | } 90 | return 91 | } 92 | } 93 | 94 | const outputError = (msg: string, bold = false) => { 95 | let control = colorFail 96 | if (bold) control += '\x1b[1m' 97 | control += '%s\x1b[0m' 98 | console.error(control, msg) 99 | } 100 | 101 | const outputPass = (msg: string, bold = false) => { 102 | let control = colorPass 103 | if (bold) control += '\x1b[1m' 104 | control += '%s\x1b[0m' 105 | console.log(control, msg) 106 | } 107 | 108 | const output = (msg: string, bold = false) => { 109 | let control = bold ? '\x1b[1m' : '' 110 | control += '%s\x1b[0m' 111 | console.log(control, msg) 112 | } 113 | -------------------------------------------------------------------------------- /integration/scripts/src/chainlinkNode.ts: -------------------------------------------------------------------------------- 1 | import url from 'url' 2 | import axios, { AxiosRequestConfig } from 'axios' 3 | import moment from 'moment' 4 | 5 | const COOKIE_NAME = 'clsession' 6 | 7 | export interface Credentials { 8 | email: string 9 | password: string 10 | } 11 | 12 | interface Session { 13 | cookie: string 14 | expiresAt: moment.Moment 15 | } 16 | 17 | export interface JobSpec { 18 | id?: string 19 | initiators: { 20 | type: string 21 | params: { 22 | name: string 23 | body: Record 24 | } 25 | }[] 26 | tasks: { 27 | type: string 28 | params?: Record 29 | }[] 30 | } 31 | 32 | export interface ExternalInitiator { 33 | name: string 34 | url: string 35 | } 36 | 37 | export interface ResponseData { 38 | type: string 39 | id: string 40 | attributes: Record 41 | } 42 | 43 | export interface Response { 44 | data: T 45 | meta?: { 46 | count: number 47 | } 48 | } 49 | 50 | export class ChainlinkNode { 51 | url: string 52 | credentials: Credentials 53 | session?: Session 54 | 55 | constructor(url: string, credentials: Credentials) { 56 | this.url = url 57 | this.credentials = credentials 58 | } 59 | 60 | async createJob(jobspec: JobSpec): Promise> { 61 | const Job = await this.postAuthenticated('/v2/specs', jobspec) 62 | return Job.data 63 | } 64 | 65 | async createExternalInitiator(ei: ExternalInitiator): Promise> { 66 | const externalInitiator = await this.postAuthenticated('/v2/external_initiators', ei) 67 | return externalInitiator.data 68 | } 69 | 70 | async getJobs(): Promise> { 71 | const { data } = await this.getAuthenticated('/v2/specs') 72 | return data 73 | } 74 | 75 | async getJobRuns(jobId: string): Promise> { 76 | const params = { jobSpecId: jobId } 77 | const { data } = await this.getAuthenticated('/v2/runs', params) 78 | return data 79 | } 80 | 81 | async authenticate(): Promise { 82 | const sessionsUrl = url.resolve(this.url, '/sessions') 83 | const response = await axios.post(sessionsUrl, this.credentials, { 84 | withCredentials: true, 85 | }) 86 | const cookies = extractCookiesFromHeader(response.headers['set-cookie']) 87 | if (!cookies[COOKIE_NAME]) { 88 | throw Error('Could not authenticate') 89 | } 90 | const clsession = cookies[COOKIE_NAME] 91 | 92 | let expiresAt 93 | if (clsession.maxAge) { 94 | expiresAt = moment().add(clsession.maxAge, 'seconds') 95 | } else if (clsession.expires) { 96 | expiresAt = clsession.expires 97 | } else { 98 | // This shouldn't happen, but let's just assume the session lasts a while 99 | expiresAt = moment().add(1, 'day') 100 | } 101 | 102 | this.session = { cookie: clsession.value, expiresAt } 103 | } 104 | 105 | private async mustAuthenticate(): Promise { 106 | if (!this.session || this.session.expiresAt.diff(moment()) <= 0) { 107 | await this.authenticate() 108 | } 109 | } 110 | 111 | private async postAuthenticated( 112 | path: string, 113 | data?: any, 114 | params?: Record, 115 | ): Promise { 116 | await this.mustAuthenticate() 117 | const fullUrl = url.resolve(this.url, path) 118 | return await axios.post(fullUrl, data, { 119 | ...this.withAuth(), 120 | params, 121 | }) 122 | } 123 | 124 | private async getAuthenticated(path: string, params?: Record): Promise { 125 | await this.mustAuthenticate() 126 | const fullUrl = url.resolve(this.url, path) 127 | return await axios.get(fullUrl, { 128 | ...this.withAuth(), 129 | params, 130 | }) 131 | } 132 | 133 | private withAuth(): Partial { 134 | return { 135 | withCredentials: true, 136 | headers: { 137 | cookie: `${COOKIE_NAME}=${this.session?.cookie}`, 138 | }, 139 | } 140 | } 141 | } 142 | 143 | interface Cookie { 144 | value: string 145 | maxAge?: number 146 | expires?: moment.Moment 147 | } 148 | 149 | const extractCookiesFromHeader = (cookiesHeader: string[]): Record => { 150 | const cookies: Record = {} 151 | 152 | const filteredCookies = cookiesHeader 153 | .map((header) => header.split(/=(.+)/)) 154 | .filter((header) => header[0] === COOKIE_NAME) 155 | if (filteredCookies.length === 0) return cookies 156 | 157 | const cookie = filteredCookies[0] 158 | const props = cookie[1].split(';') 159 | cookies[COOKIE_NAME] = { value: props[0] } 160 | 161 | props 162 | .map((prop) => prop.split('=')) 163 | .forEach((parts) => { 164 | switch (parts[0].toLowerCase().trim()) { 165 | case 'expires': 166 | cookies[COOKIE_NAME].expires = moment(parts[1]) 167 | return 168 | case 'max-age': 169 | cookies[COOKIE_NAME].maxAge = Number(parts[1]) 170 | return 171 | } 172 | }) 173 | 174 | return cookies 175 | } 176 | -------------------------------------------------------------------------------- /integration/scripts/src/common.ts: -------------------------------------------------------------------------------- 1 | import { Credentials } from './chainlinkNode' 2 | import path from 'path' 3 | import fs from 'fs' 4 | import os from 'os' 5 | 6 | export interface Config { 7 | chainlinkUrl: string 8 | initiatorUrl: string 9 | } 10 | 11 | const defaultChainlinkUrl = 'http://localhost:6688/' 12 | const chainlinkUrlEnvVar = 'CHAINLINK_URL' 13 | 14 | const defaultInitiatorUrl = 'http://external-initiator:8080/' 15 | const initiatorUrlEnvVar = 'EXTERNAL_INITIATOR_URL' 16 | 17 | export const fetchConfig = (): Config => { 18 | return { 19 | chainlinkUrl: process.env[chainlinkUrlEnvVar] || defaultChainlinkUrl, 20 | initiatorUrl: process.env[initiatorUrlEnvVar] || defaultInitiatorUrl, 21 | } 22 | } 23 | 24 | export const fetchArgs = (): string[] => process.argv.slice(2) 25 | 26 | export const fetchCredentials = (file = '../../secrets/apicredentials'): Credentials => { 27 | const filePath = path.resolve(__dirname, file) 28 | const contents = fs.readFileSync(filePath, 'utf8') 29 | const lines = contents.split(os.EOL) 30 | return { 31 | email: lines[0], 32 | password: lines[1], 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /integration/scripts/src/runTest.ts: -------------------------------------------------------------------------------- 1 | import { ChainlinkNode } from './chainlinkNode' 2 | import { fetchTests, Test } from './tests' 3 | import { fetchArgs, fetchConfig, fetchCredentials } from './common' 4 | import * as assert from './asserts' 5 | 6 | const main = async () => { 7 | const args = fetchArgs().map((arg) => arg.toLowerCase()) 8 | const _filterBlockchain = (t: Test) => 9 | args.length === 0 || args.includes(t.blockchain.toLowerCase()) 10 | const tests = fetchTests().filter(_filterBlockchain) 11 | 12 | const { chainlinkUrl } = fetchConfig() 13 | const credentials = fetchCredentials() 14 | const node = new ChainlinkNode(chainlinkUrl, credentials) 15 | 16 | const ctx = await assert.context(async (ctx) => { 17 | for (const test of tests) { 18 | await assert.newTest(test, async () => { 19 | const jobCount = (await node.getJobs()).meta?.count || 0 20 | let jobId: string 21 | await assert.it('creates job', ctx, async () => { 22 | jobId = await addJob(node, test.params) 23 | assert.isFalse(!jobId, 'got a job ID') 24 | const newJobCount = (await node.getJobs()).meta?.count 25 | assert.equals(newJobCount, jobCount + 1, 'job count should increase by 1') 26 | }) 27 | 28 | await assert.it('runs job successfully', ctx, async () => { 29 | await assert.withRetry(async () => { 30 | const jobRuns = (await node.getJobRuns(jobId!)).meta?.count 31 | assert.equals(jobRuns, test.expectedRuns, 'job runs should increase') 32 | }, 30) 33 | 34 | await assert.withRetry(async () => { 35 | const jobRunStatus = (await node.getJobRuns(jobId!)).data[test.expectedRuns - 1] 36 | .attributes.status 37 | assert.equals(jobRunStatus, 'completed', 'last job run should be marked as completed') 38 | }, 5) 39 | }) 40 | }) 41 | } 42 | }) 43 | 44 | console.log() 45 | console.log('==== TEST RESULT ====') 46 | console.log('Tests passed:', ctx.successes) 47 | console.log('Tests failed:', ctx.fails) 48 | console.log('=====================') 49 | console.log() 50 | 51 | if (ctx.fails > 0) { 52 | process.exit(1) 53 | } 54 | } 55 | 56 | const addJob = async (node: ChainlinkNode, params: Record): Promise => { 57 | const jobspec = { 58 | initiators: [ 59 | { 60 | type: 'external', 61 | params: { 62 | name: 'mock-client', 63 | body: params, 64 | }, 65 | }, 66 | ], 67 | tasks: [{ type: 'noop' }], 68 | } 69 | const Job = await node.createJob(jobspec) 70 | return Job.data.id! 71 | } 72 | 73 | main().then() 74 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/birita.ts: -------------------------------------------------------------------------------- 1 | export const name = 'BIRITA' 2 | 3 | const defaultProviderAddress = 'iaa1l4vp69jt8ghxtyrh6jm8jp022km50sg35eqcae' 4 | const providerAddressEnvVar = 'BIRITA_PROVIDER_ADDRESS' 5 | 6 | export const getTests = () => { 7 | const addresses = [process.env[providerAddressEnvVar] || defaultProviderAddress] 8 | const serviceName = 'oracle' 9 | 10 | return [ 11 | { 12 | name: 'connection over HTTP RPC', 13 | expectedRuns: 1, 14 | params: { 15 | endpoint: 'birita-mock-http', 16 | addresses, 17 | serviceName, 18 | }, 19 | }, 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/bsc.ts: -------------------------------------------------------------------------------- 1 | import { defaultEvmAddress, evmAddressEnvVar } from './index' 2 | 3 | export const name = 'BSC' 4 | 5 | export const getTests = () => { 6 | const addresses = [process.env[evmAddressEnvVar] || defaultEvmAddress] 7 | 8 | return [ 9 | { 10 | name: 'connection over HTTP RPC', 11 | expectedRuns: 1, 12 | params: { 13 | endpoint: 'bsc-mock-http', 14 | addresses, 15 | }, 16 | }, 17 | { 18 | name: 'connection over WS', 19 | expectedRuns: 1, 20 | params: { 21 | endpoint: 'bsc-mock-ws', 22 | addresses, 23 | }, 24 | }, 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/cfx.ts: -------------------------------------------------------------------------------- 1 | export const name = 'CFX' 2 | 3 | const cfxAddressEnvVar = 'CFX_EVM_SUBSCRIBED_ADDRESS' 4 | const defaultCfxAddress = 'cfxtest:acdjv47k166p1pt4e8yph9rbcumrpbn2u69wyemxv0' 5 | 6 | export const getTests = () => { 7 | const addresses = [process.env[cfxAddressEnvVar] || defaultCfxAddress] 8 | 9 | return [ 10 | { 11 | name: 'connection over HTTP RPC', 12 | expectedRuns: 1, 13 | params: { 14 | endpoint: 'cfx-mock-http', 15 | addresses, 16 | }, 17 | }, 18 | { 19 | name: 'connection over WS', 20 | expectedRuns: 1, 21 | params: { 22 | endpoint: 'cfx-mock-ws', 23 | addresses, 24 | }, 25 | }, 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/eth.ts: -------------------------------------------------------------------------------- 1 | import { defaultEvmAddress, evmAddressEnvVar } from './index' 2 | 3 | export const name = 'ETH' 4 | 5 | export const getTests = () => { 6 | const addresses = [process.env[evmAddressEnvVar] || defaultEvmAddress] 7 | 8 | return [ 9 | { 10 | name: 'connection over HTTP RPC', 11 | expectedRuns: 1, 12 | params: { 13 | endpoint: 'eth-mock-http', 14 | addresses, 15 | }, 16 | }, 17 | { 18 | name: 'connection over WS', 19 | expectedRuns: 1, 20 | params: { 21 | endpoint: 'eth-mock-ws', 22 | addresses, 23 | }, 24 | }, 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/hmy.ts: -------------------------------------------------------------------------------- 1 | import { defaultEvmAddress, evmAddressEnvVar } from './index' 2 | 3 | export const name = 'HMY' 4 | 5 | export const getTests = () => { 6 | const addresses = [process.env[evmAddressEnvVar] || defaultEvmAddress] 7 | 8 | return [ 9 | { 10 | name: 'connection over HTTP RPC', 11 | expectedRuns: 1, 12 | params: { 13 | endpoint: 'hmy-mock-http', 14 | addresses, 15 | }, 16 | }, 17 | { 18 | name: 'connection over WS', 19 | expectedRuns: 1, 20 | params: { 21 | endpoint: 'hmy-mock-ws', 22 | addresses, 23 | }, 24 | }, 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/index.ts: -------------------------------------------------------------------------------- 1 | import * as ETH from './eth' 2 | import * as HMY from './hmy' 3 | import * as XTZ from './xtz' 4 | import * as ONT from './ont' 5 | import * as BSC from './bsc' 6 | import * as IOTX from './iotx' 7 | import * as CFX from './cfx' 8 | import * as Keeper from './keeper' 9 | import * as BIRITA from './birita' 10 | import * as NEAR from './near' 11 | import * as Substrate from './substrate' 12 | import * as Klaytn from './klaytn' 13 | 14 | interface TestInterface { 15 | name: string 16 | getTests(): Partial[] 17 | } 18 | 19 | const integrations: TestInterface[] = [ 20 | ETH, 21 | HMY, 22 | XTZ, 23 | ONT, 24 | BSC, 25 | IOTX, 26 | CFX, 27 | Keeper, 28 | BIRITA, 29 | NEAR, 30 | Substrate, 31 | Klaytn, 32 | ] 33 | 34 | export const defaultEvmAddress = '0x2aD9B7b9386c2f45223dDFc4A4d81C2957bAE19A' 35 | export const zeroEvmAddress = '0x0000000000000000000000000000000000000000' 36 | export const evmAddressEnvVar = 'EVM_SUBSCRIBED_ADDRESS' 37 | 38 | export interface Test { 39 | name: string 40 | blockchain: string 41 | expectedRuns: number 42 | params: Record 43 | } 44 | 45 | export const fetchTests = (): Test[] => 46 | integrations 47 | .map((blockchain) => 48 | blockchain.getTests().map((t) => { 49 | return { ...t, blockchain: blockchain.name } as Test 50 | }), 51 | ) 52 | .flat() 53 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/iotx.ts: -------------------------------------------------------------------------------- 1 | import { defaultEvmAddress, evmAddressEnvVar } from './index' 2 | 3 | export const name = 'IOTX' 4 | 5 | export const getTests = () => { 6 | const addresses = [process.env[evmAddressEnvVar] || defaultEvmAddress] 7 | 8 | return [ 9 | { 10 | name: 'connection over gRPC', 11 | expectedRuns: 1, 12 | params: { 13 | endpoint: 'iotx-mock-grpc', 14 | addresses, 15 | }, 16 | }, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/keeper.ts: -------------------------------------------------------------------------------- 1 | import { defaultEvmAddress, evmAddressEnvVar, zeroEvmAddress } from './index' 2 | 3 | export const name = 'Keeper' 4 | 5 | export const getTests = () => { 6 | const address = process.env[evmAddressEnvVar] || defaultEvmAddress 7 | const from = zeroEvmAddress 8 | const upkeepId = '123' 9 | 10 | return [ 11 | { 12 | name: 'connection over HTTP RPC', 13 | expectedRuns: 1, 14 | params: { 15 | endpoint: 'keeper-mock-http', 16 | address, 17 | from, 18 | upkeepId, 19 | }, 20 | }, 21 | { 22 | name: 'connection over WS', 23 | expectedRuns: 1, 24 | params: { 25 | endpoint: 'keeper-mock-ws', 26 | address, 27 | from, 28 | upkeepId, 29 | }, 30 | }, 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/klaytn.ts: -------------------------------------------------------------------------------- 1 | import { defaultEvmAddress, evmAddressEnvVar } from './index' 2 | 3 | export const name = 'KLAYTN' 4 | 5 | export const getTests = () => { 6 | const addresses = [process.env[evmAddressEnvVar] || defaultEvmAddress] 7 | 8 | return [ 9 | { 10 | name: 'connection over HTTP RPC', 11 | expectedRuns: 1, 12 | params: { 13 | endpoint: 'klaytn-mock-http', 14 | addresses, 15 | }, 16 | }, 17 | { 18 | name: 'connection over WS', 19 | expectedRuns: 1, 20 | params: { 21 | endpoint: 'klaytn-mock-ws', 22 | addresses, 23 | }, 24 | }, 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/near.ts: -------------------------------------------------------------------------------- 1 | export const name = 'NEAR' 2 | 3 | const defaultAccountId = 'oracle.oracle.testnet' 4 | const accountIdEnvVar = 'NEAR_ORACLE_ACCOUNT_ID' 5 | 6 | export const getTests = () => { 7 | const accountIds = [process.env[accountIdEnvVar] || defaultAccountId] 8 | 9 | return [ 10 | { 11 | name: 'connection over HTTP RPC', 12 | expectedRuns: 3, 13 | params: { 14 | endpoint: 'near-mock-http', 15 | accountIds, 16 | }, 17 | }, 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/ont.ts: -------------------------------------------------------------------------------- 1 | import { defaultEvmAddress, evmAddressEnvVar } from './index' 2 | 3 | export const name = 'ONT' 4 | 5 | export const getTests = () => { 6 | const addresses = [process.env[evmAddressEnvVar] || defaultEvmAddress] 7 | 8 | return [ 9 | { 10 | name: 'connection over HTTP RPC', 11 | expectedRuns: 1, 12 | params: { 13 | endpoint: 'ont-mock-http', 14 | addresses, 15 | }, 16 | }, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/substrate.ts: -------------------------------------------------------------------------------- 1 | export const name = 'Substrate' 2 | 3 | export const getTests = () => { 4 | return [ 5 | { 6 | name: 'WS mock with account #1', 7 | expectedRuns: 1, 8 | params: { 9 | endpoint: 'substrate-mock-ws', 10 | accountIds: getAccountId(1), 11 | }, 12 | }, 13 | { 14 | name: 'WS mock with account #2', 15 | expectedRuns: 1, 16 | params: { 17 | endpoint: 'substrate-mock-ws', 18 | accountIds: getAccountId(2), 19 | }, 20 | }, 21 | { 22 | name: 'WS mock with account #3', 23 | expectedRuns: 1, 24 | params: { 25 | endpoint: 'substrate-mock-ws', 26 | accountIds: getAccountId(3), 27 | }, 28 | }, 29 | ] 30 | } 31 | 32 | const getAccountId = (i: number): string[] => { 33 | const defaultIds = [] 34 | // Secret phrase `dry squeeze youth enjoy provide blouse claw engage host what horn next` is account: 35 | // Secret seed: 0x2875481aae0807cf598d6097c901a33b36241c761158c85852a6d79a8f20bc62 36 | // Public key (hex): 0x7c522c8273973e7bcf4a5dbfcc745dba4a3ab08c1e410167d7b1bdf9cb924f6c 37 | // Account ID: 0x7c522c8273973e7bcf4a5dbfcc745dba4a3ab08c1e410167d7b1bdf9cb924f6c 38 | // SS58 Address: 5EsiCstpHTxarfafS3tvG7WDwbrp9Bv6BbyRvpwt3fY8PCtN 39 | defaultIds.push('0x7c522c8273973e7bcf4a5dbfcc745dba4a3ab08c1e410167d7b1bdf9cb924f6c') 40 | 41 | // Secret phrase `price trip nominee recycle walk park borrow sausage crucial only wheel joke` is account: 42 | // Secret seed: 0x00ed255f936202d04c70c02737ba322a7aaf961e94bb22c3e15d4ec7f44ab407 43 | // Public key (hex): 0x06f0d58c43477508c0e5d5901342acf93a0208088816ff303996564a1d8c1c54 44 | // Account ID: 0x06f0d58c43477508c0e5d5901342acf93a0208088816ff303996564a1d8c1c54 45 | // SS58 Address: 5CDogos4Dy2tSCvShBHkeFeMscwx9Wi2vFRijjTRRFau3vkJ 46 | defaultIds.push('0x06f0d58c43477508c0e5d5901342acf93a0208088816ff303996564a1d8c1c54') 47 | 48 | // Secret phrase `camp acid then kid between survey dentist delay actor fox ensure soccer` is account: 49 | // Secret seed: 0xb9de30043e09e2c6b6d6c3b23505aee0170ba57f8af91bb035d1f1130151755a 50 | // Public key (hex): 0xfaa31acde43e8859565f7576d5a37e6e8ee1b0f6a7c1ae2e8b0ce2bf76248467 51 | // Account ID: 0xfaa31acde43e8859565f7576d5a37e6e8ee1b0f6a7c1ae2e8b0ce2bf76248467 52 | // SS58 Address: 5HjLHE3A9L6zxUEn6uy8mcx3tqyVxZvVXu7okovfWzemadzs 53 | defaultIds.push('0xfaa31acde43e8859565f7576d5a37e6e8ee1b0f6a7c1ae2e8b0ce2bf76248467') 54 | 55 | const _accountIdEnvVar = (i: number) => `SUBSTRATE_OPERATOR_${i}_ACCOUNT_ID` 56 | return [process.env[_accountIdEnvVar(i)] || defaultIds[i - 1] || ''] 57 | } 58 | -------------------------------------------------------------------------------- /integration/scripts/src/tests/xtz.ts: -------------------------------------------------------------------------------- 1 | import { defaultEvmAddress, evmAddressEnvVar } from './index' 2 | 3 | export const name = 'XTZ' 4 | 5 | export const getTests = () => { 6 | const addresses = [process.env[evmAddressEnvVar] || defaultEvmAddress] 7 | 8 | return [ 9 | { 10 | name: 'connection over HTTP RPC', 11 | expectedRuns: 1, 12 | params: { 13 | endpoint: 'xtz-mock-http', 14 | addresses, 15 | }, 16 | }, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /integration/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2019", 6 | "es2020.promise", 7 | "es2020.bigint", 8 | "es2020.string", 9 | "es2020.symbol.wellknown" 10 | ], 11 | "moduleResolution": "node", 12 | "declaration": true, 13 | "declarationMap": true, 14 | "sourceMap": true, 15 | "outDir": "dist", 16 | "rootDir": "src", 17 | "typeRoots": [ 18 | "node_modules/@types" 19 | ] 20 | }, 21 | "include": [ 22 | "src/**/*" 23 | ], 24 | "exclude": [ 25 | "dist", 26 | "**/*.spec.ts", 27 | "**/*.test.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /integration/secrets/0x9CA9d2D5E04012C9Ed24C0e513C9bfAa4A2dD77f.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "id": "f8c91297-5bf7-458e-b8e5-39e9c79d5f2a", 4 | "address": "9ca9d2d5e04012c9ed24c0e513c9bfaa4a2dd77f", 5 | "Crypto": { 6 | "ciphertext": "ee5391a20f42e0b11a0a0824ce5f047bfc4c1391a62184f48952a0ad05deb55b", 7 | "cipherparams": { 8 | "iv": "4a27487d3892df5250fb7d1d9b5c00ac" 9 | }, 10 | "cipher": "aes-128-ctr", 11 | "kdf": "scrypt", 12 | "kdfparams": { 13 | "dklen": 32, 14 | "salt": "1839f222ed3759e0146252e9557f860ffff9575f8b4ba9c6c59ec40904c9580e", 15 | "n": 1024, 16 | "r": 8, 17 | "p": 1 18 | }, 19 | "mac": "c7099685c6903529d9e6abf356c59ee9ae70cc9365b2b700ad183671c5009058" 20 | } 21 | } -------------------------------------------------------------------------------- /integration/secrets/apicredentials: -------------------------------------------------------------------------------- 1 | notreal@fakeemail.ch 2 | twochains -------------------------------------------------------------------------------- /integration/secrets/password.txt: -------------------------------------------------------------------------------- 1 | T.tLHkcmwePT/p,]sYuntjwHKAsrhm#4eRs4LuKHwvHejWYAC2JP4M8HimwgmbaZ -------------------------------------------------------------------------------- /integration/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source ./integration/common 6 | 7 | run_setup() { 8 | mkdir -p "$LOG_PATH" 9 | 10 | title "Setting up test environment" 11 | 12 | pushd integration >/dev/null || exit 13 | # Create empty EI .env file 14 | touch external_initiator.env 15 | popd >/dev/null || exit 16 | 17 | pushd integration/scripts >/dev/null || exit 18 | local log=$LOG_PATH/integration_setup.log 19 | yarn 20 | popd >/dev/null || exit 21 | 22 | build_docker 23 | 24 | title "Done setting up test environment" 25 | } 26 | 27 | run_setup 28 | -------------------------------------------------------------------------------- /integration/stop_docker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source ./integration/common 6 | 7 | stop_docker 8 | -------------------------------------------------------------------------------- /integration/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/smartcontractkit/chainlink/core/logger" 5 | "github.com/smartcontractkit/external-initiator/client" 6 | "go.uber.org/zap/zapcore" 7 | ) 8 | 9 | func init() { 10 | logger.SetLogger(logger.CreateProductionLogger("", false, zapcore.DebugLevel, false)) 11 | } 12 | 13 | func main() { 14 | client.Run() 15 | } 16 | -------------------------------------------------------------------------------- /store/migrations/migrate.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1582671289" 10 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1587897988" 11 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1592829052" 12 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1594317706" 13 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1599849837" 14 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1603803454" 15 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1605288480" 16 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1608026935" 17 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1610281978" 18 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1611169747" 19 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1613356332" 20 | "gopkg.in/gormigrate.v1" 21 | ) 22 | 23 | // Migrate iterates through available migrations, running and tracking 24 | // migrations that have not been run. 25 | func Migrate(db *gorm.DB) error { 26 | options := *gormigrate.DefaultOptions 27 | options.UseTransaction = true 28 | 29 | migrations := []*gormigrate.Migration{ 30 | { 31 | ID: "0", 32 | Migrate: migration0.Migrate, 33 | }, 34 | { 35 | ID: "1576509489", 36 | Migrate: migration1576509489.Migrate, 37 | Rollback: migration1576509489.Rollback, 38 | }, 39 | { 40 | ID: "1576783801", 41 | Migrate: migration1576783801.Migrate, 42 | Rollback: migration1576783801.Rollback, 43 | }, 44 | { 45 | ID: "1582671289", 46 | Migrate: migration1582671289.Migrate, 47 | Rollback: migration1582671289.Rollback, 48 | }, 49 | { 50 | ID: "1587897988", 51 | Migrate: migration1587897988.Migrate, 52 | Rollback: migration1587897988.Rollback, 53 | }, 54 | { 55 | ID: "1592829052", 56 | Migrate: migration1592829052.Migrate, 57 | Rollback: migration1592829052.Rollback, 58 | }, 59 | { 60 | ID: "1594317706", 61 | Migrate: migration1594317706.Migrate, 62 | Rollback: migration1594317706.Rollback, 63 | }, 64 | { 65 | ID: "1599849837", 66 | Migrate: migration1599849837.Migrate, 67 | Rollback: migration1599849837.Rollback, 68 | }, 69 | { 70 | ID: "1603803454", 71 | Migrate: migration1603803454.Migrate, 72 | Rollback: migration1603803454.Rollback, 73 | }, 74 | { 75 | ID: "1605288480", 76 | Migrate: migration1605288480.Migrate, 77 | Rollback: migration1605288480.Rollback, 78 | }, 79 | { 80 | ID: "1608026935", 81 | Migrate: migration1608026935.Migrate, 82 | Rollback: migration1608026935.Rollback, 83 | }, 84 | { 85 | ID: "1610281978", 86 | Migrate: migration1610281978.Migrate, 87 | Rollback: migration1610281978.Rollback, 88 | }, 89 | { 90 | ID: "1611169747", 91 | Migrate: migration1611169747.Migrate, 92 | Rollback: migration1611169747.Rollback, 93 | }, 94 | { 95 | ID: "1613356332", 96 | Migrate: migration1613356332.Migrate, 97 | Rollback: migration1613356332.Rollback, 98 | }, 99 | } 100 | 101 | m := gormigrate.New(db, &options, migrations) 102 | 103 | err := m.Migrate() 104 | if err != nil { 105 | return errors.Wrap(err, "error running migrations") 106 | } 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /store/migrations/migration0/migrate.go: -------------------------------------------------------------------------------- 1 | package migration0 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type Endpoint struct { 9 | gorm.Model 10 | Url string 11 | Type string 12 | RefreshInt int 13 | Name string `gorm:"unique;not null"` 14 | } 15 | 16 | type Subscription struct { 17 | gorm.Model 18 | ReferenceId string `gorm:"unique;not null"` 19 | Job string 20 | EndpointName string 21 | Ethereum EthSubscription 22 | } 23 | 24 | type EthSubscription struct { 25 | gorm.Model 26 | SubscriptionId uint 27 | Addresses string 28 | Topics string 29 | } 30 | 31 | // Migrate runs the initial migration 32 | func Migrate(tx *gorm.DB) error { 33 | err := tx.AutoMigrate(&Subscription{}).Error 34 | if err != nil { 35 | return errors.Wrap(err, "failed to auto migrate Subscription") 36 | } 37 | 38 | err = tx.AutoMigrate(&Endpoint{}).Error 39 | if err != nil { 40 | return errors.Wrap(err, "failed to auto migrate Endpoint") 41 | } 42 | 43 | err = tx.AutoMigrate(&EthSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 44 | if err != nil { 45 | return errors.Wrap(err, "failed to auto migrate EthSubscription") 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /store/migrations/migration1576509489/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1576509489 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | ) 8 | 9 | type TezosSubscription struct { 10 | gorm.Model 11 | SubscriptionId uint `gorm:"unique;not null"` 12 | Addresses string `gorm:"not null"` 13 | } 14 | 15 | type Subscription struct { 16 | gorm.Model 17 | ReferenceId string `gorm:"unique;not null"` 18 | Job string 19 | EndpointName string 20 | Ethereum migration0.EthSubscription 21 | Tezos TezosSubscription 22 | } 23 | 24 | func Migrate(tx *gorm.DB) error { 25 | err := tx.AutoMigrate(&Subscription{}).Error 26 | if err != nil { 27 | return errors.Wrap(err, "failed to auto migrate Subscription") 28 | } 29 | 30 | err = tx.AutoMigrate(&TezosSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 31 | if err != nil { 32 | return errors.Wrap(err, "failed to auto migrate TezosSubscription") 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func Rollback(tx *gorm.DB) error { 39 | return tx.DropTable("tezos_subscriptions").Error 40 | } 41 | -------------------------------------------------------------------------------- /store/migrations/migration1576783801/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1576783801 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | ) 9 | 10 | type SubstrateSubscription struct { 11 | gorm.Model 12 | SubscriptionId uint `gorm:"unique;not null"` 13 | AccountIds string `gorm:"not null"` 14 | } 15 | 16 | type Subscription struct { 17 | gorm.Model 18 | ReferenceId string `gorm:"unique;not null"` 19 | Job string 20 | EndpointName string 21 | Ethereum migration0.EthSubscription 22 | Tezos migration1576509489.TezosSubscription 23 | Substrate SubstrateSubscription 24 | } 25 | 26 | func Migrate(tx *gorm.DB) error { 27 | err := tx.AutoMigrate(&Subscription{}).Error 28 | if err != nil { 29 | return errors.Wrap(err, "failed to auto migrate Subscription") 30 | } 31 | 32 | err = tx.AutoMigrate(&SubstrateSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 33 | if err != nil { 34 | return errors.Wrap(err, "failed to auto migrate TezosSubscription") 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func Rollback(tx *gorm.DB) error { 41 | return tx.DropTable("substrate_subscriptions").Error 42 | } 43 | -------------------------------------------------------------------------------- /store/migrations/migration1582671289/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1582671289 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 7 | ) 8 | 9 | func Migrate(tx *gorm.DB) error { 10 | err := tx.Model(&migration1576783801.Subscription{}).AddUniqueIndex("idx_job_id", "job").Error 11 | 12 | if err != nil { 13 | return errors.Wrap(err, "failed to add unique index to subscription job id") 14 | } 15 | 16 | return nil 17 | } 18 | 19 | func Rollback(tx *gorm.DB) error { 20 | return tx.Model(&migration1576783801.Subscription{}).RemoveIndex("idx_job_id").Error 21 | } 22 | -------------------------------------------------------------------------------- /store/migrations/migration1587897988/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1587897988 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | ) 10 | 11 | type OntSubscription struct { 12 | gorm.Model 13 | SubscriptionId uint `gorm:"unique;not null"` 14 | Addresses string `gorm:"not null"` 15 | } 16 | 17 | type Subscription struct { 18 | gorm.Model 19 | ReferenceId string `gorm:"unique;not null"` 20 | Job string 21 | EndpointName string 22 | Ethereum migration0.EthSubscription 23 | Tezos migration1576509489.TezosSubscription 24 | Substrate migration1576783801.SubstrateSubscription 25 | Ontology OntSubscription 26 | } 27 | 28 | func Migrate(tx *gorm.DB) error { 29 | err := tx.AutoMigrate(&Subscription{}).Error 30 | if err != nil { 31 | return errors.Wrap(err, "failed to auto migrate Subscription") 32 | } 33 | 34 | err = tx.AutoMigrate(&OntSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 35 | if err != nil { 36 | return errors.Wrap(err, "failed to auto migrate OntSubscription") 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func Rollback(tx *gorm.DB) error { 43 | return tx.DropTable("ont_subscriptions").Error 44 | } 45 | -------------------------------------------------------------------------------- /store/migrations/migration1592829052/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1592829052 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1587897988" 10 | ) 11 | 12 | type BinanceSmartChainSubscription struct { 13 | gorm.Model 14 | SubscriptionId uint 15 | Addresses string 16 | } 17 | 18 | type Subscription struct { 19 | gorm.Model 20 | ReferenceId string `gorm:"unique;not null"` 21 | Job string 22 | EndpointName string 23 | Ethereum migration0.EthSubscription 24 | Tezos migration1576509489.TezosSubscription 25 | Substrate migration1576783801.SubstrateSubscription 26 | Ontology migration1587897988.OntSubscription 27 | BinanceSmartChain BinanceSmartChainSubscription 28 | } 29 | 30 | func Migrate(tx *gorm.DB) error { 31 | err := tx.AutoMigrate(&Subscription{}).Error 32 | if err != nil { 33 | return errors.Wrap(err, "failed to auto migrate Subscription") 34 | } 35 | 36 | err = tx.AutoMigrate(&BinanceSmartChainSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 37 | if err != nil { 38 | return errors.Wrap(err, "failed to auto migrate BinanceSmartChainSubscription") 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func Rollback(tx *gorm.DB) error { 45 | return tx.DropTable("bsc_subscriptions").Error 46 | } 47 | -------------------------------------------------------------------------------- /store/migrations/migration1594317706/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1594317706 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1587897988" 10 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1592829052" 11 | ) 12 | 13 | type NEARSubscription struct { 14 | gorm.Model 15 | SubscriptionId uint 16 | AccountIds string 17 | } 18 | 19 | // TableName will set an explicit NEARSubscription table name, so table name isn't n_e_a_r_[...]. 20 | func (NEARSubscription) TableName() string { 21 | return "near_subscriptions" 22 | } 23 | 24 | type Subscription struct { 25 | gorm.Model 26 | ReferenceId string `gorm:"unique;not null"` 27 | Job string 28 | EndpointName string 29 | Ethereum migration0.EthSubscription 30 | Tezos migration1576509489.TezosSubscription 31 | Substrate migration1576783801.SubstrateSubscription 32 | Ontology migration1587897988.OntSubscription 33 | BinanceSmartChain migration1592829052.BinanceSmartChainSubscription 34 | NEAR NEARSubscription 35 | } 36 | 37 | func Migrate(tx *gorm.DB) error { 38 | err := tx.AutoMigrate(&Subscription{}).Error 39 | if err != nil { 40 | return errors.Wrap(err, "failed to auto migrate Subscription") 41 | } 42 | 43 | err = tx.AutoMigrate(&NEARSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 44 | if err != nil { 45 | return errors.Wrap(err, "failed to auto migrate NEARSubscription") 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func Rollback(tx *gorm.DB) error { 52 | return tx.DropTable("near_subscriptions").Error // TODO: is this table ID correct? Where does it come from? 53 | } 54 | -------------------------------------------------------------------------------- /store/migrations/migration1599849837/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1599849837 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1587897988" 10 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1592829052" 11 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1594317706" 12 | ) 13 | 14 | type CfxSubscription struct { 15 | gorm.Model 16 | SubscriptionId uint 17 | Addresses string 18 | Topics string 19 | } 20 | 21 | type Subscription struct { 22 | gorm.Model 23 | ReferenceId string `gorm:"unique;not null"` 24 | Job string 25 | EndpointName string 26 | Ethereum migration0.EthSubscription 27 | Tezos migration1576509489.TezosSubscription 28 | Substrate migration1576783801.SubstrateSubscription 29 | Ontology migration1587897988.OntSubscription 30 | BinanceSmartChain migration1592829052.BinanceSmartChainSubscription 31 | NEAR migration1594317706.NEARSubscription 32 | Conflux CfxSubscription 33 | } 34 | 35 | func Migrate(tx *gorm.DB) error { 36 | err := tx.AutoMigrate(&Subscription{}).Error 37 | if err != nil { 38 | return errors.Wrap(err, "failed to auto migrate Subscription") 39 | } 40 | 41 | err = tx.AutoMigrate(&CfxSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 42 | if err != nil { 43 | return errors.Wrap(err, "failed to auto migrate CfxSubscription") 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func Rollback(tx *gorm.DB) error { 50 | return tx.DropTable("cfx_subscriptions").Error 51 | } 52 | -------------------------------------------------------------------------------- /store/migrations/migration1603803454/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1603803454 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1587897988" 10 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1592829052" 11 | ) 12 | 13 | type EthCallSubscription struct { 14 | gorm.Model 15 | SubscriptionId uint `gorm:"index"` 16 | Address string 17 | ABI string 18 | ResponseKey string 19 | MethodName string 20 | } 21 | 22 | type Subscription struct { 23 | gorm.Model 24 | ReferenceId string `gorm:"unique;not null"` 25 | Job string 26 | EndpointName string 27 | Ethereum migration0.EthSubscription 28 | Tezos migration1576509489.TezosSubscription 29 | Substrate migration1576783801.SubstrateSubscription 30 | Ontology migration1587897988.OntSubscription 31 | BinanceSmartChain migration1592829052.BinanceSmartChainSubscription 32 | EthQae EthCallSubscription 33 | } 34 | 35 | func Migrate(tx *gorm.DB) error { 36 | err := tx.AutoMigrate(&Subscription{}).Error 37 | if err != nil { 38 | return errors.Wrap(err, "failed to auto migrate Subscription") 39 | } 40 | 41 | err = tx.AutoMigrate(&EthCallSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 42 | if err != nil { 43 | return errors.Wrap(err, "failed to auto migrate EthQaeSubscription") 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func Rollback(tx *gorm.DB) error { 50 | return tx.DropTable("eth_qae_subscriptions").Error 51 | } 52 | -------------------------------------------------------------------------------- /store/migrations/migration1605288480/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1605288480 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | type EthCallSubscription struct { 9 | gorm.Model 10 | SubscriptionId uint `gorm:"index"` 11 | Address string 12 | ABI string 13 | ResponseKey string 14 | MethodName string 15 | FunctionSelector [4]byte 16 | ReturnType string 17 | } 18 | 19 | func Migrate(tx *gorm.DB) error { 20 | err := tx.AutoMigrate(&EthCallSubscription{}).Error 21 | if err != nil { 22 | return errors.Wrap(err, "failed to auto migrate Subscription") 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func Rollback(tx *gorm.DB) error { 29 | return tx.DropTable("eth_call_subscriptions").Error 30 | } 31 | -------------------------------------------------------------------------------- /store/migrations/migration1608026935/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1608026935 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1587897988" 10 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1592829052" 11 | ) 12 | 13 | type KeeperSubscription struct { 14 | gorm.Model 15 | SubscriptionId uint 16 | Address string 17 | UpkeepID uint 18 | } 19 | 20 | type Subscription struct { 21 | gorm.Model 22 | ReferenceId string `gorm:"unique;not null"` 23 | Job string 24 | EndpointName string 25 | Ethereum migration0.EthSubscription 26 | Tezos migration1576509489.TezosSubscription 27 | Substrate migration1576783801.SubstrateSubscription 28 | Ontology migration1587897988.OntSubscription 29 | BinanceSmartChain migration1592829052.BinanceSmartChainSubscription 30 | Keeper KeeperSubscription 31 | } 32 | 33 | func Migrate(tx *gorm.DB) error { 34 | err := tx.AutoMigrate(&Subscription{}).Error 35 | if err != nil { 36 | return errors.Wrap(err, "failed to auto migrate Subscription") 37 | } 38 | 39 | err = tx.AutoMigrate(&KeeperSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 40 | if err != nil { 41 | return errors.Wrap(err, "failed to auto migrate EthQaeSubscription") 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func Rollback(tx *gorm.DB) error { 48 | return tx.DropTable("keeper_subscriptions").Error 49 | } 50 | -------------------------------------------------------------------------------- /store/migrations/migration1610281978/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1610281978 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1587897988" 10 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1592829052" 11 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1594317706" 12 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1599849837" 13 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1608026935" 14 | ) 15 | 16 | type BSNIritaSubscription struct { 17 | gorm.Model 18 | SubscriptionId uint 19 | Addresses string 20 | ServiceName string 21 | } 22 | 23 | type Subscription struct { 24 | gorm.Model 25 | ReferenceId string `gorm:"unique;not null"` 26 | Job string 27 | EndpointName string 28 | Ethereum migration0.EthSubscription 29 | Tezos migration1576509489.TezosSubscription 30 | Substrate migration1576783801.SubstrateSubscription 31 | Ontology migration1587897988.OntSubscription 32 | BinanceSmartChain migration1592829052.BinanceSmartChainSubscription 33 | NEAR migration1594317706.NEARSubscription 34 | Conflux migration1599849837.CfxSubscription 35 | Keeper migration1608026935.KeeperSubscription 36 | BSNIrita BSNIritaSubscription 37 | } 38 | 39 | func Migrate(tx *gorm.DB) error { 40 | err := tx.AutoMigrate(&Subscription{}).Error 41 | if err != nil { 42 | return errors.Wrap(err, "failed to auto migrate Subscription") 43 | } 44 | 45 | err = tx.AutoMigrate(&BSNIritaSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 46 | if err != nil { 47 | return errors.Wrap(err, "failed to auto migrate BSNIritaSubscription") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func Rollback(tx *gorm.DB) error { 54 | return tx.DropTable("bsn_irita_subscriptions").Error 55 | } 56 | -------------------------------------------------------------------------------- /store/migrations/migration1611169747/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1611169747 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | func Migrate(tx *gorm.DB) error { 8 | return tx.Exec(` 9 | ALTER TABLE keeper_subscriptions ADD COLUMN "from" bytea NOT NULL; 10 | `).Error 11 | } 12 | 13 | func Rollback(tx *gorm.DB) error { 14 | return tx.Exec(` 15 | ALTER TABLE keeper_subscriptions DROP COLUMN IF EXISTS "from"; 16 | `).Error 17 | } 18 | -------------------------------------------------------------------------------- /store/migrations/migration1613356332/migrate.go: -------------------------------------------------------------------------------- 1 | package migration1613356332 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/smartcontractkit/external-initiator/store/migrations/migration0" 7 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576509489" 8 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1576783801" 9 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1587897988" 10 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1592829052" 11 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1594317706" 12 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1599849837" 13 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1608026935" 14 | "github.com/smartcontractkit/external-initiator/store/migrations/migration1610281978" 15 | ) 16 | 17 | type AgoricSubscription struct { 18 | gorm.Model 19 | SubscriptionId uint 20 | } 21 | 22 | type Subscription struct { 23 | gorm.Model 24 | ReferenceId string `gorm:"unique;not null"` 25 | Job string 26 | EndpointName string 27 | Ethereum migration0.EthSubscription 28 | Tezos migration1576509489.TezosSubscription 29 | Substrate migration1576783801.SubstrateSubscription 30 | Ontology migration1587897988.OntSubscription 31 | BinanceSmartChain migration1592829052.BinanceSmartChainSubscription 32 | NEAR migration1594317706.NEARSubscription 33 | Conflux migration1599849837.CfxSubscription 34 | Keeper migration1608026935.KeeperSubscription 35 | BSNIrita migration1610281978.BSNIritaSubscription 36 | Agoric AgoricSubscription 37 | } 38 | 39 | func Migrate(tx *gorm.DB) error { 40 | err := tx.AutoMigrate(&Subscription{}).Error 41 | if err != nil { 42 | return errors.Wrap(err, "failed to auto migrate Subscription") 43 | } 44 | 45 | err = tx.AutoMigrate(&AgoricSubscription{}).AddForeignKey("subscription_id", "subscriptions(id)", "CASCADE", "CASCADE").Error 46 | if err != nil { 47 | return errors.Wrap(err, "failed to auto migrate AgoricSubscription") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func Rollback(tx *gorm.DB) error { 54 | return tx.DropTable("agoric_subscriptions").Error 55 | } 56 | -------------------------------------------------------------------------------- /store/models.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | type RuntimeConfig struct { 4 | KeeperBlockCooldown int64 5 | } 6 | -------------------------------------------------------------------------------- /subscriber/rpc.go: -------------------------------------------------------------------------------- 1 | package subscriber 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/smartcontractkit/chainlink/core/logger" 11 | "github.com/smartcontractkit/external-initiator/store" 12 | ) 13 | 14 | // RpcSubscriber holds the configuration for 15 | // a not-yet-active RPC subscription. 16 | type RpcSubscriber struct { 17 | Endpoint string 18 | Interval time.Duration 19 | Manager JsonManager 20 | } 21 | 22 | // Test sends a POST request using GetTestJson() 23 | // as payload, and returns the error from 24 | // calling ParseTestResponse() on the response. 25 | func (rpc RpcSubscriber) Test() error { 26 | resp, err := sendPostRequest(rpc.Endpoint, rpc.Manager.GetTestJson()) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return rpc.Manager.ParseTestResponse(resp) 32 | } 33 | 34 | // rpcSubscription holds an active RPC subscription. 35 | type rpcSubscription struct { 36 | endpoint string 37 | done chan struct{} 38 | events chan<- Event 39 | manager JsonManager 40 | } 41 | 42 | func (rpc rpcSubscription) Unsubscribe() { 43 | logger.Info("Unsubscribing from RPC endpoint", rpc.endpoint) 44 | close(rpc.done) 45 | } 46 | 47 | func (rpc rpcSubscription) poll() { 48 | logger.Debugf("Polling %s\n", rpc.endpoint) 49 | 50 | resp, err := sendPostRequest(rpc.endpoint, rpc.manager.GetTriggerJson()) 51 | if err != nil { 52 | logger.Errorf("Failed polling %s: %v\n", rpc.endpoint, err) 53 | return 54 | } 55 | 56 | events, ok := rpc.manager.ParseResponse(resp) 57 | if !ok { 58 | return 59 | } 60 | 61 | for _, event := range events { 62 | rpc.events <- event 63 | } 64 | } 65 | 66 | func (rpc rpcSubscription) readMessages(interval time.Duration) { 67 | timer := time.NewTicker(interval) 68 | defer timer.Stop() 69 | 70 | // Poll before waiting for ticker 71 | rpc.poll() 72 | 73 | for { 74 | select { 75 | case <-rpc.done: 76 | return 77 | case <-timer.C: 78 | rpc.poll() 79 | } 80 | } 81 | } 82 | 83 | func sendPostRequest(url string, body []byte) ([]byte, error) { 84 | request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | request.Header.Set("Content-Type", "application/json") 90 | 91 | client := &http.Client{} 92 | r, err := client.Do(request) 93 | if err != nil { 94 | return nil, err 95 | } 96 | defer logger.ErrorIfCalling(r.Body.Close) 97 | 98 | if r.StatusCode < 200 || r.StatusCode >= 400 { 99 | return nil, errors.New("got unexpected status code") 100 | } 101 | 102 | return ioutil.ReadAll(r.Body) 103 | } 104 | 105 | func (rpc RpcSubscriber) SubscribeToEvents(channel chan<- Event, _ store.RuntimeConfig) (ISubscription, error) { 106 | logger.Infof("Using RPC endpoint: %s\n", rpc.Endpoint) 107 | 108 | subscription := rpcSubscription{ 109 | endpoint: rpc.Endpoint, 110 | done: make(chan struct{}), 111 | events: channel, 112 | manager: rpc.Manager, 113 | } 114 | 115 | interval := rpc.Interval 116 | if interval <= time.Duration(0) { 117 | interval = 5 * time.Second 118 | } 119 | 120 | go subscription.readMessages(interval) 121 | 122 | return subscription, nil 123 | } 124 | -------------------------------------------------------------------------------- /subscriber/rpc_test.go: -------------------------------------------------------------------------------- 1 | package subscriber 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/smartcontractkit/external-initiator/store" 8 | ) 9 | 10 | func TestRpcSubscriber_SubscribeToEvents(t *testing.T) { 11 | t.Run("subscribes to rpc endpoint", func(t *testing.T) { 12 | u := *rpcMockUrl 13 | u.Path = "/test/1" 14 | rpc := RpcSubscriber{Endpoint: u.String(), Manager: TestsMockManager{true}, Interval: 1 * time.Second} 15 | 16 | events := make(chan Event) 17 | 18 | sub, err := rpc.SubscribeToEvents(events, store.RuntimeConfig{}) 19 | if err != nil { 20 | t.Errorf("SubscribeToEvents() error = %v", err) 21 | return 22 | } 23 | defer sub.Unsubscribe() 24 | 25 | event := <-events 26 | mockevent := string(event) 27 | if mockevent != "1" { 28 | t.Errorf("SubscribeToEvents() got unexpected first message = %v", mockevent) 29 | return 30 | } 31 | event = <-events 32 | mockevent = string(event) 33 | if mockevent != "2" { 34 | t.Errorf("SubscribeToEvents() got unexpected second message = %v", mockevent) 35 | } 36 | }) 37 | } 38 | 39 | func TestSendPostRequest(t *testing.T) { 40 | t.Run("succeeds on normal response", func(t *testing.T) { 41 | u := *rpcMockUrl 42 | u.Path = "/test/2" 43 | 44 | _, err := sendPostRequest(u.String(), TestsMockManager{}.GetTriggerJson()) 45 | if err != nil { 46 | t.Errorf("sendGetRequest() got unexpected error = %v", err) 47 | return 48 | } 49 | }) 50 | 51 | t.Run("fails on bad status", func(t *testing.T) { 52 | u := *rpcMockUrl 53 | u.Path = "/fails" 54 | 55 | _, err := sendPostRequest(u.String(), TestsMockManager{}.GetTriggerJson()) 56 | if err == nil { 57 | t.Error("sendGetRequest() expected error, but got nil") 58 | return 59 | } 60 | }) 61 | } 62 | 63 | func TestRpcSubscriber_Test(t *testing.T) { 64 | type fields struct { 65 | Endpoint string 66 | Manager JsonManager 67 | } 68 | tests := []struct { 69 | name string 70 | fields fields 71 | wantErr bool 72 | }{ 73 | { 74 | "succeeds connecting to valid endpoint", 75 | fields{Endpoint: rpcMockUrl.String(), Manager: TestsMockManager{}}, 76 | false, 77 | }, 78 | { 79 | "fails connecting to invalid endpoint", 80 | fields{Endpoint: "http://localhost:9999/invalid", Manager: TestsMockManager{}}, 81 | true, 82 | }, 83 | } 84 | for _, tt := range tests { 85 | t.Run(tt.name, func(t *testing.T) { 86 | rpc := RpcSubscriber{ 87 | Endpoint: tt.fields.Endpoint, 88 | Manager: tt.fields.Manager, 89 | } 90 | if err := rpc.Test(); (err != nil) != tt.wantErr { 91 | t.Errorf("Test() error = %v, wantErr %v", err, tt.wantErr) 92 | } 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /subscriber/subscriber.go: -------------------------------------------------------------------------------- 1 | // Package subscriber holds logic to communicate between the 2 | // external initiator service and the external endpoints it 3 | // subscribes to. 4 | package subscriber 5 | 6 | import "github.com/smartcontractkit/external-initiator/store" 7 | 8 | // Type holds the connection type for the subscription 9 | type Type int 10 | 11 | const ( 12 | // WS are connections made over WebSocket 13 | WS Type = iota 14 | // RPC are connections made by POSTing a JSON payload 15 | // to the external endpoint. 16 | RPC 17 | // Client are connections encapsulated in its 18 | // entirety by the blockchain implementation. 19 | Client 20 | // Unknown is just a placeholder for when 21 | // it cannot be determined how connections 22 | // should be made. When this is returned, 23 | // it should be considered an error. 24 | Unknown 25 | ) 26 | 27 | // SubConfig holds the configuration required to connect 28 | // to the external endpoint. 29 | type SubConfig struct { 30 | Endpoint string 31 | } 32 | 33 | // Event is the individual event that occurs during 34 | // the subscription. 35 | type Event []byte 36 | 37 | // JsonManager holds the interface for generating blockchain 38 | // specific payloads and parsing the response for the 39 | // appropriate blockchain. 40 | type JsonManager interface { 41 | // Get JSON payload to send when opening a new subscription 42 | GetTriggerJson() []byte 43 | // Parse the response returned after sending GetTriggerJson() 44 | ParseResponse(data []byte) ([]Event, bool) 45 | // Get JSON payload to send when testing a connection 46 | GetTestJson() []byte 47 | // Parse the response returned after sending GetTestJson() 48 | ParseTestResponse(data []byte) error 49 | } 50 | 51 | // ISubscription holds the interface for interacting 52 | // with an active subscription. 53 | type ISubscription interface { 54 | // Unsubscribe closes the connection to the external endpoint 55 | // and stops any processes related to this subscription. 56 | Unsubscribe() 57 | } 58 | 59 | // ISubscriber holds the interface for interacting 60 | // with a not-yet-active subscription. 61 | type ISubscriber interface { 62 | // SubscribeToEvents subscribes to events using the endpoint and configuration 63 | // as set in ISubscriber. All events will be sent in the channel. If anything is 64 | // passed as a param after channel, it will not expect to receive any confirmation 65 | // message after opening the initial subscription. 66 | SubscribeToEvents(channel chan<- Event, runtimeConfig store.RuntimeConfig) (ISubscription, error) 67 | // Test attempts to open a connection using the endpoint and configuration 68 | // as set in ISubscriber. If connection is succesful, it sends GetTestJson() as a payload 69 | // and attempts to parse response with ParseTestResponse(). 70 | Test() error 71 | } 72 | 73 | // IParser holds the interface for parsing data 74 | // from the external endpoint into an array of Events 75 | // based on the blockchain's parser. 76 | type IParser interface { 77 | ParseResponse(data []byte) ([]Event, bool) 78 | } 79 | -------------------------------------------------------------------------------- /subscriber/subscriber_test.go: -------------------------------------------------------------------------------- 1 | package subscriber 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "os" 10 | "testing" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/smartcontractkit/external-initiator/eitest" 14 | ) 15 | 16 | var rpcMockUrl *url.URL 17 | var wsMockUrl *url.URL 18 | 19 | type TestsMockManager struct { 20 | confirmation bool 21 | } 22 | 23 | func (m TestsMockManager) ParseResponse(data []byte) ([]Event, bool) { 24 | return []Event{data}, true 25 | } 26 | 27 | func (m TestsMockManager) GetTriggerJson() []byte { 28 | if m.confirmation { 29 | return []byte(`true`) 30 | } 31 | return []byte(`false`) 32 | } 33 | 34 | func (m TestsMockManager) GetTestJson() []byte { 35 | return nil 36 | } 37 | 38 | func (m TestsMockManager) ParseTestResponse(data []byte) error { 39 | return nil 40 | } 41 | 42 | func TestMain(m *testing.M) { 43 | responses := make(map[string]int) 44 | 45 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 | if r.URL.Path == "/fails" { 47 | w.WriteHeader(http.StatusForbidden) 48 | return 49 | } 50 | 51 | responses[r.URL.Path] = responses[r.URL.Path] + 1 52 | w.WriteHeader(http.StatusOK) 53 | _, _ = w.Write([]byte(fmt.Sprint(responses[r.URL.Path]))) 54 | })) 55 | defer ts.Close() 56 | 57 | u, err := url.Parse(ts.URL) 58 | rpcMockUrl = u 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | ws := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 64 | var c *websocket.Conn 65 | c, err = upgrader.Upgrade(w, r, nil) 66 | if err != nil { 67 | log.Print("upgrade:", err) 68 | return 69 | } 70 | defer eitest.MustClose(c) 71 | for { 72 | var mt int 73 | var message []byte 74 | mt, message, err = c.ReadMessage() 75 | if err != nil { 76 | log.Println("read:", err) 77 | break 78 | } 79 | log.Printf("recv: %s", message) 80 | 81 | switch string(message) { 82 | case "true": 83 | // Send confirmation message 84 | err = c.WriteMessage(mt, []byte("confirmation")) 85 | if err != nil { 86 | log.Println("write:", err) 87 | return 88 | } 89 | case "close": 90 | // Close connection prematurely 91 | return 92 | } 93 | 94 | // Send event message 95 | err = c.WriteMessage(mt, []byte("event")) 96 | if err != nil { 97 | log.Println("write:", err) 98 | return 99 | } 100 | } 101 | })) 102 | defer ws.Close() 103 | 104 | wsMockUrl, err = url.Parse(ws.URL) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | wsMockUrl.Scheme = "ws" 109 | 110 | code := m.Run() 111 | os.Exit(code) 112 | } 113 | -------------------------------------------------------------------------------- /subscriber/ws.go: -------------------------------------------------------------------------------- 1 | package subscriber 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/smartcontractkit/chainlink/core/logger" 9 | "github.com/smartcontractkit/external-initiator/store" 10 | ) 11 | 12 | // WebsocketSubscriber holds the configuration for 13 | // a not-yet-active WS subscription. 14 | type WebsocketSubscriber struct { 15 | Endpoint string 16 | Manager JsonManager 17 | } 18 | 19 | // Test sends a opens a WS connection to the endpoint. 20 | func (wss WebsocketSubscriber) Test() error { 21 | c, _, err := websocket.DefaultDialer.Dial(wss.Endpoint, nil) 22 | if err != nil { 23 | return err 24 | } 25 | defer logger.ErrorIfCalling(c.Close) 26 | 27 | testPayload := wss.Manager.GetTestJson() 28 | if testPayload == nil { 29 | return nil 30 | } 31 | 32 | resp := make(chan []byte) 33 | 34 | go func() { 35 | var body []byte 36 | _, body, err = c.ReadMessage() 37 | if err != nil { 38 | close(resp) 39 | } 40 | resp <- body 41 | }() 42 | 43 | err = c.WriteMessage(websocket.BinaryMessage, testPayload) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // Set timeout for response to 5 seconds 49 | t := time.NewTimer(5 * time.Second) 50 | defer t.Stop() 51 | 52 | select { 53 | case <-t.C: 54 | return errors.New("timeout from test payload") 55 | case body, ok := <-resp: 56 | if !ok { 57 | return errors.New("failed reading test response from WS endpoint") 58 | } 59 | return wss.Manager.ParseTestResponse(body) 60 | } 61 | } 62 | 63 | type wsConn struct { 64 | connection *websocket.Conn 65 | closing bool 66 | } 67 | 68 | type websocketSubscription struct { 69 | conn *wsConn 70 | events chan<- Event 71 | confirmed bool 72 | manager JsonManager 73 | endpoint string 74 | } 75 | 76 | func (wss websocketSubscription) Unsubscribe() { 77 | logger.Info("Unsubscribing from WS endpoint", wss.endpoint) 78 | wss.conn.closing = true 79 | _ = wss.conn.connection.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 80 | _ = wss.conn.connection.Close() 81 | } 82 | 83 | func (wss websocketSubscription) forceClose() { 84 | wss.conn.closing = false 85 | _ = wss.conn.connection.Close() 86 | } 87 | 88 | func (wss websocketSubscription) readMessages() { 89 | for { 90 | _, message, err := wss.conn.connection.ReadMessage() 91 | if err != nil { 92 | _ = wss.conn.connection.Close() 93 | if !wss.conn.closing { 94 | wss.reconnect() 95 | return 96 | } 97 | return 98 | } 99 | 100 | // First message is a confirmation with the subscription id 101 | // Ignore this 102 | if !wss.confirmed { 103 | wss.confirmed = true 104 | continue 105 | } 106 | 107 | events, ok := wss.manager.ParseResponse(message) 108 | if !ok { 109 | continue 110 | } 111 | 112 | for _, event := range events { 113 | wss.events <- event 114 | } 115 | } 116 | } 117 | 118 | func (wss websocketSubscription) init() { 119 | go wss.readMessages() 120 | 121 | err := wss.conn.connection.WriteMessage(websocket.TextMessage, wss.manager.GetTriggerJson()) 122 | if err != nil { 123 | wss.forceClose() 124 | return 125 | } 126 | 127 | logger.Infof("Connected to %s\n", wss.endpoint) 128 | } 129 | 130 | func (wss websocketSubscription) reconnect() { 131 | logger.Warnf("Lost WS connection to %s\nRetrying in %vs", wss.endpoint, 3) 132 | time.Sleep(3 * time.Second) 133 | 134 | c, _, err := websocket.DefaultDialer.Dial(wss.endpoint, nil) 135 | if err != nil { 136 | logger.Error("Reconnect failed:", err) 137 | wss.reconnect() 138 | return 139 | } 140 | 141 | wss.conn.connection = c 142 | wss.init() 143 | } 144 | 145 | func (wss WebsocketSubscriber) SubscribeToEvents(channel chan<- Event, _ store.RuntimeConfig) (ISubscription, error) { 146 | logger.Infof("Connecting to WS endpoint: %s\n", wss.Endpoint) 147 | 148 | c, _, err := websocket.DefaultDialer.Dial(wss.Endpoint, nil) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | subscription := websocketSubscription{ 154 | conn: &wsConn{connection: c}, 155 | events: channel, 156 | confirmed: false, 157 | manager: wss.Manager, 158 | endpoint: wss.Endpoint, 159 | } 160 | subscription.init() 161 | 162 | return subscription, nil 163 | } 164 | -------------------------------------------------------------------------------- /subscriber/ws_test.go: -------------------------------------------------------------------------------- 1 | package subscriber 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gorilla/websocket" 7 | "github.com/smartcontractkit/external-initiator/store" 8 | ) 9 | 10 | var upgrader = websocket.Upgrader{} // use default options 11 | 12 | func TestWebsocketSubscriber_SubscribeToEvents(t *testing.T) { 13 | t.Run("subscribes and ignores confirmation message", func(t *testing.T) { 14 | wss := WebsocketSubscriber{Endpoint: wsMockUrl.String(), Manager: TestsMockManager{true}} 15 | events := make(chan Event) 16 | 17 | sub, err := wss.SubscribeToEvents(events, store.RuntimeConfig{}) 18 | if err != nil { 19 | t.Errorf("SubscribeToEvents() error = %v", err) 20 | return 21 | } 22 | defer sub.Unsubscribe() 23 | 24 | event := <-events 25 | mockevent := string(event) 26 | 27 | if mockevent == "confirmation" { 28 | t.Error("SubscribeToEvents() got unexpected confirmation") 29 | return 30 | } 31 | 32 | if mockevent != "event" { 33 | t.Errorf("SubscribeToEvents() got unexpected message = %v", mockevent) 34 | return 35 | } 36 | }) 37 | 38 | t.Run("fails subscribe to invalid URL", func(t *testing.T) { 39 | wss := WebsocketSubscriber{Endpoint: "", Manager: TestsMockManager{false}} 40 | events := make(chan Event) 41 | 42 | sub, err := wss.SubscribeToEvents(events, store.RuntimeConfig{}) 43 | if err == nil { 44 | sub.Unsubscribe() 45 | t.Error("SubscribeToEvents() expected error, but got nil") 46 | return 47 | } 48 | }) 49 | 50 | t.Run("subscribes and attempts reconnect", func(t *testing.T) { 51 | wss := WebsocketSubscriber{Endpoint: wsMockUrl.String(), Manager: &TestsReconnectManager{}} 52 | events := make(chan Event) 53 | 54 | sub, err := wss.SubscribeToEvents(events, store.RuntimeConfig{}) 55 | if err != nil { 56 | t.Errorf("SubscribeToEvents() error = %v", err) 57 | return 58 | } 59 | defer sub.Unsubscribe() 60 | 61 | event := <-events 62 | mockevent := string(event) 63 | 64 | if mockevent != "event" { 65 | t.Errorf("SubscribeToEvents() got unexpected message = %v", mockevent) 66 | return 67 | } 68 | }) 69 | } 70 | 71 | type TestsReconnectManager struct { 72 | connections int 73 | } 74 | 75 | func (m TestsReconnectManager) ParseResponse(data []byte) ([]Event, bool) { 76 | return []Event{data}, true 77 | } 78 | 79 | func (m *TestsReconnectManager) GetTriggerJson() []byte { 80 | count := m.connections 81 | m.connections++ 82 | switch count { 83 | case 0: 84 | return []byte(`close`) 85 | default: 86 | return []byte(`true`) 87 | } 88 | } 89 | 90 | func (m TestsReconnectManager) GetTestJson() []byte { 91 | return nil 92 | } 93 | 94 | func (m TestsReconnectManager) ParseTestResponse(data []byte) error { 95 | return nil 96 | } 97 | 98 | func TestWebsocketSubscriber_Test(t *testing.T) { 99 | type fields struct { 100 | Endpoint string 101 | } 102 | tests := []struct { 103 | name string 104 | fields fields 105 | wantErr bool 106 | }{ 107 | { 108 | "succeeds connecting to valid endpoint", 109 | fields{Endpoint: wsMockUrl.String()}, 110 | false, 111 | }, 112 | { 113 | "fails connecting to invalid endpoint", 114 | fields{Endpoint: "ws://localhost:9999/invalid"}, 115 | true, 116 | }, 117 | } 118 | for _, tt := range tests { 119 | t.Run(tt.name, func(t *testing.T) { 120 | wss := WebsocketSubscriber{ 121 | Endpoint: tt.fields.Endpoint, 122 | Manager: TestsMockManager{}, 123 | } 124 | if err := wss.Test(); (err != nil) != tt.wantErr { 125 | t.Errorf("Test() error = %v, wantErr %v", err, tt.wantErr) 126 | } 127 | }) 128 | } 129 | } 130 | --------------------------------------------------------------------------------