diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 000000000..e5b6d8d6a
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets)
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 000000000..634bfce58
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "restricted",
+ "baseBranch": "main",
+ "updateInternalDependencies": "patch",
+ "ignore": []
+}
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..6751428a9
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,20 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "plugins": [
+ "@typescript-eslint"
+ ],
+ "rules": {
+ }
+}
diff --git a/.github/actions/build-test-image/action.yml b/.github/actions/build-test-image/action.yml
new file mode 100644
index 000000000..29c5fa853
--- /dev/null
+++ b/.github/actions/build-test-image/action.yml
@@ -0,0 +1,53 @@
+name: Build Test Image
+description: A composite action that allows building and publishing the test remote runner image
+
+inputs:
+ tag:
+ description: The tag to use by default and to use for checking image existance
+ default: ${{ github.sha }}
+ required: false
+ other_tags:
+ description: Other tags to push if needed
+ required: false
+ QA_AWS_ROLE_TO_ASSUME:
+ description: The AWS role to assume as the CD user, if any. Used in configuring the docker/login-action
+ required: true
+ QA_AWS_REGION:
+ description: The AWS region the ECR repository is located in, should only be needed for public ECR repositories, used in configuring docker/login-action
+ required: true
+ QA_AWS_ACCOUNT_NUMBER:
+ description: The AWS region the ECR repository is located in, should only be needed for public ECR repositories, used in configuring docker/login-action
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Install Cairo
+ uses: ./.github/actions/install-cairo
+ - name: Check if image exists
+ id: check-image
+ uses: smartcontractkit/chainlink-github-actions/docker/image-exists@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19
+ with:
+ repository: chainlink-starknet-tests
+ tag: ${{ inputs.tag }}
+ AWS_REGION: ${{ inputs.QA_AWS_REGION }}
+ AWS_ROLE_TO_ASSUME: ${{ inputs.QA_AWS_ROLE_TO_ASSUME }}
+ - name: Build and Publish Test Runner
+ if: steps.check-image.outputs.exists == 'false'
+ uses: smartcontractkit/chainlink-github-actions/docker/build-push@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19
+ with:
+ tags: |
+ ${{ inputs.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ inputs.QA_AWS_REGION }}.amazonaws.com/chainlink-starknet-tests:${{ inputs.tag }}
+ ${{ inputs.other_tags }}
+ file: ./integration-tests/test.Dockerfile
+ build-args: |
+ SUITES="smoke soak"
+ AWS_REGION: ${{ inputs.QA_AWS_REGION }}
+ AWS_ROLE_TO_ASSUME: ${{ inputs.QA_AWS_ROLE_TO_ASSUME }}
+ - name: Print Image Built
+ env:
+ INPUTS_TAG: ${{ inputs.tag }}
+ shell: sh
+ run: |
+ echo "### chainlink-starknet-tests image tag for this test run :ship:" >> $GITHUB_STEP_SUMMARY
+ echo "\`${INPUTS_TAG}\`" >> $GITHUB_STEP_SUMMARY
diff --git a/.github/actions/install-cairo/action.yml b/.github/actions/install-cairo/action.yml
new file mode 100644
index 000000000..d3f1524d6
--- /dev/null
+++ b/.github/actions/install-cairo/action.yml
@@ -0,0 +1,33 @@
+name: Install Cairo and Scarb
+description: A composite action that installs cairo and scarb binaries
+
+inputs:
+ cairo_version:
+ description: Cairo release version
+ default: "v2.6.4"
+ required: false
+ scarb_version:
+ description: Scarb release version
+ default: "v2.6.5"
+ required: false
+
+runs:
+ using: composite
+ steps:
+ - name: Setup Cairo for Linux
+ id: install-cairo
+ shell: bash
+ run: |
+ wget https://github.com/starkware-libs/cairo/releases/download/${{ inputs.cairo_version }}/release-x86_64-unknown-linux-musl.tar.gz
+ tar -xvzf release-x86_64-unknown-linux-musl.tar.gz
+ mv -vf cairo cairo-build
+ echo "$GITHUB_WORKSPACE/cairo-build/bin" >> $GITHUB_PATH
+
+ - name: Setup Scarb for Linux
+ id: install-scarb
+ shell: bash
+ run: |
+ wget https://github.com/software-mansion/scarb/releases/download/${{ inputs.scarb_version }}/scarb-${{ inputs.scarb_version }}-x86_64-unknown-linux-musl.tar.gz
+ tar -xvzf scarb-${{ inputs.scarb_version }}-x86_64-unknown-linux-musl.tar.gz
+ mv -vf scarb-${{ inputs.scarb_version }}-x86_64-unknown-linux-musl scarb-build
+ echo "$GITHUB_WORKSPACE/scarb-build/bin" >> $GITHUB_PATH
diff --git a/.github/actions/install-starknet-foundry/action.yml b/.github/actions/install-starknet-foundry/action.yml
new file mode 100644
index 000000000..02b9b59dc
--- /dev/null
+++ b/.github/actions/install-starknet-foundry/action.yml
@@ -0,0 +1,19 @@
+name: Install Starknet Foundry (snforge and sncast)
+description: A composite action that installs the snforge and sncast binaries
+
+inputs:
+ starknet_foundry_version:
+ description: Starknet Foundry release version
+ default: "0.21.0"
+ required: false
+
+runs:
+ using: composite
+ steps:
+ - name: Setup Starknet Foundry for Linux
+ id: install-starknet-foundry
+ shell: bash
+ run: |
+ curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh
+ snfoundryup -v ${{ inputs.starknet_foundry_version }}
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..ec19129b5
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml
new file mode 100644
index 000000000..f50209903
--- /dev/null
+++ b/.github/workflows/changesets.yml
@@ -0,0 +1,46 @@
+name: Changesets
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ changesets:
+ name: Changesets
+ runs-on: ubuntu-latest
+ steps:
+ # Checkout this repository
+ - name: Checkout Repo
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+ # Install nix
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ # Install dependencies using yarn
+ - name: Install Dependencies
+ run: nix develop -c yarn install --frozen-lockfile
+ # Create PR that will update versions or trigger publish
+ - name: Create Release Pull Request
+ uses: changesets/action@aba318e9165b45b7948c60273e0b72fce0a64eb9 # v1.4.7
+ id: changesets
+ with:
+ publish: nix develop -c yarn release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+ # Dispatch Relayer release
+ - name: Release relayer
+ run: gh workflow run .github/workflows/release/starknet-relayer.yml
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # Dispatch Gauntlet CLI build & release
+ - name: Build and release Gauntlet CLI
+ run: gh workflow run .github/workflows/release/starknet-gauntlet-cli.yml
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml
new file mode 100644
index 000000000..58e06e4bc
--- /dev/null
+++ b/.github/workflows/contracts.yml
@@ -0,0 +1,45 @@
+name: Contracts
+
+on:
+ push:
+ branches:
+ - develop
+ - main
+ pull_request:
+
+jobs:
+ contracts_run_ts_tests:
+ name: Run Typescript Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+
+ - name: Install Cairo
+ uses: ./.github/actions/install-cairo
+
+ - name: Test
+ run: nix develop -c make test-ts-contracts
+
+ contracts_run_cairo_tests:
+ name: Run Cairo Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+
+ - name: Install Cairo
+ uses: ./.github/actions/install-cairo
+
+ - name: Test
+ run: nix develop -c make test-cairo-contracts
diff --git a/.github/workflows/e2e_custom_cl.yml b/.github/workflows/e2e_custom_cl.yml
deleted file mode 100644
index 02ceb83c7..000000000
--- a/.github/workflows/e2e_custom_cl.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-name: E2E Custom image build tests
-on:
- push:
- workflow_dispatch:
- inputs:
- cl_branch_ref:
- description: Chainlink repo branch to integrate with
- required: true
- default: develop
- type: string
-
-jobs:
- e2e_custom_build_custom_chainlink_image:
- name: E2E Custom Build Custom CL Image
- # target branch can't be set as var, it's from where we getting pipeline code
- uses: smartcontractkit/chainlink/.github/workflows/build-custom.yml@develop
- with:
- cl_repo: smartcontractkit/chainlink
- cl_ref: ${{ github.event.inputs.cl_branch_ref }}
- # commit of the caller branch
- dep_starknet_sha: ${{ github.sha }}
- secrets:
- AWS_ACCESS_KEY_ID: ${{ secrets.QA_AWS_ACCESS_KEY_ID }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.QA_AWS_SECRET_KEY }}
- AWS_REGION: ${{ secrets.QA_AWS_REGION }}
- AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }}
- QA_KUBECONFIG: ${{ secrets.QA_KUBECONFIG }}
- QA_PRIVATE_GHA_PULL: ${{ secrets.QA_PRIVATE_GHA_PULL }}
- e2e_cutsom_run_smoke_tests:
- name: E2E Custom Run Smoke Tests
- runs-on: ubuntu-latest
- needs: [e2e_custom_build_custom_chainlink_image]
- env:
- CGO_ENABLED: 1
- steps:
- - name: Checkout the repo
- uses: actions/checkout@v2
- - name: Setup go ${{ steps.tool-versions.outputs.golang_version }}
- uses: actions/setup-go@v2
- with:
- go-version: '1.18'
- - name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v1
- with:
- aws-access-key-id: ${{ secrets.QA_AWS_ACCESS_KEY_ID }}
- aws-secret-access-key: ${{ secrets.QA_AWS_SECRET_KEY }}
- aws-region: ${{ secrets.QA_AWS_REGION }}
- role-to-assume: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }}
- role-duration-seconds: 3600
- - name: Set Kubernetes Context
- uses: azure/k8s-set-context@v1
- with:
- method: kubeconfig
- kubeconfig: ${{ secrets.QA_KUBECONFIG }}
- - name: Cache Vendor Packages
- uses: actions/cache@v2
- id: cache-packages
- with:
- path: |
- ~/.cache/go-build
- ~/go/pkg/mod
- key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ runner.os }}-go-
- - name: Download Go Vendor Packages
- if: steps.cache-packages.outputs.cache-hit != 'true'
- run: go mod download
- - name: Install Ginkgo CLI
- run: go install github.com/onsi/ginkgo/v2/ginkgo@v2.1.3
- - name: Run Tests
- env:
- CHAINLINK_IMAGE: 795953128386.dkr.ecr.us-west-2.amazonaws.com/chainlink
- CHAINLINK_VERSION: custom.${{ github.sha }}
- run: |
- export PATH=$PATH:$(go env GOPATH)/bin
- make e2e_test
- - name: Publish Test Results
- uses: mikepenz/action-junit-report@v2
- if: always()
- with:
- report_paths: "./tests-smoke-report.xml"
- github_token: ${{ secrets.GITHUB_TOKEN }}
- check_name: Smoke Test Results
- - name: Publish Artifacts
- if: failure()
- uses: actions/upload-artifact@v2.2.4
- with:
- name: test-logs
- path: /home/runner/work/chainlink-starknet/chainlink-starknet/tests/e2e/logs
\ No newline at end of file
diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml
new file mode 100644
index 000000000..4a15c9c6a
--- /dev/null
+++ b/.github/workflows/examples.yml
@@ -0,0 +1,30 @@
+name: Example Contracts
+
+on:
+ push:
+ branches:
+ - develop
+ - main
+ pull_request:
+
+jobs:
+ run_examples_tests:
+ name: Run Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+
+ - name: Install Cairo
+ uses: ./.github/actions/install-cairo
+
+ - name: Install Starknet Foundry
+ uses: ./.github/actions/install-starknet-foundry
+
+ - name: Test
+ run: nix develop -c make test-examples
diff --git a/.github/workflows/gauntlet.yml b/.github/workflows/gauntlet.yml
deleted file mode 100644
index a6e89bd11..000000000
--- a/.github/workflows/gauntlet.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-name: gauntlet
-
-on:
- push:
-
-jobs:
- gauntlet_build:
- name: Gauntlet Build
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@f25a3a9f25bd5f4c5d77189cab02ff357b5aedeb # v2.4.1
- - uses: smartcontractkit/tool-versions-to-env-action@v1.0.7
- id: tool-versions
- - name: Setup Node ${{ steps.tool-versions.outputs.nodejs_version }}
- uses: actions/setup-node@v2
- with:
- node-version: ${{ steps.tool-versions.outputs.nodejs_version }}
- - name: Install
- run: yarn install --frozen-lockfile
- - name: Build
- run: yarn build
- - name: Run Gauntlet
- run: yarn gauntlet
-
- gauntlet_format:
- name: Gauntlet Format
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@f25a3a9f25bd5f4c5d77189cab02ff357b5aedeb # v2.4.1
- - uses: smartcontractkit/tool-versions-to-env-action@v1.0.7
- id: tool-versions
- - name: Setup Node ${{ steps.tool-versions.outputs.nodejs_version }}
- uses: actions/setup-node@v2
- with:
- node-version: ${{ steps.tool-versions.outputs.nodejs_version }}
- - name: Install
- run: yarn install --frozen-lockfile
- - name: Lint
- run: yarn lint:format
-
- gauntlet_run_tests:
- name: Gauntlet Run Tests
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@f25a3a9f25bd5f4c5d77189cab02ff357b5aedeb # v2.4.1
- - uses: smartcontractkit/tool-versions-to-env-action@v1.0.7
- id: tool-versions
- - name: Setup Node ${{ steps.tool-versions.outputs.nodejs_version }}
- uses: actions/setup-node@v2
- with:
- node-version: ${{ steps.tool-versions.outputs.nodejs_version }}
- - name: Install
- run: yarn install --frozen-lockfile
- - name: test:ci
- run: yarn test:ci
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
new file mode 100644
index 000000000..4bce6fcbc
--- /dev/null
+++ b/.github/workflows/golangci-lint.yml
@@ -0,0 +1,107 @@
+name: golangci_lint
+
+on:
+ push:
+
+jobs:
+ golangci-lint-version:
+ name: Get golangci-lint version to from nix
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ - name: Parse version
+ id: parse
+ run: |
+ long=$(nix develop -c golangci-lint version | grep "golangci-lint has version")
+ stringArray=($long)
+ version=$(echo "${stringArray[3]}")
+ echo "version found: ${version}"
+ echo "version=${version}" >>$GITHUB_OUTPUT
+ outputs:
+ version: ${{ steps.parse.outputs.version }}
+
+ golang_lint_relayer:
+ name: Golang Lint Relayer
+ runs-on: ubuntu-latest
+ needs: [golangci-lint-version]
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ - name: Lint relayer
+ run: nix develop -c make lint-go-relayer
+ - name: Print Report
+ if: failure()
+ run: cat ./relayer/golangci-lint-relayer-report.xml
+ - name: Store Golangci lint relayer report artifact
+ if: always()
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ with:
+ name: golangci-lint-relayer-report
+ path: ./relayer/golangci-lint-relayer-report.xml
+
+ golang_lint_ops:
+ name: Golang Lint Ops
+ runs-on: ubuntu-latest
+ needs: [golangci-lint-version]
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ - name: Lint ops
+ run: nix develop -c make lint-go-ops
+ - name: Print Report
+ if: failure()
+ run: cat ./ops/golangci-lint-ops-report.xml
+ - name: Store Golangci lint ops report artifact
+ if: always()
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ with:
+ name: golangci-lint-ops-report
+ path: ./ops/golangci-lint-ops-report.xml
+
+ golang_lint_integration_tests:
+ name: Golang Lint Integration Tests
+ runs-on: ubuntu-latest
+ needs: [golangci-lint-version]
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ - name: Lint integration-tests
+ run: nix develop -c make lint-go-test
+ - name: Print Report
+ if: failure()
+ run: cat ./integration-tests/golangci-lint-integration-tests-report.xml
+ - name: Store Golangci lint integration tests report artifact
+ if: always()
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ with:
+ name: golangci-lint-integration-tests-report
+ path: ./integration-tests/golangci-lint-integration-tests-report.xml
+ # Note: I could not figure out why the golangci-lint-action would not work even though it is technically running the same as above, error message is this:
+ # Running [/home/runner/golangci-lint-1.50.1-linux-amd64/golangci-lint run --out-format=github-actions --path-prefix=integration-tests --exclude=dot-imports] in [/home/runner/work/chainlink-starknet/chainlink-starknet/integration-tests] ...
+ # level=warning msg="[runner] Can't run linter goanalysis_metalinter: inspect: failed to load package client: could not load export data: no export data for \"github.com/smartcontractkit/chainlink-testing-framework/client\""
+ # level=error msg="Running error: 1 error occurred:\n\t* can't run linter goanalysis_metalinter: inspect: failed to load package client: could not load export data: no export data for \"github.com/smartcontractkit/chainlink-testing-framework/client\"\n\n"
+ # - name: golangci-lint
+ # uses: golangci/golangci-lint-action@v3
+ # env:
+ # CGO_ENABLED: 1
+ # with:
+ # version: v${{ needs.golangci-lint-version.outputs.version }}
+ # working-directory: integration-tests
+ # args: --exclude=dot-imports
diff --git a/.github/workflows/integration-tests-publish.yml b/.github/workflows/integration-tests-publish.yml
new file mode 100644
index 000000000..3bbd36fc8
--- /dev/null
+++ b/.github/workflows/integration-tests-publish.yml
@@ -0,0 +1,48 @@
+name: Integration Tests Publish
+# Publish the compiled integration tests
+
+on:
+ push:
+ branches:
+ - develop
+
+env:
+ ECR_TAG: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-starknet-tests:develop
+
+jobs:
+ publish-integration-test-image:
+ environment: integration
+ permissions:
+ id-token: write
+ contents: read
+ name: Publish Integration Test Image
+ runs-on: ubuntu-latest
+ steps:
+ - name: Collect Metrics
+ id: collect-gha-metrics
+ uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1
+ with:
+ id: starknet-e2e-publish
+ org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }}
+ basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }}
+ hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }}
+ this-job-name: Publish Integration Test Image
+ continue-on-error: true
+ - name: Checkout the repo
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Build Image
+ uses: ./.github/actions/build-test-image
+ with:
+ other_tags: ${{ env.ECR_TAG }}
+ QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }}
+ QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }}
+ QA_AWS_ACCOUNT_NUMBER: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}
+ - name: Notify Slack
+ # Only run this notification for merge to develop failures
+ if: failure() && github.event_name != 'workflow_dispatch'
+ uses: slackapi/slack-github-action@6c661ce58804a1a20f6dc5fbee7f0381b469e001 # v1.25.0
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_API_KEY }}
+ with:
+ channel-id: "#team-test-tooling-internal"
+ slack-message: ":x: :mild-panic-intensifies: Publish Integration Test Image failed: \n${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }}\nRepository: Starknet\n${{ format('Notifying ', secrets.GUARDIAN_SLACK_NOTIFICATION_HANDLE)}}"
diff --git a/.github/workflows/integration-tests-smoke.yml b/.github/workflows/integration-tests-smoke.yml
new file mode 100644
index 000000000..df06af620
--- /dev/null
+++ b/.github/workflows/integration-tests-smoke.yml
@@ -0,0 +1,186 @@
+name: Integration Tests - Smoke
+
+on:
+ pull_request:
+ workflow_dispatch:
+ inputs:
+ cl_branch_ref:
+ description: Chainlink repo branch to integrate with
+ required: true
+ default: develop
+ type: string
+
+# Only run 1 of this workflow at a time per PR
+concurrency:
+ group: integration-tests-starknet-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ TEST_LOG_LEVEL: debug
+ CL_ECR: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink
+ ENV_JOB_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-starknet-tests:${{ github.sha }}
+
+jobs:
+ build_chainlink_image:
+ name: Build Chainlink Image ${{matrix.image.name}}
+ runs-on: ubuntu-latest
+ environment: integration
+ permissions:
+ id-token: write
+ contents: read
+ strategy:
+ matrix:
+ image:
+ - name: ""
+ dockerfile: core/chainlink.Dockerfile
+ tag-suffix: ""
+ - name: (plugins)
+ dockerfile: plugins/chainlink.Dockerfile
+ tag-suffix: -plugins
+ steps:
+ - name: Collect Metrics
+ id: collect-gha-metrics
+ uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1
+ with:
+ id: starknet-e2e-build${{ matrix.image.tag-suffix }}
+ org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }}
+ basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }}
+ hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }}
+ this-job-name: Build Chainlink Image${{matrix.image.name}}
+ continue-on-error: true
+ - name: Check if image exists
+ id: check-image
+ uses: smartcontractkit/chainlink-github-actions/docker/image-exists@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19
+ with:
+ repository: chainlink
+ tag: starknet.${{ github.sha }}${{ matrix.image.tag-suffix }}
+ AWS_REGION: ${{ secrets.QA_AWS_REGION }}
+ AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }}
+ - name: Build Image ${{ matrix.image.name }}
+ if: steps.check-image.outputs.exists == 'false'
+ uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/build-image@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19
+ with:
+ cl_repo: smartcontractkit/chainlink
+ cl_ref: ${{ github.event.inputs.cl_branch_ref }}
+ should_checkout: true
+ cl_dockerfile: ${{ matrix.image.dockerfile }}
+ # commit of the caller branch
+ dep_starknet_sha: ${{ github.event.pull_request.head.sha || github.sha }}
+ push_tag: ${{ env.CL_ECR }}:starknet.${{ github.sha }}${{ matrix.image.tag-suffix }}
+ QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }}
+ QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }}
+ QA_PRIVATE_GHA_PULL: ${{ secrets.QA_PRIVATE_GHA_PULL }}
+ - name: Print Chainlink Image Built
+ run: |
+ echo "### chainlink image tag used for this test run :link:" >> $GITHUB_STEP_SUMMARY
+ echo "\`starknet.${{ github.sha }}${{ matrix.image.tag-suffix }}\`" >> $GITHUB_STEP_SUMMARY
+
+ build_test_image:
+ environment: integration
+ permissions:
+ id-token: write
+ contents: read
+ name: Build Test Image
+ runs-on: ubuntu20.04-32cores-128GB
+ steps:
+ - name: Collect Metrics
+ id: collect-gha-metrics
+ uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1
+ with:
+ id: starknet-e2e-build-test-image
+ org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }}
+ basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }}
+ hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }}
+ this-job-name: Build Test Image
+ continue-on-error: true
+ - name: Checkout the repo
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ ref: ${{ github.sha }}
+ - name: Build Image
+ uses: ./.github/actions/build-test-image
+ with:
+ QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }}
+ QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }}
+ QA_AWS_ACCOUNT_NUMBER: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}
+
+ run_tests:
+ name: Run Smoke Tests ${{matrix.image.name}}
+ runs-on: ubuntu20.04-16cores-64GB
+ needs: [ build_chainlink_image, build_test_image ]
+ environment: integration
+ # these values need to match those used to build the chainlink image
+ strategy:
+ matrix:
+ image:
+ - name: ""
+ tag-suffix: ""
+ test-name: embedded
+ - name: plugins
+ tag-suffix: -plugins
+ test-name: plugins
+ env:
+ INTERNAL_DOCKER_REPO: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com
+ permissions:
+ checks: write
+ pull-requests: write
+ id-token: write
+ contents: read
+ steps:
+ - name: Collect Metrics
+ id: collect-gha-metrics
+ uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1
+ with:
+ id: starknet-e2e-smoke${{ matrix.image.name }}
+ org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }}
+ basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }}
+ hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }}
+ this-job-name: Run Smoke Tests ${{ matrix.image.name }}
+ test-results-file: '{"testType":"go","filePath":"/tmp/gotest.log"}'
+ continue-on-error: true
+ - name: Checkout the repo
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ - name: Install Cairo
+ uses: ./.github/actions/install-cairo
+ - name: Build contracts
+ run: |
+ cd contracts && scarb --profile release build
+ - name: Build gauntlet
+ run: |
+ yarn install && yarn build
+ - name: Generate config overrides
+ run: | # https://github.com/smartcontractkit/chainlink-testing-framework/blob/main/config/README.md
+ cat << EOF > config.toml
+ [ChainlinkImage]
+ image="${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink"
+ version="starknet.${{ github.sha }}${{ matrix.image.tag-suffix }}"
+ [Network]
+ selected_networks=["SIMULATED"]
+ [Common]
+ internal_docker_repo = "${{ env.INTERNAL_DOCKER_REPO }}"
+ stateful_db = false
+ EOF
+ # shellcheck disable=SC2002
+ BASE64_CONFIG_OVERRIDE=$(cat config.toml | base64 -w 0)
+ # shellcheck disable=SC2086
+ echo ::add-mask::$BASE64_CONFIG_OVERRIDE
+ # shellcheck disable=SC2086
+ echo "BASE64_CONFIG_OVERRIDE=$BASE64_CONFIG_OVERRIDE" >> $GITHUB_ENV
+ - name: Run Tests ${{ matrix.image.name }}
+ uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19
+ with:
+ aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}
+ test_command_to_run: nix develop -c sh -c "make test=${{ matrix.image.test-name }} test-integration-smoke-ci"
+ test_download_vendor_packages_command: cd integration-tests && nix develop -c go mod download
+ cl_repo: ${{ env.CL_ECR }}
+ cl_image_tag: starknet.${{ github.sha }}${{ matrix.image.tag-suffix }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ go_mod_path: ./integration-tests/go.mod
+ QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }}
+ QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }}
+ QA_KUBECONFIG: ${{ secrets.QA_KUBECONFIG }}
+ artifacts_location: /home/runner/work/chainlink-starknet/chainlink-starknet/integration-tests/smoke/logs
diff --git a/.github/workflows/integration-tests-soak.yml b/.github/workflows/integration-tests-soak.yml
new file mode 100644
index 000000000..dd8eef97a
--- /dev/null
+++ b/.github/workflows/integration-tests-soak.yml
@@ -0,0 +1,83 @@
+name: Integration Tests - Soak
+on:
+ workflow_dispatch:
+ inputs:
+ base64_config:
+ description: Your .toml file as base64
+ required: true
+ cl_image_tag:
+ description: Core image tag
+ required: true
+ default: develop
+ type: string
+ test_runner_tag:
+ description: Remote runner tag that will run the tests
+ default: develop
+ required: true
+ type: string
+
+env:
+ TEST_LOG_LEVEL: debug
+ CL_ECR: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink
+ ENV_JOB_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-starknet-tests:${{ inputs.test_runner_tag }}
+
+jobs:
+ run_tests:
+ name: Run soak Tests
+ runs-on: ubuntu20.04-16cores-64GB
+ environment: integration
+ env:
+ TEST_SUITE: soak
+ DETACH_RUNNER: true
+ INTERNAL_DOCKER_REPO: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com
+ permissions:
+ checks: write
+ pull-requests: write
+ id-token: write
+ contents: read
+ steps:
+ - name: Collect Metrics
+ id: collect-gha-metrics
+ uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1
+ with:
+ id: starknet-e2e-soak
+ org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }}
+ basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }}
+ hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }}
+ this-job-name: Run soak Tests
+ test-results-file: '{"testType":"go","filePath":"/tmp/gotest.log"}'
+ continue-on-error: true
+ - name: Checkout the repo
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ - name: Install Cairo
+ uses: ./.github/actions/install-cairo
+ - name: Build contracts
+ run: |
+ cd contracts && scarb --profile release build
+ - name: Build gauntlet
+ run: |
+ yarn install && yarn build
+ - name: Mask base64 config
+ # shellcheck disable=SC2086
+ run: |
+ BASE64_CONFIG_OVERRIDE=$(jq -r '.inputs.base64_config' "$GITHUB_EVENT_PATH")
+ echo "::add-mask::$BASE64_CONFIG_OVERRIDE"
+ echo "BASE64_CONFIG_OVERRIDE=$BASE64_CONFIG_OVERRIDE" >> "$GITHUB_ENV"
+ - name: Run Tests
+ uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@fc3e0df622521019f50d772726d6bf8dc919dd38 # v2.3.19
+ with:
+ aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}
+ test_command_to_run: cd ./integration-tests && go test -timeout 24h -count=1 -run TestOCRBasicSoak/embedded ./soak
+ test_download_vendor_packages_command: cd integration-tests && nix develop -c go mod download
+ cl_repo: ${{ env.CL_ECR }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ go_mod_path: ./integration-tests/go.mod
+ QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }}
+ QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }}
+ QA_KUBECONFIG: ${{ secrets.QA_KUBECONFIG }}
+ artifacts_location: /home/runner/work/chainlink-starknet/chainlink-starknet/integration-tests/soak/logs
+
diff --git a/.github/workflows/integration_gauntlet.yml b/.github/workflows/integration_gauntlet.yml
new file mode 100644
index 000000000..069bd92c8
--- /dev/null
+++ b/.github/workflows/integration_gauntlet.yml
@@ -0,0 +1,65 @@
+name: Integration Gauntlet
+
+on:
+ push:
+ branches:
+ - develop
+ - main
+ pull_request:
+
+jobs:
+ gauntlet_eslint:
+ name: Gauntlet ESLint
+ env:
+ CI: true
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ extra_nix_config: "sandbox = false"
+ - name: Cache Nix
+ uses: cachix/cachix-action@v15
+ with:
+ name: chainlink-cosmos
+ authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
+ - run: nix develop -c yarn install --frozen-lockfile
+ - run: nix develop -c yarn eslint
+ - name: Upload eslint report
+ if: always()
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ with:
+ name: gauntlet-eslint-report
+ path: ./eslint-report.json
+
+ integration_gauntlet_run_tests:
+ name: Run Integration Gauntlet Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Collect Metrics
+ id: collect-gha-metrics
+ uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1
+ with:
+ id: starknet-integration-gauntlet
+ org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }}
+ basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }}
+ hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }}
+ this-job-name: Run Integration Gauntlet Tests
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+
+ - name: Install Cairo
+ uses: ./.github/actions/install-cairo
+
+ - name: Test
+ run: nix develop -c make test-integration-gauntlet
+
+ - name: Test - Run Gauntlet CLI via Yarn
+ run: nix develop -c yarn gauntlet
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 000000000..f88be643a
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,34 @@
+name: Lint
+
+on:
+ push:
+ branches:
+ - develop
+ - main
+ pull_request:
+
+jobs:
+ lint_format_check:
+ name: Format Check
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+
+ - name: Install Cairo
+ uses: ./.github/actions/install-cairo
+
+ - name: Install
+ run: nix develop -c yarn install --frozen-lockfile
+
+ # NOTE: Runs outside the nix environment because starknet-devnet still pulls in 0.x cairo which ends up taking precedence.
+ - name: Check Cairo
+ run: make format-cairo-check
+
+ - name: Check Typescript
+ run: nix develop -c make format-ts-check
diff --git a/.github/workflows/monitoring-build-push-ecr.yml b/.github/workflows/monitoring-build-push-ecr.yml
new file mode 100644
index 000000000..7f0becd91
--- /dev/null
+++ b/.github/workflows/monitoring-build-push-ecr.yml
@@ -0,0 +1,53 @@
+name: "Build and push on-chain monitor image to ECR"
+
+on:
+ push:
+ branches:
+ - develop
+ paths:
+ - monitoring/**
+ - relayer/**
+
+jobs:
+ build-and-publish-monitoring:
+ runs-on: ubuntu-latest
+ environment: publish
+ permissions:
+ id-token: write
+ contents: read
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
+ with:
+ role-to-assume: ${{ secrets.AWS_OIDC_IAM_ROLE_ARN }}
+ role-duration-seconds: ${{ secrets.AWS_ROLE_DURATION_SECONDS }}
+ aws-region: ${{ secrets.AWS_REGION }}
+
+ - name: Login to ECR
+ uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
+ with:
+ registry: ${{ secrets.MONITORING_ECR_HOSTNAME }}
+
+ - name: Setup Docker Buildx
+ uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
+
+ - name: Generate docker metadata
+ id: docker_meta
+ uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
+ with:
+ flavor: | # prevent auto tagging with latest
+ latest=false
+ images: ${{ secrets.MONITORING_ECR_HOSTNAME }}
+ tags: type=sha,format=long
+
+ - name: Build and push docker image
+ uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0
+ with:
+ push: true
+ context: ./
+ file: monitoring/ops/Dockerfile
+ tags: ${{ steps.docker_meta.outputs.tags }}
+ labels: ${{ steps.docker_meta.outputs.labels }}
diff --git a/.github/workflows/relayer.yml b/.github/workflows/relayer.yml
new file mode 100644
index 000000000..d7dba57d0
--- /dev/null
+++ b/.github/workflows/relayer.yml
@@ -0,0 +1,77 @@
+name: Relayer
+
+on:
+ push:
+ branches:
+ - develop
+ - main
+ pull_request:
+
+jobs:
+ relayer_run_unit_tests:
+ name: Run Unit Tests ${{ matrix.test-type.name }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ test-type:
+ - name: test-unit-go
+ id: unit
+ - name: test-unit-go-race
+ id: race
+ - name: test-integration-go
+ id: integration
+ steps:
+ - name: Collect Metrics
+ if: matrix.test-type.id != 'race'
+ id: collect-gha-metrics
+ uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1
+ with:
+ id: starknet-relay-unit-${{ matrix.test-type.id }}
+ org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }}
+ basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }}
+ hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }}
+ this-job-name: Run Unit Tests ${{ matrix.test-type.name }}
+ test-results-file: '{"testType":"go","filePath":"/tmp/gotest.log"}'
+ - name: Checkout sources
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Install Nix
+ uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+
+ - name: Build
+ run: nix develop -c sh -c "make build-go-relayer"
+
+ - name: Run ${{ matrix.test-type.name }}
+ run: nix develop -c sh -c "make ${{ matrix.test-type.name }} LOG_PATH=/tmp/gotest.log"
+
+ - name: Upload Golangci relayer results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: go-unit-tests-results-${{ matrix.test-type.id }}
+ path: |
+ /tmp/gotest.log
+ ./relayer/output.txt
+ ./relayer/coverage.txt
+ ./relayer/race_coverage.txt
+
+ check-tidy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
+ - name: Set up Go
+ uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
+ with:
+ go-version-file: "relayer/go.mod"
+ - name: Ensure "make gomodtidy" has been run
+ run: |
+ make gomodtidy
+ git diff --exit-code
+ - name: Ensure "make generate" has been run
+ run: |
+ make rm-mocked
+ make generate
+ git diff --stat --exit-code
diff --git a/.github/workflows/release/starknet-gauntlet-cli.yml b/.github/workflows/release/starknet-gauntlet-cli.yml
new file mode 100644
index 000000000..b99bca69f
--- /dev/null
+++ b/.github/workflows/release/starknet-gauntlet-cli.yml
@@ -0,0 +1,37 @@
+name: Starknet Gauntlet CLI Release
+
+on:
+ workflow_dispatch:
+
+jobs:
+ starknet-gauntlet-cli-release:
+ name: Starknet Gauntlet CLI Release
+ runs-on: ubuntu-latest
+ steps:
+ # Checkout this repository
+ - name: Checkout Repo
+ uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
+ # Install nix
+ - name: Install Nix
+ uses: cachix/install-nix-action@29bd9290ef037a3ecbdafe83cbd2185e9dd0fa0a # v20
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ # Install dependencies using yarn
+ - name: Install Dependencies
+ run: nix develop -c yarn install --frozen-lockfile
+ # Build gauntlet binary
+ - name: Build Gauntlet
+ run: nix develop -c yarn bundle
+ # Store gauntlet-cli version
+ - name: Set Env Variables
+ run: echo "STARKNET_GAUNTLET_CLI=$(npm info @chainlink/starknet-gauntlet-cli version)" >> $GITHUB_ENV
+ # Upload gauntlet binary to gauntlet-cli release
+ - name: Upload Gauntlet Binary
+ uses: svenstaro/upload-release-action@v2
+ with:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
+ file: bin/chainlink-starknet-*
+ file_glob: true
+ tag: |
+ @chainlink/starknet-gauntlet-cli@${{ env.STARKNET_GAUNTLET_CLI }}
+ overwrite: false
diff --git a/.github/workflows/release/starknet-relayer.yml b/.github/workflows/release/starknet-relayer.yml
new file mode 100644
index 000000000..3ecb7abd9
--- /dev/null
+++ b/.github/workflows/release/starknet-relayer.yml
@@ -0,0 +1,31 @@
+name: Starknet Relayer Release
+
+on:
+ workflow_dispatch:
+
+jobs:
+ starknet-relayer-release:
+ name: Release Starknet Relayer
+ runs-on: ubuntu-latest
+ steps:
+ # Checkout this repository
+ - name: Checkout Repo
+ uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
+ # Store starknet version
+ - name: Set Env Variables
+ run: echo "STARKNET_RELAYER=$(npm info @chainlink/starknet-relayer version)" >> $GITHUB_ENV
+ # Check if release tag exists
+ - name: Check release tag
+ uses: mukunku/tag-exists-action@5dfe2bf779fe5259360bb10b2041676713dcc8a3 # v1.1.0
+ id: checkTag
+ with:
+ tag: relayer/v${{ env.STARKNET_RELAYER }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # Release code under vX.X.X
+ - name: Release Code
+ if: steps.checkTag.outputs.exists == 'false'
+ uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
+ with:
+ tag_name: relayer/v${{ env.STARKNET_RELAYER }}
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/sonar-scan.yml b/.github/workflows/sonar-scan.yml
new file mode 100644
index 000000000..21ef33d19
--- /dev/null
+++ b/.github/workflows/sonar-scan.yml
@@ -0,0 +1,110 @@
+name: SonarQube Scan
+
+on:
+ pull_request:
+
+jobs:
+ wait_for_workflows:
+ name: Wait for workflows
+ runs-on: ubuntu-latest
+ if: always()
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
+
+ - name: Wait for Workflows
+ id: wait
+ uses: smartcontractkit/chainlink-github-actions/utils/wait-for-workflows@main
+ with:
+ max-timeout: "1200"
+ polling-interval: "30"
+ exclude-workflow-names: "Amarna Analysis,Changesets,Integration Contracts (Vendor, Examples),Integration Tests Publish,Integration Tests - Smoke,Integration Tests - Soak,Build and push on-chain monitor image to ECR,Contracts,Lint"
+ exclude-workflow-ids: ""
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ env:
+ DEBUG: "true"
+
+ sonarqube:
+ name: SonarQube Scan
+ needs: [ wait_for_workflows ]
+ runs-on: ubuntu-latest
+ if: always()
+ steps:
+ - name: Checkout the repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # fetches all history for all tags and branches to provide more metadata for sonar reports
+
+ - name: Download Golangci unit tests reports
+ uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6
+ with:
+ workflow: relayer.yml
+ workflow_conclusion: ""
+ name_is_regexp: true
+ name: go-unit-tests-results
+ if_no_artifact_found: warn
+
+ - name: Download Golangci Relayer report
+ uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6
+ with:
+ workflow: golangci-lint.yml
+ workflow_conclusion: ""
+ name_is_regexp: true
+ name: golangci-lint-relayer-report
+ if_no_artifact_found: warn
+
+ - name: Download Golangcio Ops report
+ uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6
+ with:
+ workflow: golangci-lint.yml
+ workflow_conclusion: ""
+ name_is_regexp: true
+ name: golangci-lint-ops-report
+ if_no_artifact_found: warn
+
+ - name: Download Golangci-lint Integration tests report
+ uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6
+ with:
+ workflow: golangci-lint.yml
+ workflow_conclusion: ""
+ name_is_regexp: true
+ name: golangci-lint-integration-tests-report
+ if_no_artifact_found: warn
+
+ - name: Download gauntlet eslint reports
+ uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6
+ with:
+ workflow: integration_gauntlet.yml
+ workflow_conclusion: ""
+ name_is_regexp: true
+ name: gauntlet-eslint-report
+ if_no_artifact_found: warn
+
+ - name: Set SonarQube Report Paths
+ id: sonarqube_report_paths
+ shell: bash
+ run: |
+ {
+ echo "sonarqube_tests_report_paths=$(find . -type f -name output.txt | paste -sd "," -)"
+ echo "sonarqube_coverage_report_paths=$(find . -type f -name '*coverage.txt' | paste -sd "," -)"
+ echo "sonarqube_golangci_report_paths=$(find . -type f -name 'golangci-*-report.xml' -printf "%p,")"
+ echo "sonarqube_eslint_report_paths=$(find -type f -name 'eslint-report.json' -printf "%p")" >> $GITHUB_OUTPUT
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Update ESLint report symlinks
+ continue-on-error: true
+ run: sed -i 's+/home/runner/work/feeds-manager/feeds-manager/+/github/workspace/+g' ${{ steps.sonarqube_report_paths.outputs.sonarqube_eslint_report_paths }}
+
+ - name: SonarQube Scan
+ uses: sonarsource/sonarqube-scan-action@86fe81775628f1c6349c28baab87881a2170f495 # v2.1.0
+ with:
+ args: >
+ -Dsonar.go.tests.reportPaths=${{ steps.sonarqube_report_paths.outputs.sonarqube_tests_report_paths }}
+ -Dsonar.go.coverage.reportPaths=${{ steps.sonarqube_report_paths.outputs.sonarqube_coverage_report_paths }}
+ -Dsonar.go.golangci-lint.reportPaths=${{ steps.sonarqube_report_paths.outputs.sonarqube_golangci_report_paths }}
+ -Dsonar.eslint.reportPaths=${{ steps.sonarqube_report_paths.outputs.sonarqube_eslint_report_paths }}
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
diff --git a/.gitignore b/.gitignore
index e07cb5ba7..1ec5b7415 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,13 @@
.direnv
artifacts/
+vendor/
accounts.json
node.json
package-lock.json
node_modules/
flow-report.json
.env
+.idea/
report.json
bin
**/tsconfig.tsbuildinfo
@@ -56,11 +58,13 @@ pip-delete-this-directory.txt
htmlcov/
.tox/
.nox/
-.coverage
-.coverage.*
+*report.xml
+*report.json
+*.out
+*coverage*
+testdata/
.cache
nosetests.xml
-coverage.xml
*.cover
*.py,cover
.hypothesis/
@@ -145,3 +149,21 @@ dmypy.json
# vscode project settings
.vscode/
+
+# go workspace settings
+go.work
+go.work.sum
+
+.env.test*
+tmp-manifest-*
+
+# Test log files
+integration-tests/logs
+integration-tests/smoke/logs
+integration-tests/soak/logs
+remote.test
+ztarrepo.tar.gz
+eslint-report.json
+.run.id
+.local-mock-server
+override*.toml
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..82a63fcda
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,7 @@
+[submodule "vendor/cairo"]
+ path = vendor/cairo
+ url = https://github.com/starkware-libs/cairo
+[submodule "vendor/scarb"]
+ path = vendor/scarb
+ url = https://github.com/software-mansion/scarb
+ ignore = dirty
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 000000000..824bcbe72
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,150 @@
+run:
+ timeout: 10m0s
+linters:
+ enable:
+ - exhaustive
+ - exportloopref
+ - revive
+ - goimports
+ - gosec
+ - misspell
+ - rowserrcheck
+ - errorlint
+ - unconvert
+ - sqlclosecheck
+ - noctx
+ - whitespace
+ - depguard
+ - containedctx
+ - fatcontext
+ - mirror
+ - loggercheck
+linters-settings:
+ exhaustive:
+ default-signifies-exhaustive: true
+ goimports:
+ local-prefixes: github.com/smartcontractkit/chainlink-starknet
+ golint:
+ min-confidence: 1.0
+ gosec:
+ excludes:
+ - G101
+ - G104
+ # - G204
+ # - G304
+ # - G404
+ govet:
+ enable:
+ - shadow
+ settings:
+ printf:
+ # Additionally check custom logger
+ funcs:
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Debugf
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Infof
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Warnf
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Errorf
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Panicf
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Fatalf
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.SugaredLogger).AssumptionViolationf
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.SugaredLogger).Tracef
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.SugaredLogger).Criticalf
+ errorlint:
+ # Allow formatting of errors without %w
+ errorf: false
+ revive:
+ confidence: 0.8
+ rules:
+ - name: blank-imports
+ - name: context-as-argument
+ - name: context-keys-type
+ - name: dot-imports
+ - name: error-return
+ - name: error-strings
+ - name: error-naming
+ - name: exported
+ - name: if-return
+ - name: increment-decrement
+ - name: var-naming
+ - name: var-declaration
+ - name: package-comments
+ - name: range
+ - name: receiver-naming
+ - name: time-naming
+ # - name: unexported-return
+ - name: indent-error-flow
+ - name: errorf
+ - name: empty-block
+ - name: superfluous-else
+ # - name: unused-parameter
+ - name: unreachable-code
+ - name: redefines-builtin-id
+ - name: waitgroup-by-value
+ - name: unconditional-recursion
+ - name: struct-tag
+ # - name: string-format
+ - name: string-of-int
+ - name: range-val-address
+ - name: range-val-in-closure
+ - name: modifies-value-receiver
+ - name: modifies-parameter
+ - name: identical-branches
+ - name: get-return
+ # - name: flag-parameter
+ - name: early-return
+ - name: defer
+ - name: constant-logical-expr
+ # - name: confusing-naming
+ # - name: confusing-results
+ - name: bool-literal-in-expr
+ - name: atomic
+ depguard:
+ rules:
+ main:
+ list-mode: lax
+ deny:
+ - pkg: "cosmossdk.io/errors"
+ desc: Use the standard library instead
+ - pkg: "github.com/go-gorm/gorm"
+ desc: Use github.com/jmoiron/sqlx directly instead
+ - pkg: "github.com/gofrs/uuid"
+ desc: Use github.com/google/uuid instead
+ - pkg: "github.com/pkg/errors"
+ desc: Use the standard library instead, for example https://pkg.go.dev/errors#Join
+ - pkg: "github.com/satori/go.uuid"
+ desc: Use github.com/google/uuid instead
+ - pkg: "github.com/test-go/testify/assert"
+ desc: Use github.com/stretchr/testify/assert instead
+ - pkg: "github.com/test-go/testify/mock"
+ desc: Use github.com/stretchr/testify/mock instead
+ - pkg: "github.com/test-go/testify/require"
+ desc: Use github.com/stretchr/testify/require instead
+ - pkg: "go.uber.org/multierr"
+ desc: Use the standard library instead, for example https://pkg.go.dev/errors#Join
+ - pkg: "gopkg.in/guregu/null.v1"
+ desc: Use gopkg.in/guregu/null.v4 instead
+ - pkg: "gopkg.in/guregu/null.v2"
+ desc: Use gopkg.in/guregu/null.v4 instead
+ - pkg: "gopkg.in/guregu/null.v3"
+ desc: Use gopkg.in/guregu/null.v4 instead
+ - pkg: github.com/go-gorm/gorm
+ desc: Use github.com/jmoiron/sqlx directly instead
+ loggercheck:
+ # Check that *w logging functions have even number of args (i.e., well formed key-value pairs).
+ rules:
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Debugw
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Infow
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Warnw
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Errorw
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Panicw
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.Logger).Fatalw
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.SugaredLogger).AssumptionViolationw
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.SugaredLogger).Tracew
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.SugaredLogger).Criticalw
+ - (github.com/smartcontractkit/chainlink-common/pkg/logger.SugaredLogger).With
+issues:
+ exclude-rules:
+ - path: test
+ text: "^G404:"
+ linters:
+ - gosec
\ No newline at end of file
diff --git a/.helm-repositories.yaml b/.helm-repositories.yaml
new file mode 100644
index 000000000..4f875a2b7
--- /dev/null
+++ b/.helm-repositories.yaml
@@ -0,0 +1,30 @@
+apiVersion: ''
+generated: '0001-01-01T00:00:00Z'
+repositories:
+ - caFile: ''
+ certFile: ''
+ insecure_skip_tls_verify: false
+ keyFile: ''
+ name: bitnami
+ pass_credentials_all: false
+ password: ''
+ url: https://charts.bitnami.com/bitnami
+ username: ''
+ - caFile: ''
+ certFile: ''
+ insecure_skip_tls_verify: false
+ keyFile: ''
+ name: chainlink-qa
+ pass_credentials_all: false
+ password: ''
+ url: https://raw.githubusercontent.com/smartcontractkit/qa-charts/gh-pages/
+ username: ''
+ - caFile: ''
+ certFile: ''
+ insecure_skip_tls_verify: false
+ keyFile: ''
+ name: grafana
+ pass_credentials_all: false
+ password: ''
+ url: https://grafana.github.io/helm-charts
+ username: ''
diff --git a/.prettierignore b/.prettierignore
index 27b70c8d5..2e470776f 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,10 +1,18 @@
migrations
-programs
tests
ops
packages-rs
-relay
target
*.json
dist
*.yml
+typechain-types
+*.html
+*.md
+
+# Vendor files (contracts)
+vendor
+
+# Python virtual env
+.venv
+.direnv
diff --git a/.prettierrc.json b/.prettierrc.json
index f110a9d79..e4769df15 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1,7 +1,21 @@
{
"semi": false,
"singleQuote": true,
- "printWidth": 120,
+ "printWidth": 100,
"endOfLine": "auto",
- "trailingComma": "all"
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": "*.sol",
+ "options": {
+ "printWidth": 120,
+ "tabWidth": 2,
+ "useTabs": false,
+ "singleQuote": false,
+ "bracketSpacing": false,
+ "explicitTypes": "always"
+ }
+ }
+ ]
}
diff --git a/.tool-versions b/.tool-versions
index f7aaf9ffa..b00c44d37 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,8 +1,20 @@
-nodejs 14.19.1
-golang 1.18
-helmenv 1.0.75
-golangci-lint 1.45.2
-pulumi 3.25.1
-ginkgo 2.1.3
+# Languages
+nodejs 18.6.0
+yarn 1.22.19
+golang 1.21.5
+python 3.9.13
+
+# Tools
+mockery 2.22.1
+golangci-lint 1.55.0
actionlint 1.6.12
-shellcheck 0.8.0
\ No newline at end of file
+shellcheck 0.8.0
+scarb 2.6.5
+postgres 15.1
+
+# Kubernetes
+k3d 5.4.4
+kubectl 1.25.5
+k9s 0.26.3
+helm 3.9.3
+helmenv 1.2.7
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..9f9fa4c6d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "starkware.cairo",
+ "ericglau.cairo-ls",
+ "dbaeumer.vscode-eslint"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..bb4c99574
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,8 @@
+{
+ "editor.formatOnSave": true,
+ "editor.formatOnSaveTimeout": 1500,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": true
+ },
+ "files.insertFinalNewline": true
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..bdbefb7e6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,19 @@
+# Changelog Index
+
+This repository adheres to [Semantic Versioning](http://semver.org/).
+
+Chainlink-Starknet contains a number of projects, all individually versioned and released. Please consult the following changelogs for more information:
+
+- [@chainlink/starknet-relayer](/relayer/)
+- [@chainlink/starknet-gauntlet](/packages-ts/starknet-gauntlet/)
+- [@chainlink/starknet-gauntlet-cli](/packages-ts/starknet-gauntlet-cli/)
+- [@chainlink/starknet-gauntlet-ocr2](/packages-ts/starknet-gauntlet-ocr2/)
+- [@chainlink/starknet-gauntlet-oz](/packages-ts/starknet-gauntlet-oz/)
+- [@chainlink/starknet-gauntlet-argent](/packages-ts/starknet-gauntlet-argent/)
+- [@chainlink/starknet-gauntlet-token](/packages-ts/starknet-gauntlet-token/)
+- [@chainlink/starknet-contracts](/contracts/)
+- [@chainlink/starknet-integration-tests](/integration-tests/)
+
+If a project is pre-v1.0, minor version bumps may cause breaking changes. All breaking changes are noted in changelogs.
+
+Official project releases can be found here: https://github.com/smartcontractkit/chainlink-starknet/releases
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..396eb3896
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# Chainlink Starknet monorepo contributing guide
+
+🎈 Thanks for your help improving the project! We are so happy to have you!
+
+## Workflow for Pull Requests
+
+🚨 Before making any non-trivial change, please first open an issue describing the change to solicit feedback and guidance. This will increase the likelihood of the PR getting merged.
+
+In general, the smaller the diff the easier it will be for us to review quickly.
+
+In order to contribute, fork the appropriate branch, for non-breaking changes to production that is `develop` and for the next release that is normally `release/X.X.X` branch.
+
+Additionally, if you are writing a new feature, please ensure you add appropriate test cases.
+
+Follow the [Getting Started](./docs/getting-started.md) guide to set up your local development environment.
+
+We recommend using the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format on commit messages.
+
+Unless your PR is ready for immediate review and merging, please mark it as 'draft' (or simply do not open a PR yet).
+
+**Bonus:** Add comments to the diff under the "Files Changed" tab on the PR page to clarify any sections where you think we might have questions about the approach taken.
+
+### Response time:
+
+We aim to provide a meaningful response to all PRs and issues from external contributors within 5 business days.
+
+### Changesets
+
+We use [changesets](https://github.com/atlassian/changesets) to manage releases of our various packages.
+You _must_ include a `changeset` file in your PR when making a change that would require a new package release.
+
+Adding a `changeset` file is easy:
+
+1. Navigate to the root of the monorepo.
+2. Run `yarn changeset`. You'll be prompted to select packages to include in the changeset. Use the arrow keys to move the cursor up and down, hit the `spacebar` to select a package, and hit `enter` to confirm your selection. Select _all_ packages that require a new release as a result of your PR.
+3. Once you hit `enter` you'll be prompted to decide whether your selected packages need a `major`, `minor`, or `patch` release. We follow the [Semantic Versioning](https://semver.org/) scheme. Please avoid using `major` releases for any packages that are still in version `0.y.z`.
+4. Commit your changeset and push it into your PR. The changeset bot will notice your changeset file and leave a little comment to this effect on GitHub.
+
+### Rebasing
+
+We use the `git rebase` command to keep our commit history tidy.
+Rebasing is an easy way to make sure that each PR includes a series of clean commits with descriptive commit messages
+See [this tutorial](https://docs.gitlab.com/ee/topics/git/git_rebase.html) for a detailed explanation of `git rebase` and how you should use it to maintain a clean commit history.
diff --git a/Makefile b/Makefile
index aab76b3a2..7cdd24af0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-BIN_DIR = bin
+BIN_DIR = $(abspath bin)
export GOPATH ?= $(shell go env GOPATH)
export GO111MODULE ?= on
@@ -21,30 +21,263 @@ endif
.PHONY: install
install:
ifeq ($(OSFLAG),$(WINDOWS))
- echo "If you are running windows and know how to install what is needed, please contribute by adding it here!"
+ @echo "Windows system detected - no automated setup available."
+ @echo "Please install your developer enviroment manually (@see .tool-versions)."
+ @echo
exit 1
endif
ifeq ($(OSFLAG),$(OSX))
- curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
+ @echo "MacOS system detected - installing the required toolchain via asdf (@see .tool-versions)."
+ @echo
brew install asdf
- asdf plugin-add nodejs || true
- asdf plugin-add golang || true
- asdf plugin-add golangci-lint || true
- asdf plugin-add ginkgo || true
- asdf plugin-add pulumi || true
+ asdf plugin add golang || true
+ asdf plugin add nodejs || true
+ asdf plugin add python || true
+ asdf plugin add mockery || true
+ asdf plugin add golangci-lint || true
asdf plugin add actionlint || true
asdf plugin add shellcheck || true
+ asdf plugin add k3d || true
+ asdf plugin add kubectl || true
+ asdf plugin add k9s || true
+ asdf plugin add helm || true
+ asdf plugin add helmenv https://github.com/smartcontractkit/asdf-helmenv.git || true
+ @echo
asdf install
endif
ifeq ($(OSFLAG),$(LINUX))
- curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
+ @echo "Linux system detected - please install and use NIX (@see shell.nix)."
+ @echo
ifneq ($(CI),true)
# install nix
sh <(curl -L https://nixos-nix-install-tests.cachix.org/serve/vij683ly7sl95nnhb67bdjjfabclr85m/install) --daemon --tarball-url-prefix https://nixos-nix-install-tests.cachix.org/serve --nix-extra-conf-file ./nix.conf
endif
- go install github.com/onsi/ginkgo/v2/ginkgo@v$(shell cat ./.tool-versions | grep ginkgo | sed -En "s/ginkgo.(.*)/\1/p")
endif
-.PHONY: e2e_test
-e2e_test:
- ginkgo -r integration-tests/smoke
\ No newline at end of file
+.PHONY: nix-container
+nix-container:
+ docker run -it --rm -v $(shell pwd):/repo -e NIX_USER_CONF_FILES=/repo/nix.conf --workdir /repo nixos/nix:latest /bin/sh
+
+.PHONY: nix-flake-update
+nix-flake-update:
+ docker run -it --rm -v $(shell pwd):/repo -e NIX_USER_CONF_FILES=/repo/nix.conf --workdir /repo nixos/nix:latest /bin/sh -c "nix flake update"
+
+.PHONY: build
+build: build-go build-ts
+
+.PHONY: build-go
+build-go: build-go-relayer build-go-ops build-go-integration-tests
+
+.PHONY: build-go-relayer
+build-go-relayer:
+ cd relayer/ && go build ./...
+
+.PHONY: build-go-ops
+build-go-ops:
+ cd ops/ && go build ./...
+
+.PHONY: build-go-integration-tests
+build-go-integration-tests:
+ cd integration-tests/ && go build ./...
+
+# TODO: fix and readd build-ts-examples
+.PHONY: build-ts
+build-ts: build-ts-workspace build-cairo-contracts build-sol-contracts
+
+.PHONY: build-ts-workspace
+build-ts-workspace:
+ yarn install --frozen-lockfile
+ yarn build
+
+# TODO: use yarn workspaces features instead of managing separately like this
+# https://yarnpkg.com/cli/workspaces/foreach
+.PHONY: build-sol-contracts
+build-sol-contracts:
+ cd contracts/ && \
+ yarn install --frozen-lockfile && \
+ yarn compile:solidity
+
+# TODO: this should build cairo contracts when they are rewritten
+.PHONY: build-ts-examples
+build-ts-examples:
+ cd examples/contracts/aggregator-consumer && \
+ yarn install --frozen-lockfile && \
+ yarn compile:solidity
+
+.PHONY: gowork
+gowork:
+ go work init
+ go work use ./ops
+ go work use ./relayer
+ go work use ./integration-tests
+
+.PHONY: gowork_rm
+gowork_rm:
+ rm go.work*
+
+.PHONY: format
+format: format-go format-cairo format-ts
+
+.PHONY: format-check
+format-check: format-cairo-check format-ts-check
+
+.PHONY: format-go
+format-go: format-go-fmt gomodtidy
+
+.PHONY: format-go-fmt
+format-go-fmt:
+ cd ./relayer && go fmt ./...
+ cd ./ops && go fmt ./...
+ cd ./integration-tests && go fmt ./...
+
+.PHONY: gomods
+gomods: ## Install gomods
+ go install github.com/jmank88/gomods@v0.1.3
+
+.PHONY: gomodtidy
+gomodtidy: gomods
+ gomods tidy
+
+.PHONY: mockery
+mockery: $(mockery) ## Install mockery.
+ go install github.com/vektra/mockery/v2@v2.43.2
+
+.PHONY: rm-mocked
+rm-mocked:
+ grep -rl "^// Code generated by mockery" | grep .go$ | xargs -r rm
+
+.PHONY: generate
+generate: mockery gomods
+ gomods -w go generate -x ./...
+
+.PHONY: format-cairo
+format-cairo:
+ cairo-format -i ./contracts/src/**/*.cairo
+ cairo-format -i ./examples/**/*.cairo
+
+.PHONY: format-cairo-check
+format-cairo-check:
+ cairo-format -c ./contracts/src/**/*.cairo
+ cairo-format -c ./examples/**/*.cairo
+
+.PHONY: format-ts
+format-ts:
+ yarn format
+
+.PHONY: format-ts-check
+format-ts-check:
+ yarn format:check
+
+.PHONY: lint-go-ops
+lint-go-ops:
+ cd ./ops && golangci-lint --color=always --out-format checkstyle:golangci-lint-ops-report.xml run
+
+.PHONY: lint-go-relayer
+lint-go-relayer:
+ cd ./relayer && golangci-lint --color=always --out-format checkstyle:golangci-lint-relayer-report.xml run
+
+.PHONY: lint-go-test
+lint-go-test:
+ cd ./integration-tests && golangci-lint --color=always --exclude=dot-imports --out-format checkstyle:golangci-lint-integration-tests-report.xml run
+
+.PHONY: test-go
+test-go: test-unit-go test-unit-go-race test-integration-go
+
+.PHONY: test-unit
+test-unit: test-unit-go test-unit-go-race
+
+LOG_PATH ?= ./gotest.log
+.PHONY: test-unit-go
+test-unit-go:
+ cd ./relayer && go test -json ./... -covermode=atomic -coverpkg=./... -coverprofile=coverage.txt 2>&1 | tee $(LOG_PATH) | gotestloghelper -ci
+
+.PHONY: test-unit-go-race
+test-unit-go-race:
+ cd ./relayer && CGO_ENABLED=1 go test -v ./... -race -count=10 -coverpkg=./... -coverprofile=race_coverage.txt
+
+.PHONY: test-integration-go
+# only runs tests with TestIntegration_* + //go:build integration
+test-integration-go: env-devnet-hardhat
+ cd ./relayer && go test -json ./... -run TestIntegration -tags integration 2>&1 | tee $(LOG_PATH) | gotestloghelper -ci
+
+.PHONY: test-integration-prep
+test-integration-prep:
+ cd ./contracts
+ make build
+
+.PHONY: test-integration
+test-integration: test-integration-smoke test-integration-contracts test-integration-gauntlet
+
+.PHONY: test-integration-smoke
+test-integration-smoke: test-integration-prep
+ cd integration-tests/ && \
+ go test --timeout=2h -v ./smoke
+
+# CI Already has already ran test-integration-prep
+.PHONY: test-integration-smoke-ci
+test-integration-smoke-ci:
+ cd integration-tests/ && \
+ go test --timeout=2h -v -count=1 -run TestOCRBasic/$(test) -json ./smoke | tee /tmp/gotest.log | gotestloghelper -ci -singlepackage
+
+.PHONY: test-integration-soak
+test-integration-soak: test-integration-prep
+ cd integration-tests/ && \
+ go test --timeout=1h -v -json ./soak
+
+# CI Already has already ran test-integration-prep
+.PHONY: test-integration-soak-ci
+test-integration-soak-ci:
+ cd integration-tests/ && \
+ go test --timeout=1h -v -count=1 -json ./soak
+
+.PHONY: test-examples
+test-examples:
+ cd ./examples/contracts/aggregator_consumer && \
+ snforge test
+
+.PHONY: test-integration-gauntlet
+# TODO: fix example
+# cd packages-ts/starknet-gauntlet-example/ && \
+# yarn test
+test-integration-gauntlet: build-ts env-devnet-hardhat
+ cd packages-ts/starknet-gauntlet/ && \
+ yarn test
+ cd packages-ts/starknet-gauntlet-argent/ && \
+ yarn test
+ cd packages-ts/starknet-gauntlet-cli/ && \
+ yarn test
+ cd packages-ts/starknet-gauntlet-multisig/ && \
+ yarn test
+ cd packages-ts/starknet-gauntlet-ocr2/ && \
+ yarn test
+ cd packages-ts/starknet-gauntlet-oz/ && \
+ yarn test
+ cd packages-ts/starknet-gauntlet-token/ && \
+ yarn test
+ cd packages-ts/starknet-gauntlet-emergency-protocol/ && \
+ yarn test
+
+.PHONY: test-ts
+test-ts: test-ts-contracts test-integration-contracts test-integration-gauntlet
+
+.PHONY: test-ts-contracts
+test-ts-contracts: build-ts env-devnet-hardhat
+ cd contracts/ && \
+ yarn test
+
+.PHONY: build-cairo-contracts
+build-cairo-contracts:
+ cd contracts && scarb --profile release build
+
+.PHONY: test-cairo-contracts
+test-cairo-contracts:
+ cd contracts && scarb test
+
+# TODO: this script needs to be replaced with a predefined K8s enviroment
+.PHONY: env-devnet-hardhat
+env-devnet-hardhat:
+ ./ops/scripts/devnet-hardhat.sh
+
+.PHONY: env-devnet-hardhat-down
+env-devnet-hardhat-down:
+ ./ops/scripts/devnet-hardhat-down.sh
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..41282ed2c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Chainlink Starknet
+
+For more information, see the [Chainlink Starknet Documentation](./docs).
diff --git a/contracts/.gitattributes b/contracts/.gitattributes
new file mode 100644
index 000000000..0792ac6ad
--- /dev/null
+++ b/contracts/.gitattributes
@@ -0,0 +1 @@
+*.cairo linguist-language=rust
\ No newline at end of file
diff --git a/contracts/.gitignore b/contracts/.gitignore
new file mode 100644
index 000000000..0cca96d9f
--- /dev/null
+++ b/contracts/.gitignore
@@ -0,0 +1,4 @@
+starknet-artifacts/
+cache/
+target/
+node_modules/
diff --git a/contracts/.helix/languages.toml b/contracts/.helix/languages.toml
deleted file mode 100644
index 70f2df901..000000000
--- a/contracts/.helix/languages.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-[[language]]
-name = "cairo"
-language-server = { command = "node", args = ["node_modules/cairo-ls/out/server.js", "--stdio"] }
-[language.config]
-cairols = { venvCommand = ". .venv/bin/activate", sourceDir = "contracts" }
diff --git a/contracts/Makefile b/contracts/Makefile
deleted file mode 100644
index a1c04eaeb..000000000
--- a/contracts/Makefile
+++ /dev/null
@@ -1,3 +0,0 @@
-# Build and test
-build :; nile compile
-test :; pytest tests/
\ No newline at end of file
diff --git a/contracts/README.md b/contracts/README.md
new file mode 100644
index 000000000..6a3a810dd
--- /dev/null
+++ b/contracts/README.md
@@ -0,0 +1,141 @@
+# Minimal Cairo 1.0 Template ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/auditless/cairo-template/blob/main/LICENSE)
+
+[Built with **`auditless/cairo-template`**](https://github.com/auditless/cairo-template)
+
+A minimal template for building smart contracts with Cairo 1.0
+using the [Quaireaux](https://github.com/keep-starknet-strange/quaireaux) project defaults.
+
+## How it works
+
+- No submodules, forks or other heavy machinery
+- Uses the [`cairo-test-runner`](https://github.com/starkware-libs/cairo/blob/main/crates/cairo-lang-test-runner/README.md) binary for running tests
+- Built as a [Scarb](https://github.com/software-mansion/scarb) package for reusability and uses Scarb dependencies for libraries
+- Has reproducible builds using GitHub Actions
+- Uses Scarb scripts natively for custom commands
+- Includes advanced debugging views like the Sierra intermediate representation
+
+## Installing dependencies
+
+### Step 1: Install Cairo 1.0 (guide by [Abdel](https://github.com/abdelhamidbakhta))
+
+If you are on an x86 Linux system and able to use the release binary,
+you can download Cairo here https://github.com/starkware-libs/cairo/releases.
+
+For everyone, else, we recommend compiling Cairo from source like so:
+
+```bash
+# Install stable Rust
+$ rustup override set stable && rustup update
+
+# Clone the Cairo compiler in $HOME/Bin
+$ cd ~/Bin && git clone git@github.com:starkware-libs/cairo.git && cd cairo
+
+# Generate release binaries
+$ cargo build --all --release
+```
+
+**NOTE: Keeping Cairo up to date**
+
+Now that your Cairo compiler is in a cloned repository, all you will need to do
+is pull the latest changes and rebuild as follows:
+
+```bash
+$ cd ~/Bin/cairo && git fetch && git pull && cargo build --all --release
+```
+
+### Step 2: Add Cairo 1.0 executables to your path
+
+```bash
+export PATH="$HOME/Bin/cairo/target/release:$PATH"
+```
+
+**NOTE: If installing from a Linux binary, adapt the destination path accordingly.**
+
+This will make available several binaries. The one we use is called `cairo-test`.
+
+### Step 3: Install the Cairo package manager Scarb
+
+Follow the installation guide in [Scarb's Repository](https://github.com/software-mansion/scarb).
+
+### Step 4: Setup Language Server
+
+#### VS Code Extension
+
+- Disable previous Cairo 0.x extension
+- Install the Cairo 1 extension for proper syntax highlighting and code navigation.
+Just follow the steps indicated [here](https://github.com/starkware-libs/cairo/blob/main/vscode-cairo/README.md).
+
+#### Cairo Language Server
+
+From [Step 1](#step-1-install-cairo-10-guide-by-abdel), the `cairo-language-server` binary should be built and executing this command will copy its path into your clipboard.
+
+```bash
+$ which cairo-language-server | pbcopy
+```
+
+Update the `languageServerPath` of the Cairo 1.0 extension by pasting the path.
+
+## How to use this template
+
+First you will need to clone the repository or click the `Use this template` button
+at the top of the page to create a new repository based on the template.
+
+Next, you will want to update the configuration files with the name of your project:
+
+```
+├── .cairo_project.toml
+└── .Scarb.toml
+```
+
+## Working with your project
+
+The Cairo template currently supports building and testing contracts.
+
+### Build
+
+Build the contracts.
+
+```bash
+$ scarb build
+```
+
+### Test
+
+Run the tests in `src/test`:
+
+```bash
+$ scarb run test
+```
+
+### Format
+
+Format the Cairo source code (using Scarb):
+
+```bash
+$ scarb fmt
+```
+
+### Sierra (advanced)
+
+View the compiled Sierra output of your Cairo code:
+
+```bash
+$ scarb run sierra
+```
+
+## Thanks to
+
+- The [Quaireaux](https://github.com/keep-starknet-strange/quaireaux) team for coming up with
+this configuration and especially [Abdel](https://github.com/abdelhamidbakhta) for helping me with Cairo 1.0 installation
+- [Paul Berg](https://github.com/PaulRBerg) and the [foundry-template](https://github.com/paulrberg/foundry-template) project which served as inspiration
+- Last but not least, the StarkWare team for building the first smart contract language that is a joy to use
+
+## Other templates
+
+- [ArgentX template](https://github.com/argentlabs/starknet-build/tree/main/cairo1.0) is built as a fork of the compiler
+- [Eni's cairo1-template](https://github.com/msaug/cairo1-template) uses git submodules for installation
+- [Shramee's Starklings](https://github.com/shramee/starklings-cairo1) use the cairo1 crates as libraries and builds its own framework
+
+## License
+
+[MIT](https://github.com/auditless/cairo-template/blob/main/LICENSE) © [Auditless Limited](https://www.auditless.com)
diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock
new file mode 100644
index 000000000..88a112553
--- /dev/null
+++ b/contracts/Scarb.lock
@@ -0,0 +1,14 @@
+# Code generated by scarb DO NOT EDIT.
+version = 1
+
+[[package]]
+name = "chainlink"
+version = "0.1.0"
+dependencies = [
+ "openzeppelin",
+]
+
+[[package]]
+name = "openzeppelin"
+version = "0.10.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3"
diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml
new file mode 100644
index 000000000..69c519dfd
--- /dev/null
+++ b/contracts/Scarb.toml
@@ -0,0 +1,30 @@
+[package]
+name = "chainlink"
+version = "0.1.0"
+cairo-version = "2.6.3"
+description = "Chainlink contracts for Starknet"
+homepage = "https://github.com/smartcontractkit/chainlink-starknet"
+
+[scripts]
+sierra = "cairo-compile . -r"
+# Add your own custom commands and run them with scarb run
+
+# Uncomment if you want to use dependencies
+# Note: currently testing doesn't work with dependencies
+[dependencies]
+starknet = ">=2.6.3"
+openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" }
+
+[lib]
+
+[[target.starknet-contract]]
+sierra = true
+casm = true
+# pythonic hints are necessary for cairo-lang to parse the casm file:
+# Unsupported compiled class format. Cairo 1.0 compiled class must contain the attribute `pythonic_hints`.
+casm-add-pythonic-hints = true
+
+# this elevates the severity of disallowed libfuncs to compilation errors
+# https://docs.swmansion.com/scarb/docs/starknet/contract-target#allowed-libfuncs-validation
+allowed-libfuncs-deny = true
+allowed-libfuncs-list.name = "audited"
diff --git a/contracts/cairo_project.toml b/contracts/cairo_project.toml
new file mode 100644
index 000000000..df050dd0c
--- /dev/null
+++ b/contracts/cairo_project.toml
@@ -0,0 +1,2 @@
+[crate_roots]
+chainlink = "src"
diff --git a/contracts/constraints.txt b/contracts/constraints.txt
deleted file mode 100644
index 7ecf2e0c1..000000000
--- a/contracts/constraints.txt
+++ /dev/null
@@ -1 +0,0 @@
-marshmallow-dataclass==8.5.3
\ No newline at end of file
diff --git a/contracts/contracts/access_controller.cairo b/contracts/contracts/access_controller.cairo
deleted file mode 100644
index 83cff870b..000000000
--- a/contracts/contracts/access_controller.cairo
+++ /dev/null
@@ -1,81 +0,0 @@
-%lang starknet
-
-from starkware.cairo.common.cairo_builtins import HashBuiltin
-from starkware.cairo.common.bool import TRUE, FALSE
-
-@storage_var
-func access_list_(address: felt) -> (bool: felt):
-end
-
-from contracts.ownable import (
- Ownable_initializer,
- Ownable_only_owner,
- Ownable_get_owner,
- Ownable_transfer_ownership
-)
-
-@constructor
-func constructor{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- owner: felt,
-):
- Ownable_initializer(owner)
- return ()
-end
-
-@view
-func has_access{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- address: felt
-) -> (bool: felt):
- let (bool) = access_list_.read(address)
- return (bool)
-end
-
-
-@view
-func check_access{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- address: felt
-):
- let (bool) = access_list_.read(address)
- with_attr error_message("AccessController: address does not have access"):
- assert bool = TRUE
- end
- return ()
-end
-
-@external
-func add_access{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- address: felt
-):
- Ownable_only_owner()
- access_list_.write(address, TRUE)
- return ()
-end
-
-@external
-func remove_access{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- address: felt
-):
- Ownable_only_owner()
- access_list_.write(address, FALSE)
- return ()
-end
\ No newline at end of file
diff --git a/contracts/contracts/account.cairo b/contracts/contracts/account.cairo
deleted file mode 100644
index 9159a8ab6..000000000
--- a/contracts/contracts/account.cairo
+++ /dev/null
@@ -1,3 +0,0 @@
-%lang starknet
-
-from openzeppelin.account.Account import constructor
\ No newline at end of file
diff --git a/contracts/contracts/aggregator.cairo b/contracts/contracts/aggregator.cairo
deleted file mode 100644
index 87dfea0eb..000000000
--- a/contracts/contracts/aggregator.cairo
+++ /dev/null
@@ -1,1345 +0,0 @@
-%lang starknet
-
-from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin, BitwiseBuiltin
-from starkware.cairo.common.alloc import alloc
-from starkware.cairo.common.hash import hash2
-from starkware.cairo.common.hash_state import (
- hash_init, hash_finalize, hash_update, hash_update_single
-)
-from starkware.cairo.common.signature import verify_ecdsa_signature
-from starkware.cairo.common.bitwise import bitwise_and
-from starkware.cairo.common.math import (
- abs_value,
- split_felt,
- assert_lt_felt,
- assert_le_felt,
- assert_lt,
- assert_le,
- assert_not_zero, assert_not_equal, assert_nn_le, assert_nn, assert_in_range, unsigned_div_rem
-)
-from starkware.cairo.common.math_cmp import (
- is_not_zero,
-)
-from starkware.cairo.common.pow import pow
-from starkware.cairo.common.uint256 import (
- Uint256,
- uint256_sub,
-)
-from starkware.cairo.common.bool import TRUE, FALSE
-
-from starkware.starknet.common.syscalls import (
- get_caller_address,
- get_contract_address,
- get_block_timestamp,
- get_block_number,
- get_tx_info,
-)
-
-from openzeppelin.utils.constants import UINT8_MAX
-
-from openzeppelin.token.erc20.interfaces.IERC20 import IERC20
-
-from contracts.interfaces.IAccessController import IAccessController
-
-from contracts.ownable import (
- Ownable_initializer,
- Ownable_only_owner,
- Ownable_get_owner,
- Ownable_transfer_ownership,
- Ownable_accept_ownership
-)
-
-# ---
-
-const MAX_ORACLES = 31
-
-const GIGA = 10 ** 9
-
-const UINT32_MAX = 2 ** 32
-const INT192_MAX = 2 ** (192 - 1)
-const INT192_MIN = -2 ** (192 - 1)
-
-func felt_to_uint256{range_check_ptr}(x) -> (uint_x : Uint256):
- let (high, low) = split_felt(x)
- return (Uint256(low=low, high=high))
-end
-
-func uint256_to_felt{range_check_ptr}(value : Uint256) -> (value : felt):
- assert_lt_felt(value.high, 2 ** 123)
- return (value.high * (2 ** 128) + value.low)
-end
-
-# Maximum number of faulty oracles
-@storage_var
-func f_() -> (f: felt):
-end
-
-@storage_var
-func latest_epoch_and_round_() -> (res: felt):
-end
-
-@storage_var
-func latest_aggregator_round_id_() -> (round_id: felt):
-end
-
-using Range = (min: felt, max: felt)
-
-@storage_var
-func answer_range_() -> (range: Range):
-end
-
-@storage_var
-func decimals_() -> (decimals: felt):
-end
-
-@storage_var
-func description_() -> (description: felt):
-end
-
-#
-
-@storage_var
-func latest_config_block_number_() -> (block: felt):
-end
-
-@storage_var
-func config_count_() -> (count: felt):
-end
-
-@storage_var
-func latest_config_digest_() -> (digest: felt):
-end
-
-@storage_var
-func oracles_len_() -> (len: felt):
-end
-
-# TODO: should we pack into (index, payment) = split_felt()? index is u8, payment is u128
-struct Oracle:
- member index: felt
-
- # entire supply of LINK always fits into u96, so felt is safe to use
- member payment_juels: felt
-end
-
-@storage_var
-func transmitters_(pkey: felt) -> (index: Oracle):
-end
-
-@storage_var
-func signers_(pkey: felt) -> (index: felt):
-end
-
-@storage_var
-func signers_list_(index: felt) -> (pkey: felt):
-end
-
-@storage_var
-func transmitters_list_(index: felt) -> (pkey: felt):
-end
-
-@storage_var
-func reward_from_aggregator_round_id_(index: felt) -> (round_id: felt):
-end
-
-# ---
-
-struct Transmission:
- member answer: felt
- member block_num: felt
- member observation_timestamp: felt
- member transmission_timestamp: felt
-end
-
-@storage_var
-func transmissions_(round_id: felt) -> (transmission: Transmission):
-end
-
-# ---
-
-@constructor
-func constructor{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- owner: felt,
- link: felt,
- min_answer: felt,
- max_answer: felt,
- billing_access_controller: felt,
- decimals: felt,
- description: felt
-):
- Ownable_initializer(owner)
- link_token_.write(link)
- billing_access_controller_.write(billing_access_controller)
-
- assert_lt(min_answer, max_answer)
- let range : Range = (min=min_answer, max=max_answer)
- answer_range_.write(range)
-
- with_attr error_message("decimals exceed 2^8"):
- assert_lt(decimals, UINT8_MAX)
- end
- decimals_.write(decimals)
- description_.write(description)
- # TODO: initialize vars to defaults
- return ()
-end
-
-# --- Ownership ---
-
-@view
-func owner{pedersen_ptr : HashBuiltin*, syscall_ptr : felt*, range_check_ptr}() -> (owner : felt):
- let (owner) = Ownable_get_owner()
- return (owner=owner)
-end
-
-@external
-func transfer_ownership{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}(new_owner: felt) -> ():
- return Ownable_transfer_ownership(new_owner)
-end
-
-@external
-func accept_ownership{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}() -> (new_owner: felt):
- return Ownable_accept_ownership()
-end
-
-# --- Validation ---
-
-# TODO: disable validation + flags in the initial release
-# TODO: document decision in repo/docs/contracts/ocr2
-
-@contract_interface
-namespace IValidator:
- func validate(prev_round_id: felt, prev_answer: felt, round_id: felt, answer: felt) -> (valid: felt):
- end
-end
-
-# TODO: can't set gas limit
-@storage_var
-func validator_() -> (validator: felt):
-end
-
-@view
-func validator_config{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (validator: felt):
- let (validator) = validator_.read()
- return (validator)
-end
-
-@external
-func set_validator_config{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(validator: felt):
- Ownable_only_owner()
- # TODO: use openzeppelin's ERC165 to validate
- validator_.write(validator)
-
- # TODO: emit event
-
- return ()
-end
-
-# --- Configuration
-
-@event
-func config_set(
- previous_config_block_number: felt,
- latest_config_digest: felt,
- config_count: felt,
- oracles_len: felt,
- oracles: OracleConfig*,
- f: felt,
- onchain_config: felt, # TODO
- offchain_config_version: felt,
- offchain_config_len: felt,
- offchain_config: felt*,
-):
-end
-
-
-struct OracleConfig:
- member signer: felt
- member transmitter: felt
-end
-
-@external
-func set_config{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}(
- oracles_len: felt,
- oracles: OracleConfig*,
- f: felt,
- onchain_config: felt, # TODO
- offchain_config_version: felt,
- offchain_config_len: felt,
- offchain_config: felt*,
-) -> (digest: felt):
- alloc_locals
- # Ownable_only_owner() TODO: reenable
-
- assert_nn_le(oracles_len, MAX_ORACLES) # oracles_len <= MAX_ORACLES
- assert_lt(3 * f, oracles_len) # 3 * f < oracles_len
- assert_nn(f) # f is positive
-
- # pay out existing oracles
- pay_oracles()
-
- # remove old signers/transmitters
- let (len) = oracles_len_.read()
- remove_oracles(len)
-
- let (latest_round_id) = latest_aggregator_round_id_.read()
-
- # add new oracles (also sets oracle_len_)
- add_oracles(oracles, 0, oracles_len, latest_round_id)
-
- f_.write(f)
- let (block_num : felt) = get_block_number()
- let (prev_block_num) = latest_config_block_number_.read()
- latest_config_block_number_.write(block_num)
- # update config count
- let (config_count) = config_count_.read()
- let config_count = config_count + 1
- config_count_.write(config_count)
- # calculate and store config digest
- let (contract_address) = get_contract_address()
- let (tx_info) = get_tx_info()
- let (digest) = config_digest_from_data(
- tx_info.chain_id,
- contract_address,
- config_count,
- oracles_len,
- oracles,
- f,
- onchain_config,
- offchain_config_version,
- offchain_config_len,
- offchain_config
- )
- latest_config_digest_.write(digest)
-
- # reset epoch & round
- latest_epoch_and_round_.write(0)
-
- config_set.emit(
- previous_config_block_number=prev_block_num,
- latest_config_digest=digest,
- config_count=config_count,
- oracles_len=oracles_len,
- oracles=oracles,
- f=f,
- onchain_config=onchain_config,
- offchain_config_version=offchain_config_version,
- offchain_config_len=offchain_config_len,
- offchain_config=offchain_config,
- )
-
- return (digest)
-end
-
-func remove_oracles{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(n: felt):
- alloc_locals
-
- if n == 0:
- oracles_len_.write(0)
- return ()
- end
-
- # delete oracle from all maps
- let (signer) = signers_list_.read(n)
- signers_.write(signer, 0)
-
- let (transmitter) = transmitters_list_.read(n)
- transmitters_.write(transmitter, Oracle(index=0, payment_juels=0))
-
- return remove_oracles(n - 1)
-end
-
-func add_oracles{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(oracles: OracleConfig*, index: felt, len: felt, latest_round_id: felt):
- alloc_locals
-
- if len == 0:
- oracles_len_.write(index)
- return ()
- end
-
- # NOTE: index should start with 1 here because storage is 0-initialized.
- # That way signers(pkey) => 0 indicates "not present"
- let index = index + 1
-
- signers_.write(oracles.signer, index)
- signers_list_.write(index, oracles.signer)
-
- transmitters_.write(oracles.transmitter, Oracle(index=index, payment_juels=0))
- transmitters_list_.write(index, oracles.transmitter)
-
- reward_from_aggregator_round_id_.write(index, latest_round_id)
-
- return add_oracles(oracles + OracleConfig.SIZE, index, len - 1, latest_round_id)
-end
-
-func config_digest_from_data{
- pedersen_ptr : HashBuiltin*,
-}(
- chain_id: felt,
- contract_address: felt,
- config_count: felt,
- oracles_len: felt,
- oracles: OracleConfig*,
- f: felt,
- onchain_config: felt, # TODO
- offchain_config_version: felt,
- offchain_config_len: felt,
- offchain_config: felt*,
-) -> (hash: felt):
- let hash_ptr = pedersen_ptr
- with hash_ptr:
- let (hash_state_ptr) = hash_init()
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, chain_id)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, contract_address)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, config_count)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, oracles_len)
- let (hash_state_ptr) = hash_update(hash_state_ptr, oracles, oracles_len * OracleConfig.SIZE)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, f)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, onchain_config)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, offchain_config_version)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, offchain_config_len)
- let (hash_state_ptr) = hash_update(hash_state_ptr, offchain_config, offchain_config_len)
-
- let (hash) = hash_finalize(hash_state_ptr)
- let pedersen_ptr = hash_ptr
- return (hash=hash)
- end
-end
-
-@view
-func latest_config_details{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (
- config_count: felt,
- block_number: felt,
- config_digest: felt
-):
- let (config_count) = config_count_.read()
- let (block_number) = latest_config_block_number_.read()
- let (config_digest) = latest_config_digest_.read()
- return(config_count=config_count, block_number=block_number, config_digest=config_digest)
-end
-
-@view
-func transmitters{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (transmitters_len: felt, transmitters: felt*):
- alloc_locals
-
- let (result: felt*) = alloc()
- let (len) = oracles_len_.read()
-
- transmitters_inner(len, 0, result)
-
- return (transmitters_len=len, transmitters=result)
-end
-
-# unroll transmitter list into a continuous array
-func transmitters_inner{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(len: felt, index: felt, result: felt*):
- if len == 0:
- return ()
- end
-
- let index = index + 1
-
- let (transmitter) = transmitters_list_.read(index)
- assert result[0] = transmitter
-
- return transmitters_inner(len - 1, index, result + 1)
-end
-
-# --- Transmission ---
-
-@event
-func new_transmission(
- round_id: felt,
- answer: felt,
- transmitter: felt,
- observation_timestamp: felt,
- observers: felt,
- observations_len: felt,
- observations: felt*,
- juels_per_fee_coin: felt,
- config_digest: felt,
- epoch_and_round: felt,
- reimbursement: felt,
-):
-end
-
-struct Signature:
- member r : felt
- member s : felt
- # TODO: can further compress by using signer index instead of pubkey?
- # TODO: observers[i] = n => signers[n] => public_key
- member public_key: felt
-end
-
-struct ReportContext:
- member config_digest : felt
- member epoch_and_round : felt
- member extra_hash : felt
-end
-
-# TODO we can base64 encode inputs, but we could also pre-split the inputs (so instead of a binary report,
-# it's already split into observers, len and observations). Encoding would shrink the input size since each observation
-# wouldn't have to be felt-sized.
-@external
-func transmit{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- ecdsa_ptr : SignatureBuiltin*,
- bitwise_ptr : BitwiseBuiltin*,
- range_check_ptr
-}(
- report_context: ReportContext,
- observation_timestamp: felt,
- observers: felt,
- observations_len: felt,
- observations: felt*,
- juels_per_fee_coin: felt,
- signatures_len: felt,
- signatures: Signature*,
-):
- alloc_locals
-
- let (epoch_and_round) = latest_epoch_and_round_.read()
- with_attr error_message("stale report"):
- assert_lt(epoch_and_round, report_context.epoch_and_round)
- end
-
- # validate transmitter
- let (caller) = get_caller_address()
- let (oracle: Oracle) = transmitters_.read(caller)
- assert_not_equal(oracle.index, 0) # 0 index = uninitialized
- # ERROR: caller seems to be the account contract address, not the underlying transmitter key
-
- # Validate config digest matches latest_config_digest
- let (config_digest) = latest_config_digest_.read()
- with_attr error_message("config digest mismatch"):
- assert report_context.config_digest = config_digest
- end
-
- let (f) = f_.read()
- with_attr error_message("wrong number of signatures f={f}"):
- assert signatures_len = (f + 1)
- end
-
- let (msg) = hash_report(
- report_context,
- observation_timestamp,
- observers,
- observations_len,
- observations,
- juels_per_fee_coin
- )
- verify_signatures(msg, signatures, signatures_len, signed_count=0)
-
- # report():
-
- assert_nn_le(observations_len, MAX_ORACLES) # len <= MAX_ORACLES
- assert_lt(f, observations_len) # f < len
-
- latest_epoch_and_round_.write(report_context.epoch_and_round)
-
- let (median_idx : felt, _) = unsigned_div_rem(observations_len, 2)
- let median = observations[median_idx]
-
- # Check abs(median) is in i192 range.
- # NOTE: (assert_lt_felt(-i192::MAX, median) doesn't work correctly so we have to use abs!)
- let (value) = abs_value(median)
- with_attr error_message("value not in int192 range: {median}"):
- assert_lt_felt(value, INT192_MAX)
- end
-
- # Validate median in min-max range
- let (answer_range : Range) = answer_range_.read()
- assert_in_range(median, answer_range.min, answer_range.max)
- # TODO: needs to handle negative values correctly, add test
-
- let (local prev_round_id) = latest_aggregator_round_id_.read()
- # let (prev_round_id) = latest_aggregator_round_id_.read()
- let round_id = prev_round_id + 1
- latest_aggregator_round_id_.write(round_id)
-
- let (timestamp : felt) = get_block_timestamp()
- let (block_num : felt) = get_block_number()
-
- # write to storage
- transmissions_.write(round_id, Transmission(
- answer=median,
- block_num=block_num,
- observation_timestamp=observation_timestamp,
- transmission_timestamp=timestamp,
- ))
-
- # validate via validator
- let (validator) = validator_.read()
-
- if validator != 0:
- let (prev_transmission) = transmissions_.read(prev_round_id)
- IValidator.validate(
- contract_address=validator,
- prev_round_id=prev_round_id,
- prev_answer=prev_transmission.answer,
- round_id=round_id,
- answer=median
- )
-
- tempvar syscall_ptr = syscall_ptr
- tempvar range_check_ptr = range_check_ptr
- tempvar pedersen_ptr = pedersen_ptr
- else:
- tempvar syscall_ptr = syscall_ptr
- tempvar range_check_ptr = range_check_ptr
- tempvar pedersen_ptr = pedersen_ptr
- end
- tempvar syscall_ptr = syscall_ptr
- tempvar range_check_ptr = range_check_ptr
- tempvar pedersen_ptr = pedersen_ptr
-
- let (reimbursement_juels) = calculate_reimbursement()
-
- # end report()
-
- new_transmission.emit(
- round_id=round_id,
- answer=median,
- transmitter=caller,
- observation_timestamp=observation_timestamp,
- observers=observers,
- observations_len=observations_len,
- observations=observations,
- juels_per_fee_coin=juels_per_fee_coin,
- config_digest=report_context.config_digest,
- epoch_and_round=report_context.epoch_and_round,
- reimbursement=reimbursement_juels,
- )
-
- # pay transmitter
- let (billing: Billing) = billing_.read()
- let payment = reimbursement_juels + (billing.transmission_payment_gjuels * GIGA)
- # TODO: check overflow
-
- transmitters_.write(caller, Oracle(
- index=oracle.index,
- payment_juels=oracle.payment_juels + payment
- ))
-
- return ()
-end
-
-func hash_report{
- pedersen_ptr : HashBuiltin*,
-}(
- report_context: ReportContext,
- observation_timestamp: felt,
- observers: felt,
- observations_len: felt,
- observations: felt*,
- juels_per_fee_coin: felt,
-) -> (hash: felt):
- let hash_ptr = pedersen_ptr
- with hash_ptr:
- let (hash_state_ptr) = hash_init()
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, report_context.config_digest)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, report_context.epoch_and_round)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, report_context.extra_hash)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, observation_timestamp)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, observers)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, observations_len)
- let (hash_state_ptr) = hash_update(hash_state_ptr, observations, observations_len)
- let (hash_state_ptr) = hash_update_single(hash_state_ptr, juels_per_fee_coin)
-
- let (hash) = hash_finalize(hash_state_ptr)
- let pedersen_ptr = hash_ptr
- return (hash=hash)
- end
-end
-
-func verify_signatures{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- ecdsa_ptr : SignatureBuiltin*,
- bitwise_ptr : BitwiseBuiltin*,
- range_check_ptr,
-}(
- msg: felt,
- signatures: Signature*,
- signatures_len: felt,
- signed_count: felt # used for tracking duplicate signatures
-):
- alloc_locals
-
- if signatures_len == 0:
- # Check all signatures are unique (we only saw each pubkey once)
- let (masked) = bitwise_and(
- signed_count,
- 0x01010101010101010101010101010101010101010101010101010101010101
- )
- with_attr error_message("duplicate signer"):
- assert signed_count = masked
- end
- return ()
- end
-
- let signature = signatures[0]
-
- # Validate the signer key actually belongs to an oracle
- let (index) = signers_.read(signature.public_key)
- with_attr error_message("invalid signer {signature.public_key}"):
- assert_not_equal(index, 0) # 0 index = uninitialized
- end
-
- verify_ecdsa_signature(
- message=msg,
- public_key=signature.public_key,
- signature_r=signature.r,
- signature_s=signature.s
- )
-
- # TODO: Using shifts here might be expensive due to pow()?
- # evaluate using alloc() to allocate a signed_count[oracles_len] instead
-
- # signed_count + 1 << (8 * index)
- let (shift) = pow(2, 8 * index)
- let signed_count = signed_count + shift
-
- return verify_signatures(
- msg,
- signatures + Signature.SIZE,
- signatures_len - 1,
- signed_count
- )
-end
-
-# --- RequestNewRound
-
-# --- Queries
-
-@view
-func description{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (description: felt):
- let (description) = description_.read()
- return (description)
-end
-
-@view
-func decimals{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (decimals: felt):
- let (decimals) = decimals_.read()
- return (decimals)
-end
-
-struct Round:
- member round_id: felt
- member answer: felt
- member block_num: felt
- member observation_timestamp: felt
- member transmission_timestamp: felt
-end
-
-@view
-func round_data{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(round_id: felt) -> (round: Round):
- # TODO: assert round_id fits in u32
-
- let (transmission: Transmission) = transmissions_.read(round_id)
-
- let round = Round(
- round_id=round_id,
- answer=transmission.answer,
- block_num=transmission.block_num,
- observation_timestamp=transmission.observation_timestamp,
- transmission_timestamp=transmission.transmission_timestamp,
- )
- return (round)
-end
-
-@view
-func latest_round_data{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (round: Round):
- let (latest_round_id) = latest_aggregator_round_id_.read()
- let (transmission: Transmission) = transmissions_.read(latest_round_id)
-
- let round = Round(
- round_id=latest_round_id,
- answer=transmission.answer,
- block_num=transmission.block_num,
- observation_timestamp=transmission.observation_timestamp,
- transmission_timestamp=transmission.transmission_timestamp,
- )
- return (round)
-end
-
-
-# --- Set LINK Token
-
-@storage_var
-func link_token_() -> (token: felt):
-end
-
-@event
-func link_token_set(
- old_link_token: felt,
- new_link_token: felt
-):
-end
-
-
-@external
-func set_link_token{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(link_token: felt, recipient: felt):
- alloc_locals
- Ownable_only_owner()
-
- let (old_token) = link_token_.read()
- if link_token == old_token:
- return ()
- end
-
- let (contract_address) = get_contract_address()
-
- # call balanceOf as a sanity check to confirm we're talking to a token
- IERC20.balanceOf(
- contract_address=link_token,
- account=contract_address,
- )
-
- pay_oracles()
-
- # transfer remaining balance to recipient
- let (amount: Uint256) = IERC20.balanceOf(
- contract_address=link_token,
- account=contract_address,
- )
- IERC20.transfer(
- contract_address=old_token,
- recipient=recipient,
- amount=amount,
- )
-
- link_token_.write(link_token)
-
- link_token_set.emit(
- old_link_token=old_token,
- new_link_token=link_token,
- )
-
- return ()
-end
-
-@view
-func link_token{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (link_token: felt):
- let (link_token) = link_token_.read()
- return (link_token)
-end
-
-# --- Billing Access Controller
-
-@storage_var
-func billing_access_controller_() -> (access_controller: felt):
-end
-
-@event
-func billing_access_controller_set(
- old_controller: felt,
- new_controller: felt,
-):
-end
-
-@external
-func set_billing_access_controller{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(access_controller: felt):
- Ownable_only_owner()
-
- let (old_controller) = billing_access_controller_.read()
- if access_controller != old_controller:
- billing_access_controller_.write(access_controller)
-
- billing_access_controller_set.emit(
- old_controller=old_controller,
- new_controller=access_controller,
- )
-
- return ()
- end
-
- return ()
-end
-
-@view
-func billing_access_controller{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (access_controller: felt):
- let (access_controller) = billing_access_controller_.read()
- return (access_controller)
-end
-
-# --- Billing Config
-
-struct Billing:
- # TODO: use a single felt via (observation_payment, transmission_payment) = split_felt()?
- member observation_payment_gjuels : felt
- member transmission_payment_gjuels : felt
-end
-
-@storage_var
-func billing_() -> (config: Billing):
-end
-
-@view
-func billing{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (config: Billing):
- let (config: Billing) = billing_.read()
- return (config)
-end
-
-@event
-func billing_set(
- config: Billing,
-):
-end
-
-@external
-func set_billing{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(config: Billing):
- has_billing_access()
-
- # Pay out oracles using existing settings for rounds up to now
- pay_oracles()
-
- # check payment value ranges within u32 bounds
- assert_nn_le(config.observation_payment_gjuels, UINT32_MAX)
- assert_nn_le(config.transmission_payment_gjuels, UINT32_MAX)
-
- billing_.write(config)
-
- billing_set.emit(config=config)
-
- return ()
-end
-
-func has_billing_access{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (bool: felt):
- let (caller) = get_caller_address()
- let (owner) = Ownable_get_owner()
-
- # owner always has access
- if caller == owner:
- return (TRUE)
- end
-
- let (access_controller) = billing_access_controller_.read()
-
- let (has_access: felt) = IAccessController.has_access(
- contract_address=access_controller,
- address=caller
- )
- return (has_access)
-end
-
-# --- Payments and Withdrawals
-
-@event
-func oracle_paid(
- transmitter: felt,
- payee: felt,
- amount: Uint256,
- link_token: felt,
-):
-end
-
-@external
-func withdraw_payment{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(transmitter: felt):
- alloc_locals
- let (caller) = get_caller_address()
- let (payee) = payees_.read(transmitter)
- with_attr error_message("only payee can withdraw"):
- assert caller = payee
- end
-
- pay_oracle(transmitter)
- return ()
-end
-
-@external
-func owed_payment{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(transmitter: felt) -> (amount: felt):
- let (oracle: Oracle) = transmitters_.read(transmitter)
-
- if oracle.index == 0:
- return (0)
- end
-
- let (billing: Billing) = billing_.read()
-
- let (latest_round_id) = latest_aggregator_round_id_.read()
- let (from_round_id) = reward_from_aggregator_round_id_.read(transmitter)
- let rounds = latest_round_id - from_round_id
-
- let amount = (rounds * billing.observation_payment_gjuels * GIGA) + oracle.payment_juels
- return (amount)
-end
-
-func pay_oracle{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(transmitter: felt):
- alloc_locals
-
- let (oracle: Oracle) = transmitters_.read(transmitter)
-
- if oracle.index == 0:
- return ()
- end
-
- let (amount_: felt) = owed_payment(transmitter)
- assert_nn(amount_)
-
- # if zero, fastpath return to avoid empty transfers
- let (not_zero) = is_not_zero(amount_)
- if not_zero == FALSE:
- return ()
- end
-
- let (amount: Uint256) = felt_to_uint256(amount_)
- let (payee) = payees_.read(transmitter)
-
- let (link_token) = link_token_.read()
-
- # TODO: do something with the return value?
- IERC20.transfer(
- contract_address=link_token,
- recipient=payee,
- amount=amount,
- )
-
- oracle_paid.emit(
- transmitter=transmitter,
- payee=payee,
- amount=amount,
- link_token=link_token
- )
-
- return ()
-end
-
-func pay_oracles{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}():
- let (len) = oracles_len_.read()
- pay_oracles_(len)
- return ()
-end
-
-func pay_oracles_{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(index: felt):
- if index == 0:
- return ()
- end
-
- # TODO: share link_token & last_round_id between pay_oracle calls
- let (transmitter) = transmitters_list_.read(index)
- pay_oracle(transmitter)
-
- return pay_oracles_(index - 1)
-end
-
-@external
-func withdraw_funds{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(recipient: felt, amount: Uint256):
- has_billing_access()
-
- return ()
-end
-
-func total_link_due{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (due: felt):
- let (len) = oracles_len_.read()
- let (latest_round_id) = latest_aggregator_round_id_.read()
-
- let (amount) = total_link_due_(len, latest_round_id, 0, 0)
- return (amount)
-end
-
-func total_link_due_{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(index: felt, latest_round_id: felt, total_rounds: felt, payments_juels: felt) -> (due: felt):
- if index == 0:
- let (billing: Billing) = billing_.read()
- let amount = (total_rounds * billing.observation_payment_gjuels * GIGA) + payments_juels
- return (amount)
- end
-
- let (transmitter) = transmitters_list_.read(index)
- let (oracle: Oracle) = transmitters_.read(transmitter)
- assert_not_zero(oracle.index) # 0 == undefined
-
- let (from_round_id) = reward_from_aggregator_round_id_.read(transmitter)
- let rounds = latest_round_id - from_round_id
-
- let total_rounds = total_rounds + rounds
- let payments_juels = payments_juels + oracle.payment_juels
-
- return total_link_due_(index - 1, latest_round_id, total_rounds, payments_juels)
-end
-
-@view
-func link_available_for_payment{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}() -> (available: felt):
- alloc_locals
- let (link_token) = link_token_.read()
- let (contract_address) = get_contract_address()
-
- let (balance_: Uint256) = IERC20.balanceOf(
- contract_address=link_token,
- account=contract_address,
- )
- # entire link supply fits into u96 so this should not fail
- let (balance) = uint256_to_felt(balance_)
-
- let (due) = total_link_due()
- let amount = balance - due
-
- return (available=amount)
-end
-
-# --- Transmitter Payment
-
-func calculate_reimbursement() -> (amount: felt):
- # TODO:
- let amount = 0
- return (amount)
-end
-
-# --- Payee Management
-
-@storage_var
-func payees_(transmitter: felt) -> (payment_address: felt):
-end
-
-@storage_var
-func proposed_payees_(transmitter: felt) -> (payment_address: felt):
-end
-
-@event
-func payeeship_transfer_requested(
- transmitter: felt,
- current: felt,
- proposed: felt,
-):
-end
-
-@event
-func payeeship_transferred(
- transmitter: felt,
- previous: felt,
- current: felt,
-):
-end
-
-struct PayeeConfig:
- member transmitter: felt
- member payee: felt
-end
-
-@external
-func set_payees{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}(payees_len: felt, payees: PayeeConfig*):
- Ownable_only_owner()
-
- set_payee(payees, payees_len)
-
- return ()
-end
-
-
-# Returns 1 if value == 0. Returns 1 otherwise.
-func is_zero(value) -> (res):
- if value == 0:
- return (res=1)
- end
-
- return (res=0)
-end
-
-func set_payee{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}(payees: PayeeConfig*, len: felt):
- if len == 0:
- return ()
- end
-
- let (current_payee) = payees_.read(payees.transmitter)
-
- # a more convoluted way of saying
- # require(current_payee == 0 || current_payee == payee, "payee already set")
- let (is_unset) = is_zero(current_payee)
- let (is_same) = is_zero(current_payee - payees.payee)
- with_attr error_message("payee already set"):
- assert (is_unset - 1) * (is_same - 1) = 0
- end
-
- payees_.write(payees.transmitter, payees.payee)
-
- payeeship_transferred.emit(
- transmitter=payees.transmitter,
- previous=current_payee,
- current=payees.payee
- )
-
- return set_payee(payees + PayeeConfig.SIZE, len - 1)
-end
-
-@external
-func transfer_payeeship{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(transmitter: felt, proposed: felt):
- let (caller) = get_caller_address()
- let (payee) = payees_.read(transmitter)
- with_attr error_message("only current payee can update"):
- assert caller = payee
- end
- with_attr error_message("cannot transfer to self"):
- assert_not_equal(caller, proposed)
- end
-
- proposed_payees_.write(transmitter, proposed)
-
- payeeship_transfer_requested.emit(
- transmitter=transmitter,
- current=payee,
- proposed=proposed
- )
-
- return ()
-end
-
-@external
-func accept_payeeship{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(transmitter: felt):
- let (caller) = get_caller_address()
- let (proposed) = proposed_payees_.read(transmitter)
- with_attr error_message("only proposed payee can accept"):
- assert caller = proposed
- end
-
- let (previous) = payees_.read(transmitter)
- payees_.write(transmitter, caller)
- proposed_payees_.write(transmitter, 0)
-
- payeeship_transferred.emit(
- transmitter=transmitter,
- previous=previous,
- current=caller
- )
-
- return ()
-end
diff --git a/contracts/contracts/interfaces/IAccessController.cairo b/contracts/contracts/interfaces/IAccessController.cairo
deleted file mode 100644
index c7b19d25c..000000000
--- a/contracts/contracts/interfaces/IAccessController.cairo
+++ /dev/null
@@ -1,10 +0,0 @@
-%lang starknet
-
-@contract_interface
-namespace IAccessController:
- func has_access(address: felt) -> (bool: felt):
- end
-
- func check_access(address: felt):
- end
-end
\ No newline at end of file
diff --git a/contracts/contracts/ownable.cairo b/contracts/contracts/ownable.cairo
deleted file mode 100644
index 20ab08ec3..000000000
--- a/contracts/contracts/ownable.cairo
+++ /dev/null
@@ -1,69 +0,0 @@
-# Equivalent to openzeppelin/Ownable except it's a two step process to transfer ownership.
-%lang starknet
-
-from starkware.cairo.common.cairo_builtins import HashBuiltin
-from starkware.starknet.common.syscalls import get_caller_address
-
-@storage_var
-func Ownable_owner() -> (owner_address : felt):
-end
-
-@storage_var
-func Ownable_proposed_owner() -> (proposed_owner_address : felt):
-end
-
-func Ownable_initializer{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}(owner: felt):
- Ownable_owner.write(owner)
- return ()
-end
-
-func Ownable_only_owner{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}():
- let (owner) = Ownable_owner.read()
- let (caller) = get_caller_address()
- with_attr error_message("Ownable: caller is not the owner"):
- assert owner = caller
- end
- return ()
-end
-
-func Ownable_get_owner{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}() -> (owner: felt):
- let (owner) = Ownable_owner.read()
- return (owner=owner)
-end
-
-func Ownable_transfer_ownership{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}(new_owner: felt) -> ():
- Ownable_only_owner()
- Ownable_proposed_owner.write(new_owner)
- return ()
-end
-
-func Ownable_accept_ownership{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr
-}() -> (new_owner: felt):
- let (proposed_owner) = Ownable_proposed_owner.read()
- let (caller) = get_caller_address()
- with_attr error_message("Ownable: caller is not the proposed owner"):
- assert proposed_owner = caller
- end
- Ownable_owner.write(proposed_owner)
- return (new_owner=proposed_owner)
-end
-
diff --git a/contracts/contracts/token.cairo b/contracts/contracts/token.cairo
deleted file mode 100644
index 00c0ad5af..000000000
--- a/contracts/contracts/token.cairo
+++ /dev/null
@@ -1,3 +0,0 @@
-%lang starknet
-
-from openzeppelin.token.erc20.ERC20_Mintable import constructor
\ No newline at end of file
diff --git a/contracts/contracts/validator.cairo b/contracts/contracts/validator.cairo
deleted file mode 100644
index 396692aa4..000000000
--- a/contracts/contracts/validator.cairo
+++ /dev/null
@@ -1,150 +0,0 @@
-%lang starknet
-
-from starkware.cairo.common.cairo_builtins import HashBuiltin
-from starkware.cairo.common.bool import TRUE, FALSE
-from starkware.cairo.common.math import split_felt
-from starkware.cairo.common.math_cmp import is_le
-from starkware.cairo.common.uint256 import (
- Uint256,
- uint256_sub,
- uint256_mul,
- uint256_eq,
- uint256_signed_div_rem,
- uint256_cond_neg,
- uint256_le
-)
-
-const THRESHOLD_MULTIPLIER = 100000
-
-from contracts.ownable import (
- Ownable_initializer,
- Ownable_only_owner,
-)
-
-@storage_var
-func flags_() -> (address: felt):
-end
-
-@storage_var
-func threshold_() -> (threshold: felt):
-end
-
-@constructor
-func constructor{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- owner: felt,
- flags: felt,
- threshold: felt,
-):
- Ownable_initializer(owner)
- flags_.write(flags)
- threshold_.write(threshold)
- return ()
-end
-
-@external
-func validate{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- prev_round_id: felt,
- prev_answer: felt,
- round_id: felt,
- answer: felt
-) -> (valid: felt):
- alloc_locals
-
- let (valid) = is_valid(prev_answer, answer)
-
- if valid == FALSE:
- # Do stuff
- let a = 1
- end
-
- return (valid)
-end
-
-# TODO: set_ events
-
-@external
-func set_flagging_threshold{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- threshold: felt
-):
- Ownable_only_owner()
- threshold_.write(threshold)
- return ()
-end
-
-@external
-func set_flags_address{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- flags: felt
-):
- Ownable_only_owner()
- flags_.write(flags)
- return ()
-end
-
-# ---
-
-func felt_to_uint256{range_check_ptr}(x) -> (uint_x : Uint256):
- let (high, low) = split_felt(x)
- return (Uint256(low=low, high=high))
-end
-
-# TODO: quadruple test the logic in this method to ensure it can never fail & revert
-func is_valid{
- syscall_ptr : felt*,
- pedersen_ptr : HashBuiltin*,
- range_check_ptr,
-}(
- _prev_answer: felt,
- _answer: felt
-) -> (valid: felt):
- alloc_locals
-
- if _prev_answer == 0:
- # TODO: I'd rather check round_id
- return (valid=TRUE)
- end
-
- let (prev_answer: Uint256) = felt_to_uint256(_prev_answer)
- let (answer: Uint256) = felt_to_uint256(_answer)
-
- # TODO: how is underflow/overflow handled here?
- let (change: Uint256) = uint256_sub(prev_answer, answer)
- let (multiplier: Uint256) = felt_to_uint256(THRESHOLD_MULTIPLIER)
-
- let (numerator: Uint256, overflow: Uint256) = uint256_mul(change, multiplier)
- let (zero) = uint256_eq(overflow, Uint256(0, 0))
- # If overflow is not zero then we overflowed
- if zero != TRUE:
- return (valid=FALSE)
- end
-
- let (ratio: Uint256, _remainder) = uint256_signed_div_rem(numerator, prev_answer)
-
- # Take the absolute value of ratio.
- # https://github.com/starkware-libs/cairo-lang/blob/b614d1867c64f3fb2cf4a4879348cfcf87c3a5a7/src/starkware/cairo/common/uint256.cairo#L261-L264=
- let (local ratio_sign) = is_le(2 ** 127, ratio.high)
- local range_check_ptr = range_check_ptr
- let (local ratio) = uint256_cond_neg(ratio, should_neg=ratio_sign)
- # TODO: can it be simplified via sign()?
-
- let (threshold_felt) = threshold_.read()
- let (threshold: Uint256) = felt_to_uint256(threshold_felt)
- # ratio <= threshold
- let (is_le_) = uint256_le(ratio, threshold)
- return (valid=is_le_)
-end
\ No newline at end of file
diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts
new file mode 100644
index 000000000..391b13487
--- /dev/null
+++ b/contracts/hardhat.config.ts
@@ -0,0 +1,55 @@
+import { HardhatUserConfig } from 'hardhat/types'
+import '@nomiclabs/hardhat-ethers'
+import '@nomicfoundation/hardhat-chai-matchers'
+import 'solidity-coverage'
+import { prepareHardhatArtifacts } from './test/setup'
+
+const COMPILER_SETTINGS = {
+ optimizer: {
+ enabled: true,
+ runs: 1000000,
+ },
+ metadata: {
+ bytecodeHash: 'none',
+ },
+}
+
+/**
+ * @type import('hardhat/config').HardhatUserConfig
+ */
+const config: HardhatUserConfig = {
+ // NOTE: hardhat comes with a special built-in network called 'harhdat'. This network is automatically created and
+ // used if no networks are defined in our config: https://hardhat.org/hardhat-runner/docs/config#hardhat-network. It
+ // is important to note that we DO NOT want to use this network. Our testing scripts already spawn a hardhat node in
+ // a container, so we should use this for the l1 <> l2 messaging tests rather than the auto-generated one from hardhat.
+ // To achieve this, the 'defaultNetwork' and 'networks' properties have been adjusted such that they reference the
+ // containerized hardhat node.
+ defaultNetwork: 'localhost',
+ networks: {
+ localhost: {
+ url: 'http://127.0.0.1:8545',
+ },
+ },
+ solidity: {
+ compilers: [
+ {
+ version: '0.8.15',
+ settings: COMPILER_SETTINGS,
+ },
+ ],
+ },
+ mocha: {
+ timeout: 10000000,
+ rootHooks: {
+ beforeAll: prepareHardhatArtifacts,
+ },
+ },
+ paths: {
+ sources: './solidity',
+ starknetSources: './src',
+ starknetArtifacts: './target/release',
+ cairoPaths: [],
+ },
+}
+
+export default config
diff --git a/contracts/package.json b/contracts/package.json
index 8468826d3..b8a109c9e 100644
--- a/contracts/package.json
+++ b/contracts/package.json
@@ -1,5 +1,29 @@
{
+ "name": "@chainlink/starknet-contracts",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "compile:solidity": "hardhat compile",
+ "test": "hardhat test"
+ },
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ethereum-waffle/mock-contract": "^4.0.4",
+ "@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
+ "@nomiclabs/hardhat-ethers": "^2.0.5",
+ "@types/chai": "^4.3.3",
+ "@types/elliptic": "^6.4.14",
+ "@types/mocha": "^9.1.1",
+ "chai": "^4.3.6",
+ "ethers": "^5.6.8",
+ "hardhat": "^2.16.1",
+ "solidity-coverage": "^0.8.2"
+ },
"dependencies": {
- "cairo-ls": "^0.0.4"
+ "@chainlink/contracts": "^0.4.2",
+ "@openzeppelin/contracts": "^4.7.3",
+ "axios": "^0.24.0"
}
}
diff --git a/contracts/pytest.ini b/contracts/pytest.ini
deleted file mode 100644
index d280de047..000000000
--- a/contracts/pytest.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[pytest]
-asyncio_mode = auto
\ No newline at end of file
diff --git a/contracts/requirements.txt b/contracts/requirements.txt
deleted file mode 100644
index 80892a14e..000000000
--- a/contracts/requirements.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-ecdsa
-fastecdsa
-sympy
-cairo-lang
-cairo-nile
-openzeppelin-cairo-contracts
\ No newline at end of file
diff --git a/contracts/solidity/emergency/StarknetValidator.sol b/contracts/solidity/emergency/StarknetValidator.sol
new file mode 100644
index 000000000..cf4191646
--- /dev/null
+++ b/contracts/solidity/emergency/StarknetValidator.sol
@@ -0,0 +1,316 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "@chainlink/contracts/src/v0.8/interfaces/AggregatorValidatorInterface.sol";
+import "@chainlink/contracts/src/v0.8/interfaces/TypeAndVersionInterface.sol";
+import "@chainlink/contracts/src/v0.8/interfaces/AccessControllerInterface.sol";
+import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
+import "@chainlink/contracts/src/v0.8/SimpleWriteAccessController.sol";
+import "@chainlink/contracts/src/v0.8/dev/vendor/openzeppelin-solidity/v4.3.1/contracts/utils/Address.sol";
+import "../../vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessaging.sol";
+
+/// @title StarknetValidator - makes cross chain calls to update the Sequencer Uptime Feed on L2
+contract StarknetValidator is TypeAndVersionInterface, AggregatorValidatorInterface, SimpleWriteAccessController {
+ // Config for L1 -> L2 message cost approximation
+ // Message Cost = gasAdjustment * gasEstimate * gasPriceL1Feed / 100
+ struct GasConfig {
+ // gas units derived from starknet estimate_message_fee
+ // recommended value is 17300 at time of writing
+ uint256 gasEstimate;
+ address gasPriceL1Feed;
+ // gasAdjustment of 100 equals 1x (see setGasConfig for more info)
+ // recommended value is 130 (or 1.3x) because at time of writing
+ // L2 gas price is equal to L1 gas price + some margin
+ uint32 gasAdjustment;
+ }
+
+ int256 private constant ANSWER_SEQ_OFFLINE = 1;
+
+ uint256 public immutable SELECTOR_STARK_UPDATE_STATUS = _selectorStarknet("update_status");
+ uint256 public immutable L2_UPTIME_FEED_ADDR;
+
+ IStarknetMessaging public immutable STARKNET_CROSS_DOMAIN_MESSENGER;
+
+ AggregatorV3Interface private s_source;
+ AccessControllerInterface private s_configAC;
+ GasConfig private s_gasConfig;
+
+ /// @notice Starknet messaging contract address - the address is 0.
+ error InvalidStarknetMessagingAddress();
+ /// @notice Starknet uptime feed address - the address is 0.
+ error InvalidL2FeedAddress();
+ /// @notice Error thrown when the source aggregator address is 0
+ error InvalidSourceAggregatorAddress();
+ /// @notice Error thrown when the access controller address is 0
+ error InvalidAccessControllerAddress();
+ /// @notice Error thrown when the l1 gas price feed address is 0
+ error InvalidGasPriceL1FeedAddress();
+ /// @notice Error thrown when caller is not the owner and does not have access
+ error AccessForbidden();
+
+ /// @notice This event is emitted when the gas config is set.
+ event GasConfigSet(uint256 gasEstimate, address indexed gasPriceL1Feed, uint32 gasAdjustment);
+ /// @notice emitted when a new gas access-control contract is set
+ event ConfigACSet(address indexed previous, address indexed current);
+ /// @notice emitted when a new source aggregator contract is set
+ event SourceAggregatorSet(address indexed previous, address indexed current);
+ /// @notice emitted when withdrawFunds or withdrawFundsTo is call
+ event FundsWithdrawn(address indexed recipient, uint256 amount);
+
+ /**
+ * @param starknetMessaging the address of the Starknet Messaging contract
+ * @param configAC the address of the AccessController contract managing config access
+ * @param gasPriceL1Feed address of the L1 gas price feed (used to approximate bridge L1 -> L2 message cost)
+ * @param source the source aggregator that we'll read data from (on retries)
+ * @param l2Feed the address of the target L2 contract (Sequencer Uptime Feed)
+ * @param gasEstimate the initial gas estimate for sending a message from L1 -> L2
+ */
+ constructor(
+ address starknetMessaging,
+ address configAC,
+ address gasPriceL1Feed,
+ address source,
+ uint256 l2Feed,
+ uint256 gasEstimate,
+ uint32 gasAdjustment
+ ) {
+ if (starknetMessaging == address(0)) {
+ revert InvalidStarknetMessagingAddress();
+ }
+
+ if (l2Feed == 0) {
+ revert InvalidL2FeedAddress();
+ }
+
+ STARKNET_CROSS_DOMAIN_MESSENGER = IStarknetMessaging(starknetMessaging);
+ L2_UPTIME_FEED_ADDR = l2Feed;
+
+ _setSourceAggregator(source);
+ _setConfigAC(configAC);
+ _setGasConfig(gasEstimate, gasPriceL1Feed, gasAdjustment);
+ }
+
+ /// @notice converts a bool to uint256.
+ function toUInt256(bool x) internal pure returns (uint256 r) {
+ assembly {
+ r := x
+ }
+ }
+
+ /**
+ * @notice versions:
+ *
+ * - StarknetValidator 0.1.0: initial release
+ * @inheritdoc TypeAndVersionInterface
+ */
+ function typeAndVersion() external pure virtual override returns (string memory) {
+ return "StarknetValidator 0.1.0";
+ }
+
+ /// @notice Returns the gas configuration for sending cross chain messages.
+ function getGasConfig() external view returns (GasConfig memory) {
+ return s_gasConfig;
+ }
+
+ /// @return address AccessControllerInterface contract address
+ function getConfigAC() external view returns (address) {
+ return address(s_configAC);
+ }
+
+ /// @return address Aggregator contract address
+ function getSourceAggregator() external view returns (address) {
+ return address(s_source);
+ }
+
+ /**
+ * @notice validate method sends an xDomain L2 tx to update Uptime Feed contract on L2.
+ * @dev A message is sent using the L1CrossDomainMessenger. This method is accessed controlled.
+ * @param currentAnswer new aggregator answer - value of 1 considers the sequencer offline.
+ * @return bool true if transaction succeeds.
+ */
+ function validate(
+ uint256 /* previousRoundId */,
+ int256 /* previousAnswer */,
+ uint256 /* currentRoundId */,
+ int256 currentAnswer
+ ) external override checkAccess returns (bool) {
+ return _sendUpdateMessageToL2(currentAnswer);
+ }
+
+ /**
+ * @notice retries to send the latest answer as update message to L2
+ * @dev only with access, useful in cases where a previous x-domain message was handeled unsuccessfully.
+ */
+ function retry() external checkAccess returns (bool) {
+ (, int256 latestAnswer, , , ) = AggregatorV3Interface(s_source).latestRoundData();
+ return _sendUpdateMessageToL2(latestAnswer);
+ }
+
+ /**
+ * @notice sends the 'update_status(answer, timestamp)' message
+ * via the L1 -> L2 bridge to the L2 feed contract
+ * @param answer The latest Sequencer status. 0 if the Sequencer is up and
+ * 1 if it is down.
+ */
+ function _sendUpdateMessageToL2(int256 answer) internal returns (bool) {
+ // Bridge fees are paid on L1
+ uint256 fee = _approximateFee();
+
+ // Fill payload with `status` and `timestamp`
+ uint256[] memory payload = new uint256[](2);
+ bool status = answer == ANSWER_SEQ_OFFLINE;
+ payload[0] = toUInt256(status);
+ payload[1] = block.timestamp;
+
+ // Make the Starknet x-domain call.
+ // NOTICE: we ignore the output of this call (msgHash, nonce).
+ // We also don't raise any events as the 'LogMessageToL2' event will be emitted from the messaging contract.
+ STARKNET_CROSS_DOMAIN_MESSENGER.sendMessageToL2{value: fee}(
+ L2_UPTIME_FEED_ADDR,
+ SELECTOR_STARK_UPDATE_STATUS,
+ payload
+ );
+ return true;
+ }
+
+ /// @notice L1 oracle is asked for a fast L1 gas price, and the price multiplied by the configured gas estimate
+ function _approximateFee() internal view returns (uint256) {
+ uint256 gasPrice = approximateGasPrice();
+ return gasPrice * s_gasConfig.gasEstimate;
+ }
+
+ /// @notice calculates the gas price accounting for the gasAdjustment values
+ function approximateGasPrice() public view returns (uint256) {
+ (, int256 fastGasPriceInWei, , , ) = AggregatorV3Interface(s_gasConfig.gasPriceL1Feed).latestRoundData();
+ return (uint256(fastGasPriceInWei) * uint256(s_gasConfig.gasAdjustment)) / 100;
+ }
+
+ /**
+ * @notice The selector is the starknet_keccak hash of the function name
+ * @dev Starknet keccak is defined as the first 250 bits of the Keccak256 hash.
+ * This is just Keccak256 augmented in order to fit into a field element.
+ * @param fn string function name
+ */
+ function _selectorStarknet(string memory fn) internal pure returns (uint256) {
+ bytes32 digest = keccak256(abi.encodePacked(fn));
+ return uint256(digest) % 2 ** 250; // get last 250 bits
+ }
+
+ /**
+ * @notice Sets the gas configuration for sending cross chain messages
+ * @param gasEstimate The estimated units of gas to execute the transaction on L2.
+ * This value should include any buffer to include.
+ * @param gasPriceL1Feed The address of the fast gas L1 feed on L1.
+ * @param gasAdjustment Percentage of the the cost to use. Ex: gasAdjustment of 120
+ * means 120% of original value, 1.2x multiplier, or 20% increase (all mean the same thing)
+ */
+ function setGasConfig(
+ uint256 gasEstimate,
+ address gasPriceL1Feed,
+ uint32 gasAdjustment
+ ) external onlyOwnerOrConfigAccess {
+ _setGasConfig(gasEstimate, gasPriceL1Feed, gasAdjustment);
+ }
+
+ /**
+ * @notice Sets the gas configuration for sending cross chain messages
+ * @param gasEstimate The estimated units of gas to execute the transaction on L2.
+ * This value should include any buffer to include.
+ * @param gasPriceL1Feed The address of the fast gas L1 feed on L1.
+ * @param gasAdjustment Percentage of the the cost to use. Ex: gasAdjustment of 120
+ * means 120% of original value, 1.2x multiplier, or 20% increase (all mean the same thing)
+ */
+ function _setGasConfig(uint256 gasEstimate, address gasPriceL1Feed, uint32 gasAdjustment) internal {
+ if (gasPriceL1Feed == address(0)) {
+ revert InvalidGasPriceL1FeedAddress();
+ }
+ s_gasConfig = GasConfig(gasEstimate, gasPriceL1Feed, gasAdjustment);
+ emit GasConfigSet(gasEstimate, gasPriceL1Feed, gasAdjustment);
+ }
+
+ /**
+ * @notice sets config AccessControllerInterface contract
+ * @dev only owner can call this
+ * @param accessController new AccessControllerInterface contract address
+ */
+ function setConfigAC(address accessController) external onlyOwner {
+ _setConfigAC(accessController);
+ }
+
+ /**
+ * @notice Internal method that stores the configuration access controller
+ * @param accessController The address of the Access Controller for this contract
+ */
+ function _setConfigAC(address accessController) internal {
+ if (accessController == address(0)) {
+ revert InvalidAccessControllerAddress();
+ }
+
+ address previousAccessController = address(s_configAC);
+ if (accessController != previousAccessController) {
+ // NOTICE: we don't give access to the new source aggregator
+ // It is not always the case that the source aggregator is also the sender for the 'validate' invocation
+ // as we usually deploy an additional proxy in between (owner can give access):
+ // https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ValidatorProxy.sol
+ s_configAC = AccessControllerInterface(accessController);
+ emit ConfigACSet(previousAccessController, accessController);
+ }
+ }
+
+ /**
+ * @notice sets the source aggregator AggregatorInterface contract
+ * @dev only owner can call this
+ * @param source the source aggregator that we'll read data from (on retries)
+ */
+ function setSourceAggregator(address source) external onlyOwner {
+ _setSourceAggregator(source);
+ }
+
+ /// @notice Internal method that sets the source aggregator AggregatorInterface contract
+ function _setSourceAggregator(address source) internal {
+ if (source == address(0)) {
+ revert InvalidSourceAggregatorAddress();
+ }
+ address previousSource = address(s_source);
+ if (source != previousSource) {
+ s_source = AggregatorV3Interface(source);
+ emit SourceAggregatorSet(previousSource, source);
+ }
+ }
+
+ /// @dev reverts if the caller does not have access to change the configuration
+ modifier onlyOwnerOrConfigAccess() {
+ if (msg.sender != owner() && (address(s_configAC) != address(0) && !s_configAC.hasAccess(msg.sender, msg.data))) {
+ revert AccessForbidden();
+ }
+ _;
+ }
+
+ /**
+ * @notice makes this contract payable
+ * @dev funds are used to pay the bridge for x-domain messages to L2
+ */
+ receive() external payable {}
+
+ /**
+ * @notice withdraws all funds available in this contract to the msg.sender
+ * @dev only owner can call this
+ */
+ function withdrawFunds() external onlyOwner {
+ address payable recipient = payable(msg.sender);
+ uint256 amount = address(this).balance;
+ Address.sendValue(recipient, amount);
+ emit FundsWithdrawn(recipient, amount);
+ }
+
+ /**
+ * @notice withdraws all funds available in this contract to the address specified
+ * @dev only owner can call this
+ * @param recipient address where to send the funds
+ */
+ function withdrawFundsTo(address payable recipient) external onlyOwner {
+ uint256 amount = address(this).balance;
+ Address.sendValue(recipient, amount);
+ emit FundsWithdrawn(recipient, amount);
+ }
+}
diff --git a/contracts/solidity/mocks/MockStarknetMessaging.sol b/contracts/solidity/mocks/MockStarknetMessaging.sol
new file mode 100644
index 000000000..84ca1c3c1
--- /dev/null
+++ b/contracts/solidity/mocks/MockStarknetMessaging.sol
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: Apache-2.0.
+pragma solidity ^0.8.0;
+
+import '../../vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/StarknetMessaging.sol';
+
+/**
+ * @title MockStarknetMessaging make cross chain call.
+ For Devnet L1 <> L2 communication testing, we have to replace IStarknetCore with the MockStarknetMessaging.sol contract
+ */
+contract MockStarknetMessaging is StarknetMessaging {
+ constructor(uint256 MessageCancellationDelay) {
+ messageCancellationDelay(MessageCancellationDelay);
+ }
+
+ /**
+ Mocks a message from L2 to L1.
+ */
+ function mockSendMessageFromL2(
+ uint256 fromAddress,
+ uint256 toAddress,
+ uint256[] calldata payload
+ ) external {
+ bytes32 msgHash = keccak256(
+ abi.encodePacked(fromAddress, toAddress, payload.length, payload)
+ );
+ l2ToL1Messages()[msgHash] += 1;
+ }
+
+ /**
+ Mocks consumption of a message from L1 to L2.
+ */
+ function mockConsumeMessageToL2(
+ uint256 fromAddress,
+ uint256 toAddress,
+ uint256 selector,
+ uint256[] calldata payload,
+ uint256 nonce
+ ) external {
+ bytes32 msgHash = keccak256(
+ abi.encodePacked(fromAddress, toAddress, nonce, selector, payload.length, payload)
+ );
+
+ require(l1ToL2Messages()[msgHash] > 0, "INVALID_MESSAGE_TO_CONSUME");
+ l1ToL2Messages()[msgHash] = 0;
+ }
+}
diff --git a/contracts/src/access_control.cairo b/contracts/src/access_control.cairo
new file mode 100644
index 000000000..f66d52735
--- /dev/null
+++ b/contracts/src/access_control.cairo
@@ -0,0 +1 @@
+mod access_controller;
diff --git a/contracts/src/access_control/access_controller.cairo b/contracts/src/access_control/access_controller.cairo
new file mode 100644
index 000000000..37d9ef909
--- /dev/null
+++ b/contracts/src/access_control/access_controller.cairo
@@ -0,0 +1,61 @@
+#[starknet::contract]
+mod AccessController {
+ use starknet::ContractAddress;
+ use starknet::class_hash::ClassHash;
+
+ use openzeppelin::access::ownable::OwnableComponent;
+
+ use chainlink::libraries::access_control::{AccessControlComponent, IAccessController};
+ use chainlink::libraries::type_and_version::ITypeAndVersion;
+ use chainlink::libraries::upgradeable::{Upgradeable, IUpgradeable};
+
+ component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+ component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent);
+
+ #[abi(embed_v0)]
+ impl OwnableImpl = OwnableComponent::OwnableTwoStepImpl;
+ impl OwnableInternalImpl = OwnableComponent::InternalImpl;
+
+ #[abi(embed_v0)]
+ impl AccessControlImpl =
+ AccessControlComponent::AccessControlImpl;
+ impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ #[flat]
+ OwnableEvent: OwnableComponent::Event,
+ #[flat]
+ AccessControlEvent: AccessControlComponent::Event,
+ }
+
+ #[storage]
+ struct Storage {
+ #[substorage(v0)]
+ ownable: OwnableComponent::Storage,
+ #[substorage(v0)]
+ access_control: AccessControlComponent::Storage,
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState, owner_address: ContractAddress) {
+ self.ownable.initializer(owner_address);
+ self.access_control.initializer();
+ }
+
+ #[abi(embed_v0)]
+ impl TypeAndVersionImpl of ITypeAndVersion {
+ fn type_and_version(self: @ContractState) -> felt252 {
+ 'AccessController 1.0.0'
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl UpgradeableImpl of IUpgradeable {
+ fn upgrade(ref self: ContractState, new_impl: ClassHash) {
+ self.ownable.assert_only_owner();
+ Upgradeable::upgrade(new_impl);
+ }
+ }
+}
diff --git a/contracts/src/account.cairo b/contracts/src/account.cairo
new file mode 100644
index 000000000..dcda746e8
--- /dev/null
+++ b/contracts/src/account.cairo
@@ -0,0 +1,57 @@
+// copied from https://raw.githubusercontent.com/OpenZeppelin/cairo-contracts/861fc416f87addbe23a3b47f9d19ab27c10d5dc8/src/presets/account.cairo (0.9.0)
+
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.9.0 (presets/account.cairo)
+
+/// # Account Preset
+///
+/// OpenZeppelin's basic account which can change its public key and declare, deploy, or call contracts.
+#[starknet::contract(account)]
+mod Account {
+ use openzeppelin::account::AccountComponent;
+ use openzeppelin::introspection::src5::SRC5Component;
+
+ component!(path: AccountComponent, storage: account, event: AccountEvent);
+ component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+ // Account
+ #[abi(embed_v0)]
+ impl SRC6Impl = AccountComponent::SRC6Impl;
+ #[abi(embed_v0)]
+ impl SRC6CamelOnlyImpl = AccountComponent::SRC6CamelOnlyImpl;
+ #[abi(embed_v0)]
+ impl PublicKeyImpl = AccountComponent::PublicKeyImpl;
+ #[abi(embed_v0)]
+ impl PublicKeyCamelImpl = AccountComponent::PublicKeyCamelImpl;
+ #[abi(embed_v0)]
+ impl DeclarerImpl = AccountComponent::DeclarerImpl;
+ #[abi(embed_v0)]
+ impl DeployableImpl = AccountComponent::DeployableImpl;
+ impl AccountInternalImpl = AccountComponent::InternalImpl;
+
+ // SRC5
+ #[abi(embed_v0)]
+ impl SRC5Impl = SRC5Component::SRC5Impl;
+
+ #[storage]
+ struct Storage {
+ #[substorage(v0)]
+ account: AccountComponent::Storage,
+ #[substorage(v0)]
+ src5: SRC5Component::Storage
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ #[flat]
+ AccountEvent: AccountComponent::Event,
+ #[flat]
+ SRC5Event: SRC5Component::Event
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState, public_key: felt252) {
+ self.account.initializer(public_key);
+ }
+}
diff --git a/contracts/src/cairo_project.toml b/contracts/src/cairo_project.toml
new file mode 100644
index 000000000..df2c45c01
--- /dev/null
+++ b/contracts/src/cairo_project.toml
@@ -0,0 +1,2 @@
+[crate_roots]
+chainlink = "."
diff --git a/contracts/src/emergency.cairo b/contracts/src/emergency.cairo
new file mode 100644
index 000000000..5d04f312d
--- /dev/null
+++ b/contracts/src/emergency.cairo
@@ -0,0 +1 @@
+mod sequencer_uptime_feed;
diff --git a/contracts/src/emergency/sequencer_uptime_feed.cairo b/contracts/src/emergency/sequencer_uptime_feed.cairo
new file mode 100644
index 000000000..fad5ad8f7
--- /dev/null
+++ b/contracts/src/emergency/sequencer_uptime_feed.cairo
@@ -0,0 +1,318 @@
+use starknet::EthAddress;
+
+#[starknet::interface]
+trait ISequencerUptimeFeed {
+ fn l1_sender(self: @TContractState) -> EthAddress;
+ fn set_l1_sender(ref self: TContractState, address: EthAddress);
+}
+
+#[starknet::contract]
+mod SequencerUptimeFeed {
+ use starknet::EthAddress;
+ use starknet::EthAddressSerde;
+ use starknet::EthAddressIntoFelt252;
+ use starknet::Felt252TryIntoEthAddress;
+ use starknet::EthAddressZeroable;
+ use starknet::ContractAddress;
+ use starknet::StorageBaseAddress;
+ use starknet::SyscallResult;
+ use starknet::storage_read_syscall;
+ use starknet::storage_write_syscall;
+ use starknet::storage_address_from_base_and_offset;
+ use starknet::class_hash::ClassHash;
+
+ use box::BoxTrait;
+ use traits::Into;
+ use traits::TryInto;
+ use option::OptionTrait;
+ use zeroable::Zeroable;
+
+ use openzeppelin::access::ownable::OwnableComponent;
+
+ use chainlink::libraries::access_control::{AccessControlComponent, IAccessController};
+ use chainlink::libraries::access_control::AccessControlComponent::InternalTrait as AccessControlInternalTrait;
+ use chainlink::libraries::type_and_version::ITypeAndVersion;
+ use chainlink::ocr2::aggregator::Round;
+ use chainlink::ocr2::aggregator::IAggregator;
+ use chainlink::ocr2::aggregator::{Transmission};
+ use chainlink::libraries::upgradeable::Upgradeable;
+
+ component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+ component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent);
+
+ #[abi(embed_v0)]
+ impl OwnableImpl = OwnableComponent::OwnableTwoStepImpl;
+ impl OwnableInternalImpl = OwnableComponent::InternalImpl;
+
+ #[abi(embed_v0)]
+ impl AccessControlImpl =
+ AccessControlComponent::AccessControlImpl;
+ impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;
+
+ #[storage]
+ struct Storage {
+ #[substorage(v0)]
+ ownable: OwnableComponent::Storage,
+ #[substorage(v0)]
+ access_control: AccessControlComponent::Storage,
+ // l1 sender is an starknet validator ethereum address
+ _l1_sender: EthAddress,
+ // maps round id to round transmission
+ _round_transmissions: LegacyMap,
+ _latest_round_id: u128,
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ #[flat]
+ OwnableEvent: OwnableComponent::Event,
+ #[flat]
+ AccessControlEvent: AccessControlComponent::Event,
+ RoundUpdated: RoundUpdated,
+ NewRound: NewRound,
+ AnswerUpdated: AnswerUpdated,
+ UpdateIgnored: UpdateIgnored,
+ L1SenderTransferred: L1SenderTransferred,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct RoundUpdated {
+ status: u128,
+ #[key]
+ updated_at: u64
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct NewRound {
+ #[key]
+ round_id: u128,
+ #[key]
+ started_by: EthAddress,
+ started_at: u64
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct AnswerUpdated {
+ current: u128,
+ #[key]
+ round_id: u128,
+ #[key]
+ timestamp: u64
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct UpdateIgnored {
+ latest_status: u128,
+ #[key]
+ latest_timestamp: u64,
+ incoming_status: u128,
+ #[key]
+ incoming_timestamp: u64
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct L1SenderTransferred {
+ #[key]
+ from_address: EthAddress,
+ #[key]
+ to_address: EthAddress
+ }
+
+ #[abi(embed_v0)]
+ impl TypeAndVersion of ITypeAndVersion {
+ fn type_and_version(self: @ContractState) -> felt252 {
+ 'SequencerUptimeFeed 1.0.0'
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl AggregatorImpl of IAggregator {
+ fn latest_round_data(self: @ContractState) -> Round {
+ self._require_read_access();
+ let latest_round_id = self._latest_round_id.read();
+ let round_transmission = self._round_transmissions.read(latest_round_id);
+ Round {
+ round_id: latest_round_id.into(),
+ answer: round_transmission.answer,
+ block_num: round_transmission.block_num,
+ started_at: round_transmission.observation_timestamp,
+ updated_at: round_transmission.transmission_timestamp,
+ }
+ }
+
+ fn round_data(self: @ContractState, round_id: u128) -> Round {
+ self._require_read_access();
+ assert(round_id <= self._latest_round_id.read(), 'invalid round id');
+ let round_transmission = self._round_transmissions.read(round_id);
+ Round {
+ round_id: round_id.into(),
+ answer: round_transmission.answer,
+ block_num: round_transmission.block_num,
+ started_at: round_transmission.observation_timestamp,
+ updated_at: round_transmission.transmission_timestamp,
+ }
+ }
+
+ fn description(self: @ContractState) -> felt252 {
+ 'L2 Sequencer Uptime Status Feed'
+ }
+
+ fn decimals(self: @ContractState) -> u8 {
+ 0_u8
+ }
+
+ fn latest_answer(self: @ContractState) -> u128 {
+ self._require_read_access();
+ let latest_round_id = self._latest_round_id.read();
+ let round_transmission = self._round_transmissions.read(latest_round_id);
+ round_transmission.answer
+ }
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState, initial_status: u128, owner_address: ContractAddress) {
+ self._initializer(initial_status, owner_address);
+ }
+
+ #[l1_handler]
+ fn update_status(ref self: ContractState, from_address: felt252, status: u128, timestamp: u64) {
+ // Cairo enforces from_address to be a felt252 on the method signature, but we can cast it right after
+ let from_address: EthAddress = from_address.try_into().unwrap();
+ assert(self._l1_sender.read() == from_address, 'EXPECTED_FROM_BRIDGE_ONLY');
+
+ let latest_round_id = self._latest_round_id.read();
+ let latest_round = self._round_transmissions.read(latest_round_id);
+
+ if timestamp <= latest_round.observation_timestamp {
+ self
+ .emit(
+ Event::UpdateIgnored(
+ UpdateIgnored {
+ latest_status: latest_round.answer,
+ latest_timestamp: latest_round.transmission_timestamp,
+ incoming_status: status,
+ incoming_timestamp: timestamp
+ }
+ )
+ );
+ return ();
+ }
+
+ if latest_round.answer == status {
+ self._update_round(latest_round_id, latest_round);
+ } else {
+ // only increment round when status flips
+ let round_id = latest_round_id + 1_u128;
+ self._record_round(from_address, round_id, status, timestamp);
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl SequencerUptimeFeedImpl of super::ISequencerUptimeFeed {
+ fn set_l1_sender(ref self: ContractState, address: EthAddress) {
+ self.ownable.assert_only_owner();
+
+ assert(!address.is_zero(), '0 address not allowed');
+
+ let old_address = self._l1_sender.read();
+
+ if old_address != address {
+ self._l1_sender.write(address);
+ self
+ .emit(
+ Event::L1SenderTransferred(
+ L1SenderTransferred { from_address: old_address, to_address: address }
+ )
+ );
+ }
+ }
+
+ fn l1_sender(self: @ContractState) -> EthAddress {
+ self._l1_sender.read()
+ }
+ }
+
+ ///
+ /// Upgradeable
+ ///
+
+ #[abi(embed_v0)]
+ fn upgrade(ref self: ContractState, new_impl: ClassHash) {
+ self.ownable.assert_only_owner();
+ Upgradeable::upgrade(new_impl)
+ }
+
+ ///
+ /// Internals
+ ///
+
+ #[generate_trait]
+ impl Internals of InternalTrait {
+ fn _require_read_access(self: @ContractState) {
+ let sender = starknet::info::get_caller_address();
+ self.access_control.check_read_access(sender);
+ }
+
+ fn _initializer(
+ ref self: ContractState, initial_status: u128, owner_address: ContractAddress
+ ) {
+ self.ownable.initializer(owner_address);
+ self.access_control.initializer();
+ let round_id = 1_u128;
+ let timestamp = starknet::info::get_block_timestamp();
+ let from_address = EthAddress {
+ address: 0
+ }; // initial round is set by the constructor, not by an L1 sender
+ self._record_round(from_address, round_id, initial_status, timestamp);
+ }
+
+ fn _record_round(
+ ref self: ContractState,
+ sender: EthAddress,
+ round_id: u128,
+ status: u128,
+ timestamp: u64
+ ) {
+ self._latest_round_id.write(round_id);
+ let block_info = starknet::info::get_block_info().unbox();
+ let block_number = block_info.block_number;
+ let block_timestamp = block_info.block_timestamp;
+
+ let round = Transmission {
+ answer: status,
+ block_num: block_number,
+ observation_timestamp: timestamp,
+ transmission_timestamp: block_timestamp,
+ };
+ self._round_transmissions.write(round_id, round);
+
+ self
+ .emit(
+ Event::NewRound(
+ NewRound { round_id: round_id, started_by: sender, started_at: timestamp }
+ )
+ );
+ self
+ .emit(
+ Event::AnswerUpdated(
+ AnswerUpdated { current: status, round_id: round_id, timestamp: timestamp }
+ )
+ );
+ }
+
+ fn _update_round(ref self: ContractState, round_id: u128, mut round: Transmission) {
+ round.transmission_timestamp = starknet::info::get_block_timestamp();
+ self._round_transmissions.write(round_id, round);
+
+ self
+ .emit(
+ Event::RoundUpdated(
+ RoundUpdated {
+ status: round.answer, updated_at: round.transmission_timestamp
+ }
+ )
+ );
+ }
+ }
+}
diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo
new file mode 100644
index 000000000..eef049d8c
--- /dev/null
+++ b/contracts/src/lib.cairo
@@ -0,0 +1,13 @@
+// All modules must be present here
+
+mod account;
+mod ocr2;
+mod libraries;
+mod utils;
+mod emergency;
+mod multisig;
+mod token;
+mod access_control;
+
+#[cfg(test)]
+mod tests;
diff --git a/contracts/src/libraries.cairo b/contracts/src/libraries.cairo
new file mode 100644
index 000000000..02d62120e
--- /dev/null
+++ b/contracts/src/libraries.cairo
@@ -0,0 +1,5 @@
+mod access_control;
+mod token;
+mod upgradeable;
+mod mocks;
+mod type_and_version;
diff --git a/contracts/src/libraries/access_control.cairo b/contracts/src/libraries/access_control.cairo
new file mode 100644
index 000000000..b0a910d55
--- /dev/null
+++ b/contracts/src/libraries/access_control.cairo
@@ -0,0 +1,154 @@
+use starknet::ContractAddress;
+#[starknet::interface]
+trait IAccessController {
+ fn has_access(self: @TContractState, user: ContractAddress, data: Array) -> bool;
+ fn has_read_access(self: @TContractState, user: ContractAddress, data: Array) -> bool;
+ fn add_access(ref self: TContractState, user: ContractAddress);
+ fn remove_access(ref self: TContractState, user: ContractAddress);
+ fn enable_access_check(ref self: TContractState);
+ fn disable_access_check(ref self: TContractState);
+}
+
+// Requires Ownable subcomponent.
+#[starknet::component]
+mod AccessControlComponent {
+ use starknet::ContractAddress;
+ use starknet::class_hash::ClassHash;
+ use zeroable::Zeroable;
+
+ use openzeppelin::access::ownable::OwnableComponent;
+
+ use OwnableComponent::InternalImpl as OwnableInternalImpl;
+
+ #[storage]
+ struct Storage {
+ _check_enabled: bool,
+ _access_list: LegacyMap,
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ AddedAccess: AddedAccess,
+ RemovedAccess: RemovedAccess,
+ AccessControlEnabled: AccessControlEnabled,
+ AccessControlDisabled: AccessControlDisabled,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct AddedAccess {
+ #[key]
+ user: ContractAddress
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct RemovedAccess {
+ #[key]
+ user: ContractAddress
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct AccessControlEnabled {}
+
+ #[derive(Drop, starknet::Event)]
+ struct AccessControlDisabled {}
+
+ #[embeddable_as(AccessControlImpl)]
+ impl AccessControl<
+ TContractState,
+ +HasComponent,
+ impl Ownable: OwnableComponent::HasComponent,
+ +Drop,
+ > of super::IAccessController> {
+ fn has_access(
+ self: @ComponentState, user: ContractAddress, data: Array
+ ) -> bool {
+ let has_access = self._access_list.read(user);
+ if has_access {
+ return true;
+ }
+
+ let check_enabled = self._check_enabled.read();
+ if !check_enabled {
+ return true;
+ }
+
+ false
+ }
+
+ fn has_read_access(
+ self: @ComponentState, user: ContractAddress, data: Array
+ ) -> bool {
+ let _has_access = self.has_access(user, data);
+ if _has_access {
+ return true;
+ }
+
+ // NOTICE: read access is granted to direct calls, to enable off-chain reads.
+ if user.is_zero() {
+ return true;
+ }
+
+ false
+ }
+
+ fn add_access(ref self: ComponentState, user: ContractAddress) {
+ get_dep_component!(@self, Ownable).assert_only_owner();
+ let has_access = self._access_list.read(user);
+ if !has_access {
+ self._access_list.write(user, true);
+ self.emit(Event::AddedAccess(AddedAccess { user: user }));
+ }
+ }
+
+ fn remove_access(ref self: ComponentState, user: ContractAddress) {
+ get_dep_component!(@self, Ownable).assert_only_owner();
+ let has_access = self._access_list.read(user);
+ if has_access {
+ self._access_list.write(user, false);
+ self.emit(Event::RemovedAccess(RemovedAccess { user: user }));
+ }
+ }
+
+ fn enable_access_check(ref self: ComponentState) {
+ get_dep_component!(@self, Ownable).assert_only_owner();
+ let check_enabled = self._check_enabled.read();
+ if !check_enabled {
+ self._check_enabled.write(true);
+ self.emit(Event::AccessControlEnabled(AccessControlEnabled {}));
+ }
+ }
+
+ fn disable_access_check(ref self: ComponentState) {
+ get_dep_component!(@self, Ownable).assert_only_owner();
+ let check_enabled = self._check_enabled.read();
+ if check_enabled {
+ self._check_enabled.write(false);
+ self.emit(Event::AccessControlDisabled(AccessControlDisabled {}));
+ }
+ }
+ }
+
+ #[generate_trait]
+ impl InternalImpl<
+ TContractState,
+ +HasComponent,
+ impl Ownable: OwnableComponent::HasComponent,
+ +Drop,
+ > of InternalTrait {
+ fn initializer(ref self: ComponentState) {
+ self._check_enabled.write(true);
+ self.emit(Event::AccessControlEnabled(AccessControlEnabled {}));
+ }
+
+ fn check_access(self: @ComponentState, user: ContractAddress) {
+ let allowed = AccessControl::has_access(self, user, ArrayTrait::new());
+ assert(allowed, 'user does not have access');
+ }
+
+ fn check_read_access(self: @ComponentState, user: ContractAddress) {
+ let allowed = AccessControl::has_read_access(self, user, ArrayTrait::new());
+ assert(allowed, 'user does not have read access');
+ }
+ }
+}
diff --git a/contracts/src/libraries/mocks.cairo b/contracts/src/libraries/mocks.cairo
new file mode 100644
index 000000000..9a58bab7d
--- /dev/null
+++ b/contracts/src/libraries/mocks.cairo
@@ -0,0 +1,2 @@
+mod mock_upgradeable;
+mod mock_non_upgradeable;
diff --git a/contracts/src/libraries/mocks/mock_non_upgradeable.cairo b/contracts/src/libraries/mocks/mock_non_upgradeable.cairo
new file mode 100644
index 000000000..921943ea3
--- /dev/null
+++ b/contracts/src/libraries/mocks/mock_non_upgradeable.cairo
@@ -0,0 +1,20 @@
+#[starknet::interface]
+trait IMockNonUpgradeable {
+ fn bar(self: @TContractState) -> bool;
+}
+
+#[starknet::contract]
+mod MockNonUpgradeable {
+ #[storage]
+ struct Storage {}
+
+ #[constructor]
+ fn constructor(ref self: ContractState) {}
+
+ #[abi(embed_v0)]
+ impl MockNonUpgradeableImpl of super::IMockNonUpgradeable {
+ fn bar(self: @ContractState) -> bool {
+ true
+ }
+ }
+}
diff --git a/contracts/src/libraries/mocks/mock_upgradeable.cairo b/contracts/src/libraries/mocks/mock_upgradeable.cairo
new file mode 100644
index 000000000..b83fa7f84
--- /dev/null
+++ b/contracts/src/libraries/mocks/mock_upgradeable.cairo
@@ -0,0 +1,31 @@
+use starknet::class_hash::ClassHash;
+
+#[starknet::interface]
+trait IMockUpgradeable {
+ fn foo(self: @TContractState) -> bool;
+ fn upgrade(ref self: TContractState, new_impl: ClassHash);
+}
+
+#[starknet::contract]
+mod MockUpgradeable {
+ use starknet::class_hash::ClassHash;
+
+ use chainlink::libraries::upgradeable::Upgradeable;
+
+ #[storage]
+ struct Storage {}
+
+ #[constructor]
+ fn constructor(ref self: ContractState) {}
+
+ #[abi(embed_v0)]
+ impl MockUpgradeableImpl of super::IMockUpgradeable {
+ fn foo(self: @ContractState) -> bool {
+ true
+ }
+
+ fn upgrade(ref self: ContractState, new_impl: ClassHash) {
+ Upgradeable::upgrade(new_impl)
+ }
+ }
+}
diff --git a/contracts/src/libraries/token.cairo b/contracts/src/libraries/token.cairo
new file mode 100644
index 000000000..b6b4d8df3
--- /dev/null
+++ b/contracts/src/libraries/token.cairo
@@ -0,0 +1 @@
+mod erc677;
diff --git a/contracts/src/libraries/token/erc677.cairo b/contracts/src/libraries/token/erc677.cairo
new file mode 100644
index 000000000..20fd6c54a
--- /dev/null
+++ b/contracts/src/libraries/token/erc677.cairo
@@ -0,0 +1,86 @@
+use starknet::ContractAddress;
+
+#[starknet::interface]
+trait IERC677 {
+ fn transfer_and_call(
+ ref self: TContractState, to: ContractAddress, value: u256, data: Array
+ ) -> bool;
+}
+
+#[starknet::interface]
+trait IERC677Receiver {
+ fn on_token_transfer(
+ ref self: TContractState, sender: ContractAddress, value: u256, data: Array
+ );
+ // implements EIP-165, where function selectors are defined by Ethereum ABI using the ethereum function signatures
+ fn supports_interface(ref self: TContractState, interface_id: u32) -> bool;
+}
+
+#[starknet::component]
+mod ERC677Component {
+ use starknet::ContractAddress;
+ use openzeppelin::token::erc20::interface::IERC20;
+ use array::ArrayTrait;
+ use array::SpanTrait;
+ use clone::Clone;
+ use array::ArrayTCloneImpl;
+
+ use super::IERC677ReceiverDispatcher;
+ use super::IERC677ReceiverDispatcherTrait;
+
+ // ethereum function selector of "onTokenTransfer(address,uint256,bytes)"
+ const IERC677_RECEIVER_ID: u32 = 0xa4c0ed36_u32;
+
+ #[storage]
+ struct Storage {}
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ TransferAndCall: TransferAndCall,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct TransferAndCall {
+ #[key]
+ from: ContractAddress,
+ #[key]
+ to: ContractAddress,
+ value: u256,
+ data: Array
+ }
+
+ #[embeddable_as(ERC677Impl)]
+ impl ERC677<
+ TContractState,
+ +HasComponent,
+ +IERC20,
+ +Drop,
+ > of super::IERC677> {
+ fn transfer_and_call(
+ ref self: ComponentState,
+ to: ContractAddress,
+ value: u256,
+ data: Array
+ ) -> bool {
+ let sender = starknet::info::get_caller_address();
+
+ let mut contract = self.get_contract_mut();
+ contract.transfer(to, value);
+ self
+ .emit(
+ Event::TransferAndCall(
+ TransferAndCall { from: sender, to: to, value: value, data: data.clone(), }
+ )
+ );
+
+ let receiver = IERC677ReceiverDispatcher { contract_address: to };
+
+ let supports = receiver.supports_interface(IERC677_RECEIVER_ID);
+ if supports {
+ receiver.on_token_transfer(sender, value, data);
+ }
+ true
+ }
+ }
+}
diff --git a/contracts/src/libraries/type_and_version.cairo b/contracts/src/libraries/type_and_version.cairo
new file mode 100644
index 000000000..b0481055d
--- /dev/null
+++ b/contracts/src/libraries/type_and_version.cairo
@@ -0,0 +1,4 @@
+#[starknet::interface]
+trait ITypeAndVersion {
+ fn type_and_version(self: @TContractState) -> felt252;
+}
diff --git a/contracts/src/libraries/upgradeable.cairo b/contracts/src/libraries/upgradeable.cairo
new file mode 100644
index 000000000..6b9e6af73
--- /dev/null
+++ b/contracts/src/libraries/upgradeable.cairo
@@ -0,0 +1,35 @@
+use starknet::class_hash::ClassHash;
+
+// TODO: drop for OZ upgradeable
+
+#[starknet::interface]
+trait IUpgradeable {
+ // note: any contract that uses this module will have a mutable reference to contract state
+ fn upgrade(ref self: TContractState, new_impl: ClassHash);
+}
+
+#[derive(Drop, starknet::Event)]
+struct Upgraded {
+ #[key]
+ new_impl: ClassHash
+}
+
+mod Upgradeable {
+ use zeroable::Zeroable;
+
+ use starknet::SyscallResult;
+ use starknet::SyscallResultTrait;
+ use starknet::syscalls::replace_class_syscall;
+ use starknet::class_hash::ClassHash;
+ use starknet::class_hash::ClassHashZeroable;
+
+ // this method assumes replace_class_syscall has a very low possibility of being deprecated
+ // but if it does, we will either have upgraded the contract to be non-upgradeable by then
+ // because the starknet ecosystem has stabilized or we will be able to upgrade the contract to the proxy pattern
+ // #[internal]
+ fn upgrade(new_impl: ClassHash) {
+ assert(!new_impl.is_zero(), 'Class hash cannot be zero');
+ replace_class_syscall(new_impl).unwrap_syscall();
+ // TODO: Upgraded(new_impl);
+ }
+}
diff --git a/contracts/src/multisig.cairo b/contracts/src/multisig.cairo
new file mode 100644
index 000000000..b1422df45
--- /dev/null
+++ b/contracts/src/multisig.cairo
@@ -0,0 +1,551 @@
+use array::ArrayTrait;
+use option::OptionTrait;
+use starknet::ContractAddress;
+use starknet::class_hash::ClassHash;
+
+fn assert_unique_values<
+ T, impl TCopy: Copy, impl TDrop: Drop, impl TPartialEq: PartialEq,
+>(
+ a: @Array::
+) {
+ let len = a.len();
+ _assert_unique_values_loop(a, len, 0_usize, 1_usize);
+}
+
+fn _assert_unique_values_loop<
+ T, impl TCopy: Copy, impl TDrop: Drop, impl TPartialEq: PartialEq,
+>(
+ a: @Array::, len: usize, j: usize, k: usize
+) {
+ if j >= len {
+ return ();
+ }
+ if k >= len {
+ _assert_unique_values_loop(a, len, j + 1_usize, j + 2_usize);
+ return ();
+ }
+ let j_val = *a.at(j);
+ let k_val = *a.at(k);
+ assert(j_val != k_val, 'duplicate values');
+ _assert_unique_values_loop(a, len, j, k + 1_usize);
+}
+
+#[derive(Copy, Drop, Serde, starknet::Store)]
+struct Transaction {
+ to: ContractAddress,
+ function_selector: felt252,
+ calldata_len: usize,
+ executed: bool,
+ confirmations: usize,
+}
+
+#[starknet::interface]
+trait IMultisig {
+ fn is_signer(self: @TContractState, address: ContractAddress) -> bool;
+ fn get_signers_len(self: @TContractState) -> usize;
+ fn get_signers(self: @TContractState) -> Array;
+ fn get_threshold(self: @TContractState) -> usize;
+ fn get_transactions_len(self: @TContractState) -> u128;
+ fn is_confirmed(self: @TContractState, nonce: u128, signer: ContractAddress) -> bool;
+ fn is_executed(self: @TContractState, nonce: u128) -> bool;
+ fn get_transaction(self: @TContractState, nonce: u128) -> (Transaction, Array::);
+ fn submit_transaction(
+ ref self: TContractState,
+ to: ContractAddress,
+ function_selector: felt252,
+ calldata: Array
+ );
+ fn confirm_transaction(ref self: TContractState, nonce: u128);
+ fn revoke_confirmation(ref self: TContractState, nonce: u128);
+ fn execute_transaction(ref self: TContractState, nonce: u128) -> Array;
+ fn set_threshold(ref self: TContractState, threshold: usize);
+ fn set_signers(ref self: TContractState, signers: Array);
+ fn set_signers_and_threshold(
+ ref self: TContractState, signers: Array, threshold: usize
+ );
+}
+
+#[starknet::contract]
+mod Multisig {
+ use super::assert_unique_values;
+ use super::{Transaction};
+
+ use traits::Into;
+ use traits::TryInto;
+ use zeroable::Zeroable;
+ use array::ArrayTrait;
+ use array::ArrayTCloneImpl;
+ use option::OptionTrait;
+
+ use starknet::ContractAddress;
+ use starknet::ContractAddressIntoFelt252;
+ use starknet::Felt252TryIntoContractAddress;
+ use starknet::StorageBaseAddress;
+ use starknet::SyscallResult;
+ use starknet::SyscallResultTrait;
+ use starknet::call_contract_syscall;
+ use starknet::get_caller_address;
+ use starknet::get_contract_address;
+ use starknet::storage_address_from_base_and_offset;
+ use starknet::storage_read_syscall;
+ use starknet::storage_write_syscall;
+ use starknet::class_hash::ClassHash;
+
+ use chainlink::libraries::type_and_version::ITypeAndVersion;
+ use chainlink::libraries::upgradeable::{Upgradeable, IUpgradeable};
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ TransactionSubmitted: TransactionSubmitted,
+ TransactionConfirmed: TransactionConfirmed,
+ ConfirmationRevoked: ConfirmationRevoked,
+ TransactionExecuted: TransactionExecuted,
+ SignersSet: SignersSet,
+ ThresholdSet: ThresholdSet,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct TransactionSubmitted {
+ #[key]
+ signer: ContractAddress,
+ #[key]
+ nonce: u128,
+ #[key]
+ to: ContractAddress
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct TransactionConfirmed {
+ #[key]
+ signer: ContractAddress,
+ #[key]
+ nonce: u128
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct ConfirmationRevoked {
+ #[key]
+ signer: ContractAddress,
+ #[key]
+ nonce: u128
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct TransactionExecuted {
+ #[key]
+ executor: ContractAddress,
+ #[key]
+ nonce: u128
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct SignersSet {
+ #[key]
+ signers: Array
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct ThresholdSet {
+ #[key]
+ threshold: usize
+ }
+
+ #[storage]
+ struct Storage {
+ _threshold: usize,
+ _signers: LegacyMap,
+ _is_signer: LegacyMap,
+ _signers_len: usize,
+ _tx_valid_since: u128,
+ _next_nonce: u128,
+ _transactions: LegacyMap,
+ _transaction_calldata: LegacyMap<(u128, usize), felt252>,
+ _is_confirmed: LegacyMap<(u128, ContractAddress), bool>,
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState, signers: Array, threshold: usize) {
+ let signers_len = signers.len();
+ self._require_valid_threshold(threshold, signers_len);
+ self._set_signers(signers, signers_len);
+ self._set_threshold(threshold);
+ }
+
+ #[abi(embed_v0)]
+ impl TypeAndVersionImpl of ITypeAndVersion {
+ fn type_and_version(self: @ContractState,) -> felt252 {
+ 'Multisig 1.0.0'
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl UpgradeableImpl of IUpgradeable {
+ fn upgrade(ref self: ContractState, new_impl: ClassHash) {
+ self._require_multisig();
+ Upgradeable::upgrade(new_impl)
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl MultisigImpl of super::IMultisig {
+ /// Views
+
+ fn is_signer(self: @ContractState, address: ContractAddress) -> bool {
+ self._is_signer.read(address)
+ }
+
+ fn get_signers_len(self: @ContractState,) -> usize {
+ self._signers_len.read()
+ }
+
+ fn get_signers(self: @ContractState) -> Array {
+ let signers_len = self._signers_len.read();
+ let mut signers = ArrayTrait::new();
+ self._get_signers_range(0_usize, signers_len, ref signers);
+ signers
+ }
+
+ fn get_threshold(self: @ContractState,) -> usize {
+ self._threshold.read()
+ }
+
+ fn get_transactions_len(self: @ContractState,) -> u128 {
+ self._next_nonce.read()
+ }
+
+ fn is_confirmed(self: @ContractState, nonce: u128, signer: ContractAddress) -> bool {
+ self._is_confirmed.read((nonce, signer))
+ }
+
+ fn is_executed(self: @ContractState, nonce: u128) -> bool {
+ let transaction = self._transactions.read(nonce);
+ transaction.executed
+ }
+
+ fn get_transaction(self: @ContractState, nonce: u128) -> (Transaction, Array::) {
+ let transaction = self._transactions.read(nonce);
+
+ let mut calldata = ArrayTrait::new();
+ let calldata_len = transaction.calldata_len;
+ self._get_transaction_calldata_range(nonce, 0_usize, calldata_len, ref calldata);
+
+ (transaction, calldata)
+ }
+
+ /// Externals
+
+ fn submit_transaction(
+ ref self: ContractState,
+ to: ContractAddress,
+ function_selector: felt252,
+ calldata: Array,
+ ) {
+ self._require_signer();
+
+ let nonce = self._next_nonce.read();
+ let calldata_len = calldata.len();
+
+ let transaction = Transaction {
+ to: to,
+ function_selector: function_selector,
+ calldata_len: calldata_len,
+ executed: false,
+ confirmations: 0_usize
+ };
+ self._transactions.write(nonce, transaction);
+
+ self._set_transaction_calldata_range(nonce, 0_usize, calldata_len, @calldata);
+
+ let caller = get_caller_address();
+ self
+ .emit(
+ Event::TransactionSubmitted(
+ TransactionSubmitted { signer: caller, nonce: nonce, to: to }
+ )
+ );
+ self._next_nonce.write(nonce + 1_u128);
+ }
+
+ fn confirm_transaction(ref self: ContractState, nonce: u128) {
+ self._require_signer();
+ self._require_tx_exists(nonce);
+ self._require_tx_valid(nonce);
+ self._require_not_executed(nonce);
+ self._require_not_confirmed(nonce);
+
+ // TODO: write a single field instead of the whole transaction?
+ let mut transaction = self._transactions.read(nonce);
+ transaction.confirmations += 1_usize;
+ self._transactions.write(nonce, transaction);
+
+ let caller = get_caller_address();
+ self._is_confirmed.write((nonce, caller), true);
+
+ self
+ .emit(
+ Event::TransactionConfirmed(
+ TransactionConfirmed { signer: caller, nonce: nonce }
+ )
+ );
+ }
+
+ fn revoke_confirmation(ref self: ContractState, nonce: u128) {
+ self._require_signer();
+ self._require_tx_exists(nonce);
+ self._require_tx_valid(nonce);
+ self._require_not_executed(nonce);
+ self._require_confirmed(nonce);
+
+ // TODO: write a single field instead of the whole transaction?
+ let mut transaction = self._transactions.read(nonce);
+ transaction.confirmations -= 1_usize;
+ self._transactions.write(nonce, transaction);
+
+ let caller = get_caller_address();
+ self._is_confirmed.write((nonce, caller), false);
+
+ self
+ .emit(
+ Event::ConfirmationRevoked(ConfirmationRevoked { signer: caller, nonce: nonce })
+ );
+ }
+
+ fn execute_transaction(ref self: ContractState, nonce: u128) -> Array {
+ self._require_signer();
+ self._require_tx_exists(nonce);
+ self._require_tx_valid(nonce);
+ self._require_not_executed(nonce);
+
+ let mut transaction = self._transactions.read(nonce);
+
+ let threshold = self._threshold.read();
+ assert(threshold <= transaction.confirmations, 'more confirmations required');
+
+ let mut calldata = ArrayTrait::new();
+ let calldata_len = transaction.calldata_len;
+ self._get_transaction_calldata_range(nonce, 0_usize, calldata_len, ref calldata);
+
+ transaction.executed = true;
+ self._transactions.write(nonce, transaction);
+
+ let caller = get_caller_address();
+ self
+ .emit(
+ Event::TransactionExecuted(
+ TransactionExecuted { executor: caller, nonce: nonce }
+ )
+ );
+
+ let response = call_contract_syscall(
+ transaction.to, transaction.function_selector, calldata.span()
+ )
+ .unwrap_syscall();
+
+ // TODO: this shouldn't be necessary. call_contract_syscall returns a Span, which
+ // is a serialized result, but returning a Span results in an error:
+ //
+ // Trait has no implementation in context: core::serde::Serde::>
+ //
+ // Cairo docs also have an example that returns a Span:
+ // https://github.com/starkware-libs/cairo/blob/fe425d0893ff93a936bb3e8bbbac771033074bdb/docs/reference/src/components/cairo/modules/language_constructs/pages/contracts.adoc#L226
+ ArrayTCloneImpl::clone(response.snapshot)
+ }
+
+ fn set_threshold(ref self: ContractState, threshold: usize) {
+ self._require_multisig();
+
+ let signers_len = self._signers_len.read();
+ self._require_valid_threshold(threshold, signers_len);
+
+ self._update_tx_valid_since();
+
+ self._set_threshold(threshold);
+ }
+
+ fn set_signers(ref self: ContractState, signers: Array) {
+ self._require_multisig();
+
+ self._update_tx_valid_since();
+
+ let signers_len = signers.len();
+ self._set_signers(signers, signers_len);
+
+ let threshold = self._threshold.read();
+
+ if signers_len < threshold {
+ self._require_valid_threshold(signers_len, signers_len);
+ self._set_threshold(signers_len);
+ }
+ }
+
+ fn set_signers_and_threshold(
+ ref self: ContractState, signers: Array, threshold: usize
+ ) {
+ self._require_multisig();
+
+ let signers_len = signers.len();
+ self._require_valid_threshold(threshold, signers_len);
+
+ self._update_tx_valid_since();
+
+ self._set_signers(signers, signers_len);
+ self._set_threshold(threshold);
+ }
+ }
+
+ /// Internals
+ #[generate_trait]
+ impl InternalImpl of InternalTrait {
+ fn _set_signers(
+ ref self: ContractState, signers: Array, signers_len: usize
+ ) {
+ self._require_unique_signers(@signers);
+
+ let old_signers_len = self._signers_len.read();
+ self._clean_signers_range(0_usize, old_signers_len);
+
+ self._signers_len.write(signers_len);
+ self._set_signers_range(0_usize, signers_len, @signers);
+
+ self.emit(Event::SignersSet(SignersSet { signers: signers }));
+ }
+
+ fn _clean_signers_range(ref self: ContractState, index: usize, len: usize) {
+ if index >= len {
+ return ();
+ }
+
+ let signer = self._signers.read(index);
+ self._is_signer.write(signer, false);
+ self._signers.write(index, Zeroable::zero());
+
+ self._clean_signers_range(index + 1_usize, len);
+ }
+
+ fn _set_signers_range(
+ ref self: ContractState, index: usize, len: usize, signers: @Array
+ ) {
+ if index >= len {
+ return ();
+ }
+
+ let signer = *signers.at(index);
+ self._signers.write(index, signer);
+ self._is_signer.write(signer, true);
+
+ self._set_signers_range(index + 1_usize, len, signers);
+ }
+
+ fn _get_signers_range(
+ self: @ContractState, index: usize, len: usize, ref signers: Array
+ ) {
+ if index >= len {
+ return ();
+ }
+
+ let signer = self._signers.read(index);
+ signers.append(signer);
+
+ self._get_signers_range(index + 1_usize, len, ref signers);
+ }
+
+ fn _set_transaction_calldata_range(
+ ref self: ContractState,
+ nonce: u128,
+ index: usize,
+ len: usize,
+ calldata: @Array
+ ) {
+ if index >= len {
+ return ();
+ }
+
+ let calldata_arg = *calldata.at(index);
+ self._transaction_calldata.write((nonce, index), calldata_arg);
+
+ self._set_transaction_calldata_range(nonce, index + 1_usize, len, calldata);
+ }
+
+ fn _get_transaction_calldata_range(
+ self: @ContractState,
+ nonce: u128,
+ index: usize,
+ len: usize,
+ ref calldata: Array
+ ) {
+ if index >= len {
+ return ();
+ }
+
+ let calldata_arg = self._transaction_calldata.read((nonce, index));
+ calldata.append(calldata_arg);
+
+ self._get_transaction_calldata_range(nonce, index + 1_usize, len, ref calldata);
+ }
+
+ fn _set_threshold(ref self: ContractState, threshold: usize) {
+ self._threshold.write(threshold);
+ self.emit(Event::ThresholdSet(ThresholdSet { threshold: threshold }));
+ }
+
+ fn _update_tx_valid_since(ref self: ContractState) {
+ let tx_valid_since = self._next_nonce.read();
+ self._tx_valid_since.write(tx_valid_since);
+ }
+
+ fn _require_signer(self: @ContractState) {
+ let caller = get_caller_address();
+ let is_signer = self._is_signer.read(caller);
+ assert(is_signer, 'invalid signer');
+ }
+
+ fn _require_tx_exists(self: @ContractState, nonce: u128) {
+ let next_nonce = self._next_nonce.read();
+ assert(nonce < next_nonce, 'transaction does not exist');
+ }
+
+ fn _require_not_executed(self: @ContractState, nonce: u128) {
+ let transaction = self._transactions.read(nonce);
+ assert(!transaction.executed, 'transaction already executed');
+ }
+
+ fn _require_not_confirmed(self: @ContractState, nonce: u128) {
+ let caller = get_caller_address();
+ let is_confirmed = self._is_confirmed.read((nonce, caller));
+ assert(!is_confirmed, 'transaction already confirmed');
+ }
+
+ fn _require_confirmed(self: @ContractState, nonce: u128) {
+ let caller = get_caller_address();
+ let is_confirmed = self._is_confirmed.read((nonce, caller));
+ assert(is_confirmed, 'transaction not confirmed');
+ }
+
+ fn _require_unique_signers(self: @ContractState, signers: @Array) {
+ assert_unique_values(signers);
+ }
+
+ fn _require_tx_valid(self: @ContractState, nonce: u128) {
+ let tx_valid_since = self._tx_valid_since.read();
+ assert(tx_valid_since <= nonce, 'transaction invalid');
+ }
+
+ fn _require_multisig(self: @ContractState) {
+ let caller = get_caller_address();
+ let contract = get_contract_address();
+ assert(caller == contract, 'only multisig allowed');
+ }
+
+ fn _require_valid_threshold(self: @ContractState, threshold: usize, signers_len: usize) {
+ if threshold == 0_usize {
+ if signers_len == 0_usize {
+ return ();
+ }
+ }
+
+ assert(threshold >= 1_usize, 'invalid threshold, too small');
+ assert(threshold <= signers_len, 'invalid threshold, too large');
+ }
+ }
+}
diff --git a/contracts/src/ocr2.cairo b/contracts/src/ocr2.cairo
new file mode 100644
index 000000000..09a69eff0
--- /dev/null
+++ b/contracts/src/ocr2.cairo
@@ -0,0 +1,4 @@
+mod aggregator;
+mod aggregator_proxy;
+mod mocks;
+
diff --git a/contracts/src/ocr2/aggregator.cairo b/contracts/src/ocr2/aggregator.cairo
new file mode 100644
index 000000000..d494c81e8
--- /dev/null
+++ b/contracts/src/ocr2/aggregator.cairo
@@ -0,0 +1,1252 @@
+use starknet::ContractAddress;
+
+#[derive(Copy, Drop, Serde, PartialEq, starknet::Store)]
+struct Round {
+ // used as u128 internally, but necessary for phase-prefixed round ids as returned by proxy
+ round_id: felt252,
+ answer: u128,
+ block_num: u64,
+ started_at: u64,
+ updated_at: u64,
+}
+
+#[derive(Copy, Drop, Serde, starknet::Store)]
+struct Transmission {
+ answer: u128,
+ block_num: u64,
+ observation_timestamp: u64,
+ transmission_timestamp: u64,
+}
+
+// TODO: reintroduce custom storage to save on space
+// impl TransmissionStorageAccess of StorageAccess {
+// fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult:: {
+// let block_num_and_answer = storage_read_syscall(
+// address_domain, storage_address_from_base_and_offset(base, 0_u8)
+// )?;
+// let (block_num, answer) = split_felt(block_num_and_answer);
+// let timestamps = storage_read_syscall(
+// address_domain, storage_address_from_base_and_offset(base, 1_u8)
+// )?;
+// let (observation_timestamp, transmission_timestamp) = split_felt(timestamps);
+
+// Result::Ok(
+// Transmission {
+// answer,
+// block_num: block_num.try_into().unwrap(),
+// observation_timestamp: observation_timestamp.try_into().unwrap(),
+// transmission_timestamp: transmission_timestamp.try_into().unwrap(),
+// }
+// )
+// }
+//
+// fn write(
+// address_domain: u32, base: StorageBaseAddress, value: Transmission
+// ) -> SyscallResult::<()> {
+// let block_num_and_answer = value.block_num.into() * SHIFT_128 + value.answer.into();
+// let timestamps = value.observation_timestamp.into() * SHIFT_128
+// + value.transmission_timestamp.into();
+// storage_write_syscall(
+// address_domain,
+// storage_address_from_base_and_offset(base, 0_u8),
+// block_num_and_answer
+// )?;
+// storage_write_syscall(
+// address_domain, storage_address_from_base_and_offset(base, 1_u8), timestamps
+// )
+// }
+// }
+
+#[starknet::interface]
+trait IAggregator {
+ fn latest_round_data(self: @TContractState) -> Round;
+ fn round_data(self: @TContractState, round_id: u128) -> Round;
+ fn description(self: @TContractState) -> felt252;
+ fn decimals(self: @TContractState) -> u8;
+ fn latest_answer(self: @TContractState) -> u128;
+}
+
+#[derive(Copy, Drop, Serde)]
+struct OracleConfig {
+ signer: felt252,
+ transmitter: ContractAddress,
+}
+
+impl OracleConfigLegacyHash of LegacyHash {
+ fn hash(mut state: felt252, value: OracleConfig) -> felt252 {
+ state = LegacyHash::hash(state, value.signer);
+ state = LegacyHash::hash(state, value.transmitter);
+ state
+ }
+}
+
+#[starknet::interface]
+trait Configuration {
+ fn set_config(
+ ref self: TContractState,
+ oracles: Array,
+ f: u8,
+ onchain_config: Array,
+ offchain_config_version: u64,
+ offchain_config: Array,
+ ) -> felt252; // digest
+ fn latest_config_details(self: @TContractState) -> (u64, u64, felt252);
+ fn transmitters(self: @TContractState) -> Array;
+}
+
+use Aggregator::{BillingConfig, BillingConfigSerde};
+
+#[starknet::interface]
+trait Billing {
+ fn set_billing_access_controller(ref self: TContractState, access_controller: ContractAddress);
+ fn set_billing(ref self: TContractState, config: Aggregator::BillingConfig);
+ fn billing(self: @TContractState) -> Aggregator::BillingConfig;
+ //
+ fn withdraw_payment(ref self: TContractState, transmitter: ContractAddress);
+ fn owed_payment(self: @TContractState, transmitter: ContractAddress) -> u128;
+ fn withdraw_funds(ref self: TContractState, recipient: ContractAddress, amount: u256);
+ fn link_available_for_payment(
+ self: @TContractState
+ ) -> (bool, u128); // (is negative, absolute difference)
+ fn set_link_token(
+ ref self: TContractState, link_token: ContractAddress, recipient: ContractAddress
+ );
+}
+
+#[derive(Copy, Drop, Serde)]
+struct PayeeConfig {
+ transmitter: ContractAddress,
+ payee: ContractAddress,
+}
+
+#[starknet::interface]
+trait PayeeManagement {
+ fn set_payees(ref self: TContractState, payees: Array);
+ fn transfer_payeeship(
+ ref self: TContractState, transmitter: ContractAddress, proposed: ContractAddress
+ );
+ fn accept_payeeship(ref self: TContractState, transmitter: ContractAddress);
+}
+
+use array::ArrayTrait;
+use array::SpanTrait;
+use option::OptionTrait;
+use hash::LegacyHash;
+
+fn hash_span, impl TCopy: Copy>(
+ state: felt252, mut value: Span
+) -> felt252 {
+ let item = value.pop_front();
+ match item {
+ Option::Some(x) => {
+ let s = LegacyHash::hash(state, *x);
+ hash_span(s, value)
+ },
+ Option::None(_) => state,
+ }
+}
+
+// TODO: consider switching to lookups
+fn pow(n: u128, m: u128) -> u128 {
+ if m == 0_u128 {
+ return 1_u128;
+ }
+ let half = pow(n, m / 2_u128);
+ let total = half * half;
+ // TODO: check if (& 1) is cheaper
+ if (m % 2_u128) == 1_u128 {
+ total * n
+ } else {
+ total
+ }
+}
+
+// TODO: wrap hash_span
+impl SpanLegacyHash, impl TCopy: Copy> of LegacyHash> {
+ fn hash(state: felt252, mut value: Span) -> felt252 {
+ hash_span(state, value)
+ }
+}
+
+#[starknet::contract]
+mod Aggregator {
+ use super::Round;
+ use super::{Transmission};
+ use super::SpanLegacyHash;
+ use super::pow;
+
+ use array::ArrayTrait;
+ use array::SpanTrait;
+ use box::BoxTrait;
+ use hash::LegacyHash;
+ use integer::U128IntoFelt252;
+ use integer::u128s_from_felt252;
+ use integer::U128sFromFelt252Result;
+ use zeroable::Zeroable;
+ use traits::Into;
+ use traits::TryInto;
+ use option::OptionTrait;
+
+ use starknet::ContractAddress;
+ use starknet::get_caller_address;
+ use starknet::contract_address_const;
+ use starknet::StorageBaseAddress;
+ use starknet::SyscallResult;
+ use starknet::storage_read_syscall;
+ use starknet::storage_write_syscall;
+ use starknet::storage_address_from_base_and_offset;
+ use starknet::class_hash::ClassHash;
+
+ use openzeppelin::access::ownable::OwnableComponent;
+ use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait};
+
+ use chainlink::utils::split_felt;
+ use chainlink::libraries::access_control::{AccessControlComponent, IAccessController};
+ use chainlink::libraries::access_control::AccessControlComponent::InternalTrait as AccessControlInternalTrait;
+ use chainlink::libraries::upgradeable::{Upgradeable, IUpgradeable};
+
+ use chainlink::libraries::access_control::{
+ IAccessControllerDispatcher, IAccessControllerDispatcherTrait
+ };
+ use chainlink::libraries::type_and_version::ITypeAndVersion;
+
+ component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+ component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent);
+
+ #[abi(embed_v0)]
+ impl OwnableImpl = OwnableComponent::OwnableTwoStepImpl;
+ impl OwnableInternalImpl = OwnableComponent::InternalImpl;
+
+ #[abi(embed_v0)]
+ impl AccessControlImpl =
+ AccessControlComponent::AccessControlImpl;
+ impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;
+
+ const GIGA: u128 = 1000000000_u128;
+
+ const MAX_ORACLES: u32 = 31_u32;
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ #[flat]
+ OwnableEvent: OwnableComponent::Event,
+ #[flat]
+ AccessControlEvent: AccessControlComponent::Event,
+ NewTransmission: NewTransmission,
+ ConfigSet: ConfigSet,
+ LinkTokenSet: LinkTokenSet,
+ BillingAccessControllerSet: BillingAccessControllerSet,
+ BillingSet: BillingSet,
+ OraclePaid: OraclePaid,
+ PayeeshipTransferRequested: PayeeshipTransferRequested,
+ PayeeshipTransferred: PayeeshipTransferred,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct NewTransmission {
+ #[key]
+ round_id: u128,
+ answer: u128,
+ #[key]
+ transmitter: ContractAddress,
+ observation_timestamp: u64,
+ observers: felt252,
+ observations: Array,
+ juels_per_fee_coin: u128,
+ gas_price: u128,
+ config_digest: felt252,
+ epoch_and_round: u64,
+ reimbursement: u128
+ }
+
+ #[derive(Copy, Drop, Serde, starknet::Store)]
+ struct Oracle {
+ index: usize,
+ // entire supply of LINK always fits into u96, so u128 is safe to use
+ payment_juels: u128,
+ }
+
+ // TODO: reintroduce custom storage to save on space
+ // impl OracleStorageAccess of StorageAccess {
+ // fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult:: {
+ // let value = storage_read_syscall(
+ // address_domain, storage_address_from_base_and_offset(base, 0_u8)
+ // )?;
+ // let (index, payment_juels) = split_felt(value);
+ // Result::Ok(Oracle { index: index.try_into().unwrap(), payment_juels, })
+ // }
+ //
+ // fn write(
+ // address_domain: u32, base: StorageBaseAddress, value: Oracle
+ // ) -> SyscallResult::<()> {
+ // let value = value.index.into() * SHIFT_128 + value.payment_juels.into();
+ // storage_write_syscall(
+ // address_domain, storage_address_from_base_and_offset(base, 0_u8), value
+ // )
+ // }
+ // }
+
+ #[storage]
+ struct Storage {
+ #[substorage(v0)]
+ ownable: OwnableComponent::Storage,
+ #[substorage(v0)]
+ access_control: AccessControlComponent::Storage,
+ /// Maximum number of faulty oracles
+ _f: u8,
+ _latest_epoch_and_round: u64, // (u32, u32)
+ _latest_aggregator_round_id: u128,
+ _min_answer: u128,
+ _max_answer: u128,
+ _decimals: u8,
+ _description: felt252,
+ _latest_config_block_number: u64,
+ _config_count: u64,
+ _latest_config_digest: felt252,
+ // _oracles: Array, // NOTE: array can't be used in storage
+ _oracles_len: usize,
+ _transmitters: LegacyMap, //
+ _signers: LegacyMap, //
+ _signers_list: LegacyMap,
+ _transmitters_list: LegacyMap,
+ _reward_from_aggregator_round_id: LegacyMap, //
+ _transmissions: LegacyMap,
+ // link token
+ _link_token: ContractAddress,
+ // billing
+ _billing_access_controller: ContractAddress,
+ _billing: BillingConfig,
+ // payee management
+ _payees: LegacyMap, //
+ _proposed_payees: LegacyMap<
+ ContractAddress, ContractAddress
+ > //
+ }
+
+ #[generate_trait]
+ impl AccessHelperImpl of AccessHelperTrait {
+ fn _require_read_access(self: @ContractState) {
+ let caller = starknet::info::get_caller_address();
+ self.access_control.check_read_access(caller);
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl TypeAndVersionImpl of ITypeAndVersion {
+ fn type_and_version(self: @ContractState) -> felt252 {
+ 'ocr2/aggregator.cairo 1.0.0'
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl AggregatorImpl of super::IAggregator {
+ fn latest_round_data(self: @ContractState) -> Round {
+ self._require_read_access();
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let transmission = self._transmissions.read(latest_round_id);
+ Round {
+ round_id: latest_round_id.into(),
+ answer: transmission.answer,
+ block_num: transmission.block_num,
+ started_at: transmission.observation_timestamp,
+ updated_at: transmission.transmission_timestamp,
+ }
+ }
+
+ fn round_data(self: @ContractState, round_id: u128) -> Round {
+ self._require_read_access();
+ let transmission = self._transmissions.read(round_id);
+ Round {
+ round_id: round_id.into(),
+ answer: transmission.answer,
+ block_num: transmission.block_num,
+ started_at: transmission.observation_timestamp,
+ updated_at: transmission.transmission_timestamp,
+ }
+ }
+
+ fn description(self: @ContractState) -> felt252 {
+ self._require_read_access();
+ self._description.read()
+ }
+
+ fn decimals(self: @ContractState) -> u8 {
+ self._require_read_access();
+ self._decimals.read()
+ }
+
+ fn latest_answer(self: @ContractState) -> u128 {
+ self._require_read_access();
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let transmission = self._transmissions.read(latest_round_id);
+ transmission.answer
+ }
+ }
+
+ // ---
+
+ #[constructor]
+ fn constructor(
+ ref self: ContractState,
+ owner: ContractAddress,
+ link: ContractAddress,
+ min_answer: u128,
+ max_answer: u128,
+ billing_access_controller: ContractAddress,
+ decimals: u8,
+ description: felt252
+ ) {
+ self.ownable.initializer(owner);
+ self.access_control.initializer();
+ self._link_token.write(link);
+ self._billing_access_controller.write(billing_access_controller);
+
+ assert(min_answer < max_answer, 'min >= max');
+ self._min_answer.write(min_answer);
+ self._max_answer.write(max_answer);
+
+ self._decimals.write(decimals);
+ self._description.write(description);
+ }
+
+ // --- Upgradeable ---
+
+ #[abi(embed_v0)]
+ impl UpgradeableImpl of IUpgradeable {
+ fn upgrade(ref self: ContractState, new_impl: ClassHash) {
+ self.ownable.assert_only_owner();
+ Upgradeable::upgrade(new_impl)
+ }
+ }
+
+ // --- Validation ---
+
+ // NOTE: Currently unimplemented:
+
+ // --- Configuration
+
+ #[derive(Drop, starknet::Event)]
+ struct ConfigSet {
+ #[key]
+ previous_config_block_number: u64,
+ #[key]
+ latest_config_digest: felt252,
+ config_count: u64,
+ oracles: Array,
+ f: u8,
+ onchain_config: Array,
+ offchain_config_version: u64,
+ offchain_config: Array,
+ }
+
+ use super::OracleConfig;
+
+ const SHIFT_128: felt252 = 0x100000000000000000000000000000000;
+ // 4 * 2 ** (124 - 12)
+ const HALF_PREFIX: u128 = 0x40000000000000000000000000000_u128;
+ // 2 ** (124 - 12) - 1
+ const HALF_DIGEST_MASK: u128 = 0xffffffffffffffffffffffffffff_u128;
+
+ fn config_digest_from_data(
+ chain_id: felt252,
+ contract_address: ContractAddress,
+ config_count: u64,
+ oracles: @Array,
+ f: u8,
+ onchain_config: @Array,
+ offchain_config_version: u64,
+ offchain_config: @Array,
+ ) -> felt252 {
+ let mut state = 0;
+ state = LegacyHash::hash(state, chain_id);
+ state = LegacyHash::hash(state, contract_address);
+ state = LegacyHash::hash(state, config_count);
+ state = LegacyHash::hash(state, oracles.len());
+ state = LegacyHash::hash(state, oracles.span()); // for oracle in oracles, hash each
+ state = LegacyHash::hash(state, f);
+ state = LegacyHash::hash(state, onchain_config.len());
+ state = LegacyHash::hash(state, onchain_config.span());
+ state = LegacyHash::hash(state, offchain_config_version);
+ state = LegacyHash::hash(state, offchain_config.len());
+ state = LegacyHash::hash(state, offchain_config.span());
+ let len: usize = 3
+ + 1
+ + (oracles.len() * 2)
+ + 1
+ + 1
+ + onchain_config.len()
+ + 1
+ + 1
+ + offchain_config.len();
+ state = LegacyHash::hash(state, len);
+
+ // since there's no bitwise ops on felt252, we split into two u128s and recombine.
+ // we only need to clamp and prefix the top bits.
+ let (top, bottom) = split_felt(state);
+ let masked_top = (top & HALF_DIGEST_MASK) | HALF_PREFIX;
+ let masked = (masked_top.into() * SHIFT_128) + bottom.into();
+
+ masked
+ }
+
+ #[abi(embed_v0)]
+ impl ConfigurationImpl of super::Configuration {
+ fn set_config(
+ ref self: ContractState,
+ oracles: Array,
+ f: u8,
+ onchain_config: Array,
+ offchain_config_version: u64,
+ offchain_config: Array,
+ ) -> felt252 { // digest
+ self.ownable.assert_only_owner();
+ assert(oracles.len() <= MAX_ORACLES, 'too many oracles');
+ assert((3_u8 * f).into() < oracles.len(), 'faulty-oracle f too high');
+ assert(f > 0_u8, 'f must be positive');
+
+ assert(onchain_config.len() == 0_u32, 'onchain_config must be empty');
+
+ let min_answer = self._min_answer.read();
+ let max_answer = self._max_answer.read();
+
+ let mut computed_onchain_config = ArrayTrait::new();
+ computed_onchain_config.append(1); // version
+ computed_onchain_config.append(min_answer.into());
+ computed_onchain_config.append(max_answer.into());
+
+ self.pay_oracles();
+
+ // remove old signers & transmitters
+ self.remove_oracles();
+
+ let latest_round_id = self._latest_aggregator_round_id.read();
+
+ self.add_oracles(@oracles, latest_round_id);
+
+ self._f.write(f);
+ let block_num = starknet::info::get_block_info().unbox().block_number;
+ let prev_block_num = self._latest_config_block_number.read();
+ self._latest_config_block_number.write(block_num);
+ // update config count
+ let mut config_count = self._config_count.read();
+ config_count += 1_u64;
+ self._config_count.write(config_count);
+ let contract_address = starknet::info::get_contract_address();
+ let chain_id = starknet::info::get_tx_info().unbox().chain_id;
+
+ let digest = config_digest_from_data(
+ chain_id,
+ contract_address,
+ config_count,
+ @oracles,
+ f,
+ @computed_onchain_config,
+ offchain_config_version,
+ @offchain_config,
+ );
+
+ self._latest_config_digest.write(digest);
+
+ // reset epoch & round
+ self._latest_epoch_and_round.write(0_u64);
+
+ self
+ .emit(
+ Event::ConfigSet(
+ ConfigSet {
+ previous_config_block_number: prev_block_num,
+ latest_config_digest: digest,
+ config_count: config_count,
+ oracles: oracles,
+ f: f,
+ onchain_config: computed_onchain_config,
+ offchain_config_version: offchain_config_version,
+ offchain_config: offchain_config
+ }
+ )
+ );
+
+ digest
+ }
+
+ fn latest_config_details(self: @ContractState) -> (u64, u64, felt252) {
+ let config_count = self._config_count.read();
+ let block_number = self._latest_config_block_number.read();
+ let config_digest = self._latest_config_digest.read();
+
+ (config_count, block_number, config_digest)
+ }
+
+ fn transmitters(self: @ContractState) -> Array {
+ let mut index = 1;
+ let mut len = self._oracles_len.read();
+ let mut result = ArrayTrait::new();
+ while len > 0_usize {
+ let transmitter = self._transmitters_list.read(index);
+ result.append(transmitter);
+ len -= 1;
+ index += 1;
+ };
+ return result;
+ }
+ }
+
+ #[generate_trait]
+ impl ConfigurationHelperImpl of ConfigurationHelperTrait {
+ fn remove_oracles(ref self: ContractState) {
+ let mut index = self._oracles_len.read();
+ while index > 0_usize {
+ let signer = self._signers_list.read(index);
+ self._signers.write(signer, 0_usize);
+
+ let transmitter = self._transmitters_list.read(index);
+ self
+ ._transmitters
+ .write(transmitter, Oracle { index: 0_usize, payment_juels: 0_u128 });
+
+ index -= 1;
+ };
+ self._oracles_len.write(0_usize);
+ }
+
+ fn add_oracles(
+ ref self: ContractState, oracles: @Array, latest_round_id: u128
+ ) {
+ let mut index = 0;
+ let mut span = oracles.span();
+ while let Option::Some(oracle) = span
+ .pop_front() {
+ // NOTE: index should start with 1 here because storage is 0-initialized.
+ // That way signers(pkey) => 0 indicates "not present"
+ // This is why we increment first, before using the index
+ index += 1;
+
+ // check for duplicates
+ let existing_signer = self._signers.read(*oracle.signer);
+ assert(existing_signer == 0_usize, 'repeated signer');
+
+ let existing_transmitter = self._transmitters.read(*oracle.transmitter);
+ assert(existing_transmitter.index == 0_usize, 'repeated transmitter');
+
+ self._signers.write(*oracle.signer, index);
+ self._signers_list.write(index, *oracle.signer);
+
+ self
+ ._transmitters
+ .write(*oracle.transmitter, Oracle { index, payment_juels: 0_u128 });
+ self._transmitters_list.write(index, *oracle.transmitter);
+
+ self._reward_from_aggregator_round_id.write(index, latest_round_id);
+ };
+ self._oracles_len.write(index);
+ }
+ }
+
+ // --- Transmission ---
+
+ #[derive(Copy, Drop, Serde)]
+ struct Signature {
+ r: felt252,
+ s: felt252,
+ public_key: felt252,
+ }
+
+ #[derive(Copy, Drop, Serde)]
+ struct ReportContext {
+ config_digest: felt252,
+ epoch_and_round: u64,
+ extra_hash: felt252,
+ }
+
+ #[abi(per_item)]
+ #[generate_trait]
+ impl TransmissionHelperImpl of TransmissionHelperTrait {
+ fn hash_report(
+ self: @ContractState,
+ report_context: @ReportContext,
+ observation_timestamp: u64,
+ observers: felt252,
+ observations: @Array,
+ juels_per_fee_coin: u128,
+ gas_price: u128
+ ) -> felt252 {
+ let mut state = 0;
+ state = LegacyHash::hash(state, *report_context.config_digest);
+ state = LegacyHash::hash(state, *report_context.epoch_and_round);
+ state = LegacyHash::hash(state, *report_context.extra_hash);
+ state = LegacyHash::hash(state, observation_timestamp);
+ state = LegacyHash::hash(state, observers);
+ state = LegacyHash::hash(state, observations.len());
+ state = LegacyHash::hash(state, observations.span());
+ state = LegacyHash::hash(state, juels_per_fee_coin);
+ state = LegacyHash::hash(state, gas_price);
+ let len: usize = 5 + 1 + observations.len() + 2;
+ state = LegacyHash::hash(state, len);
+ state
+ }
+
+ #[external(v0)]
+ fn latest_transmission_details(self: @ContractState) -> (felt252, u64, u128, u64) {
+ let config_digest = self._latest_config_digest.read();
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let epoch_and_round = self._latest_epoch_and_round.read();
+ let transmission = self._transmissions.read(latest_round_id);
+ (
+ config_digest,
+ epoch_and_round,
+ transmission.answer,
+ transmission.transmission_timestamp
+ )
+ }
+
+ #[external(v0)]
+ fn transmit(
+ ref self: ContractState,
+ report_context: ReportContext,
+ observation_timestamp: u64,
+ observers: felt252,
+ observations: Array,
+ juels_per_fee_coin: u128,
+ gas_price: u128,
+ mut signatures: Array,
+ ) {
+ let signatures_len = signatures.len();
+
+ let epoch_and_round = self._latest_epoch_and_round.read();
+ assert(epoch_and_round < report_context.epoch_and_round, 'stale report');
+
+ // validate transmitter
+ let caller = starknet::info::get_caller_address();
+ let mut oracle = self._transmitters.read(caller);
+ assert(oracle.index != 0_usize, 'unknown sender'); // 0 index = uninitialized
+
+ // Validate config digest matches latest_config_digest
+ let config_digest = self._latest_config_digest.read();
+ assert(report_context.config_digest == config_digest, 'config digest mismatch');
+
+ let f = self._f.read();
+ assert(signatures_len == (f + 1_u8).into(), 'wrong number of signatures');
+
+ let msg = self
+ .hash_report(
+ @report_context,
+ observation_timestamp,
+ observers,
+ @observations,
+ juels_per_fee_coin,
+ gas_price
+ );
+
+ // Check all signatures are unique (we only saw each pubkey once)
+ // NOTE: This relies on protocol-level design constraints (MAX_ORACLES = 31, f = 10) which
+ // ensures we have enough bits to store a count for each oracle. Whenever the MAX_ORACLES
+ // is updated, the signed_count parameter should be reconsidered.
+ //
+ // Although 31 bits is enough, we use a u128 here for simplicity because BitAnd and BitOr
+ // operators are defined only for u128 and u256.
+ assert(MAX_ORACLES == 31_u32, '');
+ self.verify_signatures(msg, ref signatures, 0_u128);
+
+ // report():
+
+ let observations_len = observations.len();
+ assert(observations_len <= MAX_ORACLES, '');
+ assert(f.into() < observations_len, '');
+
+ self._latest_epoch_and_round.write(report_context.epoch_and_round);
+
+ let median_idx = observations_len / 2_usize;
+ let median = *observations[median_idx];
+
+ // Validate median in min-max range
+ let min_answer = self._min_answer.read();
+ let max_answer = self._max_answer.read();
+ assert(
+ (min_answer <= median) & (median <= max_answer), 'median is out of min-max range'
+ );
+
+ let prev_round_id = self._latest_aggregator_round_id.read();
+ let round_id = prev_round_id + 1_u128;
+ self._latest_aggregator_round_id.write(round_id);
+
+ let block_info = starknet::info::get_block_info().unbox();
+
+ self
+ ._transmissions
+ .write(
+ round_id,
+ Transmission {
+ answer: median,
+ block_num: block_info.block_number,
+ observation_timestamp,
+ transmission_timestamp: block_info.block_timestamp,
+ }
+ );
+
+ // NOTE: Usually validating via validator would happen here, currently disabled
+
+ let billing = self._billing.read();
+ let reimbursement_juels = calculate_reimbursement(
+ juels_per_fee_coin, signatures_len, gas_price, billing
+ );
+
+ // end report()
+
+ self
+ .emit(
+ Event::NewTransmission(
+ NewTransmission {
+ round_id: round_id,
+ answer: median,
+ transmitter: caller,
+ observation_timestamp: observation_timestamp,
+ observers: observers,
+ observations: observations,
+ juels_per_fee_coin: juels_per_fee_coin,
+ gas_price: gas_price,
+ config_digest: report_context.config_digest,
+ epoch_and_round: report_context.epoch_and_round,
+ reimbursement: reimbursement_juels,
+ }
+ )
+ );
+
+ // pay transmitter
+ let payment = reimbursement_juels + (billing.transmission_payment_gjuels.into() * GIGA);
+ // TODO: check overflow
+
+ oracle.payment_juels += payment;
+ self._transmitters.write(caller, oracle);
+ }
+
+ fn verify_signatures(
+ self: @ContractState,
+ msg: felt252,
+ ref signatures: Array,
+ mut signed_count: u128
+ ) {
+ let mut span = signatures.span();
+ while let Option::Some(signature) = span
+ .pop_front() {
+ let index = self._signers.read(*signature.public_key);
+ assert(index != 0_usize, 'invalid signer'); // 0 index == uninitialized
+
+ let indexed_bit = pow(2_u128, index.into() - 1_u128);
+ let prev_signed_count = signed_count;
+ signed_count = signed_count | indexed_bit;
+ assert(prev_signed_count != signed_count, 'duplicate signer');
+
+ let is_valid = ecdsa::check_ecdsa_signature(
+ msg, *signature.public_key, *signature.r, *signature.s
+ );
+
+ assert(is_valid, '');
+ };
+ }
+ }
+
+ // --- Billing Config
+
+ #[derive(Copy, Drop, Serde, starknet::Store)]
+ struct BillingConfig {
+ observation_payment_gjuels: u32,
+ transmission_payment_gjuels: u32,
+ gas_base: u32,
+ gas_per_signature: u32,
+ }
+
+ // --- Billing Access Controller
+
+ #[derive(Drop, starknet::Event)]
+ struct BillingAccessControllerSet {
+ #[key]
+ old_controller: ContractAddress,
+ #[key]
+ new_controller: ContractAddress,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct BillingSet {
+ config: BillingConfig
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct OraclePaid {
+ #[key]
+ transmitter: ContractAddress,
+ payee: ContractAddress,
+ amount: u256,
+ link_token: ContractAddress,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct LinkTokenSet {
+ #[key]
+ old_link_token: ContractAddress,
+ #[key]
+ new_link_token: ContractAddress
+ }
+
+ #[abi(embed_v0)]
+ impl BillingImpl of super::Billing {
+ fn set_link_token(
+ ref self: ContractState, link_token: ContractAddress, recipient: ContractAddress
+ ) {
+ self.ownable.assert_only_owner();
+
+ let old_token = self._link_token.read();
+
+ if link_token == old_token {
+ return ();
+ }
+
+ let contract_address = starknet::info::get_contract_address();
+
+ // call balanceOf as a sanity check to confirm we're talking to a token
+ let token = IERC20Dispatcher { contract_address: link_token };
+ token.balance_of(account: contract_address);
+
+ self.pay_oracles();
+
+ // transfer remaining balance of old token to recipient
+ let old_token_dispatcher = IERC20Dispatcher { contract_address: old_token };
+ let amount = old_token_dispatcher.balance_of(account: contract_address);
+ old_token_dispatcher.transfer(recipient, amount);
+
+ self._link_token.write(link_token);
+
+ self
+ .emit(
+ Event::LinkTokenSet(
+ LinkTokenSet { old_link_token: old_token, new_link_token: link_token }
+ )
+ );
+ }
+
+ fn set_billing_access_controller(
+ ref self: ContractState, access_controller: ContractAddress
+ ) {
+ self.ownable.assert_only_owner();
+
+ let old_controller = self._billing_access_controller.read();
+ if access_controller == old_controller {
+ return ();
+ }
+
+ self._billing_access_controller.write(access_controller);
+ self
+ .emit(
+ Event::BillingAccessControllerSet(
+ BillingAccessControllerSet {
+ old_controller: old_controller, new_controller: access_controller
+ }
+ )
+ );
+ }
+
+ fn set_billing(ref self: ContractState, config: BillingConfig) {
+ self.has_billing_access();
+
+ self.pay_oracles();
+
+ self._billing.write(config);
+
+ self.emit(Event::BillingSet(BillingSet { config: config }));
+ }
+
+ fn billing(self: @ContractState) -> BillingConfig {
+ self._billing.read()
+ }
+
+ // Payments and Withdrawals
+
+ fn withdraw_payment(ref self: ContractState, transmitter: ContractAddress) {
+ let caller = starknet::info::get_caller_address();
+ let payee = self._payees.read(transmitter);
+ assert(caller == payee, 'only payee can withdraw');
+
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let link_token = self._link_token.read();
+ self.pay_oracle(transmitter, latest_round_id, link_token)
+ }
+
+ fn owed_payment(self: @ContractState, transmitter: ContractAddress) -> u128 {
+ let oracle = self._transmitters.read(transmitter);
+ self._owed_payment(@oracle)
+ }
+
+ fn withdraw_funds(ref self: ContractState, recipient: ContractAddress, amount: u256) {
+ self.has_billing_access();
+
+ let link_token = self._link_token.read();
+ let contract_address = starknet::info::get_contract_address();
+
+ let due = self.total_link_due();
+ // NOTE: equivalent to converting u128 to u256
+ let due = u256 { high: 0_u128, low: due };
+
+ let token = IERC20Dispatcher { contract_address: link_token };
+ let balance = token.balance_of(account: contract_address);
+
+ assert(due <= balance, 'amount due exceeds balance');
+ let available = balance - due;
+
+ // Transfer as much as there is available
+ let amount = if available < amount {
+ available
+ } else {
+ amount
+ };
+ token.transfer(recipient, amount);
+ }
+
+ fn link_available_for_payment(
+ self: @ContractState
+ ) -> (bool, u128) { // (is negative, absolute difference)
+ let link_token = self._link_token.read();
+ let contract_address = starknet::info::get_contract_address();
+
+ let token = IERC20Dispatcher { contract_address: link_token };
+ let balance = token.balance_of(account: contract_address);
+ // entire link supply fits into u96 so this should not fail
+ assert(balance.high == 0_u128, 'balance too high');
+ let balance: u128 = balance.low;
+
+ let due = self.total_link_due();
+ if balance > due {
+ (false, balance - due)
+ } else {
+ (true, due - balance)
+ }
+ }
+ }
+
+ #[generate_trait]
+ impl BillingHelperImpl of BillingHelperTrait {
+ fn has_billing_access(self: @ContractState) {
+ let caller = starknet::info::get_caller_address();
+ let owner = self.ownable.owner();
+
+ // owner always has access
+ if caller == owner {
+ return ();
+ }
+
+ let access_controller = self._billing_access_controller.read();
+ let access_controller = IAccessControllerDispatcher {
+ contract_address: access_controller
+ };
+ assert(
+ access_controller.has_access(caller, ArrayTrait::new()),
+ 'caller does not have access'
+ );
+ }
+
+ // --- Payments and Withdrawals
+
+ fn _owed_payment(self: @ContractState, oracle: @Oracle) -> u128 {
+ if *oracle.index == 0_usize {
+ return 0_u128;
+ }
+
+ let billing = self._billing.read();
+
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let from_round_id = self._reward_from_aggregator_round_id.read(*oracle.index);
+ let rounds = latest_round_id - from_round_id;
+
+ (rounds * billing.observation_payment_gjuels.into() * GIGA) + *oracle.payment_juels
+ }
+
+ fn pay_oracle(
+ ref self: ContractState,
+ transmitter: ContractAddress,
+ latest_round_id: u128,
+ link_token: ContractAddress
+ ) {
+ let oracle = self._transmitters.read(transmitter);
+ if oracle.index == 0_usize {
+ return ();
+ }
+
+ let amount = self._owed_payment(@oracle);
+ // if zero, fastpath return to avoid empty transfers
+ if amount == 0_u128 {
+ return ();
+ }
+
+ let payee = self._payees.read(transmitter);
+
+ // NOTE: equivalent to converting u128 to u256
+ let amount = u256 { high: 0_u128, low: amount };
+
+ let token = IERC20Dispatcher { contract_address: link_token };
+ token.transfer(recipient: payee, amount: amount);
+
+ // Reset payment
+ self._reward_from_aggregator_round_id.write(oracle.index, latest_round_id);
+ self
+ ._transmitters
+ .write(transmitter, Oracle { index: oracle.index, payment_juels: 0_u128 });
+
+ self
+ .emit(
+ Event::OraclePaid(
+ OraclePaid {
+ transmitter: transmitter,
+ payee: payee,
+ amount: amount,
+ link_token: link_token
+ }
+ )
+ );
+ }
+
+ fn pay_oracles(ref self: ContractState) {
+ let mut index = self._oracles_len.read();
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let link_token = self._link_token.read();
+ while index > 0_usize {
+ let transmitter = self._transmitters_list.read(index);
+ self.pay_oracle(transmitter, latest_round_id, link_token);
+ index -= 1;
+ };
+ }
+
+ fn total_link_due(self: @ContractState) -> u128 {
+ let mut index = self._oracles_len.read();
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let mut total_rounds = 0;
+ let mut payments_juels = 0;
+
+ loop {
+ if index == 0_usize {
+ break ();
+ }
+ let transmitter = self._transmitters_list.read(index);
+ let oracle = self._transmitters.read(transmitter);
+ assert(oracle.index != 0_usize, index.into()); // 0 == undefined
+
+ let from_round_id = self._reward_from_aggregator_round_id.read(oracle.index);
+ let rounds = latest_round_id - from_round_id;
+ total_rounds += rounds;
+ payments_juels += oracle.payment_juels;
+ index -= 1;
+ };
+
+ let billing = self._billing.read();
+ return (total_rounds * billing.observation_payment_gjuels.into() * GIGA)
+ + payments_juels;
+ }
+ }
+
+ // --- Transmitter Payment
+
+ const MARGIN: u128 = 115_u128;
+
+ fn calculate_reimbursement(
+ juels_per_fee_coin: u128, signature_count: usize, gas_price: u128, config: BillingConfig
+ ) -> u128 {
+ // TODO: determine new values for these constants
+ // Based on estimateFee (f=1 14977, f=2 14989, f=3 15002 f=4 15014 f=5 15027, count = f+1)
+ // gas_base = 14951, gas_per_signature = 13
+ let signature_count_u128: u128 = signature_count.into();
+ let gas_base_u128: u128 = config.gas_base.into();
+ let gas_per_signature_u128: u128 = config.gas_per_signature.into();
+
+ let exact_gas = gas_base_u128 + (signature_count_u128 * gas_per_signature_u128);
+ let gas = exact_gas * MARGIN / 100_u128; // scale to 115% for some margin
+ let amount = gas * gas_price;
+ amount * juels_per_fee_coin
+ }
+
+ // --- Payee Management
+
+ use super::PayeeConfig;
+
+ #[derive(Drop, starknet::Event)]
+ struct PayeeshipTransferRequested {
+ #[key]
+ transmitter: ContractAddress,
+ #[key]
+ current: ContractAddress,
+ #[key]
+ proposed: ContractAddress,
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct PayeeshipTransferred {
+ #[key]
+ transmitter: ContractAddress,
+ #[key]
+ previous: ContractAddress,
+ #[key]
+ current: ContractAddress,
+ }
+
+ #[abi(embed_v0)]
+ impl PayeeManagementImpl of super::PayeeManagement {
+ fn set_payees(ref self: ContractState, mut payees: Array) {
+ self.ownable.assert_only_owner();
+ while let Option::Some(payee) = payees
+ .pop_front() {
+ let current_payee = self._payees.read(payee.transmitter);
+ let is_unset = current_payee.is_zero();
+ let is_same = current_payee == payee.payee;
+ assert(is_unset | is_same, 'payee already set');
+
+ self._payees.write(payee.transmitter, payee.payee);
+
+ self
+ .emit(
+ Event::PayeeshipTransferred(
+ PayeeshipTransferred {
+ transmitter: payee.transmitter,
+ previous: current_payee,
+ current: payee.payee
+ }
+ )
+ );
+ }
+ }
+
+ fn transfer_payeeship(
+ ref self: ContractState, transmitter: ContractAddress, proposed: ContractAddress
+ ) {
+ assert(!proposed.is_zero(), 'cannot transfer to zero address');
+ let caller = starknet::info::get_caller_address();
+ let payee = self._payees.read(transmitter);
+ assert(caller == payee, 'only current payee can update');
+ assert(caller != proposed, 'cannot transfer to self');
+
+ self._proposed_payees.write(transmitter, proposed);
+ self
+ .emit(
+ Event::PayeeshipTransferRequested(
+ PayeeshipTransferRequested {
+ transmitter: transmitter, current: payee, proposed: proposed
+ }
+ )
+ );
+ }
+
+ fn accept_payeeship(ref self: ContractState, transmitter: ContractAddress) {
+ let proposed = self._proposed_payees.read(transmitter);
+ let caller = starknet::info::get_caller_address();
+ assert(caller == proposed, 'only proposed payee can accept');
+ let previous = self._payees.read(transmitter);
+
+ self._payees.write(transmitter, proposed);
+ self._proposed_payees.write(transmitter, Zeroable::zero());
+ self
+ .emit(
+ Event::PayeeshipTransferred(
+ PayeeshipTransferred {
+ transmitter: transmitter, previous: previous, current: caller
+ }
+ )
+ );
+ }
+ }
+}
diff --git a/contracts/src/ocr2/aggregator_proxy.cairo b/contracts/src/ocr2/aggregator_proxy.cairo
new file mode 100644
index 000000000..9859c9ccd
--- /dev/null
+++ b/contracts/src/ocr2/aggregator_proxy.cairo
@@ -0,0 +1,262 @@
+use chainlink::ocr2::aggregator::Round;
+use chainlink::ocr2::aggregator::{IAggregator, IAggregatorDispatcher, IAggregatorDispatcherTrait};
+use starknet::ContractAddress;
+
+// TODO: use a generic param for the round_id?
+#[starknet::interface]
+trait IAggregatorProxy {
+ fn latest_round_data(self: @TContractState) -> Round;
+ fn round_data(self: @TContractState, round_id: felt252) -> Round;
+ fn description(self: @TContractState) -> felt252;
+ fn decimals(self: @TContractState) -> u8;
+ fn latest_answer(self: @TContractState) -> u128;
+}
+
+#[starknet::interface]
+trait IAggregatorProxyInternal {
+ fn propose_aggregator(ref self: TContractState, address: ContractAddress);
+ fn confirm_aggregator(ref self: TContractState, address: ContractAddress);
+ fn proposed_latest_round_data(self: @TContractState) -> Round;
+ fn proposed_round_data(self: @TContractState, round_id: felt252) -> Round;
+ fn aggregator(self: @TContractState) -> ContractAddress;
+ fn phase_id(self: @TContractState) -> u128;
+}
+
+#[starknet::contract]
+mod AggregatorProxy {
+ use super::IAggregatorProxy;
+ use super::IAggregatorDispatcher;
+ use super::IAggregatorDispatcherTrait;
+
+ use integer::u128s_from_felt252;
+ use option::OptionTrait;
+ use traits::Into;
+ use traits::TryInto;
+ use zeroable::Zeroable;
+
+ use starknet::ContractAddress;
+ use starknet::ContractAddressIntoFelt252;
+ use starknet::Felt252TryIntoContractAddress;
+ use integer::Felt252TryIntoU128;
+ use starknet::StorageBaseAddress;
+ use starknet::SyscallResult;
+ use integer::U128IntoFelt252;
+ use integer::U128sFromFelt252Result;
+ use starknet::storage_read_syscall;
+ use starknet::storage_write_syscall;
+ use starknet::storage_address_from_base_and_offset;
+ use starknet::class_hash::ClassHash;
+
+ use openzeppelin::access::ownable::OwnableComponent;
+
+ use chainlink::ocr2::aggregator::IAggregator;
+ use chainlink::ocr2::aggregator::Round;
+ use chainlink::libraries::access_control::{AccessControlComponent, IAccessController};
+ use chainlink::libraries::access_control::AccessControlComponent::InternalTrait as AccessControlInternalTrait;
+ use chainlink::utils::split_felt;
+ use chainlink::libraries::type_and_version::{
+ ITypeAndVersion, ITypeAndVersionDispatcher, ITypeAndVersionDispatcherTrait
+ };
+ use chainlink::libraries::upgradeable::{Upgradeable, IUpgradeable};
+
+ const SHIFT: felt252 = 0x100000000000000000000000000000000;
+ const MAX_ID: felt252 = 0xffffffffffffffffffffffffffffffff;
+
+ #[derive(Copy, Drop, Serde, starknet::Store)]
+ struct Phase {
+ id: u128,
+ aggregator: ContractAddress
+ }
+
+ component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+ component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent);
+
+ #[abi(embed_v0)]
+ impl OwnableImpl = OwnableComponent::OwnableTwoStepImpl;
+ impl OwnableInternalImpl = OwnableComponent::InternalImpl;
+
+ #[abi(embed_v0)]
+ impl AccessControlImpl =
+ AccessControlComponent::AccessControlImpl;
+ impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;
+
+ #[storage]
+ struct Storage {
+ #[substorage(v0)]
+ ownable: OwnableComponent::Storage,
+ #[substorage(v0)]
+ access_control: AccessControlComponent::Storage,
+ _current_phase: Phase,
+ _proposed_aggregator: ContractAddress,
+ _phases: LegacyMap
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ #[flat]
+ OwnableEvent: OwnableComponent::Event,
+ #[flat]
+ AccessControlEvent: AccessControlComponent::Event,
+ }
+
+ // TODO: refactor these events
+ #[event]
+ fn AggregatorProposed(current: ContractAddress, proposed: ContractAddress) {}
+
+ #[event]
+ fn AggregatorConfirmed(previous: ContractAddress, latest: ContractAddress) {}
+
+ #[abi(embed_v0)]
+ impl AggregatorProxyImpl of IAggregatorProxy {
+ fn latest_round_data(self: @ContractState) -> Round {
+ self._require_read_access();
+ let phase = self._current_phase.read();
+ let aggregator = IAggregatorDispatcher { contract_address: phase.aggregator };
+ let round = aggregator.latest_round_data();
+
+ Round {
+ round_id: (phase.id.into() * SHIFT) + round.round_id,
+ answer: round.answer,
+ block_num: round.block_num,
+ started_at: round.started_at,
+ updated_at: round.updated_at,
+ }
+ }
+ fn round_data(self: @ContractState, round_id: felt252) -> Round {
+ self._require_read_access();
+ let (phase_id, round_id) = split_felt(round_id);
+ let address = self._phases.read(phase_id);
+ assert(!address.is_zero(), 'aggregator address is 0');
+
+ let aggregator = IAggregatorDispatcher { contract_address: address };
+ let round = aggregator.round_data(round_id);
+
+ Round {
+ round_id: (phase_id.into() * SHIFT) + round.round_id,
+ answer: round.answer,
+ block_num: round.block_num,
+ started_at: round.started_at,
+ updated_at: round.updated_at,
+ }
+ }
+ fn description(self: @ContractState) -> felt252 {
+ self._require_read_access();
+ let phase = self._current_phase.read();
+ let aggregator = IAggregatorDispatcher { contract_address: phase.aggregator };
+ aggregator.description()
+ }
+
+ fn decimals(self: @ContractState) -> u8 {
+ self._require_read_access();
+ let phase = self._current_phase.read();
+ let aggregator = IAggregatorDispatcher { contract_address: phase.aggregator };
+ aggregator.decimals()
+ }
+
+ fn latest_answer(self: @ContractState) -> u128 {
+ self._require_read_access();
+ let phase = self._current_phase.read();
+ let aggregator = IAggregatorDispatcher { contract_address: phase.aggregator };
+ aggregator.latest_answer()
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl TypeAndVersion of ITypeAndVersion {
+ fn type_and_version(self: @ContractState) -> felt252 {
+ let phase = self._current_phase.read();
+ let aggregator = ITypeAndVersionDispatcher { contract_address: phase.aggregator };
+ aggregator.type_and_version()
+ }
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState, owner: ContractAddress, address: ContractAddress) {
+ self.ownable.initializer(owner);
+ self.access_control.initializer();
+ self._set_aggregator(address);
+ }
+
+ // -- Upgradeable --
+
+ #[abi(embed_v0)]
+ impl UpgradeableImpl of IUpgradeable {
+ fn upgrade(ref self: ContractState, new_impl: ClassHash) {
+ self.ownable.assert_only_owner();
+ Upgradeable::upgrade(new_impl)
+ }
+ }
+
+ //
+
+ #[abi(embed_v0)]
+ impl AggregatorProxyInternal of super::IAggregatorProxyInternal {
+ fn propose_aggregator(ref self: ContractState, address: ContractAddress) {
+ self.ownable.assert_only_owner();
+ assert(!address.is_zero(), 'proposed address is 0');
+ self._proposed_aggregator.write(address);
+
+ let phase = self._current_phase.read();
+ AggregatorProposed(phase.aggregator, address);
+ }
+
+ fn confirm_aggregator(ref self: ContractState, address: ContractAddress) {
+ self.ownable.assert_only_owner();
+ assert(!address.is_zero(), 'confirm address is 0');
+ let phase = self._current_phase.read();
+ let previous = phase.aggregator;
+
+ let proposed_aggregator = self._proposed_aggregator.read();
+ assert(address == proposed_aggregator, 'does not match proposed address');
+ self._proposed_aggregator.write(starknet::contract_address_const::<0>());
+ self._set_aggregator(proposed_aggregator);
+
+ AggregatorConfirmed(previous, address);
+ }
+
+ fn proposed_latest_round_data(self: @ContractState) -> Round {
+ self._require_read_access();
+ let address = self._proposed_aggregator.read();
+ let aggregator = IAggregatorDispatcher { contract_address: address };
+ aggregator.latest_round_data()
+ }
+
+ fn proposed_round_data(self: @ContractState, round_id: felt252) -> Round {
+ self._require_read_access();
+ let address = self._proposed_aggregator.read();
+ let round_id128: u128 = round_id.try_into().unwrap();
+ let aggregator = IAggregatorDispatcher { contract_address: address };
+ aggregator.round_data(round_id128)
+ }
+
+ fn aggregator(self: @ContractState) -> ContractAddress {
+ self._require_read_access();
+ let phase = self._current_phase.read();
+ phase.aggregator
+ }
+
+ fn phase_id(self: @ContractState) -> u128 {
+ self._require_read_access();
+ let phase = self._current_phase.read();
+ phase.id
+ }
+ }
+
+ /// Internals
+
+ #[generate_trait]
+ impl StorageImpl of StorageTrait {
+ fn _set_aggregator(ref self: ContractState, address: ContractAddress) {
+ let phase = self._current_phase.read();
+ let new_phase_id = phase.id + 1_u128;
+ self._current_phase.write(Phase { id: new_phase_id, aggregator: address });
+ self._phases.write(new_phase_id, address);
+ }
+
+ fn _require_read_access(self: @ContractState) {
+ let caller = starknet::info::get_caller_address();
+ self.access_control.check_read_access(caller);
+ }
+ }
+}
diff --git a/contracts/src/ocr2/mocks.cairo b/contracts/src/ocr2/mocks.cairo
new file mode 100644
index 000000000..16f9b3343
--- /dev/null
+++ b/contracts/src/ocr2/mocks.cairo
@@ -0,0 +1 @@
+mod mock_aggregator;
diff --git a/contracts/src/ocr2/mocks/mock_aggregator.cairo b/contracts/src/ocr2/mocks/mock_aggregator.cairo
new file mode 100644
index 000000000..eceb7d3c8
--- /dev/null
+++ b/contracts/src/ocr2/mocks/mock_aggregator.cairo
@@ -0,0 +1,127 @@
+#[starknet::interface]
+trait IMockAggregator {
+ fn set_latest_round_data(
+ ref self: TContractState,
+ answer: u128,
+ block_num: u64,
+ observation_timestamp: u64,
+ transmission_timestamp: u64
+ );
+}
+
+#[starknet::contract]
+mod MockAggregator {
+ use array::ArrayTrait;
+ use starknet::contract_address_const;
+ use traits::Into;
+
+ use chainlink::ocr2::aggregator::IAggregator;
+ use chainlink::ocr2::aggregator::Aggregator::{Transmission, NewTransmission};
+ use chainlink::ocr2::aggregator::Round;
+ use chainlink::libraries::type_and_version::ITypeAndVersion;
+
+ #[event]
+ use chainlink::ocr2::aggregator::Aggregator::Event;
+
+ #[storage]
+ struct Storage {
+ _transmissions: LegacyMap,
+ _latest_aggregator_round_id: u128,
+ _decimals: u8
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState, decimals: u8) {
+ self._decimals.write(decimals);
+ }
+
+ #[abi(embed_v0)]
+ impl MockImpl of super::IMockAggregator {
+ fn set_latest_round_data(
+ ref self: ContractState,
+ answer: u128,
+ block_num: u64,
+ observation_timestamp: u64,
+ transmission_timestamp: u64
+ ) {
+ let new_round_id = self._latest_aggregator_round_id.read() + 1_u128;
+ self
+ ._transmissions
+ .write(
+ new_round_id,
+ Transmission {
+ answer: answer,
+ block_num: block_num,
+ observation_timestamp: observation_timestamp,
+ transmission_timestamp: transmission_timestamp
+ }
+ );
+
+ let mut observations = ArrayTrait::new();
+ observations.append(2_u128);
+ observations.append(3_u128);
+
+ self._latest_aggregator_round_id.write(new_round_id);
+
+ self
+ .emit(
+ Event::NewTransmission(
+ NewTransmission {
+ round_id: new_round_id,
+ answer: answer,
+ transmitter: contract_address_const::<42>(),
+ observation_timestamp: observation_timestamp,
+ observers: 3,
+ observations: observations,
+ juels_per_fee_coin: 18_u128,
+ gas_price: 1_u128,
+ config_digest: 777,
+ epoch_and_round: 20_u64,
+ reimbursement: 100_u128
+ }
+ )
+ );
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl TypeAndVersionImpl of ITypeAndVersion {
+ fn type_and_version(self: @ContractState) -> felt252 {
+ 'mock_aggregator.cairo 1.0.0'
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl Aggregator of IAggregator {
+ fn round_data(self: @ContractState, round_id: u128) -> Round {
+ panic_with_felt252('unimplemented')
+ }
+
+ fn latest_round_data(self: @ContractState) -> Round {
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let transmission = self._transmissions.read(latest_round_id);
+
+ Round {
+ round_id: latest_round_id.into(),
+ answer: transmission.answer,
+ block_num: transmission.block_num,
+ started_at: transmission.observation_timestamp,
+ updated_at: transmission.transmission_timestamp
+ }
+ }
+
+ fn decimals(self: @ContractState) -> u8 {
+ self._decimals.read()
+ }
+
+ fn description(self: @ContractState) -> felt252 {
+ 'mock'
+ }
+
+ fn latest_answer(self: @ContractState) -> u128 {
+ let latest_round_id = self._latest_aggregator_round_id.read();
+ let transmission = self._transmissions.read(latest_round_id);
+ transmission.answer
+ }
+ }
+}
diff --git a/contracts/src/tests.cairo b/contracts/src/tests.cairo
new file mode 100644
index 000000000..4d8104499
--- /dev/null
+++ b/contracts/src/tests.cairo
@@ -0,0 +1,12 @@
+// Cairo files in folders can only be imported from a file of the same name as the folder
+
+mod test_aggregator;
+mod test_aggregator_proxy;
+mod test_multisig;
+mod test_ownable;
+mod test_erc677;
+mod test_link_token;
+mod test_upgradeable;
+mod test_access_controller;
+mod test_mock_aggregator;
+mod test_sequencer_uptime_feed;
diff --git a/contracts/src/tests/test_access_controller.cairo b/contracts/src/tests/test_access_controller.cairo
new file mode 100644
index 000000000..17d570978
--- /dev/null
+++ b/contracts/src/tests/test_access_controller.cairo
@@ -0,0 +1,85 @@
+use starknet::ContractAddress;
+use starknet::testing::set_caller_address;
+use starknet::testing::set_contract_address;
+use starknet::contract_address_const;
+use starknet::class_hash::class_hash_const;
+use starknet::class_hash::Felt252TryIntoClassHash;
+use starknet::syscalls::deploy_syscall;
+
+use array::ArrayTrait;
+use traits::Into;
+use traits::TryInto;
+use option::OptionTrait;
+use core::result::ResultTrait;
+
+use chainlink::access_control::access_controller::AccessController;
+use chainlink::access_control::access_controller::AccessController::UpgradeableImpl;
+
+use chainlink::libraries::access_control::{
+ IAccessController, IAccessControllerDispatcher, IAccessControllerDispatcherTrait
+};
+
+fn STATE() -> AccessController::ContractState {
+ AccessController::contract_state_for_testing()
+}
+
+fn setup() -> ContractAddress {
+ let account: ContractAddress = contract_address_const::<777>();
+ set_caller_address(account);
+ account
+}
+
+#[test]
+#[should_panic(expected: ('Caller is not the owner',))]
+fn test_upgrade_not_owner() {
+ let _ = setup();
+ let mut state = STATE();
+
+ UpgradeableImpl::upgrade(ref state, class_hash_const::<2>());
+}
+
+#[test]
+fn test_access_control() {
+ let owner = setup();
+ // Deploy access controller
+ let calldata = array![owner.into(), // owner
+ ];
+ let (accessControllerAddr, _) = deploy_syscall(
+ AccessController::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+
+ should_implement_access_control(accessControllerAddr, owner);
+}
+
+//
+// Tests for contracts that inherit AccessControl.
+// Write functions are assumed to be protected by Ownable::only_owner,
+// but this test does not check for that.
+//
+
+fn should_implement_access_control(contract_addr: ContractAddress, owner: ContractAddress) {
+ let contract = IAccessControllerDispatcher { contract_address: contract_addr };
+ let acc2: ContractAddress = contract_address_const::<2222987765>();
+
+ set_contract_address(owner); // required to call contract as owner
+
+ // access check is enabled by default
+ assert(!contract.has_access(acc2, array![]), 'should not have access');
+
+ // disable access check
+ contract.disable_access_check();
+ assert(contract.has_access(acc2, array![]), 'should have access');
+
+ // enable access check
+ contract.enable_access_check();
+ assert(!contract.has_access(acc2, array![]), 'should not have access');
+
+ // add_access for acc2
+ contract.add_access(acc2);
+ assert(contract.has_access(acc2, array![]), 'should have access');
+
+ // remove_access for acc2
+ contract.remove_access(acc2);
+ assert(!contract.has_access(acc2, array![]), 'should not have access');
+}
diff --git a/contracts/src/tests/test_aggregator.cairo b/contracts/src/tests/test_aggregator.cairo
new file mode 100644
index 000000000..e2c3d81c3
--- /dev/null
+++ b/contracts/src/tests/test_aggregator.cairo
@@ -0,0 +1,427 @@
+use starknet::testing::set_caller_address;
+use starknet::testing::set_contract_address;
+use starknet::ContractAddress;
+use starknet::contract_address_const;
+use starknet::class_hash::class_hash_const;
+use starknet::class_hash::Felt252TryIntoClassHash;
+use starknet::syscalls::deploy_syscall;
+
+use array::ArrayTrait;
+use clone::Clone;
+use traits::Into;
+use traits::TryInto;
+use option::OptionTrait;
+use core::result::ResultTrait;
+
+use chainlink::ocr2::aggregator::pow;
+use chainlink::ocr2::aggregator::Aggregator;
+use chainlink::ocr2::aggregator::Aggregator::{
+ AggregatorImpl, BillingImpl, PayeeManagementImpl, UpgradeableImpl
+};
+use chainlink::ocr2::aggregator::Aggregator::BillingConfig;
+use chainlink::ocr2::aggregator::Aggregator::PayeeConfig;
+use chainlink::access_control::access_controller::AccessController;
+use chainlink::token::link_token::LinkToken;
+use chainlink::tests::test_ownable::should_implement_ownable;
+use chainlink::tests::test_access_controller::should_implement_access_control;
+
+#[test]
+fn test_pow_2_0() {
+ assert(pow(2, 0) == 0x1, 'expected 0x1');
+ assert(pow(2, 1) == 0x2, 'expected 0x2');
+ assert(pow(2, 2) == 0x4, 'expected 0x4');
+ assert(pow(2, 3) == 0x8, 'expected 0x8');
+ assert(pow(2, 4) == 0x10, 'expected 0x10');
+ assert(pow(2, 5) == 0x20, 'expected 0x20');
+ assert(pow(2, 6) == 0x40, 'expected 0x40');
+ assert(pow(2, 7) == 0x80, 'expected 0x80');
+ assert(pow(2, 8) == 0x100, 'expected 0x100');
+ assert(pow(2, 9) == 0x200, 'expected 0x200');
+ assert(pow(2, 10) == 0x400, 'expected 0x400');
+ assert(pow(2, 11) == 0x800, 'expected 0x800');
+ assert(pow(2, 12) == 0x1000, 'expected 0x1000');
+ assert(pow(2, 13) == 0x2000, 'expected 0x2000');
+ assert(pow(2, 14) == 0x4000, 'expected 0x4000');
+ assert(pow(2, 15) == 0x8000, 'expected 0x8000');
+ assert(pow(2, 16) == 0x10000, 'expected 0x10000');
+ assert(pow(2, 17) == 0x20000, 'expected 0x20000');
+ assert(pow(2, 18) == 0x40000, 'expected 0x40000');
+ assert(pow(2, 19) == 0x80000, 'expected 0x80000');
+ assert(pow(2, 20) == 0x100000, 'expected 0x100000');
+ assert(pow(2, 21) == 0x200000, 'expected 0x200000');
+ assert(pow(2, 22) == 0x400000, 'expected 0x400000');
+ assert(pow(2, 23) == 0x800000, 'expected 0x800000');
+ assert(pow(2, 24) == 0x1000000, 'expected 0x1000000');
+ assert(pow(2, 25) == 0x2000000, 'expected 0x2000000');
+ assert(pow(2, 26) == 0x4000000, 'expected 0x4000000');
+ assert(pow(2, 27) == 0x8000000, 'expected 0x8000000');
+ assert(pow(2, 28) == 0x10000000, 'expected 0x10000000');
+ assert(pow(2, 29) == 0x20000000, 'expected 0x20000000');
+ assert(pow(2, 30) == 0x40000000, 'expected 0x40000000');
+ assert(pow(2, 31) == 0x80000000, 'expected 0x80000000');
+}
+
+use chainlink::libraries::access_control::{
+ IAccessController, IAccessControllerDispatcher, IAccessControllerDispatcherTrait
+};
+
+#[starknet::interface]
+trait ILinkToken {}
+
+fn STATE() -> Aggregator::ContractState {
+ Aggregator::contract_state_for_testing()
+}
+
+fn setup() -> (
+ ContractAddress, ContractAddress, IAccessControllerDispatcher, ILinkTokenDispatcher
+) {
+ let acc1: ContractAddress = contract_address_const::<777>();
+ let acc2: ContractAddress = contract_address_const::<888>();
+ // set acc1 as default caller
+ set_caller_address(acc1);
+
+ // deploy billing access controller
+ let calldata = array![acc1.into(), // owner = acc1;
+ ];
+ let (billingAccessControllerAddr, _) = deploy_syscall(
+ AccessController::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+ let billingAccessController = IAccessControllerDispatcher {
+ contract_address: billingAccessControllerAddr
+ };
+
+ // deploy link token contract
+ let calldata = array![acc1.into(), // minter = acc1;
+ acc1.into(), // owner = acc1;
+ ];
+ let (linkTokenAddr, _) = deploy_syscall(
+ LinkToken::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+ let linkToken = ILinkTokenDispatcher { contract_address: linkTokenAddr };
+
+ // return accounts, billing access controller, link token
+ (acc1, acc2, billingAccessController, linkToken)
+}
+
+#[test]
+fn test_ownable() {
+ let (account, _, _, _) = setup();
+ // Deploy aggregator
+ let calldata = array![
+ account.into(), // owner
+ contract_address_const::<777>().into(), // link token
+ 0, // min_answer
+ 100, // max_answer
+ contract_address_const::<999>().into(), // billing access controller
+ 8, // decimals
+ 123, // description
+ ];
+ let (aggregatorAddr, _) = deploy_syscall(
+ Aggregator::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+
+ should_implement_ownable(aggregatorAddr, account);
+}
+
+#[test]
+fn test_access_control() {
+ let (account, _, _, _) = setup();
+ // Deploy aggregator
+ let mut calldata = array![
+ account.into(), // owner
+ contract_address_const::<777>().into(), // link token
+ 0, // min_answer
+ 100, // max_answer
+ contract_address_const::<999>().into(), // billing access controller
+ 8, // decimals
+ 123, // description
+ ];
+ let (aggregatorAddr, _) = deploy_syscall(
+ Aggregator::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+
+ should_implement_access_control(aggregatorAddr, account);
+}
+
+
+#[test]
+#[should_panic(expected: ('Caller is not the owner',))]
+fn test_upgrade_non_owner() {
+ let _ = setup();
+ let mut state = STATE();
+
+ UpgradeableImpl::upgrade(ref state, class_hash_const::<123>());
+}
+
+// --- Billing tests ---
+
+#[test]
+#[should_panic(expected: ('Caller is not the owner',))]
+fn test_set_billing_access_controller_not_owner() {
+ let (owner, acc2, billingAccessController, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ // set billing access controller should revert if caller is not owner
+ set_caller_address(acc2);
+ BillingImpl::set_billing_access_controller(ref state, billingAccessController.contract_address);
+}
+
+#[test]
+#[should_panic(expected: ('caller does not have access',))]
+fn test_set_billing_config_no_access() {
+ let (owner, acc2, billingAccessController, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state,
+ owner,
+ contract_address_const::<777>(),
+ 0,
+ 100,
+ billingAccessController.contract_address,
+ 8,
+ 123
+ );
+
+ // set billing config as acc2 with no access
+ let config = BillingConfig {
+ observation_payment_gjuels: 1,
+ transmission_payment_gjuels: 5,
+ gas_base: 1,
+ gas_per_signature: 1,
+ };
+ set_caller_address(acc2);
+ BillingImpl::set_billing(ref state, config);
+}
+
+#[test]
+fn test_set_billing_config_as_owner() {
+ let (owner, _, billingAccessController, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state,
+ owner,
+ contract_address_const::<777>(),
+ 0,
+ 100,
+ billingAccessController.contract_address,
+ 8,
+ 123
+ );
+
+ // set billing config as owner
+ let config = BillingConfig {
+ observation_payment_gjuels: 1,
+ transmission_payment_gjuels: 5,
+ gas_base: 1,
+ gas_per_signature: 1,
+ };
+ BillingImpl::set_billing(ref state, config);
+
+ // check billing config
+ let billing = BillingImpl::billing(@state);
+ assert(billing.observation_payment_gjuels == 1, 'should be 1');
+ assert(billing.transmission_payment_gjuels == 5, 'should be 5');
+ assert(billing.gas_base == 1, 'should be 1');
+ assert(billing.gas_per_signature == 1, 'should be 1');
+}
+
+#[test]
+fn test_set_billing_config_as_acc_with_access() {
+ let (owner, acc2, billingAccessController, _) = setup();
+ let mut state = STATE();
+ // grant acc2 access on access controller
+ set_contract_address(owner);
+ billingAccessController.add_access(acc2);
+
+ Aggregator::constructor(
+ ref state,
+ owner,
+ contract_address_const::<777>(),
+ 0,
+ 100,
+ billingAccessController.contract_address,
+ 8,
+ 123
+ );
+
+ // set billing config as acc2 with access
+ let config = BillingConfig {
+ observation_payment_gjuels: 1,
+ transmission_payment_gjuels: 5,
+ gas_base: 1,
+ gas_per_signature: 1,
+ };
+ set_caller_address(acc2);
+ BillingImpl::set_billing(ref state, config);
+
+ // check billing config
+ let billing = BillingImpl::billing(@state);
+ assert(billing.observation_payment_gjuels == 1, 'should be 1');
+ assert(billing.transmission_payment_gjuels == 5, 'should be 5');
+ assert(billing.gas_base == 1, 'should be 1');
+ assert(billing.gas_per_signature == 1, 'should be 1');
+}
+
+// --- Payee Management Tests ---
+
+#[test]
+#[should_panic(expected: ('Caller is not the owner',))]
+fn test_set_payees_caller_not_owner() {
+ let (owner, acc2, _, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ let payees = array![PayeeConfig { transmitter: acc2, payee: acc2, },];
+
+ // set payee should revert if caller is not owner
+ set_caller_address(acc2);
+ PayeeManagementImpl::set_payees(ref state, payees);
+}
+
+#[test]
+fn test_set_single_payee() {
+ let (owner, acc2, _, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ let payees = array![PayeeConfig { transmitter: acc2, payee: acc2, },];
+
+ set_caller_address(owner);
+ PayeeManagementImpl::set_payees(ref state, payees);
+}
+
+#[test]
+fn test_set_multiple_payees() {
+ let (owner, acc2, _, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ let payees = array![
+ PayeeConfig { transmitter: acc2, payee: acc2, },
+ PayeeConfig { transmitter: owner, payee: owner, },
+ ];
+
+ set_caller_address(owner);
+ PayeeManagementImpl::set_payees(ref state, payees);
+}
+
+#[test]
+#[should_panic(expected: ('only current payee can update',))]
+fn test_transfer_payeeship_caller_not_payee() {
+ let (owner, acc2, _, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ let transmitter = contract_address_const::<123>();
+ let payees = array![PayeeConfig { transmitter: transmitter, payee: acc2, },];
+
+ set_caller_address(owner);
+ PayeeManagementImpl::set_payees(ref state, payees);
+ PayeeManagementImpl::transfer_payeeship(ref state, transmitter, owner);
+}
+
+#[test]
+#[should_panic(expected: ('cannot transfer to self',))]
+fn test_transfer_payeeship_to_self() {
+ let (owner, acc2, _, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ let transmitter = contract_address_const::<123>();
+ let payees = array![PayeeConfig { transmitter: transmitter, payee: acc2, },];
+
+ set_caller_address(owner);
+ PayeeManagementImpl::set_payees(ref state, payees);
+ set_caller_address(acc2);
+ PayeeManagementImpl::transfer_payeeship(ref state, transmitter, acc2);
+}
+
+#[test]
+#[should_panic(expected: ('only proposed payee can accept',))]
+fn test_accept_payeeship_caller_not_proposed_payee() {
+ let (owner, acc2, _, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ let transmitter = contract_address_const::<123>();
+ let payees = array![PayeeConfig { transmitter: transmitter, payee: acc2, },];
+
+ set_caller_address(owner);
+ PayeeManagementImpl::set_payees(ref state, payees);
+ set_caller_address(acc2);
+ PayeeManagementImpl::transfer_payeeship(ref state, transmitter, owner);
+ PayeeManagementImpl::accept_payeeship(ref state, transmitter);
+}
+
+#[test]
+fn test_transfer_and_accept_payeeship() {
+ let (owner, acc2, _, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ let transmitter = contract_address_const::<123>();
+ let payees = array![PayeeConfig { transmitter: transmitter, payee: acc2, },];
+
+ set_caller_address(owner);
+ PayeeManagementImpl::set_payees(ref state, payees);
+ set_caller_address(acc2);
+ PayeeManagementImpl::transfer_payeeship(ref state, transmitter, owner);
+ set_caller_address(owner);
+ PayeeManagementImpl::accept_payeeship(ref state, transmitter);
+}
+// --- Payments and Withdrawals Tests ---
+//
+// NOTE: this test suite largely incomplete as we cannot generate or mock
+// off-chain signatures in cairo-test, and thus cannot generate aggregator rounds.
+// We could explore testing against a mock aggregator contract with the signature
+// verification logic removed in the future.
+
+#[test]
+fn test_owed_payment_no_rounds() {
+ let (owner, acc2, _, _) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(
+ ref state, owner, contract_address_const::<777>(), 0, 100, acc2, 8, 123
+ );
+
+ let transmitter = contract_address_const::<123>();
+ let mut payees = array![PayeeConfig { transmitter: transmitter, payee: acc2, },];
+
+ set_caller_address(owner);
+ PayeeManagementImpl::set_payees(ref state, payees);
+
+ let owed = BillingImpl::owed_payment(@state, transmitter);
+ assert(owed == 0, 'owed payment should be 0');
+}
+
+#[test]
+fn test_link_available_for_payment_no_rounds_or_funds() {
+ let (owner, acc2, _, linkToken) = setup();
+ let mut state = STATE();
+ Aggregator::constructor(ref state, owner, linkToken.contract_address, 0, 100, acc2, 8, 123);
+
+ let (is_negative, diff) = BillingImpl::link_available_for_payment(@state);
+ assert(is_negative == true, 'is_negative should be true');
+ assert(diff == 0, 'absolute_diff should be 0');
+}
diff --git a/contracts/src/tests/test_aggregator_proxy.cairo b/contracts/src/tests/test_aggregator_proxy.cairo
new file mode 100644
index 000000000..ef33d0f66
--- /dev/null
+++ b/contracts/src/tests/test_aggregator_proxy.cairo
@@ -0,0 +1,216 @@
+use starknet::contract_address_const;
+use starknet::ContractAddress;
+use starknet::testing::set_caller_address;
+use starknet::syscalls::deploy_syscall;
+use starknet::class_hash::Felt252TryIntoClassHash;
+use starknet::class_hash::class_hash_const;
+
+use array::ArrayTrait;
+use traits::Into;
+use traits::TryInto;
+use option::OptionTrait;
+use core::result::ResultTrait;
+
+use chainlink::ocr2::mocks::mock_aggregator::{
+ MockAggregator, IMockAggregator, IMockAggregatorDispatcher, IMockAggregatorDispatcherTrait
+};
+use chainlink::ocr2::aggregator_proxy::AggregatorProxy;
+use chainlink::ocr2::aggregator_proxy::AggregatorProxy::{
+ AggregatorProxyImpl, AggregatorProxyInternal, UpgradeableImpl
+};
+use chainlink::libraries::access_control::AccessControlComponent::AccessControlImpl;
+use chainlink::ocr2::aggregator::Round;
+use chainlink::utils::split_felt;
+use chainlink::tests::test_ownable::should_implement_ownable;
+use chainlink::tests::test_access_controller::should_implement_access_control;
+
+fn STATE() -> AggregatorProxy::ContractState {
+ AggregatorProxy::contract_state_for_testing()
+}
+
+fn setup() -> (
+ ContractAddress,
+ ContractAddress,
+ IMockAggregatorDispatcher,
+ ContractAddress,
+ IMockAggregatorDispatcher
+) {
+ // Set account as default caller
+ let account: ContractAddress = contract_address_const::<1>();
+ set_caller_address(account);
+
+ // Deploy mock aggregator 1
+ let mut calldata = ArrayTrait::new();
+ calldata.append(8); // decimals = 8
+ let (mockAggregatorAddr1, _) = deploy_syscall(
+ MockAggregator::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+ let mockAggregator1 = IMockAggregatorDispatcher { contract_address: mockAggregatorAddr1 };
+
+ // Deploy mock aggregator 2
+ // note: deployment address is deterministic based on deploy_syscall parameters
+ // so we need to change the decimals parameter to avoid an address conflict with mock aggregator 1
+ let mut calldata2 = ArrayTrait::new();
+ calldata2.append(10); // decimals = 10
+ let (mockAggregatorAddr2, _) = deploy_syscall(
+ MockAggregator::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata2.span(), false
+ )
+ .unwrap();
+ let mockAggregator2 = IMockAggregatorDispatcher { contract_address: mockAggregatorAddr2 };
+
+ // Return account, mock aggregator address and mock aggregator contract
+ (account, mockAggregatorAddr1, mockAggregator1, mockAggregatorAddr2, mockAggregator2)
+}
+
+#[test]
+fn test_ownable() {
+ let (account, mockAggregatorAddr, _, _, _) = setup();
+ // Deploy aggregator proxy
+ let calldata = array![account.into(), // owner = account
+ mockAggregatorAddr.into(),];
+ let (aggregatorProxyAddr, _) = deploy_syscall(
+ AggregatorProxy::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+
+ should_implement_ownable(aggregatorProxyAddr, account);
+}
+
+#[test]
+fn test_access_control() {
+ let (account, mockAggregatorAddr, _, _, _) = setup();
+ // Deploy aggregator proxy
+ let calldata = array![account.into(), // owner = account
+ mockAggregatorAddr.into(),];
+ let (aggregatorProxyAddr, _) = deploy_syscall(
+ AggregatorProxy::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+
+ should_implement_access_control(aggregatorProxyAddr, account);
+}
+
+#[test]
+#[should_panic(expected: ('Caller is not the owner',))]
+fn test_upgrade_non_owner() {
+ let (_, _, _, _, _) = setup();
+ let mut state = STATE();
+ UpgradeableImpl::upgrade(ref state, class_hash_const::<123>());
+}
+
+fn test_query_latest_round_data() {
+ let (owner, mockAggregatorAddr, mockAggregator, _, _) = setup();
+ let mut state = STATE();
+ // init aggregator proxy with mock aggregator
+ AggregatorProxy::constructor(ref state, owner, mockAggregatorAddr);
+ state.add_access(owner);
+ // insert round into mock aggregator
+ mockAggregator.set_latest_round_data(10, 1, 9, 8);
+ // query latest round
+ let round = AggregatorProxyImpl::latest_round_data(@state);
+ let (phase_id, round_id) = split_felt(round.round_id);
+ assert(phase_id == 1, 'phase_id should be 1');
+ assert(round_id == 1, 'round_id should be 1');
+ assert(round.answer == 10, 'answer should be 10');
+ assert(round.block_num == 1, 'block_num should be 1');
+ assert(round.started_at == 9, 'started_at should be 9');
+ assert(round.updated_at == 8, 'updated_at should be 8');
+
+ // latest_answer matches up with latest_round_data
+ let latest_answer = AggregatorProxyImpl::latest_answer(@state);
+ assert(latest_answer == 10, '(latest) answer should be 10');
+}
+
+#[test]
+#[should_panic(expected: ('user does not have read access',))]
+fn test_query_latest_round_data_without_access() {
+ let (owner, mockAggregatorAddr, mockAggregator, _, _) = setup();
+ let mut state = STATE();
+ // init aggregator proxy with mock aggregator
+ AggregatorProxy::constructor(ref state, owner, mockAggregatorAddr);
+ state.add_access(owner);
+ // insert round into mock aggregator
+ mockAggregator.set_latest_round_data(10, 1, 9, 8);
+ // set caller to non-owner address with no read access
+ set_caller_address(contract_address_const::<2>());
+ // query latest round
+ AggregatorProxyImpl::latest_round_data(@state);
+}
+
+#[test]
+#[should_panic(expected: ('user does not have read access',))]
+fn test_query_latest_answer_without_access() {
+ let (owner, mockAggregatorAddr, mockAggregator, _, _) = setup();
+ let mut state = STATE();
+ // init aggregator proxy with mock aggregator
+ AggregatorProxy::constructor(ref state, owner, mockAggregatorAddr);
+ state.add_access(owner);
+ // insert round into mock aggregator
+ mockAggregator.set_latest_round_data(10, 1, 9, 8);
+ // set caller to non-owner address with no read access
+ set_caller_address(contract_address_const::<2>());
+ // query latest round
+ AggregatorProxyImpl::latest_answer(@state);
+}
+
+#[test]
+fn test_propose_new_aggregator() {
+ let (owner, mockAggregatorAddr1, mockAggregator1, mockAggregatorAddr2, mockAggregator2) =
+ setup();
+ let mut state = STATE();
+ // init aggregator proxy with mock aggregator 1
+ AggregatorProxy::constructor(ref state, owner, mockAggregatorAddr1);
+ state.add_access(owner);
+ // insert rounds into mock aggregators
+ mockAggregator1.set_latest_round_data(10, 1, 9, 8);
+ mockAggregator2.set_latest_round_data(12, 2, 10, 11);
+
+ // propose new mock aggregator to AggregatorProxy
+ AggregatorProxyInternal::propose_aggregator(ref state, mockAggregatorAddr2);
+
+ // latest_round_data should return old aggregator round data
+ let round = AggregatorProxyImpl::latest_round_data(@state);
+ assert(round.answer == 10, 'answer should be 10');
+
+ // mockAggregator2.set_latest_round_data(12, 2, 10, 11);
+
+ // proposed_round_data should return new aggregator round data
+ let proposed_round = AggregatorProxyInternal::proposed_latest_round_data(@state);
+ assert(proposed_round.answer == 12, 'answer should be 12');
+
+ // aggregator should still be set to the old aggregator
+ let aggregator = AggregatorProxyInternal::aggregator(@state);
+ assert(aggregator == mockAggregatorAddr1, 'aggregator should be old addr');
+}
+
+#[test]
+fn test_confirm_new_aggregator() {
+ let (owner, mockAggregatorAddr1, mockAggregator1, mockAggregatorAddr2, mockAggregator2) =
+ setup();
+ let mut state = STATE();
+ // init aggregator proxy with mock aggregator 1
+ AggregatorProxy::constructor(ref state, owner, mockAggregatorAddr1);
+ state.add_access(owner);
+ // insert rounds into mock aggregators
+ mockAggregator1.set_latest_round_data(10, 1, 9, 8);
+ mockAggregator2.set_latest_round_data(12, 2, 10, 11);
+
+ // propose new mock aggregator to AggregatorProxy
+ AggregatorProxyInternal::propose_aggregator(ref state, mockAggregatorAddr2);
+
+ // confirm new mock aggregator
+ AggregatorProxyInternal::confirm_aggregator(ref state, mockAggregatorAddr2);
+
+ // aggregator should be set to the new aggregator
+ let aggregator = AggregatorProxyInternal::aggregator(@state);
+ assert(aggregator == mockAggregatorAddr2, 'aggregator should be new addr');
+
+ // phase ID should be 2
+ let phase_id = AggregatorProxyInternal::phase_id(@state);
+ assert(phase_id == 2, 'phase_id should be 2');
+
+ // latest_round_data should return new aggregator round data
+ let round = AggregatorProxyImpl::latest_round_data(@state);
+ assert(round.answer == 12, 'answer should be 12');
+}
diff --git a/contracts/src/tests/test_erc677.cairo b/contracts/src/tests/test_erc677.cairo
new file mode 100644
index 000000000..99b9c8fbf
--- /dev/null
+++ b/contracts/src/tests/test_erc677.cairo
@@ -0,0 +1,112 @@
+use starknet::ContractAddress;
+use starknet::contract_address_const;
+use starknet::testing::set_caller_address;
+use starknet::syscalls::deploy_syscall;
+use starknet::class_hash::Felt252TryIntoClassHash;
+
+use array::ArrayTrait;
+use traits::Into;
+use traits::TryInto;
+use zeroable::Zeroable;
+use option::OptionTrait;
+use core::result::ResultTrait;
+
+use chainlink::token::mock::valid_erc667_receiver::ValidReceiver;
+use chainlink::token::mock::invalid_erc667_receiver::InvalidReceiver;
+use chainlink::libraries::token::erc677::ERC677Component;
+use chainlink::libraries::token::erc677::ERC677Component::ERC677Impl;
+
+#[starknet::interface]
+trait MockInvalidReceiver {
+ fn set_supports(ref self: TContractState, value: bool);
+}
+
+use chainlink::token::mock::valid_erc667_receiver::{
+ MockValidReceiver, MockValidReceiverDispatcher, MockValidReceiverDispatcherTrait
+};
+
+// Ignored tests are dependent on upgrading our version of cairo to include this PR https://github.com/starkware-libs/cairo/pull/2912/files
+
+fn setup() -> ContractAddress {
+ let account: ContractAddress = contract_address_const::<1>();
+ // Set account as default caller
+ set_caller_address(account);
+ account
+}
+
+fn setup_valid_receiver() -> (ContractAddress, MockValidReceiverDispatcher) {
+ let calldata = ArrayTrait::new();
+ let (address, _) = deploy_syscall(
+ ValidReceiver::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+ let contract = MockValidReceiverDispatcher { contract_address: address };
+ (address, contract)
+}
+
+
+fn setup_invalid_receiver() -> (ContractAddress, MockInvalidReceiverDispatcher) {
+ let calldata = ArrayTrait::new();
+ let (address, _) = deploy_syscall(
+ InvalidReceiver::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+ let contract = MockInvalidReceiverDispatcher { contract_address: address };
+ (address, contract)
+}
+
+type ComponentState =
+ ERC677Component::ComponentState;
+
+fn transfer_and_call(receiver: ContractAddress) {
+ let data = ArrayTrait::::new();
+ // have to send 0 because ERC20 is not initialized with starting supply when using this library by itself
+ let mut state: ComponentState = ERC677Component::component_state_for_testing();
+ state.transfer_and_call(receiver, u256 { high: 0, low: 0 }, data);
+}
+
+#[test]
+#[should_panic(expected: ('ERC20: transfer to 0',))]
+fn test_to_zero_address() {
+ setup();
+ transfer_and_call(Zeroable::zero());
+}
+
+#[test]
+fn test_valid_transfer_and_call() {
+ let sender = setup();
+ let (receiver_address, receiver) = setup_valid_receiver();
+
+ transfer_and_call(receiver_address);
+
+ assert(receiver.verify() == sender, 'on_token_transfer called');
+}
+
+#[test]
+#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))]
+fn test_invalid_receiver_supports_interface_true() {
+ setup();
+ let (receiver_address, receiver) = setup_invalid_receiver();
+
+ receiver.set_supports(true);
+
+ transfer_and_call(receiver_address);
+}
+
+#[test]
+fn test_invalid_receiver_supports_interface_false() {
+ setup();
+ let (receiver_address, _) = setup_invalid_receiver();
+
+ transfer_and_call(receiver_address);
+}
+
+
+#[test]
+#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED',))]
+fn test_nonexistent_receiver() {
+ setup();
+
+ transfer_and_call(contract_address_const::<777>());
+}
+
diff --git a/contracts/src/tests/test_link_token.cairo b/contracts/src/tests/test_link_token.cairo
new file mode 100644
index 000000000..0c0fde3ed
--- /dev/null
+++ b/contracts/src/tests/test_link_token.cairo
@@ -0,0 +1,150 @@
+use starknet::ContractAddress;
+use starknet::testing::set_caller_address;
+use starknet::contract_address_const;
+use starknet::class_hash::class_hash_const;
+use starknet::class_hash::Felt252TryIntoClassHash;
+use starknet::syscalls::deploy_syscall;
+
+use array::ArrayTrait;
+use traits::Into;
+use traits::TryInto;
+use zeroable::Zeroable;
+use option::OptionTrait;
+use core::result::ResultTrait;
+
+use chainlink::token::link_token::LinkToken;
+use chainlink::token::link_token::LinkToken::{MintableToken, UpgradeableImpl};
+use openzeppelin::token::erc20::ERC20Component::{ERC20Impl, ERC20MetadataImpl};
+use chainlink::tests::test_ownable::should_implement_ownable;
+
+// only tests link token specific functionality
+// erc20 and erc677 functionality is already tested elsewhere
+
+fn STATE() -> LinkToken::ContractState {
+ LinkToken::contract_state_for_testing()
+}
+
+fn setup() -> ContractAddress {
+ let account: ContractAddress = contract_address_const::<1>();
+ // Set account as default caller
+ set_caller_address(account);
+ account
+}
+
+#[test]
+fn test_ownable() {
+ let account = setup();
+ // Deploy LINK token
+ let mut calldata = ArrayTrait::new();
+ calldata.append(class_hash_const::<123>().into()); // minter
+ calldata.append(account.into()); // owner
+ let (linkAddr, _) = deploy_syscall(
+ LinkToken::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+
+ should_implement_ownable(linkAddr, account);
+}
+
+#[test]
+#[should_panic(expected: ('minter is 0',))]
+fn test_constructor_zero_address() {
+ let sender = setup();
+ let mut state = STATE();
+
+ LinkToken::constructor(ref state, Zeroable::zero(), sender);
+}
+
+#[test]
+fn test_constructor() {
+ let sender = setup();
+ let mut state = STATE();
+ LinkToken::constructor(ref state, sender, sender);
+
+ assert(LinkToken::minter(@state) == sender, 'minter valid');
+ assert(state.erc20.name() == "ChainLink Token", 'name valid');
+ assert(state.erc20.symbol() == "LINK", 'symbol valid');
+}
+
+#[test]
+fn test_permissioned_mint_from_minter() {
+ let sender = setup();
+ let mut state = STATE();
+ LinkToken::constructor(ref state, sender, sender);
+ let to = contract_address_const::<908>();
+
+ let zero: felt252 = 0;
+ assert(ERC20Impl::balance_of(@state, sender) == zero.into(), 'zero balance');
+ assert(ERC20Impl::balance_of(@state, to) == zero.into(), 'zero balance');
+
+ let amount: felt252 = 3000;
+ MintableToken::permissioned_mint(ref state, to, amount.into());
+
+ assert(ERC20Impl::balance_of(@state, sender) == zero.into(), 'zero balance');
+ assert(ERC20Impl::balance_of(@state, to) == amount.into(), 'expect balance');
+}
+
+#[test]
+#[should_panic(expected: ('only minter',))]
+fn test_permissioned_mint_from_nonminter() {
+ let sender = setup();
+ let mut state = STATE();
+ let minter = contract_address_const::<111>();
+ LinkToken::constructor(ref state, minter, sender);
+ let to = contract_address_const::<908>();
+
+ let amount: felt252 = 3000;
+ MintableToken::permissioned_mint(ref state, to, amount.into());
+}
+
+#[test]
+#[should_panic(expected: ('u256_sub Overflow',))]
+fn test_permissioned_burn_from_minter() {
+ let zero = 0;
+ let sender = setup();
+ let mut state = STATE();
+ LinkToken::constructor(ref state, sender, sender);
+ let to = contract_address_const::<908>();
+
+ let amount: felt252 = 3000;
+ MintableToken::permissioned_mint(ref state, to, amount.into());
+ assert(ERC20Impl::balance_of(@state, to) == amount.into(), 'expect balance');
+
+ // burn some
+ let burn_amount: felt252 = 2000;
+ let remaining_amount: felt252 = amount - burn_amount;
+ MintableToken::permissioned_burn(ref state, to, burn_amount.into());
+ assert(ERC20Impl::balance_of(@state, to) == remaining_amount.into(), 'remaining balance');
+
+ // burn remaining
+ MintableToken::permissioned_burn(ref state, to, remaining_amount.into());
+ assert(ERC20Impl::balance_of(@state, to) == zero.into(), 'no balance');
+
+ // burn too much
+ MintableToken::permissioned_burn(ref state, to, amount.into());
+}
+
+
+#[test]
+#[should_panic(expected: ('only minter',))]
+fn test_permissioned_burn_from_nonminter() {
+ let sender = setup();
+ let mut state = STATE();
+ let minter = contract_address_const::<111>();
+ LinkToken::constructor(ref state, minter, sender);
+ let to = contract_address_const::<908>();
+
+ let amount: felt252 = 3000;
+ MintableToken::permissioned_burn(ref state, to, amount.into());
+}
+
+#[test]
+#[should_panic(expected: ('Caller is not the owner',))]
+fn test_upgrade_non_owner() {
+ let sender = setup();
+ let mut state = STATE();
+ LinkToken::constructor(ref state, sender, contract_address_const::<111>());
+
+ UpgradeableImpl::upgrade(ref state, class_hash_const::<123>());
+}
+
diff --git a/contracts/src/tests/test_mock_aggregator.cairo b/contracts/src/tests/test_mock_aggregator.cairo
new file mode 100644
index 000000000..e95b7b87e
--- /dev/null
+++ b/contracts/src/tests/test_mock_aggregator.cairo
@@ -0,0 +1,65 @@
+use starknet::ContractAddress;
+use starknet::testing::set_caller_address;
+use chainlink::ocr2::mocks::mock_aggregator::MockAggregator;
+use starknet::contract_address_const;
+use chainlink::ocr2::aggregator::Round;
+
+fn STATE() -> MockAggregator::ContractState {
+ MockAggregator::contract_state_for_testing()
+}
+
+fn setup() -> ContractAddress {
+ let account: ContractAddress = contract_address_const::<777>();
+ // Set account as default caller
+ set_caller_address(account);
+ account
+}
+
+#[test]
+fn test_deploy() {
+ setup();
+
+ let mut state = STATE();
+
+ MockAggregator::constructor(ref state, 18_u8);
+
+ assert(MockAggregator::Aggregator::decimals(@state) == 18_u8, 'decimals');
+
+ let latest_round = MockAggregator::Aggregator::latest_round_data(@state);
+
+ let _ = Round {
+ round_id: 0, answer: 0_u128, block_num: 0_u64, started_at: 0_u64, updated_at: 0_u64
+ };
+
+ assert(
+ latest_round == Round {
+ round_id: 0, answer: 0_u128, block_num: 0_u64, started_at: 0_u64, updated_at: 0_u64
+ },
+ 'rounds'
+ );
+}
+
+#[test]
+fn test_set_latest_round() {
+ setup();
+
+ let mut state = STATE();
+
+ MockAggregator::constructor(ref state, 18_u8);
+
+ MockAggregator::MockImpl::set_latest_round_data(ref state, 777_u128, 777_u64, 777_u64, 777_u64);
+
+ let expected_round = Round {
+ round_id: 1, answer: 777_u128, block_num: 777_u64, started_at: 777_u64, updated_at: 777_u64
+ };
+
+ assert(
+ MockAggregator::Aggregator::latest_round_data(@state) == expected_round, 'round not equal'
+ );
+
+ assert(
+ MockAggregator::Aggregator::latest_answer(@state) == expected_round.answer,
+ 'latest answer not equal'
+ );
+}
+
diff --git a/contracts/src/tests/test_multisig.cairo b/contracts/src/tests/test_multisig.cairo
new file mode 100644
index 000000000..becfe4d7d
--- /dev/null
+++ b/contracts/src/tests/test_multisig.cairo
@@ -0,0 +1,691 @@
+use chainlink::multisig::IMultisigDispatcherTrait;
+use core::traits::Into;
+use starknet::class_hash_const;
+use starknet::contract_address_const;
+use starknet::syscalls::deploy_syscall;
+use starknet::testing::set_caller_address;
+use starknet::testing::set_contract_address;
+use starknet::Felt252TryIntoClassHash;
+
+use array::ArrayTrait;
+use option::OptionTrait;
+use result::ResultTrait;
+use traits::TryInto;
+
+use chainlink::multisig::assert_unique_values;
+use chainlink::multisig::Multisig;
+use chainlink::multisig::Multisig::{MultisigImpl, UpgradeableImpl};
+use chainlink::multisig::{IMultisigDispatcher};
+
+#[starknet::contract]
+mod MultisigTest {
+ use array::ArrayTrait;
+
+ #[storage]
+ struct Storage {}
+
+ #[abi(per_item)]
+ #[generate_trait]
+ impl HelperImpl of HelperTrait {
+ #[external(v0)]
+ fn increment(ref self: ContractState, val1: felt252, val2: felt252) -> Array {
+ array![val1 + 1, val2 + 1]
+ }
+ }
+}
+
+
+fn STATE() -> Multisig::ContractState {
+ Multisig::contract_state_for_testing()
+}
+
+fn sample_calldata() -> Array:: {
+ array![1, 2, 32]
+}
+
+#[test]
+fn test_assert_unique_values_empty() {
+ let a = ArrayTrait::::new();
+ assert_unique_values(@a);
+}
+
+#[test]
+fn test_assert_unique_values_no_duplicates() {
+ let a = array![1, 2, 3];
+ assert_unique_values(@a);
+}
+
+#[test]
+#[should_panic]
+fn test_assert_unique_values_with_duplicate() {
+ let a = array![1, 2, 3, 3];
+ assert_unique_values(@a);
+}
+
+#[test]
+fn test_is_signer_true() {
+ let mut state = STATE();
+ let signer = contract_address_const::<1>();
+ let mut signers = ArrayTrait::new();
+ signers.append(signer);
+ Multisig::constructor(ref state, :signers, threshold: 1);
+ assert(MultisigImpl::is_signer(@state, signer), 'should be signer');
+}
+
+#[test]
+fn test_is_signer_false() {
+ let mut state = STATE();
+ let not_signer = contract_address_const::<2>();
+ let mut signers = ArrayTrait::new();
+ signers.append(contract_address_const::<1>());
+ Multisig::constructor(ref state, :signers, threshold: 1);
+ assert(!MultisigImpl::is_signer(@state, not_signer), 'should be signer');
+}
+
+#[test]
+fn test_signer_len() {
+ let mut state = STATE();
+ let mut signers = ArrayTrait::new();
+ signers.append(contract_address_const::<1>());
+ signers.append(contract_address_const::<2>());
+ Multisig::constructor(ref state, :signers, threshold: 1);
+ assert(MultisigImpl::get_signers_len(@state) == 2, 'should equal 2 signers');
+}
+
+#[test]
+fn test_get_signers() {
+ let mut state = STATE();
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signers = array![signer1, signer2];
+
+ Multisig::constructor(ref state, :signers, threshold: 1);
+ let returned_signers = MultisigImpl::get_signers(@state);
+ assert(returned_signers.len() == 2, 'should match signers length');
+ assert(*returned_signers.at(0) == signer1, 'should match signer 1');
+ assert(*returned_signers.at(1) == signer2, 'should match signer 2');
+}
+
+#[test]
+fn test_get_threshold() {
+ let mut state = STATE();
+ let mut signers = ArrayTrait::new();
+ signers.append(contract_address_const::<1>());
+ signers.append(contract_address_const::<2>());
+ Multisig::constructor(ref state, :signers, threshold: 1);
+ assert(MultisigImpl::get_threshold(@state) == 1, 'should equal threshold of 1');
+}
+
+#[test]
+fn test_submit_transaction() {
+ let mut state = STATE();
+ let signer = contract_address_const::<1>();
+ let signers = array![signer];
+ Multisig::constructor(ref state, :signers, threshold: 1);
+
+ set_caller_address(signer);
+ let to = contract_address_const::<42>();
+ let function_selector = 10;
+ MultisigImpl::submit_transaction(
+ ref state, :to, :function_selector, calldata: sample_calldata()
+ );
+
+ let (transaction, _) = MultisigImpl::get_transaction(@state, 0);
+ assert(transaction.to == to, 'should match target address');
+ assert(transaction.function_selector == function_selector, 'should match function selector');
+ assert(transaction.calldata_len == sample_calldata().len(), 'should match calldata length');
+ assert(!transaction.executed, 'should not be executed');
+ assert(transaction.confirmations == 0, 'should not have confirmations');
+// TODO: compare calldata when loops are supported
+}
+
+#[test]
+#[should_panic]
+fn test_submit_transaction_not_signer() {
+ let mut state = STATE();
+ let signer = contract_address_const::<1>();
+ let signers = array![signer];
+ Multisig::constructor(ref state, :signers, threshold: 1);
+
+ set_caller_address(contract_address_const::<3>());
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+}
+
+#[test]
+fn test_confirm_transaction() {
+ let mut state = STATE();
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signers = array![signer1, signer2];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+
+ set_caller_address(signer1);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+
+ assert(MultisigImpl::is_confirmed(@state, nonce: 0, signer: signer1), 'should be confirmed');
+ assert(
+ !MultisigImpl::is_confirmed(@state, nonce: 0, signer: signer2), 'should not be confirmed'
+ );
+ let (transaction, _) = MultisigImpl::get_transaction(@state, 0);
+ assert(transaction.confirmations == 1, 'should have confirmation');
+}
+
+#[test]
+#[should_panic]
+fn test_confirm_transaction_not_signer() {
+ let mut state = STATE();
+ let signer = contract_address_const::<1>();
+ let not_signer = contract_address_const::<2>();
+ let signers = array![signer];
+ Multisig::constructor(ref state, :signers, threshold: 1);
+ set_caller_address(signer);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+
+ set_caller_address(not_signer);
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+}
+
+#[test]
+fn test_revoke_confirmation() {
+ let mut state = STATE();
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signers = array![signer1, signer2];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+ set_caller_address(signer1);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+
+ MultisigImpl::revoke_confirmation(ref state, nonce: 0);
+
+ assert(
+ !MultisigImpl::is_confirmed(@state, nonce: 0, signer: signer1), 'should not be confirmed'
+ );
+ assert(
+ !MultisigImpl::is_confirmed(@state, nonce: 0, signer: signer2), 'should not be confirmed'
+ );
+ let (transaction, _) = MultisigImpl::get_transaction(@state, 0);
+ assert(transaction.confirmations == 0, 'should not have confirmation');
+}
+
+#[test]
+#[should_panic]
+fn test_revoke_confirmation_not_signer() {
+ let mut state = STATE();
+ let signer = contract_address_const::<1>();
+ let not_signer = contract_address_const::<2>();
+ let mut signers = array![signer];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+ set_caller_address(signer);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+
+ set_caller_address(not_signer);
+ MultisigImpl::revoke_confirmation(ref state, nonce: 0);
+}
+
+#[test]
+#[should_panic]
+fn test_execute_confirmation_below_threshold() {
+ let mut state = STATE();
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signers = array![signer1, signer2];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+ set_caller_address(signer1);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ MultisigImpl::execute_transaction(ref state, nonce: 0);
+}
+
+#[test]
+#[should_panic(expected: ('only multisig allowed',))]
+fn test_upgrade_not_multisig() {
+ let mut state = STATE();
+ let account = contract_address_const::<777>();
+ set_caller_address(account);
+
+ UpgradeableImpl::upgrade(ref state, class_hash_const::<1>())
+}
+
+#[test]
+fn test_execute() {
+ let mut state = STATE();
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signers = array![signer1, signer2];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+ let (test_address, _) = deploy_syscall(
+ MultisigTest::TEST_CLASS_HASH.try_into().unwrap(), 0, ArrayTrait::new().span(), false
+ )
+ .unwrap();
+ set_caller_address(signer1);
+ let increment_calldata = array![42, 100];
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: test_address,
+ // increment()
+ function_selector: 0x7a44dde9fea32737a5cf3f9683b3235138654aa2d189f6fe44af37a61dc60d,
+ calldata: increment_calldata,
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ set_caller_address(signer2);
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+
+ let response = MultisigImpl::execute_transaction(ref state, nonce: 0);
+ assert(response.len() == 3, 'expected response length 3');
+ assert(*response.at(0) == 2, 'expected array length 2');
+ assert(*response.at(1) == 43, 'expected array value 43');
+ assert(*response.at(2) == 101, 'expected array value 101');
+}
+
+#[test]
+#[should_panic(expected: ('invalid signer',))]
+fn test_execute_not_signer() {
+ let mut state = STATE();
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signers = array![signer1, signer2];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+ set_caller_address(signer1);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ set_caller_address(signer2);
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+
+ set_caller_address(contract_address_const::<3>());
+ MultisigImpl::execute_transaction(ref state, nonce: 0);
+}
+
+#[test]
+#[should_panic(expected: ('transaction invalid',))]
+fn test_execute_after_set_signers() {
+ let mut state = STATE();
+ let contract_address = contract_address_const::<100>();
+ set_contract_address(contract_address);
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signer3 = contract_address_const::<3>();
+ let signers = array![signer1, signer2];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+ set_caller_address(signer1);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ set_caller_address(signer2);
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ set_caller_address(contract_address);
+ let new_signers = array![signer2, signer3];
+ MultisigImpl::set_signers(ref state, new_signers);
+
+ set_caller_address(signer2);
+ MultisigImpl::execute_transaction(ref state, nonce: 0);
+}
+
+#[test]
+#[should_panic(expected: ('transaction invalid',))]
+fn test_execute_after_set_signers_and_threshold() {
+ let mut state = STATE();
+ let contract_address = contract_address_const::<100>();
+ set_contract_address(contract_address);
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signer3 = contract_address_const::<3>();
+ let signers = array![signer1, signer2];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+ set_caller_address(signer1);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ set_caller_address(signer2);
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ set_caller_address(contract_address);
+ let new_signers = array![signer2, signer3];
+ MultisigImpl::set_signers_and_threshold(ref state, new_signers, 1);
+
+ set_caller_address(signer2);
+ MultisigImpl::execute_transaction(ref state, nonce: 0);
+}
+
+#[test]
+#[should_panic(expected: ('transaction invalid',))]
+fn test_execute_after_set_threshold() {
+ let mut state = STATE();
+ let contract_address = contract_address_const::<100>();
+ set_contract_address(contract_address);
+ let signer1 = contract_address_const::<1>();
+ let signer2 = contract_address_const::<2>();
+ let signers = array![signer1, signer2];
+ Multisig::constructor(ref state, :signers, threshold: 2);
+ set_caller_address(signer1);
+ MultisigImpl::submit_transaction(
+ ref state,
+ to: contract_address_const::<42>(),
+ function_selector: 10,
+ calldata: sample_calldata(),
+ );
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ set_caller_address(signer2);
+ MultisigImpl::confirm_transaction(ref state, nonce: 0);
+ set_caller_address(contract_address);
+ MultisigImpl::set_threshold(ref state, 1);
+
+ set_caller_address(signer1);
+ MultisigImpl::execute_transaction(ref state, nonce: 0);
+}
+
+// test set_threshold (non-recursive)
+#[test]
+fn test_set_threshold() {
+ let s1 = contract_address_const::<1>();
+ let s2 = contract_address_const::<2>();
+ let s3 = contract_address_const::<3>();
+ let signers = array![s1, s2, s3];
+ let init_threshold: usize = 3;
+ let new_threshold: usize = 2;
+
+ let mut deploy_calldata = ArrayTrait::new();
+ Serde::serialize(@signers, ref deploy_calldata);
+ Serde::serialize(@init_threshold, ref deploy_calldata);
+ let (multisig_address, _) = deploy_syscall(
+ Multisig::TEST_CLASS_HASH.try_into().unwrap(), 0, deploy_calldata.span(), false
+ )
+ .unwrap();
+
+ let multisig = IMultisigDispatcher { contract_address: multisig_address };
+ assert(multisig.get_threshold() == init_threshold, 'invalid init threshold');
+ set_contract_address(multisig_address);
+ multisig.set_threshold(new_threshold);
+ assert(multisig.get_threshold() == new_threshold, 'threshold was not updated');
+}
+
+// test set_threshold with recursive call
+#[test]
+fn test_recursive_set_threshold() {
+ // Defines helper variables
+ let s1 = contract_address_const::<1>();
+ let s2 = contract_address_const::<2>();
+ let signers = array![s1, s2];
+ let init_threshold: usize = 2;
+ let new_threshold: usize = 1;
+
+ // Deploys the contract
+ let mut deploy_calldata = ArrayTrait::new();
+ Serde::serialize(@signers, ref deploy_calldata);
+ Serde::serialize(@init_threshold, ref deploy_calldata);
+ let (multisig_address, _) = deploy_syscall(
+ Multisig::TEST_CLASS_HASH.try_into().unwrap(), 0, deploy_calldata.span(), false
+ )
+ .unwrap();
+
+ // Gets a dispatcher (so we can call methods on the deployed contract)
+ let multisig = IMultisigDispatcher { contract_address: multisig_address };
+
+ // Checks that the threshold was correctly initialized on deployment
+ assert(multisig.get_threshold() == init_threshold, 'invalid init threshold');
+ // Recursive call occurs here - this code proposes a transaction to the
+ // multisig contract that calls the set_threshold function on the multisig
+ // contract.
+ let mut set_threshold_calldata = ArrayTrait::new();
+ Serde::serialize(@new_threshold, ref set_threshold_calldata);
+ set_contract_address(s1);
+ multisig
+ .submit_transaction(multisig_address, selector!("set_threshold"), set_threshold_calldata);
+ // Signer 1 confirms the transaction
+ set_contract_address(s1);
+ multisig.confirm_transaction(0);
+
+ // Signer 2 confirms the transaction
+ set_contract_address(s2);
+ multisig.confirm_transaction(0);
+
+ // Once we have enough confirmations, we execute the transaction
+ set_contract_address(s1);
+ multisig.execute_transaction(0);
+
+ // Now we check that the threshold was actually updated
+ assert(multisig.get_threshold() == new_threshold, 'threshold was not updated');
+}
+
+// test set_signers (non-recursive)
+#[test]
+fn test_set_signers() {
+ let s1 = contract_address_const::<1>();
+ let s2 = contract_address_const::<2>();
+ let init_signers = array![s1, s2];
+ let new_signers = array![s1];
+ let threshold: usize = 2;
+
+ let mut deploy_calldata = ArrayTrait::new();
+ Serde::serialize(@init_signers, ref deploy_calldata);
+ Serde::serialize(@threshold, ref deploy_calldata);
+ let (multisig_address, _) = deploy_syscall(
+ Multisig::TEST_CLASS_HASH.try_into().unwrap(), 0, deploy_calldata.span(), false
+ )
+ .unwrap();
+
+ let multisig = IMultisigDispatcher { contract_address: multisig_address };
+
+ let returned_signers = multisig.get_signers();
+ assert(returned_signers.len() == 2, 'should match signers length');
+ assert(*returned_signers.at(0) == s1, 'should match signer 1');
+ assert(*returned_signers.at(1) == s2, 'should match signer 2');
+ assert(multisig.get_threshold() == 2, 'wrong init threshold');
+
+ set_contract_address(multisig_address);
+ multisig.set_signers(new_signers);
+
+ let updated_signers = multisig.get_signers();
+ assert(updated_signers.len() == 1, 'should match signers length');
+ assert(*updated_signers.at(0) == s1, 'should match signer 1');
+ assert(multisig.get_threshold() == 1, 'threshold not updated');
+}
+
+// test set_signers with recursive call
+#[test]
+fn test_recursive_set_signers() {
+ // Defines helper variables
+ let s1 = contract_address_const::<1>();
+ let s2 = contract_address_const::<2>();
+ let init_signers = array![s1, s2];
+ let new_signers = array![s1];
+ let init_threshold: usize = 2;
+
+ // Deploys the contract
+ let mut deploy_calldata = ArrayTrait::new();
+ Serde::serialize(@init_signers, ref deploy_calldata);
+ Serde::serialize(@init_threshold, ref deploy_calldata);
+ let (multisig_address, _) = deploy_syscall(
+ Multisig::TEST_CLASS_HASH.try_into().unwrap(), 0, deploy_calldata.span(), false
+ )
+ .unwrap();
+
+ // Gets a dispatcher (so we can call methods on the deployed contract)
+ let multisig = IMultisigDispatcher { contract_address: multisig_address };
+
+ // Checks that the signers were correctly initialized on deployment
+ let returned_signers = multisig.get_signers();
+ assert(returned_signers.len() == 2, 'should match signers length');
+ assert(*returned_signers.at(0) == s1, 'should match signer 1');
+ assert(*returned_signers.at(1) == s2, 'should match signer 2');
+ assert(multisig.get_threshold() == 2, 'wrong init threshold');
+
+ // Recursive call occurs here - this code proposes a transaction to the
+ // multisig contract that calls the set_signers function on the multisig
+ // contract.
+ let mut set_signers_calldata = ArrayTrait::new();
+ Serde::serialize(@new_signers, ref set_signers_calldata);
+ set_contract_address(s1);
+ multisig.submit_transaction(multisig_address, selector!("set_signers"), set_signers_calldata);
+
+ // Signer 1 confirms the transaction
+ set_contract_address(s1);
+ multisig.confirm_transaction(0);
+
+ // Signer 2 confirms the transaction
+ set_contract_address(s2);
+ multisig.confirm_transaction(0);
+
+ // Once we have enough confirmations, we execute the transaction
+ set_contract_address(s1);
+ multisig.execute_transaction(0);
+
+ // Now we check that the signers were actually updated
+ let updated_signers = multisig.get_signers();
+ assert(updated_signers.len() == 1, 'should match signers length');
+ assert(*updated_signers.at(0) == s1, 'should match signer 1');
+ assert(multisig.get_threshold() == 1, 'wrong threshold');
+}
+
+// test set_signers_and_threshold (non-recursive)
+#[test]
+fn test_set_signers_and_threshold() {
+ let s1 = contract_address_const::<1>();
+ let s2 = contract_address_const::<2>();
+ let s3 = contract_address_const::<3>();
+ let init_signers = array![s1, s2, s3];
+ let new_signers = array![s1, s2];
+ let init_threshold: usize = 3;
+ let new_threshold: usize = 1;
+
+ let mut deploy_calldata = ArrayTrait::new();
+ Serde::serialize(@init_signers, ref deploy_calldata);
+ Serde::serialize(@init_threshold, ref deploy_calldata);
+ let (multisig_address, _) = deploy_syscall(
+ Multisig::TEST_CLASS_HASH.try_into().unwrap(), 0, deploy_calldata.span(), false
+ )
+ .unwrap();
+
+ let multisig = IMultisigDispatcher { contract_address: multisig_address };
+
+ let returned_signers = multisig.get_signers();
+ assert(returned_signers.len() == 3, 'should match signers length');
+ assert(*returned_signers.at(0) == s1, 'should match signer 1');
+ assert(*returned_signers.at(1) == s2, 'should match signer 2');
+ assert(*returned_signers.at(2) == s3, 'should match signer 3');
+ assert(multisig.get_threshold() == init_threshold, 'wrong init threshold');
+
+ set_contract_address(multisig_address);
+ multisig.set_signers_and_threshold(new_signers, new_threshold);
+
+ let updated_signers = multisig.get_signers();
+ assert(updated_signers.len() == 2, 'should match signers length');
+ assert(*updated_signers.at(0) == s1, 'should match signer 1');
+ assert(*updated_signers.at(1) == s2, 'should match signer 2');
+ assert(multisig.get_threshold() == new_threshold, 'threshold not updated');
+}
+
+// test set_signers_and_threshold with recursive call
+#[test]
+fn test_recursive_set_signers_and_threshold() {
+ // Defines helper variables
+ let s1 = contract_address_const::<1>();
+ let s2 = contract_address_const::<2>();
+ let s3 = contract_address_const::<3>();
+ let init_signers = array![s1, s2, s3];
+ let new_signers = array![s1, s2];
+ let init_threshold: usize = 3;
+ let new_threshold: usize = 1;
+
+ // Deploys the contract
+ let mut deploy_calldata = ArrayTrait::new();
+ Serde::serialize(@init_signers, ref deploy_calldata);
+ Serde::serialize(@init_threshold, ref deploy_calldata);
+ let (multisig_address, _) = deploy_syscall(
+ Multisig::TEST_CLASS_HASH.try_into().unwrap(), 0, deploy_calldata.span(), false
+ )
+ .unwrap();
+
+ // Gets a dispatcher (so we can call methods on the deployed contract)
+ let multisig = IMultisigDispatcher { contract_address: multisig_address };
+
+ // Checks that the initial state is correct
+ let returned_signers = multisig.get_signers();
+ assert(returned_signers.len() == 3, 'should match signers length');
+ assert(*returned_signers.at(0) == s1, 'should match signer 1');
+ assert(*returned_signers.at(1) == s2, 'should match signer 2');
+ assert(*returned_signers.at(2) == s3, 'should match signer 3');
+ assert(multisig.get_threshold() == 3, 'wrong init threshold');
+
+ // Recursive call occurs here - this code proposes a transaction to the
+ // multisig contract that calls the set_signers_and_threshold function
+ // on the multisig contract.
+ let mut set_signers_and_threshold_calldata = ArrayTrait::new();
+ Serde::serialize(@new_signers, ref set_signers_and_threshold_calldata);
+ Serde::serialize(@new_threshold, ref set_signers_and_threshold_calldata);
+ set_contract_address(s1);
+ multisig
+ .submit_transaction(
+ multisig_address,
+ selector!("set_signers_and_threshold"),
+ set_signers_and_threshold_calldata
+ );
+
+ // Signer 1 confirms the transaction
+ set_contract_address(s1);
+ multisig.confirm_transaction(0);
+
+ // Signer 2 confirms the transaction
+ set_contract_address(s2);
+ multisig.confirm_transaction(0);
+
+ // Signer 3 confirms the transaction
+ set_contract_address(s3);
+ multisig.confirm_transaction(0);
+
+ // Once we have enough confirmations, we execute the transaction
+ set_contract_address(s1);
+ multisig.execute_transaction(0);
+
+ // Now we check that the signers were actually updated
+ let updated_signers = multisig.get_signers();
+ assert(updated_signers.len() == 2, 'should match signers length');
+ assert(*updated_signers.at(0) == s1, 'should match signer 1');
+ assert(*updated_signers.at(1) == s2, 'should match signer 2');
+ assert(multisig.get_threshold() == 1, 'wrong threshold');
+}
+
diff --git a/contracts/src/tests/test_ownable.cairo b/contracts/src/tests/test_ownable.cairo
new file mode 100644
index 000000000..40e86c1c5
--- /dev/null
+++ b/contracts/src/tests/test_ownable.cairo
@@ -0,0 +1,38 @@
+use starknet::contract_address_const;
+use starknet::ContractAddress;
+use starknet::testing::set_caller_address;
+use starknet::testing::set_contract_address;
+use zeroable::Zeroable;
+
+use openzeppelin::access::ownable::interface::{
+ IOwnableTwoStep, IOwnableTwoStepDispatcher, IOwnableTwoStepDispatcherTrait
+};
+
+//
+// General ownable contract tests
+//
+
+fn should_implement_ownable(contract_addr: ContractAddress, owner: ContractAddress) {
+ let contract = IOwnableTwoStepDispatcher { contract_address: contract_addr };
+ let acc2: ContractAddress = contract_address_const::<2222>();
+
+ // check owner is set correctly
+ assert(owner == contract.owner(), 'owner does not match');
+
+ // transfer ownership - check owner unchanged and proposed owner set correctly
+ set_contract_address(owner); // required to call contract as owner
+ contract.transfer_ownership(acc2);
+ assert(owner == contract.owner(), 'owner should remain unchanged');
+ assert(acc2 == contract.pending_owner(), 'acc2 should be proposed owner');
+
+ // accept ownership - check owner changed and proposed owner set to zero
+ set_contract_address(acc2); // required to call function as acc2
+ contract.accept_ownership();
+ assert(contract.owner() == acc2, 'failed to change ownership');
+ assert(contract.pending_owner().is_zero(), 'proposed owner should be zero');
+
+ // renounce ownership
+ contract.renounce_ownership();
+ assert(contract.owner().is_zero(), 'owner not 0 after renounce');
+}
+
diff --git a/contracts/src/tests/test_sequencer_uptime_feed.cairo b/contracts/src/tests/test_sequencer_uptime_feed.cairo
new file mode 100644
index 000000000..51dabbd25
--- /dev/null
+++ b/contracts/src/tests/test_sequencer_uptime_feed.cairo
@@ -0,0 +1,129 @@
+use starknet::ContractAddress;
+use starknet::EthAddress;
+use starknet::contract_address_const;
+use starknet::class_hash::class_hash_const;
+use starknet::class_hash::Felt252TryIntoClassHash;
+use starknet::syscalls::deploy_syscall;
+use starknet::testing::set_caller_address;
+use starknet::testing::set_contract_address;
+
+use array::ArrayTrait;
+use traits::Into;
+use traits::TryInto;
+use option::OptionTrait;
+use core::result::ResultTrait;
+
+use chainlink::emergency::sequencer_uptime_feed::SequencerUptimeFeed;
+use chainlink::libraries::access_control::{
+ IAccessController, IAccessControllerDispatcher, IAccessControllerDispatcherTrait
+};
+use chainlink::ocr2::aggregator_proxy::{
+ IAggregatorProxy, IAggregatorProxyDispatcher, IAggregatorProxyDispatcherTrait
+};
+use chainlink::ocr2::aggregator_proxy::AggregatorProxy;
+use chainlink::ocr2::aggregator_proxy::AggregatorProxy::AggregatorProxyImpl;
+use chainlink::tests::test_ownable::should_implement_ownable;
+use chainlink::tests::test_access_controller::should_implement_access_control;
+
+use chainlink::emergency::sequencer_uptime_feed::{
+ ISequencerUptimeFeed, ISequencerUptimeFeedDispatcher, ISequencerUptimeFeedDispatcherTrait
+};
+
+fn PROXY() -> AggregatorProxy::ContractState {
+ AggregatorProxy::contract_state_for_testing()
+}
+
+fn STATE() -> SequencerUptimeFeed::ContractState {
+ SequencerUptimeFeed::contract_state_for_testing()
+}
+
+fn setup() -> (ContractAddress, ContractAddress, ISequencerUptimeFeedDispatcher) {
+ let account: ContractAddress = contract_address_const::<777>();
+ set_caller_address(account);
+
+ // Deploy seqeuencer uptime feed
+ let calldata = array![0, // initial status
+ account.into() // owner
+ ];
+ let (sequencerFeedAddr, _) = deploy_syscall(
+ SequencerUptimeFeed::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+ let sequencerUptimeFeed = ISequencerUptimeFeedDispatcher {
+ contract_address: sequencerFeedAddr
+ };
+
+ (account, sequencerFeedAddr, sequencerUptimeFeed)
+}
+
+#[test]
+fn test_ownable() {
+ let (account, sequencerFeedAddr, _) = setup();
+ should_implement_ownable(sequencerFeedAddr, account);
+}
+
+#[test]
+fn test_access_control() {
+ let (account, sequencerFeedAddr, _) = setup();
+ should_implement_access_control(sequencerFeedAddr, account);
+}
+
+#[test]
+#[should_panic()]
+fn test_set_l1_sender_not_owner() {
+ let (_, _, sequencerUptimeFeed) = setup();
+ sequencerUptimeFeed.set_l1_sender(EthAddress { address: 789 });
+}
+
+#[test]
+fn test_set_l1_sender() {
+ let (owner, _, sequencerUptimeFeed) = setup();
+ set_contract_address(owner);
+ sequencerUptimeFeed.set_l1_sender(EthAddress { address: 789 });
+ assert(sequencerUptimeFeed.l1_sender().address == 789, 'l1_sender should be set to 789');
+}
+
+#[test]
+#[should_panic(expected: ('user does not have read access',))]
+fn test_latest_round_data_no_access() {
+ let (owner, sequencerFeedAddr, _) = setup();
+ let mut proxy = PROXY();
+ AggregatorProxy::constructor(ref proxy, owner, sequencerFeedAddr);
+ AggregatorProxyImpl::latest_round_data(@proxy);
+}
+
+#[test]
+#[should_panic(expected: ('user does not have read access',))]
+fn test_latest_answer_no_access() {
+ let (owner, sequencerFeedAddr, _) = setup();
+ let mut proxy = PROXY();
+ AggregatorProxy::constructor(ref proxy, owner, sequencerFeedAddr);
+ AggregatorProxyImpl::latest_answer(@proxy);
+}
+
+#[test]
+fn test_aggregator_proxy_response() {
+ let (owner, sequencerFeedAddr, _) = setup();
+
+ set_contract_address(owner);
+ let contract = IAccessControllerDispatcher { contract_address: sequencerFeedAddr };
+ contract.add_access(owner);
+
+ let proxy = IAggregatorProxyDispatcher { contract_address: sequencerFeedAddr };
+
+ // latest round data
+ let latest_round_data = proxy.latest_round_data();
+ assert(latest_round_data.answer == 0, 'latest_round_data should be 0');
+
+ // latest answer
+ let latest_answer = proxy.latest_answer();
+ assert(latest_answer == 0, 'latest_answer should be 0');
+
+ // description
+ let description = proxy.description();
+ assert(description == 'L2 Sequencer Uptime Status Feed', 'description does not match');
+
+ // decimals
+ let decimals = proxy.decimals();
+ assert(decimals == 0, 'decimals should be 0');
+}
diff --git a/contracts/src/tests/test_upgradeable.cairo b/contracts/src/tests/test_upgradeable.cairo
new file mode 100644
index 000000000..4d4213573
--- /dev/null
+++ b/contracts/src/tests/test_upgradeable.cairo
@@ -0,0 +1,51 @@
+use traits::Into;
+
+use starknet::testing::set_caller_address;
+use starknet::ContractAddress;
+use starknet::contract_address_const;
+use starknet::class_hash::class_hash_const;
+use starknet::syscalls::deploy_syscall;
+
+use chainlink::libraries::upgradeable::Upgradeable;
+use chainlink::libraries::mocks::mock_upgradeable::{
+ MockUpgradeable, IMockUpgradeableDispatcher, IMockUpgradeableDispatcherTrait,
+ IMockUpgradeableDispatcherImpl
+};
+use chainlink::libraries::mocks::mock_non_upgradeable::{
+ MockNonUpgradeable, IMockNonUpgradeableDispatcher, IMockNonUpgradeableDispatcherTrait,
+ IMockNonUpgradeableDispatcherImpl
+};
+
+fn setup() -> ContractAddress {
+ let account: ContractAddress = contract_address_const::<777>();
+ set_caller_address(account);
+ account
+}
+
+#[test]
+fn test_upgrade_and_call() {
+ let _ = setup();
+
+ let calldata = array![];
+ let (contractAddr, _) = deploy_syscall(
+ MockUpgradeable::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false
+ )
+ .unwrap();
+ let mockUpgradeable = IMockUpgradeableDispatcher { contract_address: contractAddr };
+ assert(mockUpgradeable.foo() == true, 'should call foo');
+
+ mockUpgradeable.upgrade(MockNonUpgradeable::TEST_CLASS_HASH.try_into().unwrap());
+
+ // now, contract should be different
+ let mockNonUpgradeable = IMockNonUpgradeableDispatcher { contract_address: contractAddr };
+ assert(mockNonUpgradeable.bar() == true, 'should call bar');
+}
+
+
+#[test]
+#[should_panic(expected: ('Class hash cannot be zero',))]
+fn test_upgrade_zero_hash() {
+ let _ = setup();
+
+ Upgradeable::upgrade(class_hash_const::<0>());
+}
diff --git a/contracts/src/token.cairo b/contracts/src/token.cairo
new file mode 100644
index 000000000..1c56c11b7
--- /dev/null
+++ b/contracts/src/token.cairo
@@ -0,0 +1,2 @@
+mod link_token;
+mod mock;
diff --git a/contracts/src/token/link_token.cairo b/contracts/src/token/link_token.cairo
new file mode 100644
index 000000000..24bdb2271
--- /dev/null
+++ b/contracts/src/token/link_token.cairo
@@ -0,0 +1,131 @@
+use starknet::ContractAddress;
+
+// https://github.com/starknet-io/starkgate-contracts/blob/v2.0/src/cairo/mintable_token_interface.cairo
+#[starknet::interface]
+trait IMintableToken {
+ fn permissioned_mint(ref self: TContractState, account: ContractAddress, amount: u256);
+ fn permissioned_burn(ref self: TContractState, account: ContractAddress, amount: u256);
+}
+
+#[starknet::contract]
+mod LinkToken {
+ use starknet::ContractAddress;
+ use starknet::class_hash::ClassHash;
+
+ use zeroable::Zeroable;
+
+ use openzeppelin::token::erc20::ERC20Component;
+ use openzeppelin::access::ownable::OwnableComponent;
+
+ use super::IMintableToken;
+ use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait};
+ use chainlink::libraries::token::erc677::ERC677Component;
+ use chainlink::libraries::type_and_version::ITypeAndVersion;
+ use chainlink::libraries::upgradeable::{Upgradeable, IUpgradeable};
+
+ component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
+ component!(path: ERC20Component, storage: erc20, event: ERC20Event);
+ component!(path: ERC677Component, storage: erc677, event: ERC677Event);
+
+ #[abi(embed_v0)]
+ impl OwnableImpl = OwnableComponent::OwnableTwoStepImpl;
+ impl InternalImpl = OwnableComponent::InternalImpl;
+
+ #[abi(embed_v0)]
+ impl ERC20Impl = ERC20Component::ERC20Impl;
+ #[abi(embed_v0)]
+ impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl;
+ impl ERC20InternalImpl = ERC20Component::InternalImpl;
+
+ #[abi(embed_v0)]
+ impl ERC677Impl = ERC677Component::ERC677Impl;
+
+ #[storage]
+ struct Storage {
+ #[substorage(v0)]
+ ownable: OwnableComponent::Storage,
+ _minter: ContractAddress,
+ #[substorage(v0)]
+ erc20: ERC20Component::Storage,
+ #[substorage(v0)]
+ erc677: ERC677Component::Storage
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ #[flat]
+ OwnableEvent: OwnableComponent::Event,
+ #[flat]
+ ERC20Event: ERC20Component::Event,
+ #[flat]
+ ERC677Event: ERC677Component::Event
+ }
+
+ //
+ // IMintableToken (StarkGate)
+ //
+ #[abi(embed_v0)]
+ impl MintableToken of IMintableToken {
+ fn permissioned_mint(ref self: ContractState, account: ContractAddress, amount: u256) {
+ only_minter(@self);
+ self.erc20._mint(account, amount);
+ }
+
+ fn permissioned_burn(ref self: ContractState, account: ContractAddress, amount: u256) {
+ only_minter(@self);
+ self.erc20._burn(account, amount);
+ }
+ }
+
+
+ #[constructor]
+ fn constructor(ref self: ContractState, minter: ContractAddress, owner: ContractAddress) {
+ let name = "ChainLink Token";
+ let symbol = "LINK";
+ self.erc20.initializer(name, symbol);
+ assert(!minter.is_zero(), 'minter is 0');
+ self._minter.write(minter);
+ self.ownable.initializer(owner);
+ }
+
+ // TODO #[view]
+ fn minter(self: @ContractState) -> ContractAddress {
+ self._minter.read()
+ }
+
+ #[abi(embed_v0)]
+ impl TypeAndVersionImpl of ITypeAndVersion {
+ fn type_and_version(self: @ContractState) -> felt252 {
+ 'LinkToken 1.0.0'
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl UpgradeableImpl of IUpgradeable {
+ fn upgrade(ref self: ContractState, new_impl: ClassHash) {
+ self.ownable.assert_only_owner();
+ Upgradeable::upgrade(new_impl)
+ }
+ }
+
+ // fn increase_allowance(ref self: ContractState, spender: ContractAddress, added_value: u256) -> bool {
+ // let mut state = ERC20::unsafe_new_contract_state();
+ // ERC20::ERC20Impl::increase_allowance(ref state, spender, added_value)
+ // }
+
+ // fn decrease_allowance(ref self: ContractState, spender: ContractAddress, subtracted_value: u256) -> bool {
+ // let mut state = ERC20::unsafe_new_contract_state();
+ // ERC20::ERC20Impl::decrease_allowance(ref state, spender, subtracted_value)
+ // }
+
+ //
+ // Internal
+ //
+
+ fn only_minter(self: @ContractState) {
+ let caller = starknet::get_caller_address();
+ let minter = self._minter.read();
+ assert(caller == minter, 'only minter');
+ }
+}
diff --git a/contracts/src/token/mock.cairo b/contracts/src/token/mock.cairo
new file mode 100644
index 000000000..dd1f80450
--- /dev/null
+++ b/contracts/src/token/mock.cairo
@@ -0,0 +1,2 @@
+mod valid_erc667_receiver;
+mod invalid_erc667_receiver;
diff --git a/contracts/src/token/mock/invalid_erc667_receiver.cairo b/contracts/src/token/mock/invalid_erc667_receiver.cairo
new file mode 100644
index 000000000..9332dc66f
--- /dev/null
+++ b/contracts/src/token/mock/invalid_erc667_receiver.cairo
@@ -0,0 +1,25 @@
+#[starknet::contract]
+mod InvalidReceiver {
+ #[storage]
+ struct Storage {
+ _supports: bool
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState) {}
+
+ #[abi(per_item)]
+ #[generate_trait]
+ impl HelperImpl of HelperTrait {
+ // toggle whether or not receiver says it supports the interface id
+ #[external(v0)]
+ fn set_supports(ref self: ContractState, support: bool) {
+ self._supports.write(support);
+ }
+
+ #[external(v0)]
+ fn supports_interface(self: @ContractState, interface_id: u32) -> bool {
+ self._supports.read()
+ }
+ }
+}
diff --git a/contracts/src/token/mock/valid_erc667_receiver.cairo b/contracts/src/token/mock/valid_erc667_receiver.cairo
new file mode 100644
index 000000000..cd08f7da6
--- /dev/null
+++ b/contracts/src/token/mock/valid_erc667_receiver.cairo
@@ -0,0 +1,41 @@
+use starknet::ContractAddress;
+#[starknet::interface]
+trait MockValidReceiver {
+ fn verify(self: @TContractState) -> ContractAddress;
+}
+
+#[starknet::contract]
+mod ValidReceiver {
+ use starknet::ContractAddress;
+ use array::ArrayTrait;
+ use chainlink::libraries::token::erc677::IERC677Receiver;
+
+ #[storage]
+ struct Storage {
+ _sender: ContractAddress,
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState) {}
+
+
+ #[abi(embed_v0)]
+ impl ERC677Receiver of IERC677Receiver {
+ fn on_token_transfer(
+ ref self: ContractState, sender: ContractAddress, value: u256, data: Array
+ ) {
+ self._sender.write(sender);
+ }
+
+ fn supports_interface(ref self: ContractState, interface_id: u32) -> bool {
+ true
+ }
+ }
+
+ #[abi(embed_v0)]
+ impl ValidReceiver of super::MockValidReceiver {
+ fn verify(self: @ContractState) -> ContractAddress {
+ self._sender.read()
+ }
+ }
+}
diff --git a/contracts/src/utils.cairo b/contracts/src/utils.cairo
new file mode 100644
index 000000000..13db189c9
--- /dev/null
+++ b/contracts/src/utils.cairo
@@ -0,0 +1,10 @@
+use integer::U128IntoFelt252;
+use integer::u128s_from_felt252;
+use integer::U128sFromFelt252Result;
+fn split_felt(felt: felt252) -> (u128, u128) {
+ match u128s_from_felt252(felt) {
+ U128sFromFelt252Result::Narrow(low) => (0_u128, low),
+ U128sFromFelt252Result::Wide((high, low)) => (high, low),
+ }
+}
+
diff --git a/contracts/test/account.ts b/contracts/test/account.ts
new file mode 100644
index 000000000..c5b240ed7
--- /dev/null
+++ b/contracts/test/account.ts
@@ -0,0 +1,100 @@
+import { Account, RpcProvider, ec, uint256, constants } from 'starknet'
+
+export const ERC20_ADDRESS = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7'
+
+export const DEVNET_URL = 'http://127.0.0.1:5050'
+const DEVNET_NAME = 'devnet'
+// This function loads options from the environment.
+// It returns options for Devnet as default when nothing is configured in the environment.
+export const makeFunderOptsFromEnv = () => {
+ const network = process.env.NETWORK || DEVNET_NAME
+ const gateway = process.env.NODE_URL || DEVNET_URL
+ const accountAddr = process.env.ACCOUNT?.toLowerCase()
+ const keyPair = ec.starkCurve.utils.randomPrivateKey()
+
+ return { network, gateway, accountAddr, keyPair }
+}
+
+interface FundAccounts {
+ account: string
+ amount: number
+}
+
+interface FunderOptions {
+ network?: string
+ gateway?: string
+ accountAddr?: string
+ keyPair: Uint8Array
+}
+
+// Define the Strategy to use depending on the network.
+export class Funder {
+ private opts: FunderOptions
+ private strategy: IFundingStrategy
+
+ constructor(opts: FunderOptions) {
+ this.opts = opts
+ if (this.opts.network === DEVNET_NAME) {
+ this.strategy = new DevnetFundingStrategy()
+ return
+ }
+ this.strategy = new AllowanceFundingStrategy()
+ }
+
+ // This function adds some funds to pre-deployed account that we are using in our test.
+ public async fund(accounts: FundAccounts[]) {
+ await this.strategy.fund(accounts, this.opts)
+ }
+}
+
+interface IFundingStrategy {
+ fund(accounts: FundAccounts[], opts: FunderOptions): Promise
+}
+
+// Fund the Account on Devnet
+class DevnetFundingStrategy implements IFundingStrategy {
+ public async fund(accounts: FundAccounts[], opts: FunderOptions) {
+ accounts.forEach(async (account) => {
+ const body = {
+ address: account.account,
+ amount: account.amount,
+ lite: true,
+ }
+ await fetch(`${opts.gateway}/mint`, {
+ method: 'post',
+ body: JSON.stringify(body),
+ headers: { 'Content-Type': 'application/json' },
+ })
+ })
+ }
+}
+
+// Fund the Account on Testnet
+class AllowanceFundingStrategy implements IFundingStrategy {
+ public async fund(accounts: FundAccounts[], opts: Required) {
+ const provider = new RpcProvider({
+ nodeUrl: constants.NetworkName.SN_SEPOLIA,
+ })
+
+ const operator = new Account(provider, opts.accountAddr, opts.keyPair)
+
+ for (const account of accounts) {
+ const data = [
+ account.account,
+ uint256.bnToUint256(account.amount).low.toString(),
+ uint256.bnToUint256(account.amount).high.toString(),
+ ]
+ const nonce = await operator.getNonce()
+ const hash = await operator.execute(
+ {
+ contractAddress: ERC20_ADDRESS,
+ entrypoint: 'transfer',
+ calldata: data,
+ },
+ undefined,
+ { nonce },
+ )
+ await provider.waitForTransaction(hash.transaction_hash)
+ }
+ }
+}
diff --git a/contracts/test/constants.ts b/contracts/test/constants.ts
new file mode 100644
index 000000000..e7ff055c2
--- /dev/null
+++ b/contracts/test/constants.ts
@@ -0,0 +1,5 @@
+/** 15 min */
+export const TIMEOUT = 900_000
+
+export const STARKNET_DEVNET_URL = 'http://127.0.0.1:5050'
+export const ETH_DEVNET_URL = 'http://127.0.0.1:8545'
diff --git a/contracts/test/emergency/StarknetValidator.test.ts b/contracts/test/emergency/StarknetValidator.test.ts
new file mode 100644
index 000000000..0c824d875
--- /dev/null
+++ b/contracts/test/emergency/StarknetValidator.test.ts
@@ -0,0 +1,728 @@
+import { abi as starknetMessagingAbi } from '../../artifacts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessaging.sol/IStarknetMessaging.json'
+import { abi as accessControllerAbi } from '../../artifacts/@chainlink/contracts/src/v0.8/interfaces/AccessControllerInterface.sol/AccessControllerInterface.json'
+import { abi as aggregatorAbi } from '../../artifacts/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol/AggregatorV3Interface.json'
+import { fetchStarknetAccount, getStarknetContractArtifacts, waitForTransactions } from '../utils'
+import { Contract as StarknetContract, RpcProvider, CallData, Account, hash } from 'starknet'
+import { deployMockContract, MockContract } from '@ethereum-waffle/mock-contract'
+import { BigNumber, Contract as EthersContract, ContractFactory } from 'ethers'
+import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
+import * as l1l2messaging from '../l1-l2-messaging'
+import { STARKNET_DEVNET_URL } from '../constants'
+import * as account from '../account'
+import { ethers } from 'hardhat'
+import { expect } from 'chai'
+
+describe('StarknetValidator', () => {
+ const provider = new RpcProvider({ nodeUrl: STARKNET_DEVNET_URL })
+ const opts = account.makeFunderOptsFromEnv()
+ const funder = new account.Funder(opts)
+
+ let defaultAccount: Account
+ let deployer: SignerWithAddress
+ let eoaValidator: SignerWithAddress
+ let alice: SignerWithAddress
+
+ let starknetValidatorFactory: ContractFactory
+ let starknetValidator: EthersContract
+ let mockStarknetMessagingFactory: ContractFactory
+ let mockStarknetMessaging: EthersContract
+ let mockGasPriceFeed: MockContract
+ let mockAccessController: MockContract
+ let mockAggregator: MockContract
+
+ let l2Contract: StarknetContract
+
+ before(async () => {
+ // Setup L2 account
+ defaultAccount = await fetchStarknetAccount()
+ await funder.fund([{ account: defaultAccount.address, amount: 1e21 }])
+
+ // Deploy L2 feed contract
+ const ddL2Contract = await defaultAccount.declareAndDeploy({
+ ...getStarknetContractArtifacts('SequencerUptimeFeed'),
+ constructorCalldata: CallData.compile({
+ initial_status: 0,
+ owner_address: defaultAccount.address,
+ }),
+ })
+
+ // Creates a starknet contract instance for the l2 feed
+ const { abi: l2FeedAbi } = await provider.getClassByHash(ddL2Contract.declare.class_hash)
+ l2Contract = new StarknetContract(l2FeedAbi, ddL2Contract.deploy.address, provider)
+
+ // Fetch predefined L1 EOA accounts
+ const accounts = await ethers.getSigners()
+ deployer = accounts[0]
+ eoaValidator = accounts[1]
+ alice = accounts[2]
+
+ // Deploy the mock feed
+ mockGasPriceFeed = await deployMockContract(deployer, aggregatorAbi)
+ await mockGasPriceFeed.mock.latestRoundData.returns(
+ '73786976294838220258' /** roundId */,
+ '96800000000' /** answer */,
+ '163826896' /** startedAt */,
+ '1638268960' /** updatedAt */,
+ '73786976294838220258' /** answeredInRound */,
+ )
+
+ // Deploy the mock access controller
+ mockAccessController = await deployMockContract(deployer, accessControllerAbi)
+
+ // Deploy the mock aggregator
+ mockAggregator = await deployMockContract(deployer, aggregatorAbi)
+ await mockAggregator.mock.latestRoundData.returns(
+ '73786976294838220258' /** roundId */,
+ 1 /** answer */,
+ '163826896' /** startedAt */,
+ '1638268960' /** updatedAt */,
+ '73786976294838220258' /** answeredInRound */,
+ )
+ })
+
+ beforeEach(async () => {
+ // Deploy the MockStarknetMessaging contract used to simulate L1 - L2 comms
+ mockStarknetMessagingFactory = await ethers.getContractFactory(
+ 'MockStarknetMessaging',
+ deployer,
+ )
+ const messageCancellationDelay = 5 * 60 // seconds
+ mockStarknetMessaging = await mockStarknetMessagingFactory.deploy(messageCancellationDelay)
+ await mockStarknetMessaging.deployed()
+
+ // Deploy the L1 StarknetValidator
+ starknetValidatorFactory = await ethers.getContractFactory('StarknetValidator', deployer)
+ starknetValidator = await starknetValidatorFactory.deploy(
+ mockStarknetMessaging.address,
+ mockAccessController.address,
+ mockGasPriceFeed.address,
+ mockAggregator.address,
+ l2Contract.address,
+ 0,
+ 0,
+ )
+
+ // Point the L2 feed contract to receive from the L1 StarknetValidator contract
+ await defaultAccount.execute(
+ l2Contract.populate('set_l1_sender', {
+ address: starknetValidator.address,
+ }),
+ )
+ })
+
+ describe('#constructor', () => {
+ it('reverts when the StarknetMessaging address is zero', async () => {
+ await expect(
+ starknetValidatorFactory.deploy(
+ ethers.constants.AddressZero,
+ mockAccessController.address,
+ mockGasPriceFeed.address,
+ mockAggregator.address,
+ l2Contract.address,
+ 0,
+ 0,
+ ),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidStarknetMessagingAddress')
+ })
+
+ it('reverts when the L2 feed is zero', async () => {
+ await expect(
+ starknetValidatorFactory.deploy(
+ mockStarknetMessaging.address,
+ mockAccessController.address,
+ mockGasPriceFeed.address,
+ mockAggregator.address,
+ 0,
+ 0,
+ 0,
+ ),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidL2FeedAddress')
+ })
+
+ it('reverts when the Aggregator address is zero', async () => {
+ await expect(
+ starknetValidatorFactory.deploy(
+ mockStarknetMessaging.address,
+ mockAccessController.address,
+ mockGasPriceFeed.address,
+ ethers.constants.AddressZero,
+ l2Contract.address,
+ 0,
+ 0,
+ ),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidSourceAggregatorAddress')
+ })
+
+ it('reverts when the Access Controller address is zero', async () => {
+ await expect(
+ starknetValidatorFactory.deploy(
+ mockStarknetMessaging.address,
+ ethers.constants.AddressZero,
+ mockGasPriceFeed.address,
+ mockAggregator.address,
+ l2Contract.address,
+ 0,
+ 0,
+ ),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidAccessControllerAddress')
+ })
+
+ it('reverts when the L1 Gas Price feed address is zero', async () => {
+ await expect(
+ starknetValidatorFactory.deploy(
+ mockStarknetMessaging.address,
+ mockAccessController.address,
+ ethers.constants.AddressZero,
+ mockAggregator.address,
+ l2Contract.address,
+ 0,
+ 0,
+ ),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidGasPriceL1FeedAddress')
+ })
+
+ it('is initialized with the correct gas config', async () => {
+ const gasConfig = await starknetValidator.getGasConfig()
+ expect(gasConfig.gasEstimate).to.equal(0) // Initialized with 0 in before function
+ expect(gasConfig.gasPriceL1Feed).to.hexEqual(mockGasPriceFeed.address)
+ expect(gasConfig.gasAdjustment).to.equal(0)
+ })
+
+ it('is initialized with the correct access controller address', async () => {
+ const acAddr = await starknetValidator.getConfigAC()
+ expect(acAddr).to.hexEqual(mockAccessController.address)
+ })
+
+ it('is initialized with the correct source aggregator address', async () => {
+ const aggregatorAddr = await starknetValidator.getSourceAggregator()
+ expect(aggregatorAddr).to.hexEqual(mockAggregator.address)
+ })
+
+ it('should get the selector from the name successfully', async () => {
+ const actual = hash.getSelectorFromName('update_status')
+ const expected = 1585322027166395525705364165097050997465692350398750944680096081848180365267n
+ expect(BigInt(actual)).to.equal(expected)
+
+ const computedActual = await starknetValidator.SELECTOR_STARK_UPDATE_STATUS()
+ expect(computedActual).to.equal(expected)
+ })
+ })
+
+ describe('#retry', () => {
+ describe('when called by account with no access', () => {
+ it('reverts', async () => {
+ await expect(starknetValidator.connect(alice).retry()).to.be.revertedWith('No access')
+ })
+ })
+
+ describe('when called by account with access', () => {
+ it('transaction succeeds', async () => {
+ const waffleMockStarknetMessaging = await deployMockContract(deployer, starknetMessagingAbi)
+ await waffleMockStarknetMessaging.mock.sendMessageToL2.returns(
+ ethers.utils.formatBytes32String('0'),
+ 0,
+ )
+ await mockAggregator.mock.latestRoundData.returns(
+ '0' /** roundId */,
+ 1 /** answer */,
+ '0' /** startedAt */,
+ '0' /** updatedAt */,
+ '0' /** answeredInRound */,
+ )
+ await mockGasPriceFeed.mock.latestRoundData.returns(
+ '0' /** roundId */,
+ 1 /** answer */,
+ '0' /** startedAt */,
+ '0' /** updatedAt */,
+ '0' /** answeredInRound */,
+ )
+
+ const starknetValidator = await starknetValidatorFactory.deploy(
+ waffleMockStarknetMessaging.address,
+ mockAccessController.address,
+ mockGasPriceFeed.address,
+ mockAggregator.address,
+ l2Contract.address,
+ 0,
+ 0,
+ )
+
+ await starknetValidator.addAccess(deployer.address)
+
+ await starknetValidator.retry()
+ })
+ })
+ })
+
+ describe('#setConfigAC', () => {
+ describe('when called by non owner', () => {
+ it('reverts', async () => {
+ const newACAddr = '0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4'
+ await expect(starknetValidator.connect(alice).setConfigAC(newACAddr)).to.be.revertedWith(
+ 'Only callable by owner',
+ )
+ })
+ })
+
+ describe('when called by owner', () => {
+ it('emits an event', async () => {
+ const newACAddr = '0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4'
+ await expect(starknetValidator.setConfigAC(newACAddr))
+ .to.emit(starknetValidator, 'ConfigACSet')
+ .withArgs(mockAccessController.address, newACAddr)
+ })
+
+ it('sets the access controller address', async () => {
+ const newACAddr = '0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4'
+ await starknetValidator.connect(deployer).setConfigAC(newACAddr)
+ expect(await starknetValidator.getConfigAC()).to.equal(newACAddr)
+ })
+
+ it('no-op if new address equals previous address', async () => {
+ const tx = await starknetValidator.setConfigAC(mockAccessController.address)
+ const receipt = await tx.wait()
+ expect(receipt.events).is.empty
+ expect(await starknetValidator.getConfigAC()).to.equal(mockAccessController.address)
+ })
+
+ it('reverts if address is zero', async () => {
+ await expect(
+ starknetValidator.connect(deployer).setConfigAC(ethers.constants.AddressZero),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidAccessControllerAddress')
+ })
+ })
+ })
+
+ describe('#setSourceAggregator', () => {
+ describe('when called by non owner', () => {
+ it('reverts', async () => {
+ await expect(
+ starknetValidator.connect(alice).setSourceAggregator(ethers.constants.AddressZero),
+ ).to.be.revertedWith('Only callable by owner')
+ })
+ })
+
+ describe('when source address is the zero address', () => {
+ it('reverts', async () => {
+ await expect(
+ starknetValidator.connect(deployer).setSourceAggregator(ethers.constants.AddressZero),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidSourceAggregatorAddress')
+ })
+ })
+
+ describe('when called by owner', () => {
+ it('emits an event', async () => {
+ const newSourceAddr = '0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4'
+ await expect(starknetValidator.setSourceAggregator(newSourceAddr))
+ .to.emit(starknetValidator, 'SourceAggregatorSet')
+ .withArgs(mockAggregator.address, newSourceAddr)
+ })
+
+ it('sets the source aggregator address', async () => {
+ expect(await starknetValidator.getSourceAggregator()).to.hexEqual(mockAggregator.address)
+ })
+
+ it('does nothing if new address equal to previous', async () => {
+ const tx = await starknetValidator
+ .connect(deployer)
+ .setSourceAggregator(mockAggregator.address)
+ const receipt = await tx.wait()
+ expect(receipt.events).to.be.empty
+
+ expect(await starknetValidator.getSourceAggregator()).to.hexEqual(mockAggregator.address)
+ })
+ })
+ })
+
+ describe('#setGasConfig', () => {
+ describe('when called by non owner without access', () => {
+ beforeEach(async () => {
+ await mockAccessController.mock.hasAccess.returns(false)
+ })
+
+ it('reverts', async () => {
+ await expect(
+ starknetValidator.connect(alice).setGasConfig(0, mockGasPriceFeed.address, 0),
+ ).to.be.revertedWithCustomError(starknetValidator, 'AccessForbidden')
+ })
+ })
+
+ describe('when called by owner', () => {
+ it('correctly sets the gas config', async () => {
+ const newGasEstimate = 25000
+ const newFeedAddr = '0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4'
+ // gasAdjustment of 110 equates to 1.1x
+ const newGasAdjustment = 110
+ await starknetValidator
+ .connect(deployer)
+ .setGasConfig(newGasEstimate, newFeedAddr, newGasAdjustment)
+ const gasConfig = await starknetValidator.getGasConfig()
+ expect(gasConfig.gasEstimate).to.equal(newGasEstimate)
+ expect(gasConfig.gasPriceL1Feed).to.hexEqual(newFeedAddr)
+ expect(gasConfig.gasAdjustment).to.equal(newGasAdjustment)
+ })
+
+ it('emits an event', async () => {
+ const newGasEstimate = 25000
+ const newFeedAddr = '0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4'
+ const newGasAdjustment = 110
+ await expect(
+ starknetValidator
+ .connect(deployer)
+ .setGasConfig(newGasEstimate, newFeedAddr, newGasAdjustment),
+ )
+ .to.emit(starknetValidator, 'GasConfigSet')
+ .withArgs(newGasEstimate, newFeedAddr, newGasAdjustment)
+ })
+
+ describe('when l1 gas price feed address is the zero address', () => {
+ it('reverts', async () => {
+ await expect(
+ starknetValidator
+ .connect(deployer)
+ .setGasConfig(25000, ethers.constants.AddressZero, 0),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidGasPriceL1FeedAddress')
+ })
+ })
+ })
+
+ describe('when called by an address with access', () => {
+ beforeEach(async () => {
+ await mockAccessController.mock.hasAccess.returns(true)
+ })
+
+ it('correctly sets the gas config', async () => {
+ const newGasEstimate = 25000
+ const newFeedAddr = '0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4'
+ const newGasAdjustment = 110
+ await starknetValidator
+ .connect(eoaValidator)
+ .setGasConfig(newGasEstimate, newFeedAddr, newGasAdjustment)
+ const gasConfig = await starknetValidator.getGasConfig()
+ expect(gasConfig.gasEstimate).to.equal(newGasEstimate)
+ expect(gasConfig.gasPriceL1Feed).to.hexEqual(newFeedAddr)
+ expect(gasConfig.gasAdjustment).to.equal(newGasAdjustment)
+ })
+
+ it('emits an event', async () => {
+ const newGasEstimate = 25000
+ const newFeedAddr = '0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4'
+ const newGasAdjustment = 110
+ await expect(
+ starknetValidator
+ .connect(eoaValidator)
+ .setGasConfig(newGasEstimate, newFeedAddr, newGasAdjustment),
+ )
+ .to.emit(starknetValidator, 'GasConfigSet')
+ .withArgs(newGasEstimate, newFeedAddr, newGasAdjustment)
+ })
+
+ describe('when l1 gas price feed address is the zero address', () => {
+ it('reverts', async () => {
+ await expect(
+ starknetValidator
+ .connect(eoaValidator)
+ .setGasConfig(25000, ethers.constants.AddressZero, 0),
+ ).to.be.revertedWithCustomError(starknetValidator, 'InvalidGasPriceL1FeedAddress')
+ })
+ })
+ })
+ })
+
+ describe('#approximateGasPrice', () => {
+ it('calculates gas price with scalar coefficient', async () => {
+ await mockGasPriceFeed.mock.latestRoundData.returns(
+ '0' /** roundId */,
+ 96800000000 /** answer */,
+ '0' /** startedAt */,
+ '0' /** updatedAt */,
+ '0' /** answeredInRound */,
+ )
+ // 96800000000 is the mocked value from gas feed
+ const expectedGasPrice = BigNumber.from(96800000000).mul(110).div(100)
+
+ await starknetValidator.connect(deployer).setGasConfig(0, mockGasPriceFeed.address, 110)
+
+ const gasPrice = await starknetValidator.connect(deployer).approximateGasPrice()
+
+ expect(gasPrice).to.equal(expectedGasPrice)
+ })
+ })
+
+ describe('#validate', () => {
+ beforeEach(async () => {
+ await expect(
+ deployer.sendTransaction({ to: starknetValidator.address, value: 100n }),
+ ).to.changeEtherBalance(starknetValidator, 100n)
+ })
+
+ it('reverts if `StarknetValidator.validate` called by account with no access', async () => {
+ const c = starknetValidator.connect(eoaValidator)
+ await expect(c.validate(0, 0, 1, 1)).to.be.revertedWith('No access')
+ })
+
+ it('should not revert if `sequencer_uptime_feed.latest_round_data` called by an Account with no explicit access (Accounts are allowed read access)', async () => {
+ const result = await l2Contract.latest_round_data()
+ expect(result['answer']).to.equal('0')
+ })
+
+ it('should deploy the messaging contract', async () => {
+ const { messaging_contract_address } = await l1l2messaging.loadL1MessagingContract({
+ address: mockStarknetMessaging.address,
+ })
+ expect(messaging_contract_address).not.to.be.undefined
+ })
+
+ it('should load the already deployed contract if the address is provided', async () => {
+ const { messaging_contract_address } = await l1l2messaging.loadL1MessagingContract({
+ address: mockStarknetMessaging.address,
+ })
+ expect(mockStarknetMessaging.address).to.hexEqual(messaging_contract_address)
+ })
+
+ it('should send a message to the L2 contract', async () => {
+ // Load the mock messaging contract
+ await l1l2messaging.loadL1MessagingContract({ address: mockStarknetMessaging.address })
+
+ // Return gas price of 1
+ await mockGasPriceFeed.mock.latestRoundData.returns(
+ '0' /** roundId */,
+ 1 /** answer */,
+ '0' /** startedAt */,
+ '0' /** updatedAt */,
+ '0' /** answeredInRound */,
+ )
+
+ // Simulate L1 transmit + validate
+ const newGasEstimate = 1
+ const receipts = await waitForTransactions([
+ // Add access
+ () => starknetValidator.addAccess(eoaValidator.address),
+
+ // By default the gas config is 0, we need to change it or we will submit a 0 fee
+ () =>
+ starknetValidator
+ .connect(deployer)
+ .setGasConfig(newGasEstimate, mockGasPriceFeed.address, 100),
+
+ // gasPrice (1) * newGasEstimate (1)
+ () => starknetValidator.connect(eoaValidator).validate(0, 0, 1, 1),
+ ])
+
+ // Simulate the L1 - L2 comms
+ const resp = await l1l2messaging.flush()
+ const msgFromL1 = resp.messages_to_l2
+ expect(msgFromL1).to.have.a.lengthOf(1)
+ expect(resp.messages_to_l1).to.be.empty
+
+ expect(msgFromL1.at(0)?.l1_contract_address).to.hexEqual(starknetValidator.address)
+ expect(msgFromL1.at(0)?.l2_contract_address).to.hexEqual(l2Contract.address)
+
+ // Assert L2 effects
+ const result = await l2Contract.latest_round_data()
+
+ // Logging (to help debug potential flaky test)
+ console.log(
+ JSON.stringify(
+ {
+ latestRoundData: result,
+ flushResponse: resp,
+ txReceipts: receipts,
+ },
+ (_, value) => (typeof value === 'bigint' ? value.toString() : value),
+ 2,
+ ),
+ )
+
+ expect(result['answer']).to.equal('1')
+ })
+
+ it('should always send a **boolean** message to L2 contract', async () => {
+ // Load the mock messaging contract
+ await l1l2messaging.loadL1MessagingContract({ address: mockStarknetMessaging.address })
+
+ // Return gas price of 1
+ await mockGasPriceFeed.mock.latestRoundData.returns(
+ '0' /** roundId */,
+ 1 /** answer */,
+ '0' /** startedAt */,
+ '0' /** updatedAt */,
+ '0' /** answeredInRound */,
+ )
+
+ // Simulate L1 transmit + validate
+ const newGasEstimate = 1
+ const receipts = await waitForTransactions([
+ // Add access
+ () => starknetValidator.connect(deployer).addAccess(eoaValidator.address),
+
+ // By default the gas config is 0, we need to change it or we will submit a 0 fee
+ () =>
+ starknetValidator
+ .connect(deployer)
+ .setGasConfig(newGasEstimate, mockGasPriceFeed.address, 100),
+
+ // Incorrect value
+ () => starknetValidator.connect(eoaValidator).validate(0, 0, 1, 127),
+ ])
+
+ // Simulate the L1 - L2 comms
+ const resp = await l1l2messaging.flush()
+ const msgFromL1 = resp.messages_to_l2
+ expect(msgFromL1).to.have.a.lengthOf(1)
+ expect(resp.messages_to_l1).to.be.empty
+
+ expect(msgFromL1[0].l1_contract_address).to.hexEqual(starknetValidator.address)
+ expect(msgFromL1[0].l2_contract_address).to.hexEqual(l2Contract.address)
+
+ // Assert L2 effects
+ const result = await l2Contract.latest_round_data()
+
+ // Logging (to help debug potential flaky test)
+ console.log(
+ JSON.stringify(
+ {
+ latestRoundData: result,
+ flushResponse: resp,
+ txReceipts: receipts,
+ },
+ (_, value) => (typeof value === 'bigint' ? value.toString() : value),
+ 2,
+ ),
+ )
+
+ expect(result['answer']).to.equal('0') // status unchanged - incorrect value treated as false
+ })
+
+ it('should send multiple messages', async () => {
+ // Load the mock messaging contract
+ await l1l2messaging.loadL1MessagingContract({ address: mockStarknetMessaging.address })
+
+ // Return gas price of 1
+ await mockGasPriceFeed.mock.latestRoundData.returns(
+ '0' /** roundId */,
+ 1 /** answer */,
+ '0' /** startedAt */,
+ '0' /** updatedAt */,
+ '0' /** answeredInRound */,
+ )
+
+ // Simulate L1 transmit + validate
+ const messages = new Array()
+ const newGasEstimate = 1
+ const receipts = await waitForTransactions(
+ [
+ // Add access
+ () => starknetValidator.connect(deployer).addAccess(eoaValidator.address),
+
+ // By default the gas config is 0, we need to change it or we will submit a 0 fee
+ () =>
+ starknetValidator
+ .connect(deployer)
+ .setGasConfig(newGasEstimate, mockGasPriceFeed.address, 100),
+
+ // Validate
+ () => starknetValidator.connect(eoaValidator).validate(0, 0, 1, 1),
+ () => starknetValidator.connect(eoaValidator).validate(0, 0, 1, 1),
+ () => starknetValidator.connect(eoaValidator).validate(0, 0, 1, 127), // incorrect value
+ () => starknetValidator.connect(eoaValidator).validate(0, 0, 1, 0), // final status
+ ],
+ async () => {
+ // Simulate the L1 - L2 comms
+ const resp = await l1l2messaging.flush()
+ if (resp.messages_to_l2.length !== 0) {
+ expect(resp.messages_to_l1).to.be.empty
+
+ const msgFromL1 = resp.messages_to_l2
+ expect(msgFromL1).to.have.a.lengthOf(1)
+ expect(msgFromL1[0].l1_contract_address).to.hexEqual(starknetValidator.address)
+ expect(msgFromL1[0].l2_contract_address).to.hexEqual(l2Contract.address)
+
+ messages.push(resp)
+ }
+ },
+ )
+
+ // Makes sure the correct number of messages were transmitted
+ expect(messages.length).to.eq(4)
+
+ // Assert L2 effects
+ const result = await l2Contract.latest_round_data()
+
+ // Logging (to help debug potential flaky test)
+ console.log(
+ JSON.stringify(
+ {
+ latestRoundData: result,
+ flushResponse: messages,
+ txReceipts: receipts,
+ },
+ (_, value) => (typeof value === 'bigint' ? value.toString() : value),
+ 2,
+ ),
+ )
+
+ expect(result['answer']).to.equal('0') // final status 0
+ })
+ })
+
+ describe('#withdrawFunds', () => {
+ beforeEach(async () => {
+ await expect(() =>
+ deployer.sendTransaction({ to: starknetValidator.address, value: 10 }),
+ ).to.changeEtherBalance(starknetValidator, 10n)
+ })
+
+ describe('when called by non owner', () => {
+ it('reverts', async () => {
+ await expect(starknetValidator.connect(alice).withdrawFunds()).to.be.revertedWith(
+ 'Only callable by owner',
+ )
+ })
+ })
+
+ describe('when called by owner', () => {
+ it('emits an event', async () => {
+ await expect(starknetValidator.connect(deployer).withdrawFunds())
+ .to.emit(starknetValidator, 'FundsWithdrawn')
+ .withArgs(deployer.address, 10)
+ })
+
+ it('withdraws all funds to deployer', async () => {
+ await starknetValidator.connect(deployer).withdrawFunds()
+ const balance = await ethers.provider.getBalance(starknetValidator.address)
+ expect(balance).to.equal(0n)
+ })
+ })
+ })
+
+ describe('#withdrawFundsTo', () => {
+ beforeEach(async () => {
+ await expect(() =>
+ deployer.sendTransaction({ to: starknetValidator.address, value: 10 }),
+ ).to.changeEtherBalance(starknetValidator, 10)
+ })
+
+ describe('when called by non owner', () => {
+ it('reverts', async () => {
+ await expect(
+ starknetValidator.connect(alice).withdrawFundsTo(alice.address),
+ ).to.be.revertedWith('Only callable by owner')
+ })
+ })
+
+ describe('when called by owner', () => {
+ it('emits an event', async () => {
+ await expect(starknetValidator.connect(deployer).withdrawFundsTo(eoaValidator.address))
+ .to.emit(starknetValidator, 'FundsWithdrawn')
+ .withArgs(eoaValidator.address, 10)
+ })
+
+ it('withdraws all funds to deployer', async () => {
+ await starknetValidator.connect(deployer).withdrawFunds()
+ const balance = await ethers.provider.getBalance(starknetValidator.address)
+ expect(balance).to.equal(0n)
+ })
+ })
+ })
+})
diff --git a/contracts/test/l1-l2-messaging.ts b/contracts/test/l1-l2-messaging.ts
new file mode 100644
index 000000000..b27b58520
--- /dev/null
+++ b/contracts/test/l1-l2-messaging.ts
@@ -0,0 +1,103 @@
+import { ETH_DEVNET_URL, STARKNET_DEVNET_URL } from './constants'
+
+//
+// Docs: https://github.com/0xSpaceShard/starknet-devnet-rs/blob/main/contracts/l1-l2-messaging/README.md#ethereum-setup
+//
+
+/*
+ * https://github.com/0xSpaceShard/starknet-devnet-rs/blob/7e5ff351198f799816c1857c1048bf8ee7f89428/crates/starknet-devnet-server/src/api/http/models.rs#L23
+ */
+export type PostmanLoadL1MessagingContract = Readonly<{
+ networkUrl?: string
+ address?: string
+}>
+
+/*
+ * https://github.com/0xSpaceShard/starknet-devnet-rs/blob/7e5ff351198f799816c1857c1048bf8ee7f89428/crates/starknet-devnet-server/src/api/http/models.rs#L132
+ */
+export type MessagingLoadAddress = Readonly<{
+ messaging_contract_address: string
+}>
+
+/*
+ * https://github.com/0xSpaceShard/starknet-devnet-rs/blob/7e5ff351198f799816c1857c1048bf8ee7f89428/crates/starknet-devnet-server/src/api/http/endpoints/postman.rs#L12
+ */
+export const loadL1MessagingContract = async (
+ params?: PostmanLoadL1MessagingContract,
+): Promise => {
+ const res = await fetch(`${STARKNET_DEVNET_URL}/postman/load_l1_messaging_contract`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ network_url: params?.networkUrl ?? ETH_DEVNET_URL,
+ address: params?.address,
+ }),
+ })
+
+ const result = await res.json()
+ if (result.error != null) {
+ throw new Error(result.error)
+ }
+ return result
+}
+
+/*
+ * https://github.com/0xSpaceShard/starknet-devnet-rs/blob/7e5ff351198f799816c1857c1048bf8ee7f89428/crates/starknet-devnet-server/src/api/http/models.rs#L127
+ */
+export type FlushParameters = Readonly<{
+ dryRun?: boolean
+}>
+
+/*
+ * https://github.com/0xSpaceShard/starknet-devnet-rs/blob/7e5ff351198f799816c1857c1048bf8ee7f89428/crates/starknet-devnet-types/src/rpc/messaging.rs#L52
+ */
+export type MessageToL1 = Readonly<{
+ from_address: string
+ to_address: string
+ payload: string[]
+}>
+
+/*
+ * https://github.com/0xSpaceShard/starknet-devnet-rs/blob/7e5ff351198f799816c1857c1048bf8ee7f89428/crates/starknet-devnet-types/src/rpc/messaging.rs#L14
+ */
+export type MessageToL2 = Readonly<{
+ l2_contract_address: string
+ entry_point_selector: string
+ l1_contract_address: string
+ payload: string
+ paid_fee_on_l1: string
+ nonce: string
+}>
+
+/*
+ * https://github.com/0xSpaceShard/starknet-devnet-rs/blob/7e5ff351198f799816c1857c1048bf8ee7f89428/crates/starknet-devnet-server/src/api/http/models.rs#L120
+ */
+export type FlushedMessages = Readonly<{
+ messages_to_l1: MessageToL1[]
+ messages_to_l2: MessageToL2[]
+ generated_l2_transactions: string[]
+ l1_provider: string
+}>
+
+/*
+ * https://github.com/0xSpaceShard/starknet-devnet-rs/blob/7e5ff351198f799816c1857c1048bf8ee7f89428/crates/starknet-devnet-server/src/api/http/endpoints/postman.rs#L26
+ */
+export const flush = async (params?: FlushParameters): Promise => {
+ const res = await fetch(`${STARKNET_DEVNET_URL}/postman/flush`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ dry_run: params?.dryRun ?? false,
+ }),
+ })
+
+ const result = await res.json()
+ if (result.error != null) {
+ throw new Error(result.error)
+ }
+ return result
+}
diff --git a/contracts/test/ocr2/aggregator.test.ts b/contracts/test/ocr2/aggregator.test.ts
new file mode 100644
index 000000000..a41508b52
--- /dev/null
+++ b/contracts/test/ocr2/aggregator.test.ts
@@ -0,0 +1,440 @@
+import { fetchStarknetAccount, getStarknetContractArtifacts } from '../utils'
+import { bytesToFelts } from '@chainlink/starknet-gauntlet'
+import { STARKNET_DEVNET_URL, TIMEOUT } from '../constants'
+import * as account from '../account'
+import { assert, expect } from 'chai'
+import {
+ BigNumberish,
+ ParsedStruct,
+ LibraryError,
+ RpcProvider,
+ Contract,
+ CallData,
+ Account,
+ Uint256,
+ cairo,
+ hash,
+ num,
+ ec,
+} from 'starknet'
+
+type Oracle = Readonly<{
+ // hex string
+ signer: string
+ transmitter: Account
+}>
+
+// Observers - max 31 oracles or 31 bytes
+const OBSERVERS_MAX = 31
+const OBSERVERS_HEX = '0x00010203000000000000000000000000000000000000000000000000000000'
+const UINT128_MAX = BigInt(2) ** BigInt(128) - BigInt(1)
+
+describe('Aggregator', function () {
+ this.timeout(TIMEOUT)
+ const provider = new RpcProvider({ nodeUrl: STARKNET_DEVNET_URL })
+ const opts = account.makeFunderOptsFromEnv()
+ const funder = new account.Funder(opts)
+
+ let aggregator: Contract
+ let token: Contract
+ let owner: Account
+
+ const maxAnswer = 1000000000
+ const minAnswer = 2
+ const f = 1
+ const n = 3 * f + 1
+ const oracles: Oracle[] = []
+ let config_digest: string
+
+ before(async () => {
+ // Sets up the owner account
+ owner = await fetchStarknetAccount()
+ await funder.fund([{ account: owner.address, amount: 1e21 }])
+ console.log('Owner account has been funded')
+
+ // Declares and deploys the LINK token contract
+ const ddToken = await owner.declareAndDeploy({
+ ...getStarknetContractArtifacts('LinkToken'),
+ constructorCalldata: CallData.compile({
+ minter: owner.address,
+ owner: owner.address,
+ }),
+ })
+ console.log(`Successfully deployed LinkToken: ${ddToken.deploy.address}`)
+
+ // Creates a starknet contract instance for token
+ const { abi: tokenAbi } = await provider.getClassByHash(ddToken.declare.class_hash)
+ token = new Contract(tokenAbi, ddToken.deploy.address, provider)
+
+ // Funds the owner account with some LINK
+ await owner.execute(
+ token.populate('permissioned_mint', {
+ account: owner.address,
+ amount: cairo.uint256(100_000_000_000n),
+ }),
+ )
+ console.log('Successfully funded owner account with LINK')
+
+ // Performs the following in parallel:
+ // Deploys the aggregator contract
+ // Populates the oracles array with devnet accounts
+ const [ddAggregator] = await Promise.all([
+ // Declares and deploys the aggregator
+ owner.declareAndDeploy({
+ ...getStarknetContractArtifacts('Aggregator'),
+ constructorCalldata: CallData.compile({
+ owner: owner.address,
+ link: token.address,
+ min_answer: minAnswer, // TODO: toFelt() to correctly wrap negative ints
+ max_answer: maxAnswer, // TODO: toFelt() to correctly wrap negative ints
+ billing_access_controller: 0, // TODO: billing AC
+ decimals: 8,
+ description: 0,
+ }),
+ }),
+
+ // Populates the oracles array with devnet accounts
+ ...Array.from({ length: n }).map(async (_, i) => {
+ // account index 0 is taken by the owner account, so we need to offset by 1
+ const transmitter = await fetchStarknetAccount({ accountIndex: i + 1 })
+ await funder.fund([{ account: transmitter.address, amount: 1e21 }])
+ oracles.push({
+ signer: '0x' + Buffer.from(ec.starkCurve.utils.randomPrivateKey()).toString('hex'),
+ transmitter,
+ // payee
+ })
+ }),
+ ])
+ console.log(`Successfully deployed Aggregator: ${ddAggregator.deploy.address}`)
+
+ // Creates a starknet contract instance for aggregator
+ const { abi: aggregatorAbi } = await provider.getClassByHash(ddAggregator.declare.class_hash)
+ aggregator = new Contract(aggregatorAbi, ddAggregator.deploy.address, provider)
+
+ // Defines the offchain config
+ const onchain_config = new Array()
+ const offchain_config = new Uint8Array([1])
+ const offchain_config_encoded = bytesToFelts(offchain_config)
+ const offchain_config_version = 2
+ const config = {
+ oracles: oracles.map((oracle) => {
+ return {
+ signer: ec.starkCurve.getStarkKey(oracle.signer),
+ transmitter: oracle.transmitter.address,
+ }
+ }),
+ f,
+ onchain_config,
+ offchain_config_version,
+ offchain_config: offchain_config_encoded,
+ }
+ console.log('Encoded offchain_config: %O', offchain_config_encoded)
+
+ // Sets the billing config
+ await owner.execute(
+ aggregator.populate('set_billing', {
+ config: {
+ observation_payment_gjuels: 1,
+ transmission_payment_gjuels: 1,
+ gas_base: 1,
+ gas_per_signature: 1,
+ },
+ }),
+ )
+
+ // Sets the OCR config
+ await owner.execute(aggregator.populate('set_config', config))
+ console.log('Config: %O', config)
+
+ // Gets the config details as bigints:
+ //
+ // result["0"] = config_count
+ // result["1"] = block_number
+ // result["2"] = config_digest
+ //
+ const result = await aggregator.latest_config_details()
+ const blockNumber = Number(result['1']) // we receive a bigint, but getBlock() assumes bigint = block hash, number = block number
+ const configDigest = result['2']
+ console.log(`Config digest: ${configDigest.toString(16)}`)
+ console.log(`Block number: ${blockNumber.toString(16)}`)
+ config_digest = configDigest
+
+ // Immitate the fetch done by relay to confirm latest_config_details_works
+ const block = await provider.getBlock(blockNumber)
+ const txHash = block.transactions.at(0)
+ if (txHash == null) {
+ assert.fail('unexpectedly found no transacitons')
+ }
+
+ // Gets the transaction receipt
+ const receipt = await provider.waitForTransaction(txHash)
+
+ // Checks that the receipt has events to decode
+ const events = receipt.events
+ const event = events.at(0)
+ if (event == null) {
+ assert.fail('unexpectedly received no events')
+ } else {
+ console.log("Log raw 'ConfigSet' event: %O", event)
+ }
+
+ // Decodes the events
+ const decodedEvents = aggregator.parseEvents(receipt)
+ const decodedEvent = decodedEvents.at(0)
+ if (decodedEvent == null) {
+ assert.fail('unexpectedly received no decoded events')
+ } else {
+ console.log("Log decoded 'ConfigSet' event: %O", decodedEvent)
+ }
+
+ // Double checks that the ConfigSet event exists in the decoded event payload
+ assert.isTrue(Object.prototype.hasOwnProperty.call(decodedEvent, 'ConfigSet'))
+ })
+
+ describe('OCR aggregator behavior', function () {
+ const transmit = async (epochAndRound: number, answer: num.BigNumberish) => {
+ // Defines helper variables
+ const observations = new Array()
+ const observersBuf = Buffer.alloc(31)
+ const observationTimestamp = 1
+ const juelsPerFeeCoin = 1
+ const extraHash = 1
+ const gasPrice = 1
+
+ // Updates the observer state
+ for (let i = 0; i < oracles.length; i++) {
+ observersBuf[i] = i
+ observations.push(answer)
+ }
+
+ // Converts observersBuf to a single value that will be decoded by toBN
+ const observers = `0x${observersBuf.toString('hex')}`
+ assert.equal(observers, OBSERVERS_HEX)
+
+ // Defines report data
+ const reportData = [
+ // report_context
+ config_digest,
+ epochAndRound,
+ extraHash,
+ // raw_report
+ observationTimestamp,
+ observers,
+ observations.length,
+ ...observations,
+ juelsPerFeeCoin,
+ gasPrice,
+ ]
+
+ // Hashes the report data
+ const reportDigest = hash.computeHashOnElements(reportData)
+ console.log('Report data: %O', reportData)
+ console.log(`Report digest: ${reportDigest}`)
+
+ // Generates report signatures
+ console.log('Report signatures - START')
+ const signatures = []
+ for (const { signer } of oracles.slice(0, f + 1)) {
+ const signature = ec.starkCurve.sign(reportDigest, signer)
+ const { r, s } = signature
+ const starkKey = ec.starkCurve.getStarkKey(signer)
+ const pubKey = '0x' + Buffer.from(ec.starkCurve.getPublicKey(signer)).toString('hex')
+ signatures.push({ r, s, public_key: starkKey })
+ console.log({
+ starkKey,
+ pubKey,
+ privKey: signer,
+ r,
+ s,
+ })
+ }
+ console.log('Report signatures - END\n')
+
+ // Gets the first transmitter
+ const transmitter = oracles.at(0)?.transmitter
+ if (transmitter == null) {
+ assert.fail('no oracles exist')
+ }
+
+ // Executes the transmit function on the aggregator contract
+ return await transmitter.execute(
+ aggregator.populate('transmit', {
+ report_context: {
+ config_digest,
+ epoch_and_round: epochAndRound,
+ extra_hash: extraHash,
+ },
+ observation_timestamp: observationTimestamp,
+ observers,
+ observations,
+ juels_per_fee_coin: juelsPerFeeCoin,
+ gas_price: gasPrice,
+ signatures,
+ }),
+ )
+ }
+
+ it("should emit 'NewTransmission' event on transmit", async () => {
+ // Calls the transmit function
+ const { transaction_hash } = await transmit(1, 99)
+ const receipt = await provider.getTransactionReceipt(transaction_hash)
+
+ // Double checks that some events were emitted
+ assert.isNotEmpty(receipt.events)
+ console.log("Log raw 'NewTransmission' event: %O", receipt.events[0])
+
+ // Decodes the events
+ const decodedEvents = aggregator.parseEvents(receipt)
+ const decodedEvent = decodedEvents.at(0)
+ if (decodedEvent == null) {
+ assert.fail('unexpectedly received no decoded events')
+ } else {
+ console.log("Log decoded 'NewTransmission' event: %O", decodedEvent)
+ }
+
+ // Validates the decoded event
+ const e = decodedEvent['NewTransmission']
+ assert.isTrue(Object.prototype.hasOwnProperty.call(decodedEvent, 'NewTransmission'))
+ assert.equal(e.round_id, 1n)
+ assert.equal(e.observation_timestamp, 1n)
+ assert.equal(e.epoch_and_round, 1n)
+ // assert.equal(e.data.reimbursement, 0n)
+
+ // NOTICE: Leading zeros are trimmed for an encoded felt (number).
+ // To decode, the raw felt needs to be start padded up to max felt size (252 bits or < 32 bytes).
+ const hexPadStart = (
+ data: BigNumberish | Uint256 | ParsedStruct | BigNumberish[],
+ len: number,
+ ) => {
+ return `0x${data.toString(16).padStart(len, '0')}`
+ }
+
+ // Validates the transmitter
+ const transmitterAddr = oracles[0].transmitter.address
+ const len = 32 * 2 // 32 bytes (hex)
+ expect(hexPadStart(e.transmitter, len)).to.hexEqual(transmitterAddr)
+
+ // Validates the observers and observations
+ const lenObservers = OBSERVERS_MAX * 2 // 31 bytes (hex)
+ assert.equal(hexPadStart(e.observers, lenObservers), OBSERVERS_HEX)
+ if (Array.isArray(e.observations)) {
+ assert.equal(e.observations.length, 4)
+ } else {
+ assert.fail(
+ `property 'observations' on NewTransmission event is not an array: ${JSON.stringify(
+ e,
+ null,
+ 2,
+ )}`,
+ )
+ }
+
+ // Validates the config digest
+ assert.equal(hexPadStart(e.config_digest, len), config_digest)
+ })
+
+ it('should transmit correctly', async () => {
+ await transmit(2, 99)
+
+ // Gets the latest round details as a map from string to bigint:
+ //
+ // result["round_id"] = 2n
+ // result["answer"] = 99n
+ // result["block_num"] = 8n
+ // result["started_at"] = 1n
+ // result["updated_at"] = 1710802726n
+ //
+ const round = await aggregator.latest_round_data()
+ assert.equal(round['round_id'], 2n)
+ assert.equal(round['answer'], 99n)
+
+ // await transmit(3, -10) // TODO: toFelt() to correctly wrap negative ints
+ // ;({ round } = await aggregator.call('latest_round_data'))
+ // assert.equal(round.round_id, 3)
+ // assert.equal(round.answer, -10)
+
+ try {
+ await transmit(4, 1)
+ expect.fail()
+ } catch (err) {
+ // Round should be unchanged
+ const newRound = await aggregator.latest_round_data()
+ assert.deepEqual(round, newRound)
+ }
+ })
+
+ it('should transmit with max u128 value correctly', async () => {
+ try {
+ await transmit(4, UINT128_MAX)
+ assert.fail('expected an error')
+ } catch (err) {
+ if (err instanceof LibraryError) {
+ expect(err.message).to.contain('median is out of min-max range')
+ } else {
+ assert.fail('expected a starknet LibraryError')
+ }
+ }
+ })
+
+ it('payments and withdrawals', async () => {
+ // set up payees
+ await owner.execute(
+ aggregator.populate('set_payees', {
+ payees: oracles.map((oracle) => ({
+ transmitter: oracle.transmitter.address,
+ payee: oracle.transmitter.address, // reusing transmitter acocunts as payees for simplicity
+ })),
+ }),
+ )
+
+ // Several rounds happened so we are owed payment
+ //
+ // The aggregator.owed_payment call returns a bigint
+ //
+ const payee = oracles[0].transmitter
+ const owed1 = await aggregator.owed_payment(payee.address)
+ assert.ok(owed1 > 0n)
+
+ const availableToValue = ([is_negative, abs_difference]: [boolean, bigint]): bigint => {
+ return is_negative ? -abs_difference : abs_difference
+ }
+
+ // no funds on contract, so no LINK available for payment
+ //
+ // The aggregator.link_available_for_payment call returns a map:
+ //
+ // result['0'] = is negative (e.g. true)
+ // result['1'] = absolute difference (e.g. 10000000006n)
+ //
+ let result = await aggregator.link_available_for_payment()
+ assert.ok(availableToValue([result['0'], result['1']]) < 0) // should be negative: we owe payments
+
+ // deposit LINK to contract
+ await owner.execute(
+ token.populate('transfer', {
+ recipient: aggregator.address,
+ amount: cairo.uint256(100_000_000_000n),
+ }),
+ )
+
+ // we have enough funds available now
+ result = await aggregator.link_available_for_payment()
+ assert.ok(availableToValue([result['0'], result['1']]) > 0)
+
+ // attempt to withdraw the payment
+ await payee.execute(
+ aggregator.populate('withdraw_payment', {
+ transmitter: payee.address,
+ }),
+ )
+
+ // balance as transferred to payee
+ const balance = await token.balance_of(payee.address)
+ assert.ok(owed1 === balance)
+
+ // owed payment is now zero
+ const owed2 = await aggregator.owed_payment(payee.address)
+ assert.ok(owed2 === 0n)
+ })
+ })
+})
diff --git a/contracts/test/setup.ts b/contracts/test/setup.ts
new file mode 100644
index 000000000..0ca69128c
--- /dev/null
+++ b/contracts/test/setup.ts
@@ -0,0 +1,94 @@
+import * as path from 'node:path'
+import * as fs from 'node:fs'
+
+function findCommonPrefix(path1: string, path2: string): string {
+ const segments1 = path1.split(path.sep)
+ const segments2 = path2.split(path.sep)
+
+ const minLength = Math.min(segments1.length, segments2.length)
+ const commonSegments = []
+
+ for (let i = 0; i < minLength; i++) {
+ if (segments1[i] === segments2[i]) {
+ commonSegments.push(segments1[i])
+ } else {
+ break
+ }
+ }
+
+ return commonSegments.join(path.sep)
+}
+
+function toCamelCase(str: string): string {
+ return str
+ .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
+ .replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
+ .replace(/(^| )([a-z])/g, (_, __, letter) => letter.toUpperCase())
+ .replace(/ /g, '')
+}
+
+function findCairoFiles(dir: string): string[] {
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
+ const filePaths = entries.flatMap((entry) => {
+ const entryPath = path.join(dir, entry.name)
+ if (entry.isDirectory()) {
+ return findCairoFiles(entryPath)
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.cairo')) {
+ return [entryPath]
+ } else {
+ return []
+ }
+ })
+ return filePaths
+}
+
+export function prepareHardhatArtifacts() {
+ const hre = require('hardhat')
+
+ const src = hre.config.paths.starknetSources
+ const target = hre.config.paths.starknetArtifacts
+ if (!src || !target) {
+ throw new Error('Missing starknet path config')
+ }
+
+ const root = findCommonPrefix(src, target)
+
+ console.log('Cleaning and regenerating hardhat file structure..')
+ const generatedPath = path.join(target, src.slice(root.length))
+ if (fs.existsSync(generatedPath)) {
+ fs.rmSync(generatedPath, { recursive: true })
+ }
+
+ const cairoFiles = findCairoFiles(src)
+ for (const cairoFile of cairoFiles) {
+ const relativePath = cairoFile
+ const filename = path.basename(relativePath, '.cairo')
+
+ const camelCaseFilename = toCamelCase(filename)
+
+ const sierraFile = `${target}/chainlink_${camelCaseFilename}.sierra.json`
+ const casmFile = `${target}/chainlink_${camelCaseFilename}.casm.json`
+
+ if (!fs.existsSync(sierraFile) || !fs.existsSync(casmFile)) {
+ continue
+ }
+
+ const subdir = path.dirname(relativePath).slice(root.length)
+ // Create the corresponding directory
+ const targetSubdir = path.join(target, subdir, `${filename}.cairo`)
+ fs.mkdirSync(targetSubdir, { recursive: true })
+
+ // Copy the sierra and casm files. We need to copy instead of symlink
+ // because hardhat-starknet-plugin does fs.lstatSync to check if the file
+ // exists.
+ fs.copyFileSync(sierraFile, `${targetSubdir}/${filename}.json`)
+ fs.copyFileSync(casmFile, `${targetSubdir}/${filename}.casm`)
+
+ // Parse and save the ABI JSON
+ const sierraContent = JSON.parse(fs.readFileSync(sierraFile, 'utf8'))
+ fs.writeFileSync(
+ `${targetSubdir}/${filename}_abi.json`,
+ JSON.stringify(sierraContent.abi, null, 2),
+ )
+ }
+}
diff --git a/contracts/test/utils.ts b/contracts/test/utils.ts
new file mode 100644
index 000000000..9f0cdce40
--- /dev/null
+++ b/contracts/test/utils.ts
@@ -0,0 +1,78 @@
+import { STARKNET_DEVNET_URL } from './constants'
+import { execSync } from 'node:child_process'
+import { Account } from 'starknet'
+import * as path from 'node:path'
+import { ethers } from 'hardhat'
+import { json } from 'starknet'
+import * as fs from 'node:fs'
+
+export type FetchStarknetAccountParams = Readonly<{
+ accountIndex?: number
+}>
+
+export const fetchStarknetAccount = async (params?: FetchStarknetAccountParams) => {
+ const response = await fetch(`${STARKNET_DEVNET_URL}/predeployed_accounts`)
+ const accounts = await response.json()
+ const accIndex = params?.accountIndex ?? 0
+
+ const account = accounts.at(accIndex)
+ if (account == null) {
+ throw new Error(`no account available at index ${accIndex}`)
+ }
+
+ return new Account(
+ {
+ nodeUrl: STARKNET_DEVNET_URL,
+ },
+ account.address,
+ account.private_key,
+ )
+}
+
+export const getStarknetContractArtifacts = (name: string) => {
+ const rootDir = getRootDir()
+ return {
+ contract: getStarknetContractArtifactPath(rootDir, name, false),
+ casm: getStarknetContractArtifactPath(rootDir, name, true),
+ }
+}
+
+export const waitForTransaction = async (
+ tx: () => ReturnType,
+) => {
+ const result = await tx()
+ return await result.wait()
+}
+
+export const waitForTransactions = async (
+ txs: (() => ReturnType)[],
+ cb?: () => void | Promise,
+) => {
+ const results = new Array>>()
+ for (const tx of txs) {
+ results.push(await waitForTransaction(tx))
+ cb != null && (await cb())
+ }
+ return results
+}
+
+const getRootDir = () => {
+ const result = execSync('git rev-parse --show-toplevel').toString()
+ return result.replace(/\n/g, '')
+}
+
+const getStarknetContractArtifactPath = (rootDir: string, name: string, casm: boolean) => {
+ return json.parse(
+ fs
+ .readFileSync(
+ path.join(
+ rootDir,
+ 'contracts',
+ 'target',
+ 'release',
+ `chainlink_${name}.${casm ? 'compiled_' : ''}contract_class.json`,
+ ),
+ )
+ .toString('ascii'),
+ )
+}
diff --git a/contracts/tests/conftest.py b/contracts/tests/conftest.py
deleted file mode 100644
index 866e91bb3..000000000
--- a/contracts/tests/conftest.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import pytest
-import asyncio
-
-@pytest.fixture(scope='module')
-def event_loop():
- return asyncio.new_event_loop()
\ No newline at end of file
diff --git a/contracts/tests/test_contract.py b/contracts/tests/test_contract.py
deleted file mode 100644
index 492d0a93c..000000000
--- a/contracts/tests/test_contract.py
+++ /dev/null
@@ -1,301 +0,0 @@
-"""aggregator.cairo test file."""
-import os
-
-import asyncio
-import pytest
-import pytest_asyncio
-from starkware.starknet.testing.starknet import Starknet
-from starkware.crypto.signature.signature import (
- pedersen_hash, private_to_stark_key, sign)
-from starkware.cairo.common.hash_state import compute_hash_on_elements
-from starkware.starknet.utils.api_utils import cast_to_felts
-
-from utils import (
- Signer, to_uint, add_uint, sub_uint, str_to_felt, MAX_UINT256, ZERO_ADDRESS, INVALID_UINT256,
- get_contract_def, contract_path, cached_contract, assert_revert, assert_event_emitted, uint
-)
-
-signer = Signer(999654321123456789)
-
-oracles = [
- { 'signer': Signer(123456789987654321), 'transmitter': Signer(987654321123456789) },
- { 'signer': Signer(123456789987654322), 'transmitter': Signer(987654321123456788) },
- { 'signer': Signer(123456789987654323), 'transmitter': Signer(987654321123456787) },
- { 'signer': Signer(123456789987654324), 'transmitter': Signer(987654321123456786) },
-]
-
-
-@pytest.fixture(scope='module')
-def contract_defs():
- aggregator_def = get_contract_def('aggregator.cairo')
- account_def = get_contract_def('account.cairo')
- erc20_def = get_contract_def('token.cairo')
- return aggregator_def, account_def, erc20_def
-
-@pytest_asyncio.fixture(scope='module')
-async def token_factory(contract_defs):
- _aggregator_def, account_def, erc20_def = contract_defs
-
- # Create a new Starknet class that simulates the StarkNet system.
- starknet = await Starknet.empty()
-
- owner = await starknet.deploy(
- contract_def=account_def,
- constructor_calldata=[signer.public_key]
- )
-
- token = await starknet.deploy(
- contract_def=erc20_def,
- constructor_calldata=[
- str_to_felt("LINK Token"),
- str_to_felt("LINK"),
- 18,
- *uint(1000),
- owner.contract_address,
- owner.contract_address
- ]
- )
- return starknet, token, owner
-
-# @pytest.mark.asyncio
-# async def test_ownership(token_factory):
-# """Test constructor method."""
-# starknet, token, owner = token_factory
-
-# # Deploy the contract.
-# contract = await starknet.deploy(
-# source=contract_path("aggregator.cairo"),
-# constructor_calldata=[
-# owner.contract_address,
-# token.contract_address,
-# 0,
-# 1000000000,
-# 0, # TODO: billing AC
-# 8, # decimals
-# str_to_felt("ETH/BTC")
-# ]
-# )
-
-# # # Invoke increase_balance() twice.
-# # await contract.increase_balance(amount=10).invoke()
-# # await contract.increase_balance(amount=20).invoke()
-
-# # Check the result of owner().
-# execution_info = await contract.owner().call()
-# assert execution_info.result == (owner.contract_address,)
-
-# TODO: module scope won't work, need to wrap with a state copy
-@pytest_asyncio.fixture(scope='module')
-async def setup(token_factory, contract_defs):
- starknet, token, owner = token_factory
- aggregator_def, account_def, _erc20_def = contract_defs
-
- # Deploy the contract.
- min_answer = -10
- max_answer = 1000000000
-
- contract = await starknet.deploy(
- contract_def=aggregator_def,
- constructor_calldata=cast_to_felts([
- owner.contract_address,
- token.contract_address,
- *cast_to_felts(values=[
- min_answer,
- max_answer
- ]),
- 0, # TODO: billing AC
- 8, # decimals
- str_to_felt("ETH/BTC")
- ])
- )
-
- # Deploy an account for each oracle
- accounts = await asyncio.gather(*[
- starknet.deploy(
- contract_def=account_def,
- constructor_calldata=[oracle['transmitter'].public_key]
- ) for oracle in oracles
- ])
- for i in range(len(accounts)):
- oracles[i]['account'] = accounts[i]
-
- # Call set_config
-
- f = 1
- # onchain_config = []
- onchain_config = 1
- offchain_config_version = 2
- offchain_config = [1]
-
- # TODO: need to call via owner
- execution_info = await contract.set_config(
- oracles=[(
- oracle['signer'].public_key,
- oracle['account'].contract_address
- ) for oracle in oracles],
- # TODO: dict was supposed to be ok but it asks for a tuple
- # oracles=[{
- # 'signer': oracle['signer'].public_key,
- # 'transmitter': oracle['transmitter'].public_key
- # } for oracle in oracles],
- f=f,
- onchain_config=onchain_config,
- offchain_config_version=2,
- offchain_config=offchain_config
- ).invoke()
-
- digest = execution_info.result.digest
-
- return {
- "starknet": starknet,
- "token": token,
- "owner": owner,
- "contract": contract,
- "f": f,
- "digest": digest,
- "oracles": oracles
- }
-
-@pytest.fixture
-def aggregator_factory(contract_defs, setup):
- aggregator_def, account_def, erc20_def = contract_defs
- env = setup
- _state = env["starknet"].state.copy()
- token = cached_contract(_state, erc20_def, env["token"])
- owner = cached_contract(_state, account_def, env["owner"])
- contract = cached_contract(_state, aggregator_def, env["contract"])
-
- # TODO: need to replace all oracles with cache too?
-
- return {
- "token": token,
- "owner": owner,
- "contract": contract,
- "f": env["f"],
- "digest": env["digest"],
- "oracles": env["oracles"]
- }
-
-
-@pytest.mark.asyncio
-async def test_transmit(aggregator_factory):
- """Test transmit method."""
- env = aggregator_factory
- print(f"digest = {env['digest']}")
-
- oracle = env["oracles"][0]
-
- n = env["f"] + 1
-
- def transmit(
- epoch_and_round, # TODO: split into two values
- answer
- ):
- # TODO:
- observation_timestamp = 1
- extra_hash = 1
- juels_per_fee_coin = 1
- report_context = [env["digest"], epoch_and_round, extra_hash]
- # int.from_bytes(report_context, "big"),
-
- l = len(env["oracles"])
- observers = bytes([i for i in range(l)])
- observations = [answer for _ in range(l)]
-
-
- raw_report = [
- observation_timestamp,
- int.from_bytes(observers, "big"),
- len(observations),
- *cast_to_felts(observations), # convert negative numbers to valid felts
- juels_per_fee_coin,
- ]
-
- msg = compute_hash_on_elements([
- *report_context,
- *raw_report
- ])
-
- signatures = []
-
- # TODO: test with duplicate signers
- # for o in oracles[:n]:
- # oracle = oracles[0]
-
- for oracle in env["oracles"][:n]:
- # Sign with a single oracle
- sig_r, sig_s = sign(msg_hash=msg, priv_key=oracle['signer'].private_key)
-
- signature = [
- sig_r, # r
- sig_s, # s
- oracle['signer'].public_key # public_key
- ]
- signatures.extend(signature)
-
- calldata = [
- *report_context,
- *raw_report,
- n, # len signatures
- *signatures
- ]
-
- print(calldata)
-
- return oracle['transmitter'].send_transaction(
- oracle['account'],
- env["contract"].contract_address,
- 'transmit',
- calldata
- )
-
- await transmit(epoch_and_round=1, answer=99)
- # TODO: test latest_round_data, round_data
- await transmit(epoch_and_round=2, answer=-1)
-
-@pytest.mark.asyncio
-async def test_payees(aggregator_factory):
- """Test payee related functionality."""
- env = aggregator_factory
-
- payees = []
-
- for oracle in env["oracles"]:
- payees.extend([
- oracle['transmitter'].public_key,
- oracle['transmitter'].public_key # reusing transmitter accounts as payees for simplicity
- ])
-
- calldata = [
- len(env["oracles"]),
- *payees
- ]
-
- # should succeed because all payees are zero
- execution_info = await signer.send_transaction(
- env['owner'],
- env["contract"].contract_address,
- 'set_payees',
- calldata
- )
-
- # should succeed because all payees equal current payees
- execution_info = await signer.send_transaction(
- env['owner'],
- env["contract"].contract_address,
- 'set_payees',
- calldata
- )
-
- # can't transfer to self
- assert_revert(signer.send_transaction(
- env['owner'],
- env["contract"].contract_address,
- 'transfer_payeeship',
- [transmitter, proposed]
- ))
- # only payee can transfer
- # successful transfer
- # only proposed payee can accept
- # successful accept
-
diff --git a/contracts/tests/utils.py b/contracts/tests/utils.py
deleted file mode 100644
index 836aa4146..000000000
--- a/contracts/tests/utils.py
+++ /dev/null
@@ -1,213 +0,0 @@
-"""Utilities for testing Cairo contracts."""
-
-from pathlib import Path
-import math
-from starkware.cairo.common.hash_state import compute_hash_on_elements
-from starkware.crypto.signature.signature import private_to_stark_key, sign
-from starkware.starknet.public.abi import get_selector_from_name
-from starkware.starknet.compiler.compile import compile_starknet_files
-from starkware.starkware_utils.error_handling import StarkException
-from starkware.starknet.testing.starknet import StarknetContract
-from starkware.starknet.business_logic.execution.objects import Event
-
-MAX_UINT256 = (2**128 - 1, 2**128 - 1)
-INVALID_UINT256 = (MAX_UINT256[0] + 1, MAX_UINT256[1])
-ZERO_ADDRESS = 0
-TRUE = 1
-FALSE = 0
-
-TRANSACTION_VERSION = 0
-
-
-_root = Path(__file__).parent.parent
-
-
-def contract_path(name):
- if name.startswith("tests/"):
- return str(_root / name)
- else:
- return str(_root / "contracts" / name)
-
-
-def str_to_felt(text):
- b_text = bytes(text, "ascii")
- return int.from_bytes(b_text, "big")
-
-
-def felt_to_str(felt):
- b_felt = felt.to_bytes(31, "big")
- return b_felt.decode()
-
-
-def assert_event_emitted(tx_exec_info, from_address, name, data):
- assert Event(
- from_address=from_address,
- keys=[get_selector_from_name(name)],
- data=data,
- ) in tx_exec_info.raw_events
-
-
-def uint(a):
- return(a, 0)
-
-
-def to_uint(a):
- """Takes in value, returns uint256-ish tuple."""
- return (a & ((1 << 128) - 1), a >> 128)
-
-
-def from_uint(uint):
- """Takes in uint256-ish tuple, returns value."""
- return uint[0] + (uint[1] << 128)
-
-
-def add_uint(a, b):
- """Returns the sum of two uint256-ish tuples."""
- a = from_uint(a)
- b = from_uint(b)
- c = a + b
- return to_uint(c)
-
-
-def sub_uint(a, b):
- """Returns the difference of two uint256-ish tuples."""
- a = from_uint(a)
- b = from_uint(b)
- c = a - b
- return to_uint(c)
-
-
-def mul_uint(a, b):
- """Returns the product of two uint256-ish tuples."""
- a = from_uint(a)
- b = from_uint(b)
- c = a * b
- return to_uint(c)
-
-
-def div_rem_uint(a, b):
- """Returns the quotient and remainder of two uint256-ish tuples."""
- a = from_uint(a)
- b = from_uint(b)
- c = math.trunc(a / b)
- m = a % b
- return (to_uint(c), to_uint(m))
-
-
-async def assert_revert(fun, reverted_with=None):
- try:
- await fun
- assert False
- except StarkException as err:
- _, error = err.args
- if reverted_with is not None:
- assert reverted_with in error['message']
-
-
-def assert_event_emitted(tx_exec_info, from_address, name, data):
- assert Event(
- from_address=from_address,
- keys=[get_selector_from_name(name)],
- data=data,
- ) in tx_exec_info.raw_events
-
-
-def get_contract_def(path):
- """Returns the contract definition from the contract path"""
- path = contract_path(path)
- contract_def = compile_starknet_files(
- files=[path],
- debug_info=True
- )
- return contract_def
-
-
-def cached_contract(state, definition, deployed):
- """Returns the cached contract"""
- contract = StarknetContract(
- state=state,
- abi=definition.abi,
- contract_address=deployed.contract_address,
- deploy_execution_info=deployed.deploy_execution_info
- )
- return contract
-
-
-class Signer():
- """
- Utility for sending signed transactions to an Account on Starknet.
-
- Parameters
- ----------
-
- private_key : int
-
- Examples
- ---------
- Constructing a Signer object
-
- >>> signer = Signer(1234)
-
- Sending a transaction
-
- >>> await signer.send_transaction(account,
- account.contract_address,
- 'set_public_key',
- [other.public_key]
- )
-
- """
-
- def __init__(self, private_key):
- self.private_key = private_key
- self.public_key = private_to_stark_key(private_key)
-
- def sign(self, message_hash):
- return sign(msg_hash=message_hash, priv_key=self.private_key)
-
- async def send_transaction(self, account, to, selector_name, calldata, nonce=None, max_fee=0):
- return await self.send_transactions(account, [(to, selector_name, calldata)], nonce, max_fee)
-
- async def send_transactions(self, account, calls, nonce=None, max_fee=0):
- if nonce is None:
- execution_info = await account.get_nonce().call()
- nonce, = execution_info.result
-
- calls_with_selector = [
- (call[0], get_selector_from_name(call[1]), call[2]) for call in calls]
- (call_array, calldata) = from_call_to_call_array(calls)
-
- message_hash = hash_multicall(
- account.contract_address, calls_with_selector, nonce, max_fee)
- sig_r, sig_s = self.sign(message_hash)
-
- return await account.__execute__(call_array, calldata, nonce).invoke(signature=[sig_r, sig_s])
-
-
-def from_call_to_call_array(calls):
- call_array = []
- calldata = []
- for i, call in enumerate(calls):
- assert len(call) == 3, "Invalid call parameters"
- entry = (call[0], get_selector_from_name(
- call[1]), len(calldata), len(call[2]))
- call_array.append(entry)
- calldata.extend(call[2])
- return (call_array, calldata)
-
-
-def hash_multicall(sender, calls, nonce, max_fee):
- hash_array = []
- for call in calls:
- call_elements = [call[0], call[1], compute_hash_on_elements(call[2])]
- hash_array.append(compute_hash_on_elements(call_elements))
-
- message = [
- str_to_felt('StarkNet Transaction'),
- sender,
- compute_hash_on_elements(hash_array),
- nonce,
- max_fee,
- TRANSACTION_VERSION
- ]
- return compute_hash_on_elements(message)
diff --git a/contracts/tsconfig.json b/contracts/tsconfig.json
new file mode 100644
index 000000000..13fad6402
--- /dev/null
+++ b/contracts/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "es2020",
+ "module": "commonjs",
+ "strict": true,
+ "esModuleInterop": true,
+ "outDir": "dist",
+ "resolveJsonModule": true
+ },
+ "include": [
+ "./scripts",
+ "./test",
+ "./typechain-types"
+ ],
+ "files": [
+ "./hardhat.config.ts"
+ ]
+}
diff --git a/contracts/vendor/starkware-libs/README.md b/contracts/vendor/starkware-libs/README.md
new file mode 100644
index 000000000..a3cf938b6
--- /dev/null
+++ b/contracts/vendor/starkware-libs/README.md
@@ -0,0 +1,4 @@
+# starkware-libs vendor contracts
+
+- `starkware-libs/starkgate-contracts` - fork of the original repo at [c08863a](https://github.com/starkware-libs/starkgate-contracts/commit/c08863a1f08226c09f1d0748124192e848d73db9) includes only `std_contracts/ERC20/permitted.cairo`
+- `starkware-libs/cairo-lang` fork of the original repo at [v0.11.0.2](https://github.com/starkware-libs/cairo-lang/tree/v0.11.0.2/src/starkware/starknet) which loosens the `pragma` declaration for a few interfaces to support v0.8 (includes only the files we use)
\ No newline at end of file
diff --git a/contracts/vendor/starkware-libs/cairo-lang/src/starkware/solidity/libraries/NamedStorage.sol b/contracts/vendor/starkware-libs/cairo-lang/src/starkware/solidity/libraries/NamedStorage.sol
new file mode 100644
index 000000000..5f20ba86a
--- /dev/null
+++ b/contracts/vendor/starkware-libs/cairo-lang/src/starkware/solidity/libraries/NamedStorage.sol
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: Apache-2.0.
+pragma solidity ^0.8.0;
+
+/*
+ Library to provide basic storage, in storage location out of the low linear address space.
+
+ New types of storage variables should be added here upon need.
+*/
+library NamedStorage {
+ function bytes32ToUint256Mapping(string memory tag_)
+ internal
+ pure
+ returns (mapping(bytes32 => uint256) storage randomVariable)
+ {
+ bytes32 location = keccak256(abi.encodePacked(tag_));
+ assembly {
+ randomVariable.slot := location
+ }
+ }
+
+ function bytes32ToAddressMapping(string memory tag_)
+ internal
+ pure
+ returns (mapping(bytes32 => address) storage randomVariable)
+ {
+ bytes32 location = keccak256(abi.encodePacked(tag_));
+ assembly {
+ randomVariable.slot := location
+ }
+ }
+
+ function uintToAddressMapping(string memory tag_)
+ internal
+ pure
+ returns (mapping(uint256 => address) storage randomVariable)
+ {
+ bytes32 location = keccak256(abi.encodePacked(tag_));
+ assembly {
+ randomVariable.slot := location
+ }
+ }
+
+ function addressToBoolMapping(string memory tag_)
+ internal
+ pure
+ returns (mapping(address => bool) storage randomVariable)
+ {
+ bytes32 location = keccak256(abi.encodePacked(tag_));
+ assembly {
+ randomVariable.slot := location
+ }
+ }
+
+ function getUintValue(string memory tag_) internal view returns (uint256 retVal) {
+ bytes32 slot = keccak256(abi.encodePacked(tag_));
+ assembly {
+ retVal := sload(slot)
+ }
+ }
+
+ function setUintValue(string memory tag_, uint256 value) internal {
+ bytes32 slot = keccak256(abi.encodePacked(tag_));
+ assembly {
+ sstore(slot, value)
+ }
+ }
+
+ function setUintValueOnce(string memory tag_, uint256 value) internal {
+ require(getUintValue(tag_) == 0, "ALREADY_SET");
+ setUintValue(tag_, value);
+ }
+
+ function getAddressValue(string memory tag_) internal view returns (address retVal) {
+ bytes32 slot = keccak256(abi.encodePacked(tag_));
+ assembly {
+ retVal := sload(slot)
+ }
+ }
+
+ function setAddressValue(string memory tag_, address value) internal {
+ bytes32 slot = keccak256(abi.encodePacked(tag_));
+ assembly {
+ sstore(slot, value)
+ }
+ }
+
+ function setAddressValueOnce(string memory tag_, address value) internal {
+ require(getAddressValue(tag_) == address(0x0), "ALREADY_SET");
+ setAddressValue(tag_, value);
+ }
+
+ function getBoolValue(string memory tag_) internal view returns (bool retVal) {
+ bytes32 slot = keccak256(abi.encodePacked(tag_));
+ assembly {
+ retVal := sload(slot)
+ }
+ }
+
+ function setBoolValue(string memory tag_, bool value) internal {
+ bytes32 slot = keccak256(abi.encodePacked(tag_));
+ assembly {
+ sstore(slot, value)
+ }
+ }
+}
diff --git a/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessaging.sol b/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessaging.sol
new file mode 100644
index 000000000..7ce51dad3
--- /dev/null
+++ b/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessaging.sol
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: Apache-2.0.
+pragma solidity ^0.8.0;
+
+import "./IStarknetMessagingEvents.sol";
+
+interface IStarknetMessaging is IStarknetMessagingEvents {
+ /**
+ Returns the max fee (in Wei) that StarkNet will accept per single message.
+ */
+ function getMaxL1MsgFee() external pure returns (uint256);
+
+ /**
+ Sends a message to an L2 contract.
+ This function is payable, the payed amount is the message fee.
+
+ Returns the hash of the message and the nonce of the message.
+ */
+ function sendMessageToL2(
+ uint256 toAddress,
+ uint256 selector,
+ uint256[] calldata payload
+ ) external payable returns (bytes32, uint256);
+
+ /**
+ Consumes a message that was sent from an L2 contract.
+
+ Returns the hash of the message.
+ */
+ function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload)
+ external
+ returns (bytes32);
+
+ /**
+ Starts the cancellation of an L1 to L2 message.
+ A message can be canceled messageCancellationDelay() seconds after this function is called.
+
+ Note: This function may only be called for a message that is currently pending and the caller
+ must be the sender of the that message.
+ */
+ function startL1ToL2MessageCancellation(
+ uint256 toAddress,
+ uint256 selector,
+ uint256[] calldata payload,
+ uint256 nonce
+ ) external returns (bytes32);
+
+ /**
+ Cancels an L1 to L2 message, this function should be called at least
+ messageCancellationDelay() seconds after the call to startL1ToL2MessageCancellation().
+ A message may only be cancelled by its sender.
+ If the message is missing, the call will revert.
+
+ Note that the message fee is not refunded.
+ */
+ function cancelL1ToL2Message(
+ uint256 toAddress,
+ uint256 selector,
+ uint256[] calldata payload,
+ uint256 nonce
+ ) external returns (bytes32);
+}
diff --git a/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessagingEvents.sol b/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessagingEvents.sol
new file mode 100644
index 000000000..2f52bfe77
--- /dev/null
+++ b/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessagingEvents.sol
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: Apache-2.0.
+pragma solidity ^0.8.0;
+
+interface IStarknetMessagingEvents {
+ // This event needs to be compatible with the one defined in Output.sol.
+ event LogMessageToL1(uint256 indexed fromAddress, address indexed toAddress, uint256[] payload);
+
+ // An event that is raised when a message is sent from L1 to L2.
+ event LogMessageToL2(
+ address indexed fromAddress,
+ uint256 indexed toAddress,
+ uint256 indexed selector,
+ uint256[] payload,
+ uint256 nonce,
+ uint256 fee
+ );
+
+ // An event that is raised when a message from L2 to L1 is consumed.
+ event ConsumedMessageToL1(
+ uint256 indexed fromAddress,
+ address indexed toAddress,
+ uint256[] payload
+ );
+
+ // An event that is raised when a message from L1 to L2 is consumed.
+ event ConsumedMessageToL2(
+ address indexed fromAddress,
+ uint256 indexed toAddress,
+ uint256 indexed selector,
+ uint256[] payload,
+ uint256 nonce
+ );
+
+ // An event that is raised when a message from L1 to L2 Cancellation is started.
+ event MessageToL2CancellationStarted(
+ address indexed fromAddress,
+ uint256 indexed toAddress,
+ uint256 indexed selector,
+ uint256[] payload,
+ uint256 nonce
+ );
+
+ // An event that is raised when a message from L1 to L2 is canceled.
+ event MessageToL2Canceled(
+ address indexed fromAddress,
+ uint256 indexed toAddress,
+ uint256 indexed selector,
+ uint256[] payload,
+ uint256 nonce
+ );
+}
diff --git a/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/StarknetMessaging.sol b/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/StarknetMessaging.sol
new file mode 100644
index 000000000..04e9a6518
--- /dev/null
+++ b/contracts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/StarknetMessaging.sol
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: Apache-2.0.
+pragma solidity ^0.8.0;
+
+import "./IStarknetMessaging.sol";
+import "../../solidity/libraries/NamedStorage.sol";
+
+/**
+ Implements sending messages to L2 by adding them to a pipe and consuming messages from L2 by
+ removing them from a different pipe. A deriving contract can handle the former pipe and add items
+ to the latter pipe while interacting with L2.
+*/
+contract StarknetMessaging is IStarknetMessaging {
+ /*
+ Random slot storage elements and accessors.
+ */
+ string constant L1L2_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L1TOL2_MAPPPING_V2";
+ string constant L2L1_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L2TOL1_MAPPPING";
+
+ string constant L1L2_MESSAGE_NONCE_TAG = "STARKNET_1.0_MSGING_L1TOL2_NONCE";
+
+ string constant L1L2_MESSAGE_CANCELLATION_MAP_TAG = (
+ "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_MAPPPING"
+ );
+
+ string constant L1L2_MESSAGE_CANCELLATION_DELAY_TAG = (
+ "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_DELAY"
+ );
+
+ uint256 constant MAX_L1_MSG_FEE = 1 ether;
+
+ function getMaxL1MsgFee() public pure override returns (uint256) {
+ return MAX_L1_MSG_FEE;
+ }
+
+ /**
+ Returns the msg_fee + 1 for the message with the given 'msgHash',
+ or 0 if no message with such a hash is pending.
+ */
+ function l1ToL2Messages(bytes32 msgHash) external view returns (uint256) {
+ return l1ToL2Messages()[msgHash];
+ }
+
+ function l2ToL1Messages(bytes32 msgHash) external view returns (uint256) {
+ return l2ToL1Messages()[msgHash];
+ }
+
+ function l1ToL2Messages() internal pure returns (mapping(bytes32 => uint256) storage) {
+ return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_MAP_TAG);
+ }
+
+ function l2ToL1Messages() internal pure returns (mapping(bytes32 => uint256) storage) {
+ return NamedStorage.bytes32ToUint256Mapping(L2L1_MESSAGE_MAP_TAG);
+ }
+
+ function l1ToL2MessageNonce() public view returns (uint256) {
+ return NamedStorage.getUintValue(L1L2_MESSAGE_NONCE_TAG);
+ }
+
+ function messageCancellationDelay() public view returns (uint256) {
+ return NamedStorage.getUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG);
+ }
+
+ function messageCancellationDelay(uint256 delayInSeconds) internal {
+ NamedStorage.setUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG, delayInSeconds);
+ }
+
+ /**
+ Returns the timestamp at the time cancelL1ToL2Message was called with a message
+ matching 'msgHash'.
+
+ The function returns 0 if cancelL1ToL2Message was never called.
+ */
+ function l1ToL2MessageCancellations(bytes32 msgHash) external view returns (uint256) {
+ return l1ToL2MessageCancellations()[msgHash];
+ }
+
+ function l1ToL2MessageCancellations()
+ internal
+ pure
+ returns (mapping(bytes32 => uint256) storage)
+ {
+ return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_CANCELLATION_MAP_TAG);
+ }
+
+ /**
+ Returns the hash of an L1 -> L2 message from msg.sender.
+ */
+ function getL1ToL2MsgHash(
+ uint256 toAddress,
+ uint256 selector,
+ uint256[] calldata payload,
+ uint256 nonce
+ ) internal view returns (bytes32) {
+ return
+ keccak256(
+ abi.encodePacked(
+ uint256(uint160(msg.sender)),
+ toAddress,
+ nonce,
+ selector,
+ payload.length,
+ payload
+ )
+ );
+ }
+
+ /**
+ Sends a message to an L2 contract.
+ */
+ function sendMessageToL2(
+ uint256 toAddress,
+ uint256 selector,
+ uint256[] calldata payload
+ ) external payable override returns (bytes32, uint256) {
+ require(msg.value > 0, "L1_MSG_FEE_MUST_BE_GREATER_THAN_0");
+ require(msg.value <= getMaxL1MsgFee(), "MAX_L1_MSG_FEE_EXCEEDED");
+ uint256 nonce = l1ToL2MessageNonce();
+ NamedStorage.setUintValue(L1L2_MESSAGE_NONCE_TAG, nonce + 1);
+ emit LogMessageToL2(msg.sender, toAddress, selector, payload, nonce, msg.value);
+ bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce);
+ // Note that the inclusion of the unique nonce in the message hash implies that
+ // l1ToL2Messages()[msgHash] was not accessed before.
+ l1ToL2Messages()[msgHash] = msg.value + 1;
+ return (msgHash, nonce);
+ }
+
+ /**
+ Consumes a message that was sent from an L2 contract.
+
+ Returns the hash of the message.
+ */
+ function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload)
+ external
+ override
+ returns (bytes32)
+ {
+ bytes32 msgHash = keccak256(
+ abi.encodePacked(fromAddress, uint256(uint160(msg.sender)), payload.length, payload)
+ );
+
+ require(l2ToL1Messages()[msgHash] > 0, "INVALID_MESSAGE_TO_CONSUME");
+ emit ConsumedMessageToL1(fromAddress, msg.sender, payload);
+ l2ToL1Messages()[msgHash] -= 1;
+ return msgHash;
+ }
+
+ function startL1ToL2MessageCancellation(
+ uint256 toAddress,
+ uint256 selector,
+ uint256[] calldata payload,
+ uint256 nonce
+ ) external override returns (bytes32) {
+ emit MessageToL2CancellationStarted(msg.sender, toAddress, selector, payload, nonce);
+ bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce);
+ uint256 msgFeePlusOne = l1ToL2Messages()[msgHash];
+ require(msgFeePlusOne > 0, "NO_MESSAGE_TO_CANCEL");
+ l1ToL2MessageCancellations()[msgHash] = block.timestamp;
+ return msgHash;
+ }
+
+ function cancelL1ToL2Message(
+ uint256 toAddress,
+ uint256 selector,
+ uint256[] calldata payload,
+ uint256 nonce
+ ) external override returns (bytes32) {
+ emit MessageToL2Canceled(msg.sender, toAddress, selector, payload, nonce);
+ // Note that the message hash depends on msg.sender, which prevents one contract from
+ // cancelling another contract's message.
+ // Trying to do so will result in NO_MESSAGE_TO_CANCEL.
+ bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce);
+ uint256 msgFeePlusOne = l1ToL2Messages()[msgHash];
+ require(msgFeePlusOne != 0, "NO_MESSAGE_TO_CANCEL");
+
+ uint256 requestTime = l1ToL2MessageCancellations()[msgHash];
+ require(requestTime != 0, "MESSAGE_CANCELLATION_NOT_REQUESTED");
+
+ uint256 cancelAllowedTime = requestTime + messageCancellationDelay();
+ require(cancelAllowedTime >= requestTime, "CANCEL_ALLOWED_TIME_OVERFLOW");
+ require(block.timestamp >= cancelAllowedTime, "MESSAGE_CANCELLATION_NOT_ALLOWED_YET");
+
+ l1ToL2Messages()[msgHash] = 0;
+ return (msgHash);
+ }
+}
diff --git a/contracts/vendor/starkware-libs/starkgate-contracts/src/starkware/starknet/std_contracts/ERC20/permitted.cairo b/contracts/vendor/starkware-libs/starkgate-contracts/src/starkware/starknet/std_contracts/ERC20/permitted.cairo
new file mode 100644
index 000000000..2d1c0baf1
--- /dev/null
+++ b/contracts/vendor/starkware-libs/starkgate-contracts/src/starkware/starknet/std_contracts/ERC20/permitted.cairo
@@ -0,0 +1,39 @@
+%lang starknet
+
+from starkware.cairo.common.cairo_builtins import HashBuiltin
+from starkware.cairo.common.math import assert_not_zero
+from starkware.starknet.common.syscalls import get_caller_address
+
+@storage_var
+func permitted_minter() -> (res: felt) {
+}
+
+// Constructor.
+
+func permitted_initializer{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
+ minter_address: felt
+) {
+ assert_not_zero(minter_address);
+ permitted_minter.write(minter_address);
+ return ();
+}
+
+// Getters.
+
+@view
+func permittedMinter{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (
+ minter: felt
+) {
+ let (minter) = permitted_minter.read();
+ return (minter,);
+}
+
+// Internals.
+
+func permitted_minter_only{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() {
+ let (caller_address) = get_caller_address();
+ let (permitted_address) = permittedMinter();
+ assert_not_zero(permitted_address);
+ assert caller_address = permitted_address;
+ return ();
+}
diff --git a/contracts/yarn.lock b/contracts/yarn.lock
new file mode 100644
index 000000000..f5c0f320a
--- /dev/null
+++ b/contracts/yarn.lock
@@ -0,0 +1,3700 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@cspotcode/source-map-support@^0.8.0":
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
+ integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
+ dependencies:
+ "@jridgewell/trace-mapping" "0.3.9"
+
+"@ethereumjs/block@^3.5.0", "@ethereumjs/block@^3.6.2":
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-3.6.2.tgz#63d1e26d0b7a7a3684fce920de6ebabec1e5b674"
+ integrity sha512-mOqYWwMlAZpYUEOEqt7EfMFuVL2eyLqWWIzcf4odn6QgXY8jBI2NhVuJncrMCKeMZrsJAe7/auaRRB6YcdH+Qw==
+ dependencies:
+ "@ethereumjs/common" "^2.6.3"
+ "@ethereumjs/tx" "^3.5.1"
+ ethereumjs-util "^7.1.4"
+ merkle-patricia-tree "^4.2.4"
+
+"@ethereumjs/block@^3.6.3":
+ version "3.6.3"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-3.6.3.tgz#d96cbd7af38b92ebb3424223dbf773f5ccd27f84"
+ integrity sha512-CegDeryc2DVKnDkg5COQrE0bJfw/p0v3GBk2W5/Dj5dOVfEmb50Ux0GLnSPypooLnfqjwFaorGuT9FokWB3GRg==
+ dependencies:
+ "@ethereumjs/common" "^2.6.5"
+ "@ethereumjs/tx" "^3.5.2"
+ ethereumjs-util "^7.1.5"
+ merkle-patricia-tree "^4.2.4"
+
+"@ethereumjs/blockchain@^5.5.2":
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/blockchain/-/blockchain-5.5.2.tgz#1848abd9dc1ee56acf8cec4c84304d7f4667d027"
+ integrity sha512-Jz26iJmmsQtngerW6r5BDFaew/f2mObLrRZo3rskLOx1lmtMZ8+TX/vJexmivrnWgmAsTdNWhlKUYY4thPhPig==
+ dependencies:
+ "@ethereumjs/block" "^3.6.2"
+ "@ethereumjs/common" "^2.6.3"
+ "@ethereumjs/ethash" "^1.1.0"
+ debug "^4.3.3"
+ ethereumjs-util "^7.1.4"
+ level-mem "^5.0.1"
+ lru-cache "^5.1.1"
+ semaphore-async-await "^1.5.1"
+
+"@ethereumjs/blockchain@^5.5.3":
+ version "5.5.3"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/blockchain/-/blockchain-5.5.3.tgz#aa49a6a04789da6b66b5bcbb0d0b98efc369f640"
+ integrity sha512-bi0wuNJ1gw4ByNCV56H0Z4Q7D+SxUbwyG12Wxzbvqc89PXLRNR20LBcSUZRKpN0+YCPo6m0XZL/JLio3B52LTw==
+ dependencies:
+ "@ethereumjs/block" "^3.6.2"
+ "@ethereumjs/common" "^2.6.4"
+ "@ethereumjs/ethash" "^1.1.0"
+ debug "^4.3.3"
+ ethereumjs-util "^7.1.5"
+ level-mem "^5.0.1"
+ lru-cache "^5.1.1"
+ semaphore-async-await "^1.5.1"
+
+"@ethereumjs/common@^2.6.3", "@ethereumjs/common@^2.6.4":
+ version "2.6.4"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.4.tgz#1b3cdd3aa4ee3b0ca366756fc35e4a03022a01cc"
+ integrity sha512-RDJh/R/EAr+B7ZRg5LfJ0BIpf/1LydFgYdvZEuTraojCbVypO2sQ+QnpP5u2wJf9DASyooKqu8O4FJEWUV6NXw==
+ dependencies:
+ crc-32 "^1.2.0"
+ ethereumjs-util "^7.1.4"
+
+"@ethereumjs/common@^2.6.5":
+ version "2.6.5"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.5.tgz#0a75a22a046272579d91919cb12d84f2756e8d30"
+ integrity sha512-lRyVQOeCDaIVtgfbowla32pzeDv2Obr8oR8Put5RdUBNRGr1VGPGQNGP6elWIpgK3YdpzqTOh4GyUGOureVeeA==
+ dependencies:
+ crc-32 "^1.2.0"
+ ethereumjs-util "^7.1.5"
+
+"@ethereumjs/ethash@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/ethash/-/ethash-1.1.0.tgz#7c5918ffcaa9cb9c1dc7d12f77ef038c11fb83fb"
+ integrity sha512-/U7UOKW6BzpA+Vt+kISAoeDie1vAvY4Zy2KF5JJb+So7+1yKmJeJEHOGSnQIj330e9Zyl3L5Nae6VZyh2TJnAA==
+ dependencies:
+ "@ethereumjs/block" "^3.5.0"
+ "@types/levelup" "^4.3.0"
+ buffer-xor "^2.0.1"
+ ethereumjs-util "^7.1.1"
+ miller-rabin "^4.0.0"
+
+"@ethereumjs/tx@^3.5.1":
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.1.tgz#8d941b83a602b4a89949c879615f7ea9a90e6671"
+ integrity sha512-xzDrTiu4sqZXUcaBxJ4n4W5FrppwxLxZB4ZDGVLtxSQR4lVuOnFR6RcUHdg1mpUhAPVrmnzLJpxaeXnPxIyhWA==
+ dependencies:
+ "@ethereumjs/common" "^2.6.3"
+ ethereumjs-util "^7.1.4"
+
+"@ethereumjs/tx@^3.5.2":
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.2.tgz#197b9b6299582ad84f9527ca961466fce2296c1c"
+ integrity sha512-gQDNJWKrSDGu2w7w0PzVXVBNMzb7wwdDOmOqczmhNjqFxFuIbhVJDwiGEnxFNC2/b8ifcZzY7MLcluizohRzNw==
+ dependencies:
+ "@ethereumjs/common" "^2.6.4"
+ ethereumjs-util "^7.1.5"
+
+"@ethereumjs/vm@^5.9.0":
+ version "5.9.3"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.9.3.tgz#6d69202e4c132a4a1e1628ac246e92062e230823"
+ integrity sha512-Ha04TeF8goEglr8eL7hkkYyjhzdZS0PsoRURzYlTF6I0VVId5KjKb0N7MrA8GMgheN+UeTncfTgYx52D/WhEmg==
+ dependencies:
+ "@ethereumjs/block" "^3.6.3"
+ "@ethereumjs/blockchain" "^5.5.3"
+ "@ethereumjs/common" "^2.6.5"
+ "@ethereumjs/tx" "^3.5.2"
+ async-eventemitter "^0.2.4"
+ core-js-pure "^3.0.1"
+ debug "^4.3.3"
+ ethereumjs-util "^7.1.5"
+ functional-red-black-tree "^1.0.1"
+ mcl-wasm "^0.7.1"
+ merkle-patricia-tree "^4.2.4"
+ rustbn.js "~0.2.0"
+
+"@ethersproject/abi@5.6.3", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.3":
+ version "5.6.3"
+ resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.6.3.tgz#2d643544abadf6e6b63150508af43475985c23db"
+ integrity sha512-CxKTdoZY4zDJLWXG6HzNH6znWK0M79WzzxHegDoecE3+K32pzfHOzuXg2/oGSTecZynFgpkjYXNPOqXVJlqClw==
+ dependencies:
+ "@ethersproject/address" "^5.6.1"
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/constants" "^5.6.1"
+ "@ethersproject/hash" "^5.6.1"
+ "@ethersproject/keccak256" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/strings" "^5.6.1"
+
+"@ethersproject/abi@^5.1.2":
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.6.2.tgz#f2956f2ac724cd720e581759d9e3840cd9744818"
+ integrity sha512-40Ixjhy+YzFtnvzIqFU13FW9hd1gMoLa3cJfSDnfnL4o8EnEG1qLiV8sNJo3sHYi9UYMfFeRuZ7kv5+vhzU7gQ==
+ dependencies:
+ "@ethersproject/address" "^5.6.0"
+ "@ethersproject/bignumber" "^5.6.0"
+ "@ethersproject/bytes" "^5.6.0"
+ "@ethersproject/constants" "^5.6.0"
+ "@ethersproject/hash" "^5.6.0"
+ "@ethersproject/keccak256" "^5.6.0"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/strings" "^5.6.0"
+
+"@ethersproject/abstract-provider@5.6.1", "@ethersproject/abstract-provider@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.6.1.tgz#02ddce150785caf0c77fe036a0ebfcee61878c59"
+ integrity sha512-BxlIgogYJtp1FS8Muvj8YfdClk3unZH0vRMVX791Z9INBNT/kuACZ9GzaY1Y4yFq+YSy6/w4gzj3HCRKrK9hsQ==
+ dependencies:
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/networks" "^5.6.3"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/transactions" "^5.6.2"
+ "@ethersproject/web" "^5.6.1"
+
+"@ethersproject/abstract-signer@5.6.2", "@ethersproject/abstract-signer@^5.6.2":
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.2.tgz#491f07fc2cbd5da258f46ec539664713950b0b33"
+ integrity sha512-n1r6lttFBG0t2vNiI3HoWaS/KdOt8xyDjzlP2cuevlWLG6EX0OwcKLyG/Kp/cuwNxdy/ous+R/DEMdTUwWQIjQ==
+ dependencies:
+ "@ethersproject/abstract-provider" "^5.6.1"
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+
+"@ethersproject/address@5.6.1", "@ethersproject/address@^5.6.0", "@ethersproject/address@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.1.tgz#ab57818d9aefee919c5721d28cd31fd95eff413d"
+ integrity sha512-uOgF0kS5MJv9ZvCz7x6T2EXJSzotiybApn4XlOgoTX0xdtyVIJ7pF+6cGPxiEq/dpBiTfMiw7Yc81JcwhSYA0Q==
+ dependencies:
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/keccak256" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/rlp" "^5.6.1"
+
+"@ethersproject/base64@5.6.1", "@ethersproject/base64@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.1.tgz#2c40d8a0310c9d1606c2c37ae3092634b41d87cb"
+ integrity sha512-qB76rjop6a0RIYYMiB4Eh/8n+Hxu2NIZm8S/Q7kNo5pmZfXhHGHmS4MinUainiBC54SCyRnwzL+KZjj8zbsSsw==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+
+"@ethersproject/basex@5.6.1", "@ethersproject/basex@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.6.1.tgz#badbb2f1d4a6f52ce41c9064f01eab19cc4c5305"
+ integrity sha512-a52MkVz4vuBXR06nvflPMotld1FJWSj2QT0985v7P/emPZO00PucFAkbcmq2vpVU7Ts7umKiSI6SppiLykVWsA==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/properties" "^5.6.0"
+
+"@ethersproject/bignumber@5.6.2", "@ethersproject/bignumber@^5.6.0", "@ethersproject/bignumber@^5.6.2":
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.2.tgz#72a0717d6163fab44c47bcc82e0c550ac0315d66"
+ integrity sha512-v7+EEUbhGqT3XJ9LMPsKvXYHFc8eHxTowFCG/HgJErmq4XHJ2WR7aeyICg3uTOAQ7Icn0GFHAohXEhxQHq4Ubw==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ bn.js "^5.2.1"
+
+"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.6.0", "@ethersproject/bytes@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7"
+ integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g==
+ dependencies:
+ "@ethersproject/logger" "^5.6.0"
+
+"@ethersproject/constants@5.6.1", "@ethersproject/constants@^5.6.0", "@ethersproject/constants@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.1.tgz#e2e974cac160dd101cf79fdf879d7d18e8cb1370"
+ integrity sha512-QSq9WVnZbxXYFftrjSjZDUshp6/eKp6qrtdBtUCm0QxCV5z1fG/w3kdlcsjMCQuQHUnAclKoK7XpXMezhRDOLg==
+ dependencies:
+ "@ethersproject/bignumber" "^5.6.2"
+
+"@ethersproject/contracts@5.6.2":
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.6.2.tgz#20b52e69ebc1b74274ff8e3d4e508de971c287bc"
+ integrity sha512-hguUA57BIKi6WY0kHvZp6PwPlWF87MCeB4B7Z7AbUpTxfFXFdn/3b0GmjZPagIHS+3yhcBJDnuEfU4Xz+Ks/8g==
+ dependencies:
+ "@ethersproject/abi" "^5.6.3"
+ "@ethersproject/abstract-provider" "^5.6.1"
+ "@ethersproject/abstract-signer" "^5.6.2"
+ "@ethersproject/address" "^5.6.1"
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/constants" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/transactions" "^5.6.2"
+
+"@ethersproject/hash@5.6.1", "@ethersproject/hash@^5.6.0", "@ethersproject/hash@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.1.tgz#224572ea4de257f05b4abf8ae58b03a67e99b0f4"
+ integrity sha512-L1xAHurbaxG8VVul4ankNX5HgQ8PNCTrnVXEiFnE9xoRnaUcgfD12tZINtDinSllxPLCtGwguQxJ5E6keE84pA==
+ dependencies:
+ "@ethersproject/abstract-signer" "^5.6.2"
+ "@ethersproject/address" "^5.6.1"
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/keccak256" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/strings" "^5.6.1"
+
+"@ethersproject/hdnode@5.6.2", "@ethersproject/hdnode@^5.6.2":
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.6.2.tgz#26f3c83a3e8f1b7985c15d1db50dc2903418b2d2"
+ integrity sha512-tERxW8Ccf9CxW2db3WsN01Qao3wFeRsfYY9TCuhmG0xNpl2IO8wgXU3HtWIZ49gUWPggRy4Yg5axU0ACaEKf1Q==
+ dependencies:
+ "@ethersproject/abstract-signer" "^5.6.2"
+ "@ethersproject/basex" "^5.6.1"
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/pbkdf2" "^5.6.1"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/sha2" "^5.6.1"
+ "@ethersproject/signing-key" "^5.6.2"
+ "@ethersproject/strings" "^5.6.1"
+ "@ethersproject/transactions" "^5.6.2"
+ "@ethersproject/wordlists" "^5.6.1"
+
+"@ethersproject/json-wallets@5.6.1", "@ethersproject/json-wallets@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.6.1.tgz#3f06ba555c9c0d7da46756a12ac53483fe18dd91"
+ integrity sha512-KfyJ6Zwz3kGeX25nLihPwZYlDqamO6pfGKNnVMWWfEVVp42lTfCZVXXy5Ie8IZTN0HKwAngpIPi7gk4IJzgmqQ==
+ dependencies:
+ "@ethersproject/abstract-signer" "^5.6.2"
+ "@ethersproject/address" "^5.6.1"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/hdnode" "^5.6.2"
+ "@ethersproject/keccak256" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/pbkdf2" "^5.6.1"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/random" "^5.6.1"
+ "@ethersproject/strings" "^5.6.1"
+ "@ethersproject/transactions" "^5.6.2"
+ aes-js "3.0.0"
+ scrypt-js "3.0.1"
+
+"@ethersproject/keccak256@5.6.1", "@ethersproject/keccak256@^5.6.0", "@ethersproject/keccak256@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.6.1.tgz#b867167c9b50ba1b1a92bccdd4f2d6bd168a91cc"
+ integrity sha512-bB7DQHCTRDooZZdL3lk9wpL0+XuG3XLGHLh3cePnybsO3V0rdCAOQGpn/0R3aODmnTOOkCATJiD2hnL+5bwthA==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ js-sha3 "0.8.0"
+
+"@ethersproject/logger@5.6.0", "@ethersproject/logger@^5.6.0":
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a"
+ integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg==
+
+"@ethersproject/networks@5.6.3", "@ethersproject/networks@^5.6.3":
+ version "5.6.3"
+ resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.3.tgz#3ee3ab08f315b433b50c99702eb32e0cf31f899f"
+ integrity sha512-QZxRH7cA5Ut9TbXwZFiCyuPchdWi87ZtVNHWZd0R6YFgYtes2jQ3+bsslJ0WdyDe0i6QumqtoYqvY3rrQFRZOQ==
+ dependencies:
+ "@ethersproject/logger" "^5.6.0"
+
+"@ethersproject/pbkdf2@5.6.1", "@ethersproject/pbkdf2@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.6.1.tgz#f462fe320b22c0d6b1d72a9920a3963b09eb82d1"
+ integrity sha512-k4gRQ+D93zDRPNUfmduNKq065uadC2YjMP/CqwwX5qG6R05f47boq6pLZtV/RnC4NZAYOPH1Cyo54q0c9sshRQ==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/sha2" "^5.6.1"
+
+"@ethersproject/properties@5.6.0", "@ethersproject/properties@^5.6.0":
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.6.0.tgz#38904651713bc6bdd5bdd1b0a4287ecda920fa04"
+ integrity sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg==
+ dependencies:
+ "@ethersproject/logger" "^5.6.0"
+
+"@ethersproject/providers@5.6.8":
+ version "5.6.8"
+ resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.6.8.tgz#22e6c57be215ba5545d3a46cf759d265bb4e879d"
+ integrity sha512-Wf+CseT/iOJjrGtAOf3ck9zS7AgPmr2fZ3N97r4+YXN3mBePTG2/bJ8DApl9mVwYL+RpYbNxMEkEp4mPGdwG/w==
+ dependencies:
+ "@ethersproject/abstract-provider" "^5.6.1"
+ "@ethersproject/abstract-signer" "^5.6.2"
+ "@ethersproject/address" "^5.6.1"
+ "@ethersproject/base64" "^5.6.1"
+ "@ethersproject/basex" "^5.6.1"
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/constants" "^5.6.1"
+ "@ethersproject/hash" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/networks" "^5.6.3"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/random" "^5.6.1"
+ "@ethersproject/rlp" "^5.6.1"
+ "@ethersproject/sha2" "^5.6.1"
+ "@ethersproject/strings" "^5.6.1"
+ "@ethersproject/transactions" "^5.6.2"
+ "@ethersproject/web" "^5.6.1"
+ bech32 "1.1.4"
+ ws "7.4.6"
+
+"@ethersproject/random@5.6.1", "@ethersproject/random@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.6.1.tgz#66915943981bcd3e11bbd43733f5c3ba5a790255"
+ integrity sha512-/wtPNHwbmng+5yi3fkipA8YBT59DdkGRoC2vWk09Dci/q5DlgnMkhIycjHlavrvrjJBkFjO/ueLyT+aUDfc4lA==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+
+"@ethersproject/rlp@5.6.1", "@ethersproject/rlp@^5.5.0", "@ethersproject/rlp@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.1.tgz#df8311e6f9f24dcb03d59a2bac457a28a4fe2bd8"
+ integrity sha512-uYjmcZx+DKlFUk7a5/W9aQVaoEC7+1MOBgNtvNg13+RnuUwT4F0zTovC0tmay5SmRslb29V1B7Y5KCri46WhuQ==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+
+"@ethersproject/sha2@5.6.1", "@ethersproject/sha2@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.1.tgz#211f14d3f5da5301c8972a8827770b6fd3e51656"
+ integrity sha512-5K2GyqcW7G4Yo3uenHegbXRPDgARpWUiXc6RiF7b6i/HXUoWlb7uCARh7BAHg7/qT/Q5ydofNwiZcim9qpjB6g==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ hash.js "1.1.7"
+
+"@ethersproject/signing-key@5.6.2", "@ethersproject/signing-key@^5.6.2":
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.2.tgz#8a51b111e4d62e5a62aee1da1e088d12de0614a3"
+ integrity sha512-jVbu0RuP7EFpw82vHcL+GP35+KaNruVAZM90GxgQnGqB6crhBqW/ozBfFvdeImtmb4qPko0uxXjn8l9jpn0cwQ==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ bn.js "^5.2.1"
+ elliptic "6.5.4"
+ hash.js "1.1.7"
+
+"@ethersproject/solidity@5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.6.1.tgz#5845e71182c66d32e6ec5eefd041fca091a473e2"
+ integrity sha512-KWqVLkUUoLBfL1iwdzUVlkNqAUIFMpbbeH0rgCfKmJp0vFtY4AsaN91gHKo9ZZLkC4UOm3cI3BmMV4N53BOq4g==
+ dependencies:
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/keccak256" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/sha2" "^5.6.1"
+ "@ethersproject/strings" "^5.6.1"
+
+"@ethersproject/strings@5.6.1", "@ethersproject/strings@^5.6.0", "@ethersproject/strings@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.1.tgz#dbc1b7f901db822b5cafd4ebf01ca93c373f8952"
+ integrity sha512-2X1Lgk6Jyfg26MUnsHiT456U9ijxKUybz8IM1Vih+NJxYtXhmvKBcHOmvGqpFSVJ0nQ4ZCoIViR8XlRw1v/+Cw==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/constants" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+
+"@ethersproject/transactions@5.6.2", "@ethersproject/transactions@^5.6.2":
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.2.tgz#793a774c01ced9fe7073985bb95a4b4e57a6370b"
+ integrity sha512-BuV63IRPHmJvthNkkt9G70Ullx6AcM+SDc+a8Aw/8Yew6YwT51TcBKEp1P4oOQ/bP25I18JJr7rcFRgFtU9B2Q==
+ dependencies:
+ "@ethersproject/address" "^5.6.1"
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/constants" "^5.6.1"
+ "@ethersproject/keccak256" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/rlp" "^5.6.1"
+ "@ethersproject/signing-key" "^5.6.2"
+
+"@ethersproject/units@5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.6.1.tgz#ecc590d16d37c8f9ef4e89e2005bda7ddc6a4e6f"
+ integrity sha512-rEfSEvMQ7obcx3KWD5EWWx77gqv54K6BKiZzKxkQJqtpriVsICrktIQmKl8ReNToPeIYPnFHpXvKpi068YFZXw==
+ dependencies:
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/constants" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+
+"@ethersproject/wallet@5.6.2":
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.6.2.tgz#cd61429d1e934681e413f4bc847a5f2f87e3a03c"
+ integrity sha512-lrgh0FDQPuOnHcF80Q3gHYsSUODp6aJLAdDmDV0xKCN/T7D99ta1jGVhulg3PY8wiXEngD0DfM0I2XKXlrqJfg==
+ dependencies:
+ "@ethersproject/abstract-provider" "^5.6.1"
+ "@ethersproject/abstract-signer" "^5.6.2"
+ "@ethersproject/address" "^5.6.1"
+ "@ethersproject/bignumber" "^5.6.2"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/hash" "^5.6.1"
+ "@ethersproject/hdnode" "^5.6.2"
+ "@ethersproject/json-wallets" "^5.6.1"
+ "@ethersproject/keccak256" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/random" "^5.6.1"
+ "@ethersproject/signing-key" "^5.6.2"
+ "@ethersproject/transactions" "^5.6.2"
+ "@ethersproject/wordlists" "^5.6.1"
+
+"@ethersproject/web@5.6.1", "@ethersproject/web@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.6.1.tgz#6e2bd3ebadd033e6fe57d072db2b69ad2c9bdf5d"
+ integrity sha512-/vSyzaQlNXkO1WV+RneYKqCJwualcUdx/Z3gseVovZP0wIlOFcCE1hkRhKBH8ImKbGQbMl9EAAyJFrJu7V0aqA==
+ dependencies:
+ "@ethersproject/base64" "^5.6.1"
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/strings" "^5.6.1"
+
+"@ethersproject/wordlists@5.6.1", "@ethersproject/wordlists@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.6.1.tgz#1e78e2740a8a21e9e99947e47979d72e130aeda1"
+ integrity sha512-wiPRgBpNbNwCQFoCr8bcWO8o5I810cqO6mkdtKfLKFlLxeCWcnzDi4Alu8iyNzlhYuS9npCwivMbRWF19dyblw==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ "@ethersproject/hash" "^5.6.1"
+ "@ethersproject/logger" "^5.6.0"
+ "@ethersproject/properties" "^5.6.0"
+ "@ethersproject/strings" "^5.6.1"
+
+"@joriksch/oz-cairo@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@joriksch/oz-cairo/-/oz-cairo-1.0.1.tgz#660d7fc9ccfb78d4a77354ff8a43150cc6a14899"
+ integrity sha512-UhDE9gCiUKtz2ryrr9a8hA24ivFdBy5cdlI4BcmRmhpRChCdfaPjI6w2xdiDwxCh/ZhC/Ij9EnG1CMLqOd5g0w==
+
+"@jridgewell/resolve-uri@^3.0.3":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe"
+ integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==
+
+"@jridgewell/sourcemap-codec@^1.4.10":
+ version "1.4.13"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c"
+ integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==
+
+"@jridgewell/trace-mapping@0.3.9":
+ version "0.3.9"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
+ integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.0.3"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+
+"@ledgerhq/cryptoassets@^6.28.2":
+ version "6.28.2"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/cryptoassets/-/cryptoassets-6.28.2.tgz#fabc77c46830348d121452976cdcc19908e0acb2"
+ integrity sha512-i+33VVNE+54HrC0mHly6JXWO6Th+/7n7vNpxjhUQq+1IL3K/ex1HUCwB61O/siDInjq7OZ1Roq9CEx7tAsED2Q==
+ dependencies:
+ invariant "2"
+
+"@ledgerhq/devices@^6.27.1":
+ version "6.27.1"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-6.27.1.tgz#3b13ab1d1ba8201e9e74a08f390560483978c962"
+ integrity sha512-jX++oy89jtv7Dp2X6gwt3MMkoajel80JFWcdc0HCouwDsV1mVJ3SQdwl/bQU0zd8HI6KebvUP95QTwbQLLK/RQ==
+ dependencies:
+ "@ledgerhq/errors" "^6.10.0"
+ "@ledgerhq/logs" "^6.10.0"
+ rxjs "6"
+ semver "^7.3.5"
+
+"@ledgerhq/errors@^6.10.0":
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.10.0.tgz#dda9127b65f653fbb2f74a55e8f0e550d69de6e4"
+ integrity sha512-fQFnl2VIXh9Yd41lGjReCeK+Q2hwxQJvLZfqHnKqWapTz68NHOv5QcI0OHuZVNEbv0xhgdLhi5b65kgYeQSUVg==
+
+"@ledgerhq/hw-app-eth@^6.26.0":
+ version "6.28.2"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-6.28.2.tgz#06fd19b9277442f4eb58ed76f4bc8299acd15a81"
+ integrity sha512-/pGJW5QKsci5mkjcUeP8RvDw4sV9gldp7RWKTKlkldqeRh2kV75bAdStL2p99fGdDfGkfYotpTfm3oOXQpOwiQ==
+ dependencies:
+ "@ethersproject/abi" "^5.5.0"
+ "@ethersproject/rlp" "^5.5.0"
+ "@ledgerhq/cryptoassets" "^6.28.2"
+ "@ledgerhq/errors" "^6.10.0"
+ "@ledgerhq/hw-transport" "^6.27.1"
+ "@ledgerhq/logs" "^6.10.0"
+ axios "^0.26.1"
+ bignumber.js "^9.0.2"
+
+"@ledgerhq/hw-transport-webhid@^6.24.1":
+ version "6.27.1"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.27.1.tgz#8fd1710d23b6bd7cbe2382dd02054dfabe788447"
+ integrity sha512-u74rBYlibpbyGblSn74fRs2pMM19gEAkYhfVibq0RE1GNFjxDMFC1n7Sb+93Jqmz8flyfB4UFJsxs8/l1tm2Kw==
+ dependencies:
+ "@ledgerhq/devices" "^6.27.1"
+ "@ledgerhq/errors" "^6.10.0"
+ "@ledgerhq/hw-transport" "^6.27.1"
+ "@ledgerhq/logs" "^6.10.0"
+
+"@ledgerhq/hw-transport@^6.24.1", "@ledgerhq/hw-transport@^6.27.1":
+ version "6.27.1"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.27.1.tgz#88072278f69c279cb6569352acd4ae2fec33ace3"
+ integrity sha512-hnE4/Fq1YzQI4PA1W0H8tCkI99R3UWDb3pJeZd6/Xs4Qw/q1uiQO+vNLC6KIPPhK0IajUfuI/P2jk0qWcMsuAQ==
+ dependencies:
+ "@ledgerhq/devices" "^6.27.1"
+ "@ledgerhq/errors" "^6.10.0"
+ events "^3.3.0"
+
+"@ledgerhq/logs@^6.10.0":
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.10.0.tgz#c012c1ecc1a0e53d50e6af381618dca5268461c1"
+ integrity sha512-lLseUPEhSFUXYTKj6q7s2O3s2vW2ebgA11vMAlKodXGf5AFw4zUoEbTz9CoFOC9jS6xY4Qr8BmRnxP/odT4Uuw==
+
+"@metamask/eth-sig-util@^4.0.0":
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088"
+ integrity sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ==
+ dependencies:
+ ethereumjs-abi "^0.6.8"
+ ethereumjs-util "^6.2.1"
+ ethjs-util "^0.1.6"
+ tweetnacl "^1.0.3"
+ tweetnacl-util "^0.15.1"
+
+"@noble/hashes@1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.1.tgz#c056d9b7166c1e7387a7453c2aff199bf7d88e5f"
+ integrity sha512-Lkp9+NijmV7eSVZqiUvt3UCuuHeJpUVmRrvh430gyJjJiuJMqkeHf6/A9lQ/smmbWV/0spDeJscscPzyB4waZg==
+
+"@noble/hashes@^0.5.7":
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-0.5.9.tgz#9f3051a4cc6f7c168022b3b7fbbe9fe2a35cccf0"
+ integrity sha512-7lN1Qh6d8DUGmfN36XRsbN/WcGIPNtTGhkw26vWId/DlCIGsYJJootTtPGghTLcn/AaXPx2Q0b3cacrwXa7OVw==
+
+"@noble/hashes@~1.1.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183"
+ integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==
+
+"@noble/secp256k1@1.6.0", "@noble/secp256k1@~1.6.0":
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.0.tgz#602afbbfcfb7e169210469b697365ef740d7e930"
+ integrity sha512-DWSsg8zMHOYMYBqIQi96BQuthZrp98LCeMNcUOaffCIVYQ5yxDbNikLF+H7jEnmNNmXbtVic46iCuVWzar+MgA==
+
+"@noble/secp256k1@^1.4.0":
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.5.5.tgz#315ab5745509d1a8c8e90d0bdf59823ccf9bcfc3"
+ integrity sha512-sZ1W6gQzYnu45wPrWx8D3kwI2/U29VYTx9OjbDAd7jwRItJ0cSTMPRL/C8AWZFn9kWFLQGqEXVEE86w4Z8LpIQ==
+
+"@nomiclabs/hardhat-docker@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-docker/-/hardhat-docker-2.0.2.tgz#ae964be17951275a55859ff7358e9e7c77448846"
+ integrity sha512-XgGEpRT3wlA1VslyB57zyAHV+oll8KnV1TjwnxxC1tpAL04/lbdwpdO5KxInVN8irMSepqFpsiSkqlcnvbE7Ng==
+ dependencies:
+ dockerode "^2.5.8"
+ fs-extra "^7.0.1"
+ node-fetch "^2.6.0"
+
+"@nomiclabs/hardhat-ethers@^2.0.5":
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.6.tgz#1c695263d5b46a375dcda48c248c4fba9dfe2fc2"
+ integrity sha512-q2Cjp20IB48rEn2NPjR1qxsIQBvFVYW9rFRCFq+bC4RUrn1Ljz3g4wM8uSlgIBZYBi2JMXxmOzFqHraczxq4Ng==
+
+"@scure/base@~1.1.0":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
+ integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
+
+"@scure/bip32@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.0.tgz#dea45875e7fbc720c2b4560325f1cf5d2246d95b"
+ integrity sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==
+ dependencies:
+ "@noble/hashes" "~1.1.1"
+ "@noble/secp256k1" "~1.6.0"
+ "@scure/base" "~1.1.0"
+
+"@scure/bip39@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a"
+ integrity sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==
+ dependencies:
+ "@noble/hashes" "~1.1.1"
+ "@scure/base" "~1.1.0"
+
+"@sentry/core@5.30.0":
+ version "5.30.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3"
+ integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==
+ dependencies:
+ "@sentry/hub" "5.30.0"
+ "@sentry/minimal" "5.30.0"
+ "@sentry/types" "5.30.0"
+ "@sentry/utils" "5.30.0"
+ tslib "^1.9.3"
+
+"@sentry/hub@5.30.0":
+ version "5.30.0"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100"
+ integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==
+ dependencies:
+ "@sentry/types" "5.30.0"
+ "@sentry/utils" "5.30.0"
+ tslib "^1.9.3"
+
+"@sentry/minimal@5.30.0":
+ version "5.30.0"
+ resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b"
+ integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==
+ dependencies:
+ "@sentry/hub" "5.30.0"
+ "@sentry/types" "5.30.0"
+ tslib "^1.9.3"
+
+"@sentry/node@^5.18.1":
+ version "5.30.0"
+ resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48"
+ integrity sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==
+ dependencies:
+ "@sentry/core" "5.30.0"
+ "@sentry/hub" "5.30.0"
+ "@sentry/tracing" "5.30.0"
+ "@sentry/types" "5.30.0"
+ "@sentry/utils" "5.30.0"
+ cookie "^0.4.1"
+ https-proxy-agent "^5.0.0"
+ lru_map "^0.3.3"
+ tslib "^1.9.3"
+
+"@sentry/tracing@5.30.0":
+ version "5.30.0"
+ resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f"
+ integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==
+ dependencies:
+ "@sentry/hub" "5.30.0"
+ "@sentry/minimal" "5.30.0"
+ "@sentry/types" "5.30.0"
+ "@sentry/utils" "5.30.0"
+ tslib "^1.9.3"
+
+"@sentry/types@5.30.0":
+ version "5.30.0"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402"
+ integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==
+
+"@sentry/utils@5.30.0":
+ version "5.30.0"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980"
+ integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==
+ dependencies:
+ "@sentry/types" "5.30.0"
+ tslib "^1.9.3"
+
+"@shardlabs/starknet-hardhat-plugin@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@shardlabs/starknet-hardhat-plugin/-/starknet-hardhat-plugin-0.6.0.tgz#94fd4ba5f7e788474ad55a5b21b7acc23b104ed6"
+ integrity sha512-rqKZqfE9uOLTMxQ44pZLVs3eAm1ylxjQGVZuHvTR292N8KwHl7IaSd2k9yORaUobFdpHxt5LQoxFPlO8+pjCoQ==
+ dependencies:
+ "@nomiclabs/hardhat-docker" "^2.0.2"
+ axios "^0.24.0"
+ exit-hook "2.2.1"
+ form-data "^4.0.0"
+ glob "^7.2.0"
+ is-wsl "^2.2.0"
+ starknet "^3.15.0"
+
+"@solidity-parser/parser@^0.14.2":
+ version "0.14.2"
+ resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.2.tgz#2d8f2bddb217621df882ceeae7d7b42ae8664db3"
+ integrity sha512-10cr0s+MtRtqjEw0WFJrm2rwULN30xx7btd/v9cmqME2617/2M5MbHDkFIGIGTa7lwNw4bN9mVGfhlLzrYw8pA==
+ dependencies:
+ antlr4ts "^0.5.0-alpha.4"
+
+"@toruslabs/starkware-crypto@^1.0.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@toruslabs/starkware-crypto/-/starkware-crypto-1.1.0.tgz#754da7f0d6529de619ded73bd7636372d3da4b5d"
+ integrity sha512-IWqri38bd6kIlj7GDhxLby2Sw9LKJ+ShbfbKUPl0CAuv2QLOjGFFL17o9m7s2CUIZ9IMmz1a+yDT9uEfQvNRKg==
+ dependencies:
+ assert "^2.0.0"
+ bip39 "^3.0.4"
+ bn.js "^5.2.0"
+ elliptic "~6.5.4"
+ enc-utils "^3.0.0"
+ ethereumjs-wallet "^1.0.2"
+ hash.js "^1.1.7"
+
+"@tsconfig/node10@^1.0.7":
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
+ integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
+
+"@tsconfig/node12@^1.0.7":
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
+ integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
+
+"@tsconfig/node14@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
+ integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
+
+"@tsconfig/node16@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
+ integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
+
+"@types/abstract-leveldown@*":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz#f055979a99f7654e84d6b8e6267419e9c4cfff87"
+ integrity sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ==
+
+"@types/bn.js@*", "@types/bn.js@^5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68"
+ integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==
+ dependencies:
+ "@types/node" "*"
+
+"@types/bn.js@^4.11.3":
+ version "4.11.6"
+ resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c"
+ integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==
+ dependencies:
+ "@types/node" "*"
+
+"@types/chai@^4.2.22":
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.1.tgz#e2c6e73e0bdeb2521d00756d099218e9f5d90a04"
+ integrity sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==
+
+"@types/elliptic@^6.4.14":
+ version "6.4.14"
+ resolved "https://registry.yarnpkg.com/@types/elliptic/-/elliptic-6.4.14.tgz#7bbaad60567a588c1f08b10893453e6b9b4de48e"
+ integrity sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==
+ dependencies:
+ "@types/bn.js" "*"
+
+"@types/level-errors@*":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/level-errors/-/level-errors-3.0.0.tgz#15c1f4915a5ef763b51651b15e90f6dc081b96a8"
+ integrity sha512-/lMtoq/Cf/2DVOm6zE6ORyOM+3ZVm/BvzEZVxUhf6bgh8ZHglXlBqxbxSlJeVp8FCbD3IVvk/VbsaNmDjrQvqQ==
+
+"@types/levelup@^4.3.0":
+ version "4.3.3"
+ resolved "https://registry.yarnpkg.com/@types/levelup/-/levelup-4.3.3.tgz#4dc2b77db079b1cf855562ad52321aa4241b8ef4"
+ integrity sha512-K+OTIjJcZHVlZQN1HmU64VtrC0jC3dXWQozuEIR9zVvltIk90zaGPM2AgT+fIkChpzHhFE3YnvFLCbLtzAmexA==
+ dependencies:
+ "@types/abstract-leveldown" "*"
+ "@types/level-errors" "*"
+ "@types/node" "*"
+
+"@types/lru-cache@^5.1.0":
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef"
+ integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==
+
+"@types/mocha@^9.0.0":
+ version "9.1.1"
+ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4"
+ integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==
+
+"@types/node@*":
+ version "17.0.34"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.34.tgz#3b0b6a50ff797280b8d000c6281d229f9c538cef"
+ integrity sha512-XImEz7XwTvDBtzlTnm8YvMqGW/ErMWBsKZ+hMTvnDIjGCKxwK5Xpc+c/oQjOauwq8M4OS11hEkpjX8rrI/eEgA==
+
+"@types/node@11.11.6":
+ version "11.11.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a"
+ integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==
+
+"@types/node@^16.11.10":
+ version "16.11.38"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.38.tgz#be0edd097b23eace6c471c525a74b3f98803017f"
+ integrity sha512-hjO/0K140An3GWDw2HJfq7gko3wWeznbjXgg+rzPdVzhe198hp4x2i1dgveAOEiFKd8sOilAxzoSJiVv5P/CUg==
+
+"@types/pbkdf2@^3.0.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/pbkdf2/-/pbkdf2-3.1.0.tgz#039a0e9b67da0cdc4ee5dab865caa6b267bb66b1"
+ integrity sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/secp256k1@^4.0.1":
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.3.tgz#1b8e55d8e00f08ee7220b4d59a6abe89c37a901c"
+ integrity sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==
+ dependencies:
+ "@types/node" "*"
+
+"@ungap/promise-all-settled@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
+ integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
+
+JSONStream@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
+ integrity sha512-mn0KSip7N4e0UDPZHnqDsHECo5uGQrixQKnAskOM1BIB8hd7QKbd6il8IPRPudPHOeHiECoCFqhyMaRO9+nWyA==
+ dependencies:
+ jsonparse "^1.2.0"
+ through ">=2.2.7 <3"
+
+abort-controller@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
+ integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+ dependencies:
+ event-target-shim "^5.0.0"
+
+abstract-leveldown@^6.2.1:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a"
+ integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==
+ dependencies:
+ buffer "^5.5.0"
+ immediate "^3.2.3"
+ level-concat-iterator "~2.0.0"
+ level-supports "~1.0.0"
+ xtend "~4.0.0"
+
+abstract-leveldown@~6.2.1:
+ version "6.2.3"
+ resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz#036543d87e3710f2528e47040bc3261b77a9a8eb"
+ integrity sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==
+ dependencies:
+ buffer "^5.5.0"
+ immediate "^3.2.3"
+ level-concat-iterator "~2.0.0"
+ level-supports "~1.0.0"
+ xtend "~4.0.0"
+
+acorn-walk@^8.1.1:
+ version "8.2.0"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
+ integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
+
+acorn@^8.4.1:
+ version "8.7.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
+ integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
+
+adm-zip@^0.4.16:
+ version "0.4.16"
+ resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365"
+ integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==
+
+aes-js@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d"
+ integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==
+
+aes-js@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a"
+ integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==
+
+agent-base@6:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+ integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+ dependencies:
+ debug "4"
+
+aggregate-error@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
+ integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
+ dependencies:
+ clean-stack "^2.0.0"
+ indent-string "^4.0.0"
+
+ansi-colors@4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+ integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
+ansi-colors@^4.1.1:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
+ integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
+
+ansi-escapes@^4.3.0:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
+ integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+ dependencies:
+ type-fest "^0.21.3"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+antlr4ts@^0.5.0-alpha.4:
+ version "0.5.0-alpha.4"
+ resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a"
+ integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==
+
+anymatch@~3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+ integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+arg@^4.1.0:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+ integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+assert@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
+ integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==
+ dependencies:
+ es6-object-assign "^1.1.0"
+ is-nan "^1.2.1"
+ object-is "^1.0.1"
+ util "^0.12.0"
+
+assertion-error@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
+ integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+
+async-eventemitter@^0.2.4:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/async-eventemitter/-/async-eventemitter-0.2.4.tgz#f5e7c8ca7d3e46aab9ec40a292baf686a0bafaca"
+ integrity sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==
+ dependencies:
+ async "^2.4.0"
+
+async@^2.4.0:
+ version "2.6.4"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
+ integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
+ dependencies:
+ lodash "^4.17.14"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+available-typed-arrays@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
+ integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
+
+axios@^0.23.0:
+ version "0.23.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.23.0.tgz#b0fa5d0948a8d1d75e3d5635238b6c4625b05149"
+ integrity sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==
+ dependencies:
+ follow-redirects "^1.14.4"
+
+axios@^0.24.0:
+ version "0.24.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
+ integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
+ dependencies:
+ follow-redirects "^1.14.4"
+
+axios@^0.26.1:
+ version "0.26.1"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
+ integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
+ dependencies:
+ follow-redirects "^1.14.8"
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base-x@^3.0.2:
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
+ integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
+ dependencies:
+ safe-buffer "^5.0.1"
+
+base64-js@^1.3.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+bech32@1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
+ integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
+
+bignumber.js@^9.0.0, bignumber.js@^9.0.2:
+ version "9.0.2"
+ resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673"
+ integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==
+
+binary-extensions@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+ integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+bip39@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0"
+ integrity sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==
+ dependencies:
+ "@types/node" "11.11.6"
+ create-hash "^1.1.0"
+ pbkdf2 "^3.0.9"
+ randombytes "^2.0.1"
+
+bl@^1.0.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7"
+ integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==
+ dependencies:
+ readable-stream "^2.3.5"
+ safe-buffer "^5.1.1"
+
+blakejs@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814"
+ integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==
+
+bluebird@^3.5.0:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+ integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+bn.js@^4.0.0, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9:
+ version "4.12.0"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
+ integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
+
+bn.js@^5.1.2:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
+ integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
+
+bn.js@^5.2.0, bn.js@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
+ integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+ integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+ dependencies:
+ balanced-match "^1.0.0"
+
+braces@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
+brorand@^1.0.1, brorand@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+ integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==
+
+browser-stdout@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+ integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+
+browserify-aes@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+ integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+bs58@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
+ integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==
+ dependencies:
+ base-x "^3.0.2"
+
+bs58check@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
+ integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==
+ dependencies:
+ bs58 "^4.0.0"
+ create-hash "^1.1.0"
+ safe-buffer "^5.1.2"
+
+buffer-alloc-unsafe@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
+ integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+
+buffer-alloc@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
+ integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+ dependencies:
+ buffer-alloc-unsafe "^1.1.0"
+ buffer-fill "^1.0.0"
+
+buffer-fill@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
+ integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==
+
+buffer-from@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+ integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+ integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==
+
+buffer-xor@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-2.0.2.tgz#34f7c64f04c777a1f8aac5e661273bb9dd320289"
+ integrity sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ==
+ dependencies:
+ safe-buffer "^5.1.1"
+
+buffer@^5.5.0, buffer@^5.6.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+ integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.1.13"
+
+bytes@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+ integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+
+cairo-ls@^0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/cairo-ls/-/cairo-ls-0.0.4.tgz#42c4eae4c1eac65eed32fe66896fcc44531ddffc"
+ integrity sha512-n/dWpvbt0g2ioQZJAneCgFe0kxMo6LncSF/GwdjffbjRJC/gCCGrYNPoa3OgKCoLwGBny+DwXiJhJY5UPV5pdw==
+ dependencies:
+ file-uri-to-path "^2.0.0"
+ glob "^7.2.0"
+ index-of-regex "^1.0.0"
+ request-promise "4.2.5"
+ vscode-languageserver "^6.1.1"
+ vscode-languageserver-textdocument "^1.0.1"
+
+call-bind@^1.0.0, call-bind@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+ integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+ dependencies:
+ function-bind "^1.1.1"
+ get-intrinsic "^1.0.2"
+
+camelcase@^6.0.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+ integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
+chai@^4.3.4:
+ version "4.3.6"
+ resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c"
+ integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==
+ dependencies:
+ assertion-error "^1.1.0"
+ check-error "^1.0.2"
+ deep-eql "^3.0.1"
+ get-func-name "^2.0.0"
+ loupe "^2.3.1"
+ pathval "^1.1.1"
+ type-detect "^4.0.5"
+
+chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chalk@^4.1.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+check-error@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
+ integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
+
+chokidar@3.5.3, chokidar@^3.4.0:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+chownr@^1.0.1:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+ integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+ci-info@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+ integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+clean-stack@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
+ integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+
+cliui@^7.0.2:
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+ integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^7.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+command-exists@^1.2.8:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
+ integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
+
+commander@3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
+ integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+concat-stream@~1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+ integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+ dependencies:
+ buffer-from "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+cookie@^0.4.1:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
+ integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
+
+core-js-pure@^3.0.1:
+ version "3.22.5"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.5.tgz#bdee0ed2f9b78f2862cda4338a07b13a49b6c9a9"
+ integrity sha512-8xo9R00iYD7TcV7OrC98GwxiUEAabVWO3dix+uyWjnYrx9fyASLlIX+f/3p5dW5qByaP2bcZ8X/T47s55et/tA==
+
+core-util-is@~1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+crc-32@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
+ integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
+
+create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+ integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ md5.js "^1.3.4"
+ ripemd160 "^2.0.1"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.4, create-hmac@^1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+ integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+create-require@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
+ integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+
+cross-fetch@^3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
+ integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
+ dependencies:
+ node-fetch "2.6.7"
+
+debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.3.3:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
+debug@^3.2.6:
+ version "3.2.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+ integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+ dependencies:
+ ms "^2.1.1"
+
+decamelize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
+ integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
+
+deep-eql@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
+ integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==
+ dependencies:
+ type-detect "^4.0.0"
+
+deferred-leveldown@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz#27a997ad95408b61161aa69bd489b86c71b78058"
+ integrity sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==
+ dependencies:
+ abstract-leveldown "~6.2.1"
+ inherits "^2.0.3"
+
+define-properties@^1.1.3, define-properties@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
+ integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==
+ dependencies:
+ has-property-descriptors "^1.0.0"
+ object-keys "^1.1.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+depd@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+ integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+diff@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
+ integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
+
+diff@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+ integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
+docker-modem@^1.0.8:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-1.0.9.tgz#a1f13e50e6afb6cf3431b2d5e7aac589db6aaba8"
+ integrity sha512-lVjqCSCIAUDZPAZIeyM125HXfNvOmYYInciphNrLrylUtKyW66meAjSPXWchKVzoIYZx69TPnAepVSSkeawoIw==
+ dependencies:
+ JSONStream "1.3.2"
+ debug "^3.2.6"
+ readable-stream "~1.0.26-4"
+ split-ca "^1.0.0"
+
+dockerode@^2.5.8:
+ version "2.5.8"
+ resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-2.5.8.tgz#1b661e36e1e4f860e25f56e0deabe9f87f1d0acc"
+ integrity sha512-+7iOUYBeDTScmOmQqpUYQaE7F4vvIt6+gIZNHWhqAQEI887tiPFB9OvXI/HzQYqfUNvukMK+9myLW63oTJPZpw==
+ dependencies:
+ concat-stream "~1.6.2"
+ docker-modem "^1.0.8"
+ tar-fs "~1.16.3"
+
+elliptic@6.5.4, elliptic@^6.5.2, elliptic@^6.5.4, elliptic@~6.5.4:
+ version "6.5.4"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
+ integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
+ dependencies:
+ bn.js "^4.11.9"
+ brorand "^1.1.0"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.1"
+ inherits "^2.0.4"
+ minimalistic-assert "^1.0.1"
+ minimalistic-crypto-utils "^1.0.1"
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+enc-utils@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/enc-utils/-/enc-utils-3.0.0.tgz#65935d2d6a867fa0ae995f05f3a2f055ce764dcf"
+ integrity sha512-e57t/Z2HzWOLwOp7DZcV0VMEY8t7ptWwsxyp6kM2b2zrk6JqIpXxzkruHAMiBsy5wg9jp/183GdiRXCvBtzsYg==
+ dependencies:
+ is-typedarray "1.0.0"
+ typedarray-to-buffer "3.1.5"
+
+encoding-down@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/encoding-down/-/encoding-down-6.3.0.tgz#b1c4eb0e1728c146ecaef8e32963c549e76d082b"
+ integrity sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==
+ dependencies:
+ abstract-leveldown "^6.2.1"
+ inherits "^2.0.3"
+ level-codec "^9.0.0"
+ level-errors "^2.0.0"
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+ integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+ dependencies:
+ once "^1.4.0"
+
+enquirer@^2.3.0:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+ integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+ dependencies:
+ ansi-colors "^4.1.1"
+
+env-paths@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
+ integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
+
+errno@~0.1.1:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
+ integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
+ dependencies:
+ prr "~1.0.1"
+
+es-abstract@^1.19.0, es-abstract@^1.19.5, es-abstract@^1.20.0:
+ version "1.20.1"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
+ integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==
+ dependencies:
+ call-bind "^1.0.2"
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ function.prototype.name "^1.1.5"
+ get-intrinsic "^1.1.1"
+ get-symbol-description "^1.0.0"
+ has "^1.0.3"
+ has-property-descriptors "^1.0.0"
+ has-symbols "^1.0.3"
+ internal-slot "^1.0.3"
+ is-callable "^1.2.4"
+ is-negative-zero "^2.0.2"
+ is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.2"
+ is-string "^1.0.7"
+ is-weakref "^1.0.2"
+ object-inspect "^1.12.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.2"
+ regexp.prototype.flags "^1.4.3"
+ string.prototype.trimend "^1.0.5"
+ string.prototype.trimstart "^1.0.5"
+ unbox-primitive "^1.0.2"
+
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+es6-object-assign@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
+ integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=
+
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-string-regexp@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+ethereum-cryptography@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191"
+ integrity sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==
+ dependencies:
+ "@types/pbkdf2" "^3.0.0"
+ "@types/secp256k1" "^4.0.1"
+ blakejs "^1.1.0"
+ browserify-aes "^1.2.0"
+ bs58check "^2.1.2"
+ create-hash "^1.2.0"
+ create-hmac "^1.1.7"
+ hash.js "^1.1.7"
+ keccak "^3.0.0"
+ pbkdf2 "^3.0.17"
+ randombytes "^2.1.0"
+ safe-buffer "^5.1.2"
+ scrypt-js "^3.0.0"
+ secp256k1 "^4.0.1"
+ setimmediate "^1.0.5"
+
+ethereum-cryptography@^0.2.0:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.2.5.tgz#dfa636f0fa1978d962a13362d0850befb9ab8e7e"
+ integrity sha512-aWvqiegXgSTwbuDE1DDnM7taLteLcHVHh5nMZnnD2dwlvH6w5bOxcdXW20oS+1aLDorDlrK1c82stB8jsLDN5Q==
+ dependencies:
+ "@noble/hashes" "^0.5.7"
+ "@noble/secp256k1" "^1.4.0"
+ micro-base "^0.10.1"
+
+ethereum-cryptography@^1.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.1.0.tgz#7048d184ff365a5255ced5cc9eb7682a273c4db7"
+ integrity sha512-wyNVTBR4wIR2yoXdMv4Qt44mTVBpPgSW/DQCTmNO6nQluwpyrAIvmL4mxPbziFuc6VWJQa3rwUxn0nUFU03nyQ==
+ dependencies:
+ "@noble/hashes" "1.1.1"
+ "@noble/secp256k1" "1.6.0"
+ "@scure/bip32" "1.1.0"
+ "@scure/bip39" "1.1.0"
+
+ethereumjs-abi@^0.6.8:
+ version "0.6.8"
+ resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz#71bc152db099f70e62f108b7cdfca1b362c6fcae"
+ integrity sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA==
+ dependencies:
+ bn.js "^4.11.8"
+ ethereumjs-util "^6.0.0"
+
+ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69"
+ integrity sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==
+ dependencies:
+ "@types/bn.js" "^4.11.3"
+ bn.js "^4.11.0"
+ create-hash "^1.1.2"
+ elliptic "^6.5.2"
+ ethereum-cryptography "^0.1.3"
+ ethjs-util "0.1.6"
+ rlp "^2.2.3"
+
+ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.4:
+ version "7.1.4"
+ resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.4.tgz#a6885bcdd92045b06f596c7626c3e89ab3312458"
+ integrity sha512-p6KmuPCX4mZIqsQzXfmSx9Y0l2hqf+VkAiwSisW3UKUFdk8ZkAt+AYaor83z2nSi6CU2zSsXMlD80hAbNEGM0A==
+ dependencies:
+ "@types/bn.js" "^5.1.0"
+ bn.js "^5.1.2"
+ create-hash "^1.1.2"
+ ethereum-cryptography "^0.1.3"
+ rlp "^2.2.4"
+
+ethereumjs-util@^7.1.5:
+ version "7.1.5"
+ resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181"
+ integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==
+ dependencies:
+ "@types/bn.js" "^5.1.0"
+ bn.js "^5.1.2"
+ create-hash "^1.1.2"
+ ethereum-cryptography "^0.1.3"
+ rlp "^2.2.4"
+
+ethereumjs-wallet@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-1.0.2.tgz#2c000504b4c71e8f3782dabe1113d192522e99b6"
+ integrity sha512-CCWV4RESJgRdHIvFciVQFnCHfqyhXWchTPlkfp28Qc53ufs+doi5I/cV2+xeK9+qEo25XCWfP9MiL+WEPAZfdA==
+ dependencies:
+ aes-js "^3.1.2"
+ bs58check "^2.1.2"
+ ethereum-cryptography "^0.1.3"
+ ethereumjs-util "^7.1.2"
+ randombytes "^2.1.0"
+ scrypt-js "^3.0.1"
+ utf8 "^3.0.0"
+ uuid "^8.3.2"
+
+ethers@^5.4.6:
+ version "5.6.8"
+ resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.8.tgz#d36b816b4896341a80a8bbd2a44e8cb6e9b98dd4"
+ integrity sha512-YxIGaltAOdvBFPZwIkyHnXbW40f1r8mHUgapW6dxkO+6t7H6wY8POUn0Kbxrd/N7I4hHxyi7YCddMAH/wmho2w==
+ dependencies:
+ "@ethersproject/abi" "5.6.3"
+ "@ethersproject/abstract-provider" "5.6.1"
+ "@ethersproject/abstract-signer" "5.6.2"
+ "@ethersproject/address" "5.6.1"
+ "@ethersproject/base64" "5.6.1"
+ "@ethersproject/basex" "5.6.1"
+ "@ethersproject/bignumber" "5.6.2"
+ "@ethersproject/bytes" "5.6.1"
+ "@ethersproject/constants" "5.6.1"
+ "@ethersproject/contracts" "5.6.2"
+ "@ethersproject/hash" "5.6.1"
+ "@ethersproject/hdnode" "5.6.2"
+ "@ethersproject/json-wallets" "5.6.1"
+ "@ethersproject/keccak256" "5.6.1"
+ "@ethersproject/logger" "5.6.0"
+ "@ethersproject/networks" "5.6.3"
+ "@ethersproject/pbkdf2" "5.6.1"
+ "@ethersproject/properties" "5.6.0"
+ "@ethersproject/providers" "5.6.8"
+ "@ethersproject/random" "5.6.1"
+ "@ethersproject/rlp" "5.6.1"
+ "@ethersproject/sha2" "5.6.1"
+ "@ethersproject/signing-key" "5.6.2"
+ "@ethersproject/solidity" "5.6.1"
+ "@ethersproject/strings" "5.6.1"
+ "@ethersproject/transactions" "5.6.2"
+ "@ethersproject/units" "5.6.1"
+ "@ethersproject/wallet" "5.6.2"
+ "@ethersproject/web" "5.6.1"
+ "@ethersproject/wordlists" "5.6.1"
+
+ethjs-util@0.1.6, ethjs-util@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536"
+ integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==
+ dependencies:
+ is-hex-prefixed "1.0.0"
+ strip-hex-prefix "1.0.0"
+
+event-target-shim@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
+ integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
+events@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+exit-hook@2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-2.2.1.tgz#007b2d92c6428eda2b76e7016a34351586934593"
+ integrity sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==
+
+fetch-intercept@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/fetch-intercept/-/fetch-intercept-2.4.0.tgz#be8b0f17abaadeea6de52ecdb6e9ed081f03480a"
+ integrity sha512-BPZ2LM9Dh1ua2ovQf03N6rhWg1qxdVD5qK/G4llvcemt6M+jjxCuIDxJ+6IiG+uz//3UQmgfKEv0gOGvYIxZ7g==
+
+file-uri-to-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba"
+ integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==
+
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
+find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+ dependencies:
+ locate-path "^2.0.0"
+
+flat@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
+ integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
+
+follow-redirects@^1.12.1:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4"
+ integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==
+
+follow-redirects@^1.14.4, follow-redirects@^1.14.8:
+ version "1.15.1"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
+ integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
+
+for-each@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
+ integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+ dependencies:
+ is-callable "^1.1.3"
+
+form-data@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+ integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
+fp-ts@1.19.3:
+ version "1.19.3"
+ resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.3.tgz#261a60d1088fbff01f91256f91d21d0caaaaa96f"
+ integrity sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==
+
+fp-ts@^1.0.0:
+ version "1.19.5"
+ resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.5.tgz#3da865e585dfa1fdfd51785417357ac50afc520a"
+ integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A==
+
+fs-constants@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+ integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
+fs-extra@^0.30.0:
+ version "0.30.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
+ integrity sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+ path-is-absolute "^1.0.0"
+ rimraf "^2.2.8"
+
+fs-extra@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
+ integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+function.prototype.name@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
+ integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.0"
+ functions-have-names "^1.2.2"
+
+functional-red-black-tree@^1.0.1, functional-red-black-tree@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+ integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
+functions-have-names@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+ integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+
+get-caller-file@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-func-name@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
+ integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=
+
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+ integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+
+get-symbol-description@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+ integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.1"
+
+glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob@7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+ integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.1.3, glob@^7.2.0:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+ version "4.2.10"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
+ integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
+
+hardhat@^2.9.9:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.10.0.tgz#23a640293d52ce79388660b2872624b2c6f94e5d"
+ integrity sha512-9VUorKvWNyW96qFXkwkpDUSeWND3gOZpm0oJ8l63JQJvWhxyxTJ92BcOrNylOKy9hzNNGdMfM2QWNP80fGOjpA==
+ dependencies:
+ "@ethereumjs/block" "^3.6.2"
+ "@ethereumjs/blockchain" "^5.5.2"
+ "@ethereumjs/common" "^2.6.4"
+ "@ethereumjs/tx" "^3.5.1"
+ "@ethereumjs/vm" "^5.9.0"
+ "@ethersproject/abi" "^5.1.2"
+ "@metamask/eth-sig-util" "^4.0.0"
+ "@sentry/node" "^5.18.1"
+ "@solidity-parser/parser" "^0.14.2"
+ "@types/bn.js" "^5.1.0"
+ "@types/lru-cache" "^5.1.0"
+ abort-controller "^3.0.0"
+ adm-zip "^0.4.16"
+ aggregate-error "^3.0.0"
+ ansi-escapes "^4.3.0"
+ chalk "^2.4.2"
+ chokidar "^3.4.0"
+ ci-info "^2.0.0"
+ debug "^4.1.1"
+ enquirer "^2.3.0"
+ env-paths "^2.2.0"
+ ethereum-cryptography "^1.0.3"
+ ethereumjs-abi "^0.6.8"
+ ethereumjs-util "^7.1.4"
+ find-up "^2.1.0"
+ fp-ts "1.19.3"
+ fs-extra "^7.0.1"
+ glob "7.2.0"
+ immutable "^4.0.0-rc.12"
+ io-ts "1.10.4"
+ lodash "^4.17.11"
+ merkle-patricia-tree "^4.2.4"
+ mnemonist "^0.38.0"
+ mocha "^10.0.0"
+ p-map "^4.0.0"
+ qs "^6.7.0"
+ raw-body "^2.4.1"
+ resolve "1.17.0"
+ semver "^6.3.0"
+ slash "^3.0.0"
+ solc "0.7.3"
+ source-map-support "^0.5.13"
+ stacktrace-parser "^0.1.10"
+ "true-case-path" "^2.2.1"
+ tsort "0.0.1"
+ undici "^5.4.0"
+ uuid "^8.3.2"
+ ws "^7.4.6"
+
+has-bigints@^1.0.1, has-bigints@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
+ integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-property-descriptors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
+ integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
+ dependencies:
+ get-intrinsic "^1.1.1"
+
+has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+ integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+has-tostringtag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
+ integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
+ dependencies:
+ has-symbols "^1.0.2"
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hash-base@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
+ integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==
+ dependencies:
+ inherits "^2.0.4"
+ readable-stream "^3.6.0"
+ safe-buffer "^5.2.0"
+
+hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+ integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.1"
+
+he@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+ integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+hmac-drbg@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+http-errors@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
+ integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
+ dependencies:
+ depd "2.0.0"
+ inherits "2.0.4"
+ setprototypeof "1.2.0"
+ statuses "2.0.1"
+ toidentifier "1.0.1"
+
+https-proxy-agent@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
+ integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
+ dependencies:
+ agent-base "6"
+ debug "4"
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+ieee754@^1.1.13:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+immediate@^3.2.3:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266"
+ integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==
+
+immediate@~3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c"
+ integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=
+
+immutable@^4.0.0-rc.12:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
+ integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
+
+indent-string@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+ integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
+index-of-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/index-of-regex/-/index-of-regex-1.0.0.tgz#3a7637b4e27665f6673a4751a20b01477ffbd3ec"
+ integrity sha1-OnY3tOJ2ZfZnOkdRogsBR3/70+w=
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+internal-slot@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
+ integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
+ dependencies:
+ get-intrinsic "^1.1.0"
+ has "^1.0.3"
+ side-channel "^1.0.4"
+
+invariant@2:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
+io-ts@1.10.4:
+ version "1.10.4"
+ resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-1.10.4.tgz#cd5401b138de88e4f920adbcb7026e2d1967e6e2"
+ integrity sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==
+ dependencies:
+ fp-ts "^1.0.0"
+
+is-arguments@^1.0.4:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
+ integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-bigint@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
+ integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
+ dependencies:
+ has-bigints "^1.0.1"
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-boolean-object@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
+ integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
+ integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
+
+is-date-object@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
+ integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-docker@^2.0.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
+ integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-generator-function@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
+ integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-hex-prefixed@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554"
+ integrity sha1-fY035q135dEnFIkTxXPggtd39VQ=
+
+is-nan@^1.2.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
+ integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
+ dependencies:
+ call-bind "^1.0.0"
+ define-properties "^1.1.3"
+
+is-negative-zero@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
+ integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
+
+is-number-object@^1.0.4:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
+ integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-obj@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+ integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
+is-regex@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
+ integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-shared-array-buffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
+ integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
+ dependencies:
+ call-bind "^1.0.2"
+
+is-string@^1.0.5, is-string@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
+ integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-symbol@^1.0.2, is-symbol@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
+ integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
+ dependencies:
+ has-symbols "^1.0.2"
+
+is-typed-array@^1.1.3, is-typed-array@^1.1.9:
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.9.tgz#246d77d2871e7d9f5aeb1d54b9f52c71329ece67"
+ integrity sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ es-abstract "^1.20.0"
+ for-each "^0.3.3"
+ has-tostringtag "^1.0.0"
+
+is-typedarray@1.0.0, is-typedarray@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-unicode-supported@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
+ integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
+
+is-weakref@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+ integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+ dependencies:
+ call-bind "^1.0.2"
+
+is-wsl@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+ integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+ dependencies:
+ is-docker "^2.0.0"
+
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+ integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
+
+isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
+js-sha3@0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
+ integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
+
+"js-tokens@^3.0.0 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ dependencies:
+ argparse "^2.0.1"
+
+json-bigint@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
+ integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
+ dependencies:
+ bignumber.js "^9.0.0"
+
+jsonfile@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug=
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+ integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonparse@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+ integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
+
+keccak@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0"
+ integrity sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==
+ dependencies:
+ node-addon-api "^2.0.0"
+ node-gyp-build "^4.2.0"
+ readable-stream "^3.6.0"
+
+klaw@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+ integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk=
+ optionalDependencies:
+ graceful-fs "^4.1.9"
+
+level-codec@^9.0.0:
+ version "9.0.2"
+ resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-9.0.2.tgz#fd60df8c64786a80d44e63423096ffead63d8cbc"
+ integrity sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==
+ dependencies:
+ buffer "^5.6.0"
+
+level-concat-iterator@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz#1d1009cf108340252cb38c51f9727311193e6263"
+ integrity sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==
+
+level-errors@^2.0.0, level-errors@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-2.0.1.tgz#2132a677bf4e679ce029f517c2f17432800c05c8"
+ integrity sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==
+ dependencies:
+ errno "~0.1.1"
+
+level-iterator-stream@~4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz#7ceba69b713b0d7e22fcc0d1f128ccdc8a24f79c"
+ integrity sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==
+ dependencies:
+ inherits "^2.0.4"
+ readable-stream "^3.4.0"
+ xtend "^4.0.2"
+
+level-mem@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/level-mem/-/level-mem-5.0.1.tgz#c345126b74f5b8aa376dc77d36813a177ef8251d"
+ integrity sha512-qd+qUJHXsGSFoHTziptAKXoLX87QjR7v2KMbqncDXPxQuCdsQlzmyX+gwrEHhlzn08vkf8TyipYyMmiC6Gobzg==
+ dependencies:
+ level-packager "^5.0.3"
+ memdown "^5.0.0"
+
+level-packager@^5.0.3:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/level-packager/-/level-packager-5.1.1.tgz#323ec842d6babe7336f70299c14df2e329c18939"
+ integrity sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==
+ dependencies:
+ encoding-down "^6.3.0"
+ levelup "^4.3.2"
+
+level-supports@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d"
+ integrity sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==
+ dependencies:
+ xtend "^4.0.2"
+
+level-ws@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/level-ws/-/level-ws-2.0.0.tgz#207a07bcd0164a0ec5d62c304b4615c54436d339"
+ integrity sha512-1iv7VXx0G9ec1isqQZ7y5LmoZo/ewAsyDHNA8EFDW5hqH2Kqovm33nSFkSdnLLAK+I5FlT+lo5Cw9itGe+CpQA==
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^3.1.0"
+ xtend "^4.0.1"
+
+levelup@^4.3.2:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.4.0.tgz#f89da3a228c38deb49c48f88a70fb71f01cafed6"
+ integrity sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==
+ dependencies:
+ deferred-leveldown "~5.3.0"
+ level-errors "~2.0.0"
+ level-iterator-stream "~4.0.0"
+ level-supports "~1.0.0"
+ xtend "~4.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
+lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+log-symbols@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
+ integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
+ dependencies:
+ chalk "^4.1.0"
+ is-unicode-supported "^0.1.0"
+
+loose-envify@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+loupe@^2.3.1:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3"
+ integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==
+ dependencies:
+ get-func-name "^2.0.0"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
+
+lru_map@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd"
+ integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=
+
+ltgt@~2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
+ integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=
+
+make-error@^1.1.1:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+ integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
+mcl-wasm@^0.7.1:
+ version "0.7.9"
+ resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-0.7.9.tgz#c1588ce90042a8700c3b60e40efb339fc07ab87f"
+ integrity sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ==
+
+md5.js@^1.3.4:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
+ integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+ safe-buffer "^5.1.2"
+
+memdown@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/memdown/-/memdown-5.1.0.tgz#608e91a9f10f37f5b5fe767667a8674129a833cb"
+ integrity sha512-B3J+UizMRAlEArDjWHTMmadet+UKwHd3UjMgGBkZcKAxAYVPS9o0Yeiha4qvz7iGiL2Sb3igUft6p7nbFWctpw==
+ dependencies:
+ abstract-leveldown "~6.2.1"
+ functional-red-black-tree "~1.0.1"
+ immediate "~3.2.3"
+ inherits "~2.0.1"
+ ltgt "~2.2.0"
+ safe-buffer "~5.2.0"
+
+memorystream@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
+ integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
+
+merkle-patricia-tree@^4.2.4:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/merkle-patricia-tree/-/merkle-patricia-tree-4.2.4.tgz#ff988d045e2bf3dfa2239f7fabe2d59618d57413"
+ integrity sha512-eHbf/BG6eGNsqqfbLED9rIqbsF4+sykEaBn6OLNs71tjclbMcMOk1tEPmJKcNcNCLkvbpY/lwyOlizWsqPNo8w==
+ dependencies:
+ "@types/levelup" "^4.3.0"
+ ethereumjs-util "^7.1.4"
+ level-mem "^5.0.1"
+ level-ws "^2.0.0"
+ readable-stream "^3.6.0"
+ semaphore-async-await "^1.5.1"
+
+micro-base@^0.10.1:
+ version "0.10.2"
+ resolved "https://registry.yarnpkg.com/micro-base/-/micro-base-0.10.2.tgz#f6f9f0bd949ce511883e5a99f9147d80ddc32f5a"
+ integrity sha512-lqqJrT7lfJtDmmiQ4zRLZuIJBk96t0RAc5pCrrWpL9zDeH5i/SUL85mku9HqzTI/OCZ8EQ3aicbMW+eK5Nyu5w==
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+ integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==
+
+minimatch@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b"
+ integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==
+ dependencies:
+ brace-expansion "^2.0.1"
+
+minimatch@^3.0.4, minimatch@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+ integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
+mkdirp@^0.5.1:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+ integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+ dependencies:
+ minimist "^1.2.6"
+
+mnemonist@^0.38.0:
+ version "0.38.5"
+ resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade"
+ integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==
+ dependencies:
+ obliterator "^2.0.0"
+
+mocha@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9"
+ integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==
+ dependencies:
+ "@ungap/promise-all-settled" "1.1.2"
+ ansi-colors "4.1.1"
+ browser-stdout "1.3.1"
+ chokidar "3.5.3"
+ debug "4.3.4"
+ diff "5.0.0"
+ escape-string-regexp "4.0.0"
+ find-up "5.0.0"
+ glob "7.2.0"
+ he "1.2.0"
+ js-yaml "4.1.0"
+ log-symbols "4.1.0"
+ minimatch "5.0.1"
+ ms "2.1.3"
+ nanoid "3.3.3"
+ serialize-javascript "6.0.0"
+ strip-json-comments "3.1.1"
+ supports-color "8.1.1"
+ workerpool "6.2.1"
+ yargs "16.2.0"
+ yargs-parser "20.2.4"
+ yargs-unparser "2.0.0"
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@2.1.3, ms@^2.1.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+nanoid@3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
+ integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
+
+node-addon-api@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
+ integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
+
+node-fetch@2.6.7, node-fetch@^2.6.0:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+ integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
+ dependencies:
+ whatwg-url "^5.0.0"
+
+node-gyp-build@^4.2.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4"
+ integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+object-inspect@^1.12.0, object-inspect@^1.9.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
+ integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
+
+object-is@^1.0.1:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
+ integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+
+object-keys@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+ integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
+ dependencies:
+ call-bind "^1.0.0"
+ define-properties "^1.1.3"
+ has-symbols "^1.0.1"
+ object-keys "^1.1.1"
+
+obliterator@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816"
+ integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+os-tmpdir@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+ integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+p-limit@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+ integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+ dependencies:
+ p-try "^1.0.0"
+
+p-limit@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+ dependencies:
+ p-limit "^1.1.0"
+
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
+p-map@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
+ integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
+ dependencies:
+ aggregate-error "^3.0.0"
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+ integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
+pako@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d"
+ integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-parse@^1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+pathval@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
+ integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
+
+pbkdf2@^3.0.17, pbkdf2@^3.0.9:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
+ integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+picomatch@^2.0.4, picomatch@^2.2.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+ integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+
+psl@^1.1.28:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+ integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+
+pump@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
+ integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+qs@^6.7.0:
+ version "6.10.3"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
+ integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
+ dependencies:
+ side-channel "^1.0.4"
+
+randombytes@^2.0.1, randombytes@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+ integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+raw-body@^2.4.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
+ integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
+ dependencies:
+ bytes "3.1.2"
+ http-errors "2.0.0"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
+readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5:
+ version "2.3.7"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+ integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.1.0, readable-stream@^3.4.0, readable-stream@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readable-stream@~1.0.26-4:
+ version "1.0.34"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+ integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+regexp.prototype.flags@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
+ integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ functions-have-names "^1.2.2"
+
+request-promise-core@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
+ integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+ dependencies:
+ lodash "^4.17.15"
+
+request-promise@4.2.5:
+ version "4.2.5"
+ resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.5.tgz#186222c59ae512f3497dfe4d75a9c8461bd0053c"
+ integrity sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==
+ dependencies:
+ bluebird "^3.5.0"
+ request-promise-core "1.1.3"
+ stealthy-require "^1.1.1"
+ tough-cookie "^2.3.3"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-from-string@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+resolve@1.17.0:
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
+ integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+ dependencies:
+ path-parse "^1.0.6"
+
+rimraf@^2.2.8:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+ integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+ dependencies:
+ glob "^7.1.3"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+ integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+rlp@^2.2.3, rlp@^2.2.4:
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf"
+ integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==
+ dependencies:
+ bn.js "^5.2.0"
+
+rustbn.js@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca"
+ integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==
+
+rxjs@6:
+ version "6.6.7"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+ integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+ dependencies:
+ tslib "^1.9.0"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+scrypt-js@3.0.1, scrypt-js@^3.0.0, scrypt-js@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312"
+ integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==
+
+secp256k1@^4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303"
+ integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==
+ dependencies:
+ elliptic "^6.5.4"
+ node-addon-api "^2.0.0"
+ node-gyp-build "^4.2.0"
+
+semaphore-async-await@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz#857bef5e3644601ca4b9570b87e9df5ca12974fa"
+ integrity sha1-hXvvXjZEYBykuVcLh+nfXKEpdPo=
+
+semver@^5.5.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+ integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+semver@^7.3.5:
+ version "7.3.7"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
+ integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
+ dependencies:
+ lru-cache "^6.0.0"
+
+serialize-javascript@6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+ integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+ dependencies:
+ randombytes "^2.1.0"
+
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+ integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.11"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+ integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+side-channel@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+ integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+ dependencies:
+ call-bind "^1.0.0"
+ get-intrinsic "^1.0.2"
+ object-inspect "^1.9.0"
+
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+ integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+solc@0.7.3:
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/solc/-/solc-0.7.3.tgz#04646961bd867a744f63d2b4e3c0701ffdc7d78a"
+ integrity sha512-GAsWNAjGzIDg7VxzP6mPjdurby3IkGCjQcM8GFYZT6RyaoUZKmMU6Y7YwG+tFGhv7dwZ8rmR4iwFDrrD99JwqA==
+ dependencies:
+ command-exists "^1.2.8"
+ commander "3.0.2"
+ follow-redirects "^1.12.1"
+ fs-extra "^0.30.0"
+ js-sha3 "0.8.0"
+ memorystream "^0.3.1"
+ require-from-string "^2.0.0"
+ semver "^5.5.0"
+ tmp "0.0.33"
+
+source-map-support@^0.5.13:
+ version "0.5.21"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+ integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+split-ca@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6"
+ integrity sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=
+
+stacktrace-parser@^0.1.10:
+ version "0.1.10"
+ resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a"
+ integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==
+ dependencies:
+ type-fest "^0.7.1"
+
+starknet@3.9.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/starknet/-/starknet-3.9.0.tgz#ee7a3d99effef0998b365d95dd85f9b01cdb463c"
+ integrity sha512-/sn3WHFX7f+A3vVf3I+Kamg2KIW6494vpCcZWA09IQ5RZXrozjaBtYWT41SoKKWZ3b0Xg8WF//SOkRVCTZOZkQ==
+ dependencies:
+ "@ledgerhq/hw-app-eth" "^6.26.0"
+ "@ledgerhq/hw-transport" "^6.24.1"
+ "@ledgerhq/hw-transport-webhid" "^6.24.1"
+ axios "^0.23.0"
+ bn.js "^5.2.0"
+ elliptic "^6.5.4"
+ ethereum-cryptography "^0.2.0"
+ hash.js "^1.1.7"
+ json-bigint "^1.0.0"
+ minimalistic-assert "^1.0.1"
+ pako "^2.0.4"
+ superstruct "^0.15.3"
+ url-join "^4.0.1"
+
+starknet@^3.15.0:
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/starknet/-/starknet-3.15.0.tgz#3f9ab20c6d6a094b33272f971f01fc5fa9b92c2a"
+ integrity sha512-+wqAHky7ffTpyJGGSLkzVt8c2FrMyu/8R1cAB3n++VyuBuTTS9+qD8SLXhXWJiBxCzlznP8KkdWQC9N0SCTokw==
+ dependencies:
+ "@ethersproject/bytes" "^5.6.1"
+ bn.js "^5.2.1"
+ cross-fetch "^3.1.5"
+ elliptic "^6.5.4"
+ ethereum-cryptography "^1.0.3"
+ fetch-intercept "^2.4.0"
+ hash.js "^1.1.7"
+ json-bigint "^1.0.0"
+ minimalistic-assert "^1.0.1"
+ pako "^2.0.4"
+ url-join "^4.0.1"
+
+statuses@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
+ integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+
+stealthy-require@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+ integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+string-width@^4.1.0, string-width@^4.2.0:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string.prototype.trimend@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0"
+ integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
+
+string.prototype.trimstart@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef"
+ integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+ integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-hex-prefix@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f"
+ integrity sha1-DF8VX+8RUTczd96du1iNoFUA428=
+ dependencies:
+ is-hex-prefixed "1.0.0"
+
+strip-json-comments@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+superstruct@^0.15.3:
+ version "0.15.4"
+ resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.4.tgz#e3381dd84ca07e704e19f69eda74eee1a5efb1f9"
+ integrity sha512-eOoMeSbP9ZJChNOm/9RYjE+F36rYR966AAqeG3xhQB02j2sfAUXDp4EQ/7bAOqnlJnuFDB8yvOu50SocvKpUEw==
+
+supports-color@8.1.1:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+ integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+tar-fs@~1.16.3:
+ version "1.16.3"
+ resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509"
+ integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==
+ dependencies:
+ chownr "^1.0.1"
+ mkdirp "^0.5.1"
+ pump "^1.0.0"
+ tar-stream "^1.1.2"
+
+tar-stream@^1.1.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
+ integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
+ dependencies:
+ bl "^1.0.0"
+ buffer-alloc "^1.2.0"
+ end-of-stream "^1.0.0"
+ fs-constants "^1.0.0"
+ readable-stream "^2.3.0"
+ to-buffer "^1.1.1"
+ xtend "^4.0.0"
+
+"through@>=2.2.7 <3":
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+ integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+tmp@0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+to-buffer@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
+ integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+toidentifier@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+ integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
+tough-cookie@^2.3.3:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+ integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
+
+"true-case-path@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-2.2.1.tgz#c5bf04a5bbec3fd118be4084461b3a27c4d796bf"
+ integrity sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q==
+
+ts-node@^10.4.0:
+ version "10.8.0"
+ resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.0.tgz#3ceb5ac3e67ae8025c1950626aafbdecb55d82ce"
+ integrity sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==
+ dependencies:
+ "@cspotcode/source-map-support" "^0.8.0"
+ "@tsconfig/node10" "^1.0.7"
+ "@tsconfig/node12" "^1.0.7"
+ "@tsconfig/node14" "^1.0.0"
+ "@tsconfig/node16" "^1.0.2"
+ acorn "^8.4.1"
+ acorn-walk "^8.1.1"
+ arg "^4.1.0"
+ create-require "^1.1.0"
+ diff "^4.0.1"
+ make-error "^1.1.1"
+ v8-compile-cache-lib "^3.0.1"
+ yn "3.1.1"
+
+tslib@^1.9.0, tslib@^1.9.3:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tsort@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786"
+ integrity sha1-4igPXoF/i/QnVlf9D5rr1E9aJ4Y=
+
+tweetnacl-util@^0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b"
+ integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==
+
+tweetnacl@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
+ integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
+
+type-detect@^4.0.0, type-detect@^4.0.5:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
+type-fest@^0.21.3:
+ version "0.21.3"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+ integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
+type-fest@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48"
+ integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==
+
+typedarray-to-buffer@3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+ integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+ dependencies:
+ is-typedarray "^1.0.0"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+ integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+typescript@^4.5.2:
+ version "4.7.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4"
+ integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==
+
+unbox-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
+ integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
+ dependencies:
+ call-bind "^1.0.2"
+ has-bigints "^1.0.2"
+ has-symbols "^1.0.3"
+ which-boxed-primitive "^1.0.2"
+
+undici@^5.4.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/undici/-/undici-5.6.0.tgz#3fd695d4454970bae3d151326ee4ab645b8d1962"
+ integrity sha512-mc+8SY1fXubTrdx4CXDkeFFGV8lI3Tq4I/70U1V8Z6g4iscGII0uLO7CPnDt56bXEbvaKwo2T2+VrteWbZiXiQ==
+
+universalify@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+ integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+unpipe@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+url-join@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
+ integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
+
+utf8@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
+ integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util@^0.12.0:
+ version "0.12.4"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253"
+ integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==
+ dependencies:
+ inherits "^2.0.3"
+ is-arguments "^1.0.4"
+ is-generator-function "^1.0.7"
+ is-typed-array "^1.1.3"
+ safe-buffer "^5.1.2"
+ which-typed-array "^1.1.2"
+
+uuid@^8.3.2:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+v8-compile-cache-lib@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
+ integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
+
+vscode-jsonrpc@8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz#f30b0625ebafa0fb3bc53e934ca47b706445e57e"
+ integrity sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ==
+
+vscode-languageserver-protocol@^3.15.3:
+ version "3.17.1"
+ resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz#e801762c304f740208b6c804a0cf21f2c87509ed"
+ integrity sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg==
+ dependencies:
+ vscode-jsonrpc "8.0.1"
+ vscode-languageserver-types "3.17.1"
+
+vscode-languageserver-textdocument@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157"
+ integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==
+
+vscode-languageserver-types@3.17.1:
+ version "3.17.1"
+ resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16"
+ integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ==
+
+vscode-languageserver@^6.1.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz#d76afc68172c27d4327ee74332b468fbc740d762"
+ integrity sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==
+ dependencies:
+ vscode-languageserver-protocol "^3.15.3"
+
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+ integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+ integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
+which-boxed-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+ integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+ dependencies:
+ is-bigint "^1.0.1"
+ is-boolean-object "^1.1.0"
+ is-number-object "^1.0.4"
+ is-string "^1.0.5"
+ is-symbol "^1.0.3"
+
+which-typed-array@^1.1.2:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f"
+ integrity sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ es-abstract "^1.20.0"
+ for-each "^0.3.3"
+ has-tostringtag "^1.0.0"
+ is-typed-array "^1.1.9"
+
+workerpool@6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
+ integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
+
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+ws@7.4.6:
+ version "7.4.6"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
+ integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
+
+ws@^7.4.6:
+ version "7.5.7"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67"
+ integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
+
+xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+ integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y18n@^5.0.5:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+ integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yargs-parser@20.2.4:
+ version "20.2.4"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
+ integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
+
+yargs-parser@^20.2.2:
+ version "20.2.9"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+ integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
+yargs-unparser@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
+ integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
+ dependencies:
+ camelcase "^6.0.0"
+ decamelize "^4.0.0"
+ flat "^5.0.2"
+ is-plain-obj "^2.1.0"
+
+yargs@16.2.0:
+ version "16.2.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+ integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+ dependencies:
+ cliui "^7.0.2"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.0"
+ y18n "^5.0.5"
+ yargs-parser "^20.2.2"
+
+yn@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+ integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..a56da31f5
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,16 @@
+# Chainlink Starknet Documentation
+
+This repository is a monorepo of the various components required for Chainlink on Starknet.
+
+- [Contracts](../contracts)
+
+- [Gauntlet & TS toolchain](../packages-ts)
+
+- [Ops](../ops)
+- [Integration Tests](../integration-tests)
+- [Demos & Examples](../examples)
+
+Additional documentation can be found in this directory.
+
+- [Getting Started](./getting-started.md)
+- [L2 Emergency Protocol](./emergency-protocol/)
diff --git a/docs/artifacts.md b/docs/artifacts.md
new file mode 100644
index 000000000..216a336a4
--- /dev/null
+++ b/docs/artifacts.md
@@ -0,0 +1,19 @@
+# Artifacts
+
+## A Note on Starknet Artifacts
+
+In `starknet.js` v6.7.0, it is necessary to supply both the sierra and sierra casm compilation outputs to a declare transaction as shown [here](https://www.starknetjs.com/docs/next/guides/create_contract#declare-for-a-new-class).
+
+The [`declare`](https://github.com/starknet-io/starknet.js/blob/a85d48ee73acb1365da6bef3f9d3a65153f9a422/src/account/default.ts#L393) method uses the [`artifact` field to compute the `class_hash`](https://github.com/starknet-io/starknet.js/blob/a85d48ee73acb1365da6bef3f9d3a65153f9a422/src/utils/contract.ts#L38), and the [`casm` field to compute the `compiled_class_hash`](https://github.com/starknet-io/starknet.js/blob/a85d48ee73acb1365da6bef3f9d3a65153f9a422/src/utils/contract.ts#L30).
+
+For V2 and V3 declare transactions, both the `class_hash` and `compiled_class_hash` are required to construct the tx hash:
+
+- [V3 declare transaction docs](https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/transactions/#v3_hash_calculation_2)
+- [V2 declare transaction docs](https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/transactions/#v2_deprecated_hash_calculation)
+ - Side note: even though V2 is deprecated, I'm still including it bc [`starknet.js` defaults to transaction version 2 for backwards compatibility in v6.7.0](https://github.com/starknet-io/starknet.js/blob/a85d48ee73acb1365da6bef3f9d3a65153f9a422/src/account/default.ts#L75)
+
+If you attempt to pass only the casm artifact alone or the sierra artifact alone, this causes an error like the one below:
+
+```sh
+"Extract compiledClassHash failed, provide (CairoAssembly).casm file or compiledClassHash"
+```
diff --git a/docs/assets/godepgraph_starknet_relayer.png b/docs/assets/godepgraph_starknet_relayer.png
new file mode 100644
index 000000000..9d60c82a7
Binary files /dev/null and b/docs/assets/godepgraph_starknet_relayer.png differ
diff --git a/docs/emergency-protocol/README.md b/docs/emergency-protocol/README.md
new file mode 100644
index 000000000..21272c81e
--- /dev/null
+++ b/docs/emergency-protocol/README.md
@@ -0,0 +1,161 @@
+# L2 Emergency Protocol - Starknet
+
+## Overview
+
+Today Chainlink price feeds are used by many DeFi protocols to secure billions of dollars. Whilst feeds report fresh prices the majority of the time, L2 feeds will report stale price data whenever the L2 chain stops producing new blocks. This can happen whenever the L2 Sequencer fails to process any new transactions. Whenever this happens, an arbitrage opportunity is created for malicious actors to take advantage of the price difference between the price inside and outside the L2 chain.
+
+The Starknet Emergency Protocol provides a way for Chainlink price feed consumers to guard against the scenario described above. The protocol tracks the last known health of the Sequencer and reports its health on chain along with the timestamp of when it either comes back online or goes offline. This allows consuming contracts to implement a grace period in their contracts to revert transactions whenever the Sequencer is down.
+
+For more background information check the [official docs.](https://docs.chain.link/docs/l2-sequencer-flag/)
+
+**WARNING:** The current implementation of the protocol supports health status detection for Starknet **centralized** Sequencer architecture. As Starknet plans to decentralize the Sequencer in the future, this protocol will either need to be redesigned and reevaluated. The reason for this is that the current protocol relies on polling the `pending` block from the Sequencer to determine if new transactions are being added. A decentralized Starknet sequencer will no longer allow this hence breaking the protocol. [The documentation](https://docs.starknet.io/documentation/develop/Blocks/transaction-life-cycle/#the_pending_block) states:
+
+> Today, Starknet supports querying the new block before its construction is complete. This feature improves the responsiveness of the system prior to the decentralization phase, but will probably become obsolete once the system is decentralized, as full nodes will only propagate finalized blocks through the network.
+
+For more information on how the pending block is used, take a look at the [Layer2 Sequencer Health External Adapter](#layer2-sequencer-health-external-adapter) section.
+
+## Architecture
+
+The diagram above illustrates the general path of how the Sequencer’s status is relayed from L1 to L2.
+
+[![](https://mermaid.ink/img/pako:eNqNk99PwjAQx_-VprxCwo8YtSQmOtgTCQaiL46H2l1HQ9fOrosSwv9u59aBzBH3svXu8737Xi87YKZjwARzqT_ZlhqLFqtIIfcwSfN8Bhy5sFBSqB3iQkrSm0wmQRhOUSfGtNSG9DjnLSi31OwU2LrU03B0G46nXdCVQkyrvEjB1IXC-9Hd8GbaBbULLYMVIeTkeTB4QI9JYiCh1qFnqYpf155cxtsrJWv4KEAxMGfxWuAzP6W9_CWzIoUQIK6ovHhPDM22aDFumCoT1N7d2xrKysZ-nAr47fjZ6K992_bfvdscqEtDXoWWnAclfDFWl24xQnO7BQNFWmWu3aqz90qliP85QcNexzqtQdIEAy8PdJppBcq-NaFN9_pPtI9sWpv3m2ttzDXHfeyOKRWx--cOZTjC7rZSiDBxn7GrE-FIHR1XZG5UmMfCzYuJNQX0MS2sXu8V8-eKmQnqBkwx4VTmcPwGV_dK6g)](https://mermaid.live/edit#pako:eNqNk99PwjAQx_-VprxCwo8YtSQmOtgTCQaiL46H2l1HQ9fOrosSwv9u59aBzBH3svXu8737Xi87YKZjwARzqT_ZlhqLFqtIIfcwSfN8Bhy5sFBSqB3iQkrSm0wmQRhOUSfGtNSG9DjnLSi31OwU2LrU03B0G46nXdCVQkyrvEjB1IXC-9Hd8GbaBbULLYMVIeTkeTB4QI9JYiCh1qFnqYpf155cxtsrJWv4KEAxMGfxWuAzP6W9_CWzIoUQIK6ovHhPDM22aDFumCoT1N7d2xrKysZ-nAr47fjZ6K992_bfvdscqEtDXoWWnAclfDFWl24xQnO7BQNFWmWu3aqz90qliP85QcNexzqtQdIEAy8PdJppBcq-NaFN9_pPtI9sWpv3m2ttzDXHfeyOKRWx--cOZTjC7rZSiDBxn7GrE-FIHR1XZG5UmMfCzYuJNQX0MS2sXu8V8-eKmQnqBkwx4VTmcPwGV_dK6g)
+
+### Contracts
+
+- L1 Ethereum (Solditiy):
+ - [StarknetValidator.sol](https://github.com/smartcontractkit/chainlink-starknet/blob/develop/contracts/solidity/emergency/StarknetValidator.sol)
+- L2 Starknet (Cairo):
+ - [SequencerUptimeFeed.cairo](https://github.com/smartcontractkit/chainlink-starknet/blob/develop/contracts/src/chainlink/cairo/emergency/SequencerUptimeFeed/sequencer_uptime_feed.cairo)
+
+**L1**
+
+1. The EA is run by a network of Node operators to post the latest sequencer status to the `Aggregator` contract and relayed to the `ValidatorProxy` contract. The `Aggregator` contract then calls the `validate` function in the `ValidatorProxy` contract, which proxies the call to the `StarknetValidator` contract.
+2. The `StarknetValidator` then calls the `sendMessageToL2` function on the `Starknet` contract. This message will contain instructions to call the `updateStatus(bool status, uint64 timestamp)` function in the `StarknetSequencerUptimeFeed` contract deployed on L2
+3. The core `Starknet` contract then emits a new `LogMessageToL2` event to to signal that a new message needs to be sent from L1 to L2.
+
+```javascript
+event LogMessageToL2(
+ address indexed fromAddress,
+ uint256 indexed toAddress,
+ uint256 indexed selector,
+ uint256[] payload,
+ uint256 nonce
+);
+```
+
+4. The `Sequencer` will then pickup the `LogMessageToL2` event emitted above and forward the message to the target contract on L2.
+
+**L2**
+
+1. The Sequencer posts the message to the `starknet_sequencer_uptime_feed` contract and calls the `update_status` function to update the Sequencer status.
+2. Consumers can then read from the `aggregator_proxy` contract, which fetches the latest round data from the `starknet_sequencer_uptime_feed` contract.
+
+## Sequencer Downtime
+
+### L1 → L2 Transactions
+
+In the event that the Sequencer is down, messages will not be transmitted from L1 to L2 and **no L2 transactions are executed**. Instead messages will be enqueued in the Sequencer and only processed in the order they arrived later once the Sequencer comes back up. This means that as long as the message from the `StarknetValidator` on L1 is already enqueued in the Starknet Sequencer, the flag on the `starknet_sequencer_uptime_feed` on L2 will be guaranteed to be flipped prior to any subsequent transactions. This happens as the transaction flipping the flag on the uptime feed will get executed before transactions that were enqueued after it. This is further explained in the diagrams below.
+
+**During Sequencer downtime**
+
+- New `LogMessageToL2` events emitted are not picked up whilst the Sequencer is down.
+- When the Sequencer is down, all L2 transactions sent from L1 are stuck in the pending queue, which lives in Starknet’s centralized Sequencer.
+- **Tx1** contains Chainlink’s transaction to set the status of the Sequencer as being down on L2.
+- **Tx2** is a transaction made by a consumer that is dependent on
+
+[![](https://mermaid.ink/img/pako:eNo1jrEOwjAMRH8l8twFxsywMQBlzOImbhPRJMWNBajqvxNU1dO709PJC9jsCDT0Y35bj1zU5W6SqjdLNzBOXrX0EkqWWF0puZAGdRMS2qzH57DDcYPqQAOROGJwdXn51waKp0gGdEWH_DRg0lo9mRwWOrtQMoMuLNQASsntN9k9b84pYP0ngu5xnGn9ATwDPgo)](https://mermaid.live/edit#pako:eNo1jrEOwjAMRH8l8twFxsywMQBlzOImbhPRJMWNBajqvxNU1dO709PJC9jsCDT0Y35bj1zU5W6SqjdLNzBOXrX0EkqWWF0puZAGdRMS2qzH57DDcYPqQAOROGJwdXn51waKp0gGdEWH_DRg0lo9mRwWOrtQMoMuLNQASsntN9k9b84pYP0ngu5xnGn9ATwDPgo)
+
+**After Sequencer comes back online**
+
+- `LogMessageToL2` events are picked up and added to the pending queue.
+- Transactions in the pending queue are processed chronologically so **Tx1** is processed before **Tx2.**
+- As **Tx1** happens before **Tx2, Tx2** will read the status of the Sequencer as being down
+
+### Bridge Fees
+
+As of writing, on version v0.11.0, Starknet has begun charging mandatory fees to send messages from L1 to L2. These fees are used to pay for the transaction
+on L2. As the Emergency Protocol needs to send messages cross chain,
+the protocol needs a way to estimate gas fees. Currently, the `StarkwareValidator` contract on L1 does the following to estimate the amount of required
+gas.
+
+1. Estimate gas fees by running the command below. The command is from Starkware's standard CLI (using version 0.11.0.x)
+
+```
+starknet estimate_message_fee \
+ --feeder_gateway_url=https://alpha4.starknet.io/feeder_gateway/
+ --from_address ${L1_SENDER_ADDR} \
+ --address ${UPTIME_FEED_ADDR} \
+ --function update_status \
+ --inputs ${STATUS} ${TIMESTAMP}
+```
+
+Make sure that the `L1_SENDER_ADDR` is equal to the l1 sender storage variable on the uptime feed, or else the gateway will respond with a revert instead of the values. If you don't set the l1 sender storage variable, it'll be 0 by default (as in the example below)
+
+Example Query and response:
+
+```
+starknet estimate_message_fee \
+ --feeder_gateway_url=https://alpha4.starknet.io/feeder_gateway/ \
+ --from_address 0x0 \
+ --address=0x06f4279f832de1afd94ab79aa1766628d2c1e70bc7f74bfba3335db8e728a7e6 \
+ --function update_status \
+ --inputs 0x1 123123
+
+The estimated fee is: 3739595758116898 WEI (0.003740 ETH).
+Gas usage: 17266
+Gas price: 216587267353 WEI
+```
+
+In order to reliably ensure that cross chain messages are sent with sufficient gas, the estimate is multiplied by a buffer. At the time of writing (Starknet v.0.11.0), Starkware has told us that L2 gas prices are equal to L1 gas prices and are denominated in Ethereum Wei, so we use L1 gas price feed to get the gas price:
+
+1. Read the current L1 gas price from Chainlink's L1 gas price feed
+2. Multiply gas price by a buffer
+3. Multiply product of above by the number of gas units
+
+```solidity
+gasFee = buffer * l1GasPrice * numGasUnits
+```
+
+The gas units that it costs is also derived from the starknet estimate_message_fee command (as shown above).
+
+As of the time of writing (Starknet v. 0.11.0), we recommend a gasAdjustment of 130 (or 1.3x buffer) and a gas units to be 17300.
+
+### Layer2 Sequencer Health External Adapter
+
+[Code](https://github.com/smartcontractkit/external-adapters-js/tree/develop/packages/sources/layer2-sequencer-health)
+
+The emergency protocol requires an off chain component to tracks the health of the centralized Starkware sequencer. Today, this is made up by a DON (Decentralized Oracle Network) that triggers using OCR (Offchain Reporting). A new OCR round is initiated every 30s whereby each node in the DON checks the health of the Sequencer using the Layer2 Sequencer Health External Adapter. If the nodes in the DON determine that the Sequencer’s health has changed, they elect a new leader to write the updated result onto chain as shown in the diagram above.
+
+**How the External Adapter Works**
+
+Checking the Starkware Sequencer’s health is currently a two step process
+
+1. Call the Sequencer directly to fetch the pending block’s details.
+ 1. Verify that a new block has been produced within 2 minutes by checking the pending block’s `parentHash`
+ 2. If the pending block’s `parentHash` has not changed, then check the length of the `transactions` field to see if it has increased since the last round
+2. Send an empty transaction to a dummy contract at address `0x00000000000000000000000000000000000000000000000000000000000001`
+
+ The EA sends the empty transaction using the StarknetJS library. This transaction tries to call the dummy contract’s `initialize` function with a `maxFee` of 0
+
+ ```javascript
+ const DUMMY_ADDRESS = '0x00000000000000000000000000000000000000000000000000000000000001'
+ const DEFAULT_PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001'
+ const starkKeyPair = ec.genKeyPair(DEFAULT_PRIVATE_KEY)
+ const starkKeyPub = ec.getStarkKey(starkKeyPair)
+ const provider = config.starkwareConfig.provider
+ const account = new Account(provider, DUMMY_ADDRESS, starkKeyPair)
+
+ account.execute(
+ {
+ contractAddress: DUMMY_ADDRESS,
+ entrypoint: 'initialize',
+ calldata: [starkKeyPub, '0'],
+ },
+ undefined,
+ { maxFee: '0' },
+ )
+ ```
+
+3. As the above transaction is expected to fail, the EA will consider the Sequencer as healthy if it receives any of the expected error statuses
+ 1. `StarknetErrorCode.UNINITIALIZED_CONTRACT` if the dummy contract has not been initialized
+ 2. `StarknetErrorCode.OUT_OF_RANGE_FEE` if the dummy contract has been initialized by accident. As Starknet is a permissionless network, we cannot guarantee that a user deploys and initializes a contract at the dummy address. As a result, the EA will set the `maxFee` to 0 so that the transaction will fail with the `StarknetErrorCode.OUT_OF_RANGE_FEE` status code.
diff --git a/docs/gauntlet/README.md b/docs/gauntlet/README.md
index 2c0e03871..dc05f6e2d 100644
--- a/docs/gauntlet/README.md
+++ b/docs/gauntlet/README.md
@@ -4,10 +4,12 @@
- [Local Install](./getting_started.md#setup)
- [Binary](./getting_started.md#binary)
- [Basic Setup](./getting_started.md#basic-setup)
- - [CLI](../../packages-ts/gauntlet-starknet-cli/README.md)
- - [Example Contract](../../packages-ts/gauntlet-starknet-example/README.md)
- - [OZ Contracts](../../packages-ts/gauntlet-starknet-oz/README.md)
- - [Starkgate Contracts](../../packages-ts/gauntlet-starknet-starkgate/README.md)
- - [Argent Contracts](../../packages-ts/gauntlet-starknet-argent/README.md)
- - [OCR2 Contracts](../../packages-ts/gauntlet-starknet-ocr2/README.md)
+ - [CLI](../../packages-ts/starknet-gauntlet-cli/README.md)
+ - [Example Contract](../../packages-ts/starknet-gauntlet-example/README.md)
+ - [OZ Contracts](../../packages-ts/starknet-gauntlet-oz/README.md)
+ - [Starkgate Contracts](../../packages-ts/starknet-gauntlet-token/README.md)
+ - [Emergency Protocol Contracts](../../packages-ts/starknet-gauntlet-emergency-protocol/README.md)
+ - [Argent Contracts](../../packages-ts/starknet-gauntlet-argent/README.md)
+ - [OCR2 Contracts](../../packages-ts/starknet-gauntlet-ocr2/README.md)
+ - [Multisig Contracts](../../packages-ts/starknet-gauntlet-multisig/README.md)
- Contribute
diff --git a/docs/gauntlet/getting_started.md b/docs/gauntlet/getting_started.md
index 455ce0ee3..64b785d99 100644
--- a/docs/gauntlet/getting_started.md
+++ b/docs/gauntlet/getting_started.md
@@ -68,6 +68,6 @@ ACCOUNT=0x...
PRIVATE_KEY=0x...
```
-In order to get this configuration, go to [how to setup an account](../../packages-ts/gauntlet-starknet-account/README.md#setup-an-account)
+In order to get this configuration, go to [how to setup an account](../../packages-ts/starknet-gauntlet-account/README.md#setup-an-account)
If you are interacting with a local network and do not want to use any wallet, execute every command with the flag `--noWallet`
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 000000000..a0a943b45
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,30 @@
+# Getting Started
+
+## Local setup
+
+```bash
+make install
+```
+
+## Go Modules Dependency
+
+```mermaid
+flowchart LR
+ subgraph ops
+ test-helpers
+ k8s
+ end
+
+ subgraph relayer
+ contract-bindings
+ end
+
+ subgraph integration-tests
+ smoke
+ soak
+ end
+
+ contract-bindings --> integration-tests
+ test-helpers --> relayer
+ k8s --> integration-tests
+```
diff --git a/docs/integration-tests/README.md b/docs/integration-tests/README.md
index 7c845f5e2..779379121 100644
--- a/docs/integration-tests/README.md
+++ b/docs/integration-tests/README.md
@@ -1,3 +1,249 @@
## Integration tests
-See examples [here](testing.md)
+### Run tests
+
+### Prerequisites
+
+1. `yarn install`
+2. `yarn build`
+
+#### Smoke
+
+`cd integration-tests/smoke/ && go test --timeout=2h -v` (from core of repo)
+
+#### Soak
+
+Soak tests will run a modified version of the smoke test via a remote runner for the set period. The difference is that
+there is no panic when an
+error appears, but instead log it.
+
+##### Run
+
+`make test-integration-soak`
+
+##### Env vars
+
+`TTL=72h` - duration of soak
+
+`NODE_COUNT` - number of OCR nodes
+
+`CHAINLINK_IMAGE` - Chainlink docker image repo
+
+`CHAINLINK_VERSION` - Chainlink docker image version
+
+`L2_RPC_URL` - This will override the L2 url, used for testnet (optional)
+
+`PRIVATE_KEY` - Private key for Testnet (optional)
+
+`ACCOUNT` - Account address on Testnet (optional)
+
+### Structure
+
+[Commons](../../integration-tests/common/common.go) - Common Chainlink methods to generate chains, nodes, key bundles
+
+[Test Commons](../../integration-tests/common/test_common.go) - Test methods to deploy env, configure clients, fetch
+client details
+
+[Starknet Commons](../../ops/devnet/devnet.go) - Methods related to starknet and L2 actions such as minting, L1<>L2 sync
+
+[Gauntlet wrapper](../../relayer/pkg/starknet/gauntlet_starknet.go) - Wrapper for Starknet gauntlet
+
+[OCRv2 tests](../../integration-tests/smoke/ocr2_test.go) - Example smoke test to set up environment, configure it and
+run the smoke test
+
+### Writing tests
+
+See smoke examples [here](../../integration-tests/smoke/ocr2_test.go)
+
+See soak examples [here](../../integration-tests/soak/tests/ocr_test.go)
+and [here](../../integration-tests/soak/soak_runner_test.go)
+
+1. Instantiate Gauntlet
+2. Deploy Cluster
+3. Set Gauntlet network
+4. Deploy accounts on L2 for the nodes
+5. Fund the accounts
+6. Deploy L2 LINK token via Gauntlet
+7. Deploy L2 Access controller contract via Gauntlet
+8. Deploy L2 OCR2 contract via Gauntlet
+9. Set OCR2 billing via Gauntlet
+10. Set OCR2 config details via Gauntlet
+11. Set up boostrap and oracle nodes
+
+### Metrics and logs (K8)
+
+1. Navigate to Grafana
+2. Search for `chainlink-testing-insights` dashboard
+3. Select the starknet namespace
+
+Here you will find pod logs for all the chainlink nodes as well as Devnet / Geth
+
+# Testing wiki
+
+## Testnet
+
+- Chain name - `Starknet`
+- Chain ID - `SN_SEPOLIA`
+ - Testnet 1 - `[https://alpha4.starknet.io](https://alpha4.starknet.io)`
+ - Testnet 2 - [`https://alpha4-2.starknet.io`](https://alpha4-2.starknet.io/)
+
+## Mainnet
+
+- Chain name - `Starknet`
+- Chain ID - `SN_MAIN`
+ - `[https://alpha-mainnet.starknet.io](https://alpha-mainnet.starknet.io)`
+
+# Node config
+
+```bash
+[[Starknet]]
+Enabled = true
+ChainID = ''
+[[Starknet.Nodes]]
+Name = 'primary'
+URL = ''
+
+[OCR2]
+Enabled = true
+
+[P2P]
+[P2P.V2]
+Enabled = true
+DeltaDial = '5s'
+DeltaReconcile = '5s'
+ListenAddresses = ['0.0.0.0:6690']
+```
+
+# Gauntlet steps
+
+## Environment file
+
+```bash
+NODE_URL=
+ACCOUNT=
+PRIVATE_KEY=
+CHAINLINK_ENV_USER=John;
+CHAINLINK_IMAGE={AWS_OIDC}.dkr.ecr.{AWS_REGION}.amazonaws.com/chainlink;
+CHAINLINK_VERSION=develop;
+INTERNAL_DOCKER_REPO={AWS_OIDC}.dkr.ecr.{AWS_REGION}.amazonaws.com; # required for mock adapter
+L2_RPC_URL=https://alpha4.starknet.io; # testnet only
+NODE_COUNT=5;
+TEST_DURATION=70h; # for soak
+TEST_USE_ENV_VAR_CONFIG=true; # for soak
+TTL=72h # for soak
+```
+
+1. Deploy link
+
+```bash
+yarn gauntlet token:deploy --link
+```
+
+2. Deploy access controller
+
+```bash
+yarn gauntlet access_controller:deploy
+```
+
+3. Deploy OCR2
+
+```bash
+yarn gauntlet ocr2:deploy --minSubmissionValue= --maxSubmissionValue= --decimals= --name= --link=
+```
+
+4. Deploy proxy
+
+```bash
+yarn gauntlet proxy:deploy
+```
+
+5. Add access to proxy
+
+```bash
+yarn gauntlet ocr2:add_access --address=
+```
+
+6. Mint LINK
+
+```bash
+yarn gauntlet token:mint --recipient --amount=
+```
+
+7. Set billing
+
+```bash
+yarn gauntlet ocr2:set_billing --observationPaymentGjuels= --transmissionPaymentGjuels=
+```
+
+8. Set config
+
+ 1. Example config testnet
+
+ ```bash
+ {
+ "f": 1,
+ "signers": [
+ "ocr2on_starknet_0371028377bfd793b7e2965757e348309e7242802d20253da6ab81c8eb4b4051",
+ "ocr2on_starknet_073cadfc4474e8c6c79f66fa609da1dbcd5be4299ff9b1f71646206d1faca1fc",
+ "ocr2on_starknet_0386d1a9d93792c426739f73afa1d0b19782fbf30ae27ce33c9fbd4da659cd80",
+ "ocr2on_starknet_005360052758819ba2af790469a28353b7ff6f8b84176064ab572f6cc20e5fb4"
+ ],
+ "transmitters": [
+ "0x0...",
+ "0x0...",
+ "0x0...",
+ "0x0..."
+ ],
+ "onchainConfig": "",
+ "offchainConfig": {
+ "deltaProgressNanoseconds": 8000000000,
+ "deltaResendNanoseconds": 30000000000,
+ "deltaRoundNanoseconds": 3000000000,
+ "deltaGraceNanoseconds": 1000000000,
+ "deltaStageNanoseconds": 20000000000,
+ "rMax": 5,
+ "s": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "offchainPublicKeys": [
+ "ocr2off_starknet_0...",
+ "ocr2off_starknet_0...",
+ "ocr2off_starknet_0...",
+ "ocr2off_starknet_0..."
+ ],
+ "peerIds": [
+ "12D3..",
+ "12D3..",
+ "12D3..",
+ "12D3.."
+ ],
+ "reportingPluginConfig": {
+ "alphaReportInfinite": false,
+ "alphaReportPpb": 0,
+ "alphaAcceptInfinite": false,
+ "alphaAcceptPpb": 0,
+ "deltaCNanoseconds": 1000000000
+ },
+ "maxDurationQueryNanoseconds": 2000000000,
+ "maxDurationObservationNanoseconds": 1000000000,
+ "maxDurationReportNanoseconds": 2000000000,
+ "maxDurationShouldAcceptFinalizedReportNanoseconds": 2000000000,
+ "maxDurationShouldTransmitAcceptedReportNanoseconds": 2000000000,
+ "configPublicKeys": [
+ "ocr2cfg_starknet_...",
+ "ocr2cfg_starknet_...",
+ "ocr2cfg_starknet_...",
+ "ocr2cfg_starknet_..."
+ ]
+ },
+ "offchainConfigVersion": 2,
+ "secret": "some secret you want"
+ }
+ ```
+
+```bash
+yarn gauntlet ocr2:set_config --input=
+```
diff --git a/docs/integration-tests/on-demand-soak.md b/docs/integration-tests/on-demand-soak.md
new file mode 100644
index 000000000..1c91d30e3
--- /dev/null
+++ b/docs/integration-tests/on-demand-soak.md
@@ -0,0 +1,22 @@
+## On demand soak test
+
+
+Soak tests can be triggered in GHA remotely with custom duration on devnet / testnet
+
+1. Navigate to Actions
+2. Select Integration Tests - Soak
+3. Click run workflow
+4. Enter RPC url (Optional this is for testing on testnet)
+5. Specify node count (default is 4+1)
+6. Specify TTL of the namespace (This is when to destroy the env)
+7. Specify duration of the soak (Should be lower than TTL)
+8. Enter private key L2 (Optional, only for testnet)
+9. Enter account address L2 (Optional, only for testnet)
+
+
+## Monitoring
+Tests will print out a namespace in the "TestOCRSoak" phase in the Run tests step (e.g chainlink-ocr-starknet-472d5)
+
+1. Enter the namespace in grafana chainlink testing insights dashboard and the logs will be visible
+
+The remote runner contains the test run and outputs.
\ No newline at end of file
diff --git a/docs/integration-tests/testing.md b/docs/integration-tests/testing.md
index e5b0628a6..5b8dc7e04 100644
--- a/docs/integration-tests/testing.md
+++ b/docs/integration-tests/testing.md
@@ -1,54 +1,5 @@
## Integration tests usage
-Setup k8s context, if you don't have k8s, spin up a local cluster using [this](../kubernetes.md) guide
+The testing suite uses `chainlink-env` as the base for programmatic control of kubernetes clusters.
-### Run tests using ephemeral envs
-
-```
-make e2e_test
-```
-
-### Run tests on a standalone local env
-
-1. Spin up an env, for example, see yaml file for more options with a stark-devnet/pathfinder real node
-
-```
-envcli new -p ops/chainlink-starknet.yaml
-```
-
-2. Check created file in a previous command output, example `Environment setup and written to file environmentFile=chainlink-stark-k42hp.yaml`
-3. Run the tests
-
-```
-ENVIRONMENT_FILE="$(pwd)/chainlink-stark-k42hp.yaml" KEEP_ENVIRONMENTS="Always" make e2e_test
-```
-
-4. Check the env file or connect command logs for a forwarded `local_ports` and try it in the browser
-5. Destroy the env
-
-```
-envcli rm -e chainlink-stark-b7mt9.yaml
-```
-
-### Interact with an env using other scripts
-
-1. Spin up an env, for example, see yaml file for more options with a stark-devnet/pathfinder real node
-
-```
-envcli new -p ops/chainlink-starknet.yaml
-```
-
-2. Check created file in a previous command output, example `Environment setup and written to file environmentFile=chainlink-stark-mx7rg.yaml`
-3. Connect to your env
-
-```
-envcli connect -e ${your_env_file_yaml}
-```
-
-4. Check the env file or connect command logs for a forwarded `local_ports` and try it in the browser
-5. Interact using other scripts
-6. Destroy the env
-
-```
-envcli rm -e chainlink-stark-b7mt9.yaml
-```
+[Repo + Docs](https://github.com/smartcontractkit/chainlink-env)
diff --git a/examples/contracts/aggregator_consumer/.gitignore b/examples/contracts/aggregator_consumer/.gitignore
new file mode 100644
index 000000000..73aa31e60
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/.gitignore
@@ -0,0 +1,2 @@
+target
+.snfoundry_cache/
diff --git a/examples/contracts/aggregator_consumer/Makefile b/examples/contracts/aggregator_consumer/Makefile
new file mode 100644
index 000000000..01dbccf2a
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/Makefile
@@ -0,0 +1,72 @@
+# The starknet-devnet-rs container version - this version
+# supports rpc v0.7 which is needed for snfoundry v0.20.1
+CONTAINER_VERSION="7743a089a33beb75d7012e4aa24745bee8ae0d71"
+
+export TESTNET_ACCOUNTS_FILE=~/.starknet_accounts/starknet_open_zeppelin_accounts.json
+export TESTNET_ACCOUNT_NAME=testnet-account
+
+export DEVNET_ACCOUNTS_FILE=$(shell pwd)/accounts.json
+export DEVNET_ACCOUNT_NAME=devnet-account
+
+# General Commands
+
+test:
+ @snforge test
+
+devnet:
+ @printf "\nStarting a local starknet devnet docker container:\n\n" \
+ && CONTAINER_VERSION="$(CONTAINER_VERSION)" bash ../../../ops/scripts/devnet-hardhat.sh
+
+# Account Management Commands
+
+create-account:
+ @printf "\nCreating starknet account details for testnet...\n\n" \
+ && sncast --profile testnet account create --name "$(TESTNET_ACCOUNT_NAME)" \
+ && printf "\nYour accounts:\n\n" \
+ && cat $(TESTNET_ACCOUNTS_FILE) \
+ && printf "\n\nYou can fund your account here: https://sepolia.starkgate.starknet.io\n"
+
+add-account:
+ @printf "\nImporting a prefunded account from starknet devnet container...\n\n" \
+ && sncast --profile devnet account add \
+ --name "$(DEVNET_ACCOUNT_NAME)" \
+ --address "0x4b3f4ba8c00a02b66142a4b1dd41a4dfab4f92650922a3280977b0f03c75ee1" \
+ --private-key "0x57b2f8431c772e647712ae93cc616638" \
+ && printf "\nYour accounts:\n\n" \
+ && cat $(DEVNET_ACCOUNTS_FILE) \
+
+deploy-account:
+ @sncast --profile testnet account deploy --name "$(TESTNET_ACCOUNT_NAME)" --max-fee 0x5af3107a3fff
+
+# MockAggregator Commands
+
+ma-deploy:
+ @cd ./scripts && sncast --profile "$(NETWORK)" script run deploy_mock_aggregator --no-state-file
+
+ma-set-latest-round:
+ @cd ./scripts && sncast --profile "$(NETWORK)" script run set_latest_round --no-state-file
+
+# Aggregator Commands
+
+agg-read-latest-round:
+ @cd ./scripts && sncast --profile "$(NETWORK)" script run read_latest_round --no-state-file
+
+agg-read-decimals:
+ @cd ./scripts && sncast --profile "$(NETWORK)" script run read_decimals --no-state-file
+
+# AggregatorConsumer commands
+
+ac-deploy:
+ @cd ./scripts && sncast --profile "$(NETWORK)" script run deploy_aggregator_consumer --no-state-file
+
+ac-read-answer:
+ @cd ./scripts && sncast --profile "$(NETWORK)" script run read_answer --no-state-file
+
+ac-set-answer:
+ @cd ./scripts && sncast --profile "$(NETWORK)" script run set_answer --no-state-file
+
+# Helpers
+
+devnet-deploy:
+ @make ma-deploy NETWORK=devnet && make ac-deploy NETWORK=devnet
+
diff --git a/examples/contracts/aggregator_consumer/README.md b/examples/contracts/aggregator_consumer/README.md
new file mode 100644
index 000000000..cffdf0d1b
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/README.md
@@ -0,0 +1,238 @@
+# Examples
+
+## Overview
+
+In this directory you'll find three top-level folders:
+
+- `src/`: contains sample cairo contracts that demonstrate how one can integrate with Chainlink's core starknet contracts.
+- `tests/`: contains cairo tests for the example contracts in `src/`. They showcase some simple usage patterns for the contracts.
+- `scripts/`: contains cairo scripts that allow you to interact with the example contracts over testnet or a local starknet devnet container.
+
+## Prerequisites
+
+To get started, ensure that you have the following tools installed on your machine:
+
+- [starknet-foundry (v0.21.0)](https://github.com/foundry-rs/starknet-foundry/releases/tag/v0.21.0)
+- [scarb (v2.6.4)](https://github.com/software-mansion/scarb/releases/tag/v2.6.4)
+
+## Tests
+
+To run all test cases in the `tests/` directory, you can use the following command:
+
+```sh
+make test
+```
+
+## Scripts
+
+### Setup
+
+#### Using a Local Starknet Docker Devnet
+
+If you would like to run the scripts against a local starknet devnet container:
+
+- First, execute the following command to run a [starknet-devnet-rs](https://github.com/0xSpaceShard/starknet-devnet-rs) container:
+
+ ```sh
+ make devnet
+ ```
+
+ If this command is re-run, it will fully stop the container and recreate it. This can be useful in case you'd like to restart from a completely clean state and re-run the deploy scripts.
+
+- The starknet devnet container comes with a set of prefunded accounts. In order to run the scripts, we'll need to add one of these accounts to a local `accounts.json` file. This can be done using the following command:
+
+ ```sh
+ make add-account
+ ```
+
+At this point you should be ready to start executing scripts! Feel free to move onto the next section.
+
+#### Using Testnet
+
+If you would like to run the scripts against testnet:
+
+- First, let's generate our account details. We can do this by running the following command:
+
+ ```sh
+ make create-account
+ ```
+
+ Once the account has been created, its info should be stored in an accounts file on your local machine (usually at `~/.starknet_accounts/starknet_open_zeppelin_accounts.json`).
+
+- Next, you'll need to fund the account with some tokens. This can be achieved by sending tokens from another starknet account or by bridging them with [StarkGate](https://sepolia.starkgate.starknet.io).
+
+- After you fund the account, you can deploy it to testnet using the following command:
+
+ ```sh
+ make deploy-account
+ ```
+
+At this point you should be ready to start executing scripts! Feel free to move onto the next section.
+
+### Running Scripts
+
+There are several different ways to use the scripts in this repo. We'll cover a few different options below.
+
+#### Reading Data from an Aggregator
+
+##### Devnet
+
+First, let's deploy a mock aggregator contract to our container:
+
+```sh
+make ma-deploy NETWORK=devnet
+```
+
+Under the hood this command will run a declare transaction followed by a deploy transaction for the MockAggregator contract. This command should output something similar to:
+
+```text
+Declaring and deploying MockAggregator
+Declaring contract...
+Transaction hash = 0x568d29d07128cba750845b57a4bb77a31f628b6f4288861d8b31d12e71e4c3b
+Class hash = 301563338814178704943249302673347019225052832575378055777678731916437560881
+Deploying contract...
+Transaction hash = 0xfbc49eb82894a704ce536ab904cdee0fd021b0fba335900f8b9b12cfcd005f
+MockAggregator deployed at address: 1566652744716179301065270359129119857774335542042051464747302084192731701184
+
+command: script run
+status: success
+```
+
+Once the MockAggregator is deployed, you can read the latest round data using the following command:
+
+```sh
+make agg-read-latest-round NETWORK=devnet
+```
+
+This should return an output like:
+
+```text
+Result::Ok(CallResult { data: [0, 0, 0, 0, 0] })
+command: script run
+status: success
+```
+
+In this case, there isn't any round data yet since we haven't added any mock data. To set the latest round data, you can run the following command:
+
+```sh
+make ma-set-latest-round NETWORK=devnet
+```
+
+This should result in an output like:
+
+```text
+Transaction hash = 0x5b57df1db0898caefd01f7d7ff9a300814ef8869a6c475f135d8b5d56e0e3a8
+Result::Ok(InvokeResult { transaction_hash: 2582232800348643522264958893576302212545891688073192089151947336582678242216 })
+command: script run
+status: success
+```
+
+Under the hood, this script sends a transaction to the network which calls `set_latest_round_data` on the `MockAggregator`. The data sent to the function is hardcoded in the script and can be modified in any way you like. Now when we read the latest round data:
+
+```sh
+make agg-read-latest-round NETWORK=devnet
+```
+
+We should see something like:
+
+```text
+Result::Ok(CallResult { data: [1, 1, 12345, 100000, 200000] })
+command: script run
+status: success
+```
+
+This array of values represents the following:
+
+```text
+[ 1, 1, 12345, 100000, 200000 ]
+[roundId, answer, block_num, observation_timestamp, transmittion_timestamp]
+```
+
+##### Testnet
+
+The steps and commands used for devnet can also be applied to testnet! However, there are a few noticeable differences:
+
+- You do not need to deploy a mock aggregator. If you already have the address of a pre-deployed aggregator, you can use it in the `read_latest_round.cairo` script!
+
+- For all the commands, make sure you use `NETWORK=testnet`.
+
+#### Deploying an Aggregator Consumer Contract
+
+##### Devnet
+
+First, let's restart our devnet container to ensure we're starting from a clean slate:
+
+```sh
+make devnet
+```
+
+Once the container is restarted, let's deploy the MockAggregator contract and the AggregatorConsumer contract to it:
+
+```sh
+make devnet-deploy
+```
+
+The AggregatorConsumer is a simple contract that can be used to store the latest answer of an Aggregator contract. It takes the address of an aggregator contract as input (in this case it is the MockAggregator), and it comes with the following methods:
+
+- `set_answer`: this function sets the answer to a new value.
+- `read_answer`: this function reads the answer from storage. The answer is initially set to 0 on deployment.
+- `read_ocr_address`: this function returns the address of the aggregator contract.
+
+At this point, the AggregatorConsumer's answer has not been set, so calling `read_answer` on the AggregatorConsumer contract will return 0. You can run the following commands to verify this:
+
+Command:
+
+```sh
+make ac-read-answer NETWORK=devnet
+```
+
+Output:
+
+
+```text
+Result::Ok(CallResult { data: [0] })
+command: script run
+status: success
+```
+
+To change this, let's use the set the MockAggregator's latest round data to some dummy values:
+
+```sh
+make ma-set-latest-round NETWORK=devnet
+```
+
+Now let's query the latest round data from the MockAggregator and store the latest round answer in the AggregatorConsumer:
+
+```sh
+make ac-set-answer NETWORK=devnet
+```
+
+Now, reading the AggregatorConsumer's answer returns a non-zero value:
+
+Command:
+
+```sh
+make ac-read-answer NETWORK=devnet
+```
+
+Output:
+
+```text
+Result::Ok(CallResult { data: [1] })
+command: script run
+status: success
+```
+
+##### Testnet
+
+The steps and commands used for devnet can also be applied to testnet! However, there are a few noticeable differences:
+
+- You do not need to use the `make devnet` command or the `make devnet-deploy` command. For contract deployment, you'll most likely want to use the address of a pre-deployed aggregator instead of the MockAggregator. If this is the case, you won't have control over the latest round data, so you can ignore the commands that interact with the MockAggregator (i.e. `make ma-set-latest-round`). For deployment, you can perform the following:
+ 1. note the address of the Aggregator contract you'd like to use
+ 1. input the address in the `deploy_aggregator_consumer.cairo` script
+ 1. run `make ac-deploy NETWORK=testnet`
+
+- For all the commands, make sure you use `NETWORK=testnet`.
+
+- The AggregatorConsumer scripts (e.g. `read_answer.cairo` and `set_answer.cairo`) contain the hardcoded address of the AggregatorConsumer for devnet. If you'd like to use these scripts on testnet, the hardcoded AggregatorConsumer address in these scripts will need to be swapped with the address that you receive from `make ac-deploy`. Keep in mind that the address that is printed from `make ac-deploy` may not be hex encoded - if this is the case you'll need to convert it to hex before adding it to the script.
+
diff --git a/examples/contracts/aggregator_consumer/Scarb.lock b/examples/contracts/aggregator_consumer/Scarb.lock
new file mode 100644
index 000000000..586d04a23
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/Scarb.lock
@@ -0,0 +1,27 @@
+# Code generated by scarb DO NOT EDIT.
+version = 1
+
+[[package]]
+name = "aggregator_consumer"
+version = "0.1.0"
+dependencies = [
+ "chainlink",
+ "snforge_std",
+]
+
+[[package]]
+name = "chainlink"
+version = "0.1.0"
+dependencies = [
+ "openzeppelin",
+]
+
+[[package]]
+name = "openzeppelin"
+version = "0.10.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3"
+
+[[package]]
+name = "snforge_std"
+version = "0.21.0"
+source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.21.0#2996b8c1dd66b2715fc67e69578089f278a46790"
diff --git a/examples/contracts/aggregator_consumer/Scarb.toml b/examples/contracts/aggregator_consumer/Scarb.toml
new file mode 100644
index 000000000..e7868b138
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/Scarb.toml
@@ -0,0 +1,25 @@
+# This project was generated using snforge init
+#
+# https://foundry-rs.github.io/starknet-foundry/appendix/snforge/init.html
+#
+
+[package]
+name = "aggregator_consumer"
+version = "0.1.0"
+cairo-version = "2.6.3"
+
+# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html
+
+[dependencies]
+snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.21.0" }
+chainlink = { path = "../../../contracts" }
+starknet = ">=2.6.3"
+
+[lib]
+
+[[target.starknet-contract]]
+casm = true
+build-external-contracts = [
+ "chainlink::emergency::sequencer_uptime_feed::SequencerUptimeFeed",
+ "chainlink::ocr2::mocks::mock_aggregator::MockAggregator",
+]
diff --git a/examples/contracts/aggregator_consumer/scripts/Scarb.lock b/examples/contracts/aggregator_consumer/scripts/Scarb.lock
new file mode 100644
index 000000000..efc15f9c8
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/Scarb.lock
@@ -0,0 +1,41 @@
+# Code generated by scarb DO NOT EDIT.
+version = 1
+
+[[package]]
+name = "aggregator_consumer"
+version = "0.1.0"
+dependencies = [
+ "chainlink",
+ "snforge_std",
+]
+
+[[package]]
+name = "chainlink"
+version = "0.1.0"
+dependencies = [
+ "openzeppelin",
+]
+
+[[package]]
+name = "openzeppelin"
+version = "0.10.0"
+source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3"
+
+[[package]]
+name = "sncast_std"
+version = "0.21.0"
+source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.21.0#2996b8c1dd66b2715fc67e69578089f278a46790"
+
+[[package]]
+name = "snforge_std"
+version = "0.21.0"
+source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.21.0#2996b8c1dd66b2715fc67e69578089f278a46790"
+
+[[package]]
+name = "src"
+version = "0.1.0"
+dependencies = [
+ "aggregator_consumer",
+ "chainlink",
+ "sncast_std",
+]
diff --git a/examples/contracts/aggregator_consumer/scripts/Scarb.toml b/examples/contracts/aggregator_consumer/scripts/Scarb.toml
new file mode 100644
index 000000000..bf06f1d83
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/Scarb.toml
@@ -0,0 +1,24 @@
+[package]
+name = "src"
+version = "0.1.0"
+cairo-version = "2.6.3"
+
+# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html
+
+[dependencies]
+sncast_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.21.0" }
+chainlink = { path = "../../../../contracts" }
+aggregator_consumer = { path = "../" }
+starknet = ">=2.6.3"
+
+[lib]
+casm = true
+
+[[target.starknet-contract]]
+casm = true
+build-external-contracts = [
+ "chainlink::emergency::sequencer_uptime_feed::SequencerUptimeFeed",
+ "chainlink::ocr2::mocks::mock_aggregator::MockAggregator",
+ "aggregator_consumer::ocr2::consumer::AggregatorConsumer"
+]
+
diff --git a/examples/contracts/aggregator_consumer/scripts/src/aggregator.cairo b/examples/contracts/aggregator_consumer/scripts/src/aggregator.cairo
new file mode 100644
index 000000000..affdffa8b
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/aggregator.cairo
@@ -0,0 +1,2 @@
+mod read_latest_round;
+mod read_decimals;
diff --git a/examples/contracts/aggregator_consumer/scripts/src/aggregator/read_decimals.cairo b/examples/contracts/aggregator_consumer/scripts/src/aggregator/read_decimals.cairo
new file mode 100644
index 000000000..01067fd78
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/aggregator/read_decimals.cairo
@@ -0,0 +1,20 @@
+use sncast_std::{call, CallResult};
+
+use starknet::ContractAddress;
+
+fn main() {
+ // If you are using testnet, this address may need to be changed
+ // If you are using the local starknet-devnet-rs container, this can be left alone
+ let aggregator_address = 0x3c6f82da5dbfa89ec9dbe414f33d23d1720d15568e4a880afcc9b0c3d98d127
+ .try_into()
+ .unwrap();
+
+ let result = call(aggregator_address, selector!("decimals"), array![]);
+ if result.is_err() {
+ println!("{:?}", result.unwrap_err());
+ panic_with_felt252('call failed');
+ } else {
+ println!("{:?}", result);
+ }
+}
+
diff --git a/examples/contracts/aggregator_consumer/scripts/src/aggregator/read_latest_round.cairo b/examples/contracts/aggregator_consumer/scripts/src/aggregator/read_latest_round.cairo
new file mode 100644
index 000000000..4a7c5c1b4
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/aggregator/read_latest_round.cairo
@@ -0,0 +1,20 @@
+use sncast_std::{call, CallResult};
+
+use starknet::ContractAddress;
+
+fn main() {
+ // If you are using testnet, this address may need to be changed
+ // If you are using the local starknet-devnet-rs container, this can be left alone
+ let aggregator_address = 0x3c6f82da5dbfa89ec9dbe414f33d23d1720d15568e4a880afcc9b0c3d98d127
+ .try_into()
+ .unwrap();
+
+ let result = call(aggregator_address, selector!("latest_round_data"), array![]);
+ if result.is_err() {
+ println!("{:?}", result.unwrap_err());
+ panic_with_felt252('call failed');
+ } else {
+ println!("{:?}", result);
+ }
+}
+
diff --git a/examples/contracts/aggregator_consumer/scripts/src/consumer.cairo b/examples/contracts/aggregator_consumer/scripts/src/consumer.cairo
new file mode 100644
index 000000000..a7a56e7ce
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/consumer.cairo
@@ -0,0 +1,3 @@
+mod deploy_aggregator_consumer;
+mod read_answer;
+mod set_answer;
diff --git a/examples/contracts/aggregator_consumer/scripts/src/consumer/deploy_aggregator_consumer.cairo b/examples/contracts/aggregator_consumer/scripts/src/consumer/deploy_aggregator_consumer.cairo
new file mode 100644
index 000000000..4d55a13db
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/consumer/deploy_aggregator_consumer.cairo
@@ -0,0 +1,57 @@
+use sncast_std::{
+ declare, deploy, DeclareResult, DeployResult, get_nonce, DisplayContractAddress,
+ DisplayClassHash
+};
+
+use starknet::{ContractAddress, ClassHash};
+
+fn declare_and_deploy(
+ contract_name: ByteArray, constructor_calldata: Array
+) -> DeployResult {
+ let mut class_hash: ClassHash =
+ 0x6d1dd0e5fa4e0284dcf341997f1d781bc2fb7d76ada684da7a2a33c38031df5
+ .try_into()
+ .unwrap();
+
+ println!("Declaring contract...");
+ let declare_result = declare(contract_name, Option::None, Option::None);
+ if declare_result.is_err() {
+ println!("{:?}", declare_result.unwrap_err());
+ } else {
+ class_hash = declare_result.unwrap().class_hash;
+ }
+ println!("Class hash = {:?}", class_hash);
+
+ println!("Deploying contract...");
+ let nonce = get_nonce('latest');
+ let salt = get_nonce('pending');
+ let deploy_result = deploy(
+ class_hash,
+ constructor_calldata,
+ Option::Some(salt),
+ true,
+ Option::None,
+ Option::Some(nonce)
+ );
+ if deploy_result.is_err() {
+ println!("{:?}", deploy_result.unwrap_err());
+ panic_with_felt252('deploy failed');
+ }
+
+ return deploy_result.unwrap();
+}
+
+fn main() {
+ // Point this to the address of the aggregator contract you'd like to use
+ let aggregator_address: ContractAddress =
+ 0x3c6f82da5dbfa89ec9dbe414f33d23d1720d15568e4a880afcc9b0c3d98d127
+ .try_into()
+ .unwrap();
+
+ println!("\nDeclaring and deploying AggregatorConsumer");
+ let mut calldata = ArrayTrait::new();
+ calldata.append(aggregator_address.into());
+ let consumer = declare_and_deploy("AggregatorConsumer", calldata);
+ println!("AggregatorConsumer deployed at address: {}\n", consumer.contract_address);
+}
+
diff --git a/examples/contracts/aggregator_consumer/scripts/src/consumer/read_answer.cairo b/examples/contracts/aggregator_consumer/scripts/src/consumer/read_answer.cairo
new file mode 100644
index 000000000..743431665
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/consumer/read_answer.cairo
@@ -0,0 +1,20 @@
+use sncast_std::{call, CallResult};
+
+use starknet::ContractAddress;
+
+fn main() {
+ // If you are using testnet, this address may need to be changed
+ // If you are using the local starknet-devnet-rs container, this can be left alone
+ let consumer_address = 0x56e078ee90929f13f2ca83545c71b98136c99b22822ada66ad2aff9595439fc
+ .try_into()
+ .unwrap();
+
+ let result = call(consumer_address, selector!("read_answer"), array![]);
+ if result.is_err() {
+ println!("{:?}", result.unwrap_err());
+ panic_with_felt252('call failed');
+ } else {
+ println!("{:?}", result);
+ }
+}
+
diff --git a/examples/contracts/aggregator_consumer/scripts/src/consumer/set_answer.cairo b/examples/contracts/aggregator_consumer/scripts/src/consumer/set_answer.cairo
new file mode 100644
index 000000000..d53d266a7
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/consumer/set_answer.cairo
@@ -0,0 +1,53 @@
+use sncast_std::{invoke, InvokeResult, call, CallResult, get_nonce};
+
+use starknet::ContractAddress;
+
+fn main() {
+ // If you are using testnet, this address may need to be changed
+ // If you are using the local starknet-devnet-rs container, this can be left alone
+ let consumer_address = 0x56e078ee90929f13f2ca83545c71b98136c99b22822ada66ad2aff9595439fc
+ .try_into()
+ .unwrap();
+
+ // Reads the aggregator address from the AggregatorConsumer
+ let read_ocr_address = call(consumer_address, selector!("read_ocr_address"), array![]);
+ if read_ocr_address.is_err() {
+ println!("{:?}", read_ocr_address.unwrap_err());
+ panic_with_felt252('call failed');
+ } else {
+ println!("{:?}", read_ocr_address);
+ }
+
+ // Queries the aggregator for the latest round data
+ let mut read_ocr_address_data = read_ocr_address.unwrap().data.span();
+ let aggregator_address = Serde::<
+ starknet::ContractAddress
+ >::deserialize(ref read_ocr_address_data)
+ .unwrap();
+ let latest_round = call(aggregator_address, selector!("latest_round_data"), array![]);
+ if latest_round.is_err() {
+ println!("{:?}", latest_round.unwrap_err());
+ panic_with_felt252('call failed');
+ } else {
+ println!("{:?}", latest_round);
+ }
+
+ // Uses the latest round data to set a new answer on the AggregatorConsumer
+ let mut latest_round_data = latest_round.unwrap().data.span();
+ let round = Serde::::deserialize(ref latest_round_data)
+ .unwrap();
+ let result = invoke(
+ consumer_address,
+ selector!("set_answer"),
+ array![round.answer.into()],
+ Option::None,
+ Option::Some(get_nonce('pending'))
+ );
+ if result.is_err() {
+ println!("{:?}", result.unwrap_err());
+ panic_with_felt252('invoke failed');
+ } else {
+ println!("{:?}", result);
+ }
+}
+
diff --git a/examples/contracts/aggregator_consumer/scripts/src/example.cairo b/examples/contracts/aggregator_consumer/scripts/src/example.cairo
new file mode 100644
index 000000000..3d64bded3
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/example.cairo
@@ -0,0 +1,15 @@
+use sncast_std::{call, CallResult};
+
+fn main() {
+ let address = 0x775ee7f2f0b3e15953f4688f7a2ce5a0d1e7c8e18e5f929d461c037f14b690e
+ .try_into()
+ .unwrap();
+ let result = call(address, selector!("description"), array![]);
+ if result.is_err() {
+ println!("{:?}", result.unwrap_err());
+ panic_with_felt252('call failed');
+ } else {
+ println!("{:?}", result);
+ }
+}
+
diff --git a/examples/contracts/aggregator_consumer/scripts/src/lib.cairo b/examples/contracts/aggregator_consumer/scripts/src/lib.cairo
new file mode 100644
index 000000000..f4dee19ba
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/lib.cairo
@@ -0,0 +1,4 @@
+mod mock_aggregator;
+mod aggregator;
+mod consumer;
+mod example;
diff --git a/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator.cairo b/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator.cairo
new file mode 100644
index 000000000..e403e772f
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator.cairo
@@ -0,0 +1,2 @@
+mod deploy_mock_aggregator;
+mod set_latest_round;
diff --git a/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator/deploy_mock_aggregator.cairo b/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator/deploy_mock_aggregator.cairo
new file mode 100644
index 000000000..e3ab4e9a6
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator/deploy_mock_aggregator.cairo
@@ -0,0 +1,52 @@
+use sncast_std::{
+ declare, deploy, DeclareResult, DeployResult, get_nonce, DisplayContractAddress,
+ DisplayClassHash
+};
+
+use starknet::{ContractAddress, ClassHash};
+
+fn declare_and_deploy(
+ contract_name: ByteArray, constructor_calldata: Array
+) -> DeployResult {
+ let mut class_hash: ClassHash =
+ 0x728d8a221e2204c88df0642b7c6dcee60f7c3d3b3d5c190cac1ceba5baf15e8
+ .try_into()
+ .unwrap();
+
+ println!("Declaring contract...");
+ let declare_result = declare(contract_name, Option::None, Option::None);
+ if declare_result.is_err() {
+ println!("{:?}", declare_result.unwrap_err());
+ } else {
+ class_hash = declare_result.unwrap().class_hash;
+ }
+ println!("Class hash = {:?}", class_hash);
+
+ println!("Deploying contract...");
+ let nonce = get_nonce('latest');
+ let salt = get_nonce('pending');
+ let deploy_result = deploy(
+ class_hash,
+ constructor_calldata,
+ Option::Some(salt),
+ true,
+ Option::None,
+ Option::Some(nonce)
+ );
+ if deploy_result.is_err() {
+ println!("{:?}", deploy_result.unwrap_err());
+ panic_with_felt252('deploy failed');
+ }
+
+ return deploy_result.unwrap();
+}
+
+fn main() {
+ let decimals = 16;
+ println!("\nDeclaring and deploying MockAggregator");
+ let mut calldata = ArrayTrait::new();
+ calldata.append(decimals.into());
+ let mock_aggregator = declare_and_deploy("MockAggregator", calldata);
+ println!("MockAggregator deployed at address: {}\n", mock_aggregator.contract_address);
+}
+
diff --git a/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator/set_latest_round.cairo b/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator/set_latest_round.cairo
new file mode 100644
index 000000000..d5a2a0150
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/scripts/src/mock_aggregator/set_latest_round.cairo
@@ -0,0 +1,33 @@
+use sncast_std::{invoke, InvokeResult, get_nonce};
+
+use starknet::ContractAddress;
+
+fn main() {
+ // If you are using testnet, this address may need to be changed
+ // If you are using the local starknet-devnet-rs container, this can be left alone
+ let mock_aggregator_address = 0x3c6f82da5dbfa89ec9dbe414f33d23d1720d15568e4a880afcc9b0c3d98d127
+ .try_into()
+ .unwrap();
+
+ // Feel free to modify these
+ let answer = 1;
+ let block_num = 12345;
+ let observation_timestamp = 1711716556;
+ let transmission_timestamp = 1711716514;
+
+ let result = invoke(
+ mock_aggregator_address,
+ selector!("set_latest_round_data"),
+ array![answer, block_num, observation_timestamp, transmission_timestamp],
+ Option::None,
+ Option::Some(get_nonce('pending'))
+ );
+
+ if result.is_err() {
+ println!("{:?}", result.unwrap_err());
+ panic_with_felt252('invoke failed');
+ } else {
+ println!("{:?}", result);
+ }
+}
+
diff --git a/examples/contracts/aggregator_consumer/snfoundry.toml b/examples/contracts/aggregator_consumer/snfoundry.toml
new file mode 100644
index 000000000..e9a340351
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/snfoundry.toml
@@ -0,0 +1,16 @@
+# A full list of RPC endpoints can be found here:
+#
+# https://blastapi.io/public-api/starknet
+#
+[sncast.devnet]
+url = "http://127.0.0.1:5050/rpc"
+accounts-file = "$DEVNET_ACCOUNTS_FILE"
+account = "$DEVNET_ACCOUNT_NAME"
+hex-format = true
+
+[sncast.testnet]
+url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_7"
+accounts-file = "$TESTNET_ACCOUNTS_FILE"
+account = "$TESTNET_ACCOUNT_NAME"
+hex-format = true
+
diff --git a/examples/contracts/aggregator_consumer/src/lib.cairo b/examples/contracts/aggregator_consumer/src/lib.cairo
new file mode 100644
index 000000000..ea174388f
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/src/lib.cairo
@@ -0,0 +1,2 @@
+pub mod ocr2;
+
diff --git a/examples/contracts/aggregator_consumer/src/ocr2.cairo b/examples/contracts/aggregator_consumer/src/ocr2.cairo
new file mode 100644
index 000000000..8947aa72c
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/src/ocr2.cairo
@@ -0,0 +1,2 @@
+pub mod price_consumer;
+pub mod consumer;
diff --git a/examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo b/examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo
new file mode 100644
index 000000000..6a8587bbb
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo
@@ -0,0 +1,52 @@
+#[starknet::interface]
+pub trait IAggregatorConsumer {
+ fn read_latest_round(self: @TContractState) -> chainlink::ocr2::aggregator::Round;
+ fn read_ocr_address(self: @TContractState) -> starknet::ContractAddress;
+ fn read_answer(self: @TContractState) -> u128;
+ fn set_answer(ref self: TContractState, answer: u128);
+}
+
+#[starknet::contract]
+mod AggregatorConsumer {
+ use starknet::ContractAddress;
+ use traits::Into;
+
+ use chainlink::ocr2::aggregator::Round;
+
+ use chainlink::ocr2::aggregator_proxy::IAggregator;
+ use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcher;
+ use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcherTrait;
+
+ #[storage]
+ struct Storage {
+ _ocr_address: ContractAddress,
+ _answer: u128,
+ }
+
+ #[constructor]
+ fn constructor(ref self: ContractState, ocr_address: ContractAddress) {
+ self._ocr_address.write(ocr_address);
+ self._answer.write(0);
+ }
+
+ #[abi(embed_v0)]
+ impl AggregatorConsumerImpl of super::IAggregatorConsumer {
+ fn read_latest_round(self: @ContractState) -> Round {
+ return IAggregatorDispatcher { contract_address: self._ocr_address.read() }
+ .latest_round_data();
+ }
+
+
+ fn set_answer(ref self: ContractState, answer: u128) {
+ self._answer.write(answer);
+ }
+
+ fn read_answer(self: @ContractState) -> u128 {
+ return self._answer.read();
+ }
+
+ fn read_ocr_address(self: @ContractState) -> ContractAddress {
+ return self._ocr_address.read();
+ }
+ }
+}
diff --git a/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo b/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo
new file mode 100644
index 000000000..9f2d2e381
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo
@@ -0,0 +1,68 @@
+#[starknet::interface]
+pub trait IAggregatorPriceConsumer {
+ fn get_latest_price(self: @TContractState) -> u128;
+}
+
+#[starknet::contract]
+mod AggregatorPriceConsumer {
+ use box::BoxTrait;
+ use starknet::ContractAddress;
+ use zeroable::Zeroable;
+ use traits::Into;
+
+ use chainlink::ocr2::aggregator::Round;
+ use chainlink::ocr2::aggregator_proxy::IAggregator;
+ use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcher;
+ use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcherTrait;
+
+ #[storage]
+ struct Storage {
+ _uptime_feed_address: ContractAddress,
+ _aggregator_address: ContractAddress,
+ }
+
+ // Sequencer-aware aggregator consumer
+ // retrieves the latest price from the data feed only if
+ // the uptime feed is not stale (stale = older than 60 seconds)
+ #[constructor]
+ fn constructor(
+ ref self: ContractState,
+ uptime_feed_address: ContractAddress,
+ aggregator_address: ContractAddress
+ ) {
+ assert(!uptime_feed_address.is_zero(), 'uptime feed is 0');
+ assert(!aggregator_address.is_zero(), 'aggregator is 0');
+ self._uptime_feed_address.write(uptime_feed_address);
+ self._aggregator_address.write(aggregator_address);
+ }
+
+
+ #[abi(embed_v0)]
+ impl AggregatorPriceConsumerImpl of super::IAggregatorPriceConsumer {
+ fn get_latest_price(self: @ContractState) -> u128 {
+ assert_sequencer_healthy(self);
+ let round = IAggregatorDispatcher { contract_address: self._aggregator_address.read() }
+ .latest_round_data();
+ round.answer
+ }
+ }
+
+ fn assert_sequencer_healthy(self: @ContractState) {
+ let round = IAggregatorDispatcher { contract_address: self._uptime_feed_address.read() }
+ .latest_round_data();
+ let timestamp = starknet::get_block_info().unbox().block_timestamp;
+
+ // After 60 sec the report is considered stale
+ let report_stale = timestamp - round.updated_at > 60_u64;
+
+ // 0 if the sequencer is up and 1 if it is down. No other options besides 1 and 0
+ match round.answer.into() {
+ 0 => { assert(!report_stale, 'L2 seq up & report stale'); },
+ _ => {
+ assert(!report_stale, 'L2 seq down & report stale');
+ assert(false, 'L2 seq down & report ok');
+ }
+ }
+ }
+}
+
diff --git a/examples/contracts/aggregator_consumer/tests/test_consumer.cairo b/examples/contracts/aggregator_consumer/tests/test_consumer.cairo
new file mode 100644
index 000000000..31d86386c
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/tests/test_consumer.cairo
@@ -0,0 +1,113 @@
+use snforge_std::{declare, ContractClassTrait};
+
+use chainlink::ocr2::mocks::mock_aggregator::IMockAggregatorDispatcherTrait;
+use chainlink::ocr2::mocks::mock_aggregator::IMockAggregatorDispatcher;
+use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcherTrait;
+use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcher;
+
+use aggregator_consumer::ocr2::consumer::IAggregatorConsumerDispatcherTrait;
+use aggregator_consumer::ocr2::consumer::IAggregatorConsumerDispatcher;
+
+use starknet::ContractAddress;
+
+fn deploy_mock_aggregator(decimals: u8) -> ContractAddress {
+ let mut calldata = ArrayTrait::new();
+ calldata.append(decimals.into());
+ return declare("MockAggregator").deploy(@calldata).unwrap();
+}
+
+fn deploy_consumer(aggregator_address: ContractAddress) -> ContractAddress {
+ let mut calldata = ArrayTrait::new();
+ calldata.append(aggregator_address.into());
+ return declare("AggregatorConsumer").deploy(@calldata).unwrap();
+}
+
+#[test]
+fn test_read_decimals() {
+ // Deploys the mock aggregator
+ let decimals = 16;
+ let mock_aggregator_address = deploy_mock_aggregator(decimals);
+ let aggregator_dispatcher = IAggregatorDispatcher { contract_address: mock_aggregator_address };
+
+ // Let's make sure the constructor arguments were passed in correctly
+ assert(decimals == aggregator_dispatcher.decimals(), 'Invalid decimals');
+}
+#[test]
+fn test_set_and_read_latest_round() {
+ // Deploys the mock aggregator
+ let mock_aggregator_address = deploy_mock_aggregator(16);
+ let mock_aggregator_dispatcher = IMockAggregatorDispatcher {
+ contract_address: mock_aggregator_address
+ };
+ let aggregator_dispatcher = IAggregatorDispatcher { contract_address: mock_aggregator_address };
+
+ // No round data has been initialized, so reading the latest round should return no data
+ let empty_latest_round = aggregator_dispatcher.latest_round_data();
+ assert(empty_latest_round.round_id == 0, 'round_id != 0');
+ assert(empty_latest_round.answer == 0, 'answer != 0');
+ assert(empty_latest_round.block_num == 0, 'block_num != 0');
+ assert(empty_latest_round.started_at == 0, 'started_at != 0');
+ assert(empty_latest_round.updated_at == 0, 'updated_at != 0');
+
+ // Now let's set the latest round data to some random values
+ let answer = 1;
+ let block_num = 12345;
+ let observation_timestamp = 100000;
+ let transmission_timestamp = 200000;
+ mock_aggregator_dispatcher
+ .set_latest_round_data(answer, block_num, observation_timestamp, transmission_timestamp);
+
+ // The latest round should now have some data
+ let latest_round = aggregator_dispatcher.latest_round_data();
+ assert(latest_round.round_id == 1, 'round_id != 1');
+ assert(latest_round.answer == answer, 'bad answer');
+ assert(latest_round.block_num == block_num, 'bad block_num');
+ assert(latest_round.started_at == observation_timestamp, 'bad started_at');
+ assert(latest_round.updated_at == transmission_timestamp, 'bad updated_at');
+}
+
+#[test]
+fn test_set_and_read_answer() {
+ // Deploys the mock aggregator
+ let mock_aggregator_address = deploy_mock_aggregator(16);
+ let mock_aggregator_dispatcher = IMockAggregatorDispatcher {
+ contract_address: mock_aggregator_address
+ };
+
+ // Deploys the consumer
+ let consumer_address = deploy_consumer(mock_aggregator_address);
+ let consumer_dispatcher = IAggregatorConsumerDispatcher { contract_address: consumer_address };
+
+ // Let's make sure the AggregatorConsumer was initialized correctly
+ assert(consumer_dispatcher.read_ocr_address() == mock_aggregator_address, 'Invalid OCR address');
+ assert(consumer_dispatcher.read_answer() == 0, 'Invalid initial answer');
+
+ // No round data has been initialized, so reading the latest round should return no data
+ let empty_latest_round = consumer_dispatcher.read_latest_round();
+ assert(empty_latest_round.round_id == 0, 'round_id != 0');
+ assert(empty_latest_round.answer == 0, 'answer != 0');
+ assert(empty_latest_round.block_num == 0, 'block_num != 0');
+ assert(empty_latest_round.started_at == 0, 'started_at != 0');
+ assert(empty_latest_round.updated_at == 0, 'updated_at != 0');
+
+ // Now let's set the latest round data to some random values
+ let answer = 1;
+ let block_num = 12345;
+ let observation_timestamp = 100000;
+ let transmission_timestamp = 200000;
+ mock_aggregator_dispatcher
+ .set_latest_round_data(answer, block_num, observation_timestamp, transmission_timestamp);
+
+ // The consumer should be able to query the aggregator for the new latest round data
+ let latest_round = consumer_dispatcher.read_latest_round();
+ assert(latest_round.round_id == 1, 'round_id != 1');
+ assert(latest_round.answer == answer, 'bad answer');
+ assert(latest_round.block_num == block_num, 'bad block_num');
+ assert(latest_round.started_at == observation_timestamp, 'bad started_at');
+ assert(latest_round.updated_at == transmission_timestamp, 'bad updated_at');
+
+ // Now let's test that we can set the answer
+ consumer_dispatcher.set_answer(latest_round.answer);
+ assert(answer == consumer_dispatcher.read_answer(), 'Invalid answer');
+}
+
diff --git a/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo b/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo
new file mode 100644
index 000000000..9582c8030
--- /dev/null
+++ b/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo
@@ -0,0 +1,89 @@
+use snforge_std::{declare, ContractClassTrait, start_prank, stop_prank, CheatTarget};
+
+use chainlink::emergency::sequencer_uptime_feed::ISequencerUptimeFeedDispatcherTrait;
+use chainlink::emergency::sequencer_uptime_feed::ISequencerUptimeFeedDispatcher;
+use chainlink::libraries::access_control::IAccessControllerDispatcherTrait;
+use chainlink::libraries::access_control::IAccessControllerDispatcher;
+use chainlink::ocr2::mocks::mock_aggregator::IMockAggregatorDispatcherTrait;
+use chainlink::ocr2::mocks::mock_aggregator::IMockAggregatorDispatcher;
+
+use aggregator_consumer::ocr2::price_consumer::IAggregatorPriceConsumerDispatcherTrait;
+use aggregator_consumer::ocr2::price_consumer::IAggregatorPriceConsumerDispatcher;
+
+use starknet::contract_address_const;
+use starknet::get_caller_address;
+use starknet::ContractAddress;
+
+fn deploy_mock_aggregator(decimals: u8) -> ContractAddress {
+ let mut calldata = ArrayTrait::new();
+ calldata.append(decimals.into());
+ return declare("MockAggregator").deploy(@calldata).unwrap();
+}
+
+fn deploy_uptime_feed(initial_status: u128, owner_address: ContractAddress) -> ContractAddress {
+ let mut calldata = ArrayTrait::new();
+ calldata.append(initial_status.into());
+ calldata.append(owner_address.into());
+ return declare("SequencerUptimeFeed").deploy(@calldata).unwrap();
+}
+
+fn deploy_price_consumer(
+ uptime_feed_address: ContractAddress, aggregator_address: ContractAddress
+) -> ContractAddress {
+ let mut calldata = ArrayTrait::new();
+ calldata.append(uptime_feed_address.into());
+ calldata.append(aggregator_address.into());
+ return declare("AggregatorPriceConsumer").deploy(@calldata).unwrap();
+}
+
+#[test]
+fn test_get_latest_price() {
+ // Defines helper variables
+ let owner = contract_address_const::<1>();
+ let init_status = 0;
+ let decimals = 18;
+
+ // Deploys contracts
+ let mock_aggregator_address = deploy_mock_aggregator(decimals);
+ let uptime_feed_address = deploy_uptime_feed(init_status, owner);
+ let price_consumer_address = deploy_price_consumer(
+ uptime_feed_address, mock_aggregator_address
+ );
+
+ // Adds the price consumer contract to the sequencer uptime feed access control list
+ // which allows the price consumer to call the get_latest_price function
+ start_prank(CheatTarget::All, owner);
+ IAccessControllerDispatcher { contract_address: uptime_feed_address }
+ .add_access(price_consumer_address);
+
+ // The get_latest_price function returns the mock aggregator's latest round answer. At
+ // this point in the test, there is only one round that is initialized and that is the
+ // one that the sequencer uptime feed creates when it is deployed. In its constructor,
+ // a new round is initialized using its initial status as the round's answer, so the
+ // latest price should be the initial status that was passed into the sequencer uptime
+ // feed's constructor.
+ start_prank(CheatTarget::All, price_consumer_address);
+ let latest_price = IAggregatorPriceConsumerDispatcher {
+ contract_address: price_consumer_address
+ }
+ .get_latest_price();
+ assert(latest_price == init_status, 'latest price is incorrect');
+
+ // Now let's update the round
+ stop_prank(CheatTarget::All);
+ let answer = 1;
+ let block_num = 12345;
+ let observation_timestamp = 100000;
+ let transmission_timestamp = 200000;
+ IMockAggregatorDispatcher { contract_address: mock_aggregator_address }
+ .set_latest_round_data(answer, block_num, observation_timestamp, transmission_timestamp);
+
+ // This should now return the updated answer
+ start_prank(CheatTarget::All, price_consumer_address);
+ let updated_latest_price = IAggregatorPriceConsumerDispatcher {
+ contract_address: price_consumer_address
+ }
+ .get_latest_price();
+ assert(updated_latest_price == answer, 'updated price is incorrect');
+}
+
diff --git a/examples/spec/ocr2-bootstrap.spec.toml b/examples/spec/ocr2-bootstrap.spec.toml
new file mode 100644
index 000000000..b8a387593
--- /dev/null
+++ b/examples/spec/ocr2-bootstrap.spec.toml
@@ -0,0 +1,10 @@
+type = "bootstrap"
+schemaVersion = 1
+relay = "starknet"
+name = ""
+contractID = ""
+p2pPeerID = "" # optional, overrides P2P_PEER_ID
+
+[relayConfig]
+chainID = "goerli-alpha-4"
+nodeName = "goerli-alpha-4-node-1" # optional, defaults to random node with 'chainID'
diff --git a/examples/spec/ocr2-oracle-simple.spec.toml b/examples/spec/ocr2-oracle-simple.spec.toml
new file mode 100644
index 000000000..d95c553a6
--- /dev/null
+++ b/examples/spec/ocr2-oracle-simple.spec.toml
@@ -0,0 +1,45 @@
+type = "offchainreporting2"
+pluginType = "median"
+schemaVersion = 1
+relay = "starknet"
+name = ""
+contractID = ""
+p2pBootstrapPeers = ["somep2pkey@localhost-tcp:port"] # optional, overrides P2PV2_BOOTSTRAPPERS
+p2pPeerID = "" # optional, overrides P2P_PEER_ID
+ocrKeyBundleID = "" # optional, overrides OCR2_KEY_BUNDLE_ID
+transmitterID = ""
+observationSource = """
+ // data source 1
+ ds1 [type="bridge" name="bridge-coingecko" requestData=<{"data": {"from":"LINK","to":"USD"}}>]
+ ds1_parse [type="jsonparse" path="result"]
+ ds1_multiply [type="multiply" times=100000000]
+ ds1 -> ds1_parse -> ds1_multiply
+"""
+
+[pluginConfig]
+juelsPerFeeCoinSource = """
+ // Fetch the LINK price from a data source
+ // data source 1
+ ds1_link [type="bridge" name="bridge-coingecko" requestData=<{"data": {"from":"LINK","to":"USD"}}>]
+ ds1_link_parse [type="jsonparse" path="result"]
+ ds1_link -> ds1_link_parse -> divide
+
+ // Fetch the ETH price from a data source
+ // data source 1
+ ds1_coin [type="bridge" name="bridge-coingecko" requestData=<{"data": {"from":"ETH","to":"USD"}}>]
+ ds1_coin_parse [type="jsonparse" path="result"]
+ ds1_coin -> ds1_coin_parse -> divide
+
+ // ds1_link_parse (dollars/LINK)
+ // ds1_coin_parse (dollars/ETH)
+ // ds1_coin_parse / ds1_link_parse = LINK/ETH
+ divide [type="divide" input="$(ds1_coin_parse)" divisor="$(ds1_link_parse)" precision="18"]
+ scale [type="multiply" times=1000000000000000000]
+
+ divide -> scale
+"""
+
+[relayConfig]
+chainID = "goerli-alpha-4"
+accountAddress = ""
+nodeName = "goerli-alpha-4-node-1" # optional, defaults to random node with 'chainID'
diff --git a/examples/spec/ocr2-oracle.spec.toml b/examples/spec/ocr2-oracle.spec.toml
new file mode 100644
index 000000000..4092ade16
--- /dev/null
+++ b/examples/spec/ocr2-oracle.spec.toml
@@ -0,0 +1,79 @@
+type = "offchainreporting2"
+pluginType = "median"
+schemaVersion = 1
+relay = "starknet"
+name = ""
+contractID = ""
+p2pBootstrapPeers = ["somep2pkey@localhost-tcp:port"] # optional, overrides P2PV2_BOOTSTRAPPERS
+p2pPeerID = "" # optional, overrides P2P_PEER_ID
+ocrKeyBundleID = "" # optional, overrides OCR2_KEY_BUNDLE_ID
+transmitterID = ""
+observationSource = """
+ // data source 1
+ ds1 [type="bridge" name="bridge-tiingo" requestData=<{"data": {"from":"BTC","to":"USD"}}>]
+ ds1_parse [type="jsonparse" path="result"]
+ ds1_multiply [type="multiply" times=100000000]
+ ds1 -> ds1_parse -> ds1_multiply -> answer
+ // data source 2
+ ds2 [type="bridge" name="bridge-nomics" requestData=<{"data": {"from":"BTC","to":"USD"}}>]
+ ds2_parse [type="jsonparse" path="result"]
+ ds2_multiply [type="multiply" times=100000000]
+ ds2 -> ds2_parse -> ds2_multiply -> answer
+ // data source 3
+ ds3 [type="bridge" name="bridge-coinmarketcap" requestData=<{"data": {"from":"BTC","to":"USD"}}>]
+ ds3_parse [type="jsonparse" path="result"]
+ ds3_multiply [type="multiply" times=100000000]
+ ds3 -> ds3_parse -> ds3_multiply -> answer
+ answer [type="median" index=0]
+"""
+
+[pluginConfig]
+juelsPerFeeCoinSource = """
+ // Fetch the LINK price from three data sources
+ // data source 1
+ ds1_link [type="bridge" name="bridge-tiingo" requestData=<{"data": {"from":"LINK","to":"USD"}}>]
+ ds1_link_parse [type="jsonparse" path="result"]
+ ds1_link -> ds1_link_parse -> median_link
+ // data source 2
+ ds2_link [type="bridge" name="bridge-nomics" requestData=<{"data": {"from":"LINK","to":"USD"}}>]
+ ds2_link_parse [type="jsonparse" path="result"]
+ ds2_link -> ds2_link_parse -> median_link
+ // data source 3
+ ds3_link [type="bridge" name="bridge-coinmarketcap" requestData=<{"data": {"from":"LINK","to":"USD"}}>]
+ ds3_link_parse [type="jsonparse" path="result"]
+ ds3_link -> ds3_link_parse -> median_link
+
+ // Fetch the ETH price from three data sources
+ // data source 1
+ ds1_coin [type="bridge" name="bridge-tiingo" requestData=<{"data": {"from":"ETH","to":"USD"}}>]
+ ds1_coin_parse [type="jsonparse" path="result"]
+ ds1_coin -> ds1_coin_parse -> median_coin
+ // data source 2
+ ds2_coin [type="bridge" name="bridge-nomics" requestData=<{"data": {"from":"ETH","to":"USD"}}>]
+ ds2_coin_parse [type="jsonparse" path="result"]
+ ds2_coin -> ds2_coin_parse -> median_coin
+ // data source 3
+ ds3_coin [type="bridge" name="bridge-coinmarketcap" requestData=<{"data": {"from":"ETH","to":"USD"}}>]
+ ds3_coin_parse [type="jsonparse" path="result"]
+ ds3_coin -> ds3_coin_parse -> median_coin
+
+ // Compute the medians from all data sources
+ median_link [type="median" values=<[ $(ds1_link_parse), $(ds2_link_parse), $(ds3_link_parse) ]> allowedFaults=2]
+ median_coin [type="median" values=<[ $(ds1_coin_parse), $(ds2_coin_parse), $(ds3_coin_parse) ]> allowedFaults=2]
+
+ // Divide and scale appropriately
+ // median_link (dollars/LINK)
+ // median_coin (dollars/ETH)
+ // median_coin / median_link = LINK/ETH
+ divide [type="divide" input="$(median_coin)" divisor="$(median_link)" precision="18"]
+ scale [type="multiply" times=1000000000000000000]
+
+ median_link -> divide
+ median_coin -> divide
+ divide -> scale
+"""
+
+[relayConfig]
+chainID = "goerli-alpha-4"
+accountAddress = "