From e13ba3c19e7d688c14061355fde8f7ed13a80a9d Mon Sep 17 00:00:00 2001 From: trajan0x <83933037+trajan0x@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:24:10 -0400 Subject: [PATCH 1/8] Fix git-changes-action when module was unchanged (#1399) As #1397 & #579 pointed out if a module is changed and then unchanged, the needs-go-generate action is not removed (this is blocking #1382 rn). For this reason, this pr: - consolidates actions for `include_deps`=true and `include_deps`=false. This will also help with #1390 by halving api requests by git-changes-action. Further work could be done here by consolidating goreleaser into `go.yml` the future - Remove unchanged module labels. This is honestly kind of a pain and could lead to api rate limiting issues. The way we're going to deal with this is by starting an action for each label, but only running it if the event is a pull request and [this condition](https://stackoverflow.com/a/59588725) is met. Once this is done we can also add this to the needs-go-generate check to reduce the number of api calls to remove a lebl further helping w/ #1390 --------- Co-authored-by: Trajan0x --- .github/workflows/close-stale.yml | 8 +- .github/workflows/fail-on-needs-generate.yaml | 2 - .github/workflows/go.yml | 132 +++++++++++++----- .github/workflows/goreleaser-actions.yml | 11 +- .github/workflows/helm-test.yml | 3 +- .github/workflows/jaeger-ui.yml | 2 +- contrib/git-changes-action/README.md | 57 ++++---- contrib/git-changes-action/main.go | 64 +++++---- 8 files changed, 175 insertions(+), 104 deletions(-) diff --git a/.github/workflows/close-stale.yml b/.github/workflows/close-stale.yml index d36e8d6c6b..d07e7e555c 100644 --- a/.github/workflows/close-stale.yml +++ b/.github/workflows/close-stale.yml @@ -2,16 +2,20 @@ name: 'Close stale issues and PRs' on: schedule: - cron: '30 1 * * *' + workflow_dispatch: + push: + paths: + - '.github/workflows/close-stale.yml' jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v4 + - uses: actions/stale@v8 with: stale-pr-message: 'This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 5 days.' exempt-pr-labels: exempt-stale days-before-issue-stale: 999 days-before-pr-stale: 14 days-before-close: 5 - repo-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/fail-on-needs-generate.yaml b/.github/workflows/fail-on-needs-generate.yaml index 25f42e7838..a66264a3f4 100644 --- a/.github/workflows/fail-on-needs-generate.yaml +++ b/.github/workflows/fail-on-needs-generate.yaml @@ -10,7 +10,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - name: Fail on needs-go-generate run: echo Failed, needs go generate. && exit 1 check-generate-yarn: @@ -18,6 +17,5 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - name: Fail on needs-go-yarn run: echo Failed, needs yarn install. && exit 1 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 02c138595e..9d27c84ed5 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -23,29 +23,29 @@ jobs: name: Change Detection runs-on: ubuntu-latest outputs: - # Expose matched filters as job 'packages' output variable - packages_deps: ${{ steps.filter_go_deps.outputs.changed_modules }} - packages_nodeps: ${{ steps.filter_go_nodeps.outputs.changed_modules }} + # Expose matched filters as job 'packages' output variable with a trailing _deps for dependencies + packages_deps: ${{ steps.filter_go.outputs.changed_modules_deps }} package_count_deps: ${{ steps.length.outputs.FILTER_LENGTH_DEPS }} + + unchanged_deps: ${{ steps.filter_go.outputs.unchanged_modules_deps }} + unchanged_package_count_deps: ${{ steps.length.outputs.FILTER_LENGTH_UNCHANGED_DEPS }} + + # These have no dependencies included in the change outputs + packages_nodeps: ${{ steps.filter_go_nodeps.outputs.changed_modules }} package_count_nodeps: ${{ steps.length.outputs.FILTER_LENGTH_NODEPS }} solidity_changes: ${{ steps.filter_solidity.outputs.any_changed }} steps: - uses: actions/checkout@v4 with: + # TODO: this can be shorter, maybe 500-1,000? This may be slower (see: https://github.com/CocoaPods/CocoaPods/issues/4989#issuecomment-193772935) fetch-depth: 0 + # TODO: is this neccesary? submodules: 'recursive' - - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:latest - id: filter_go_deps + # note: after this action is pushed, whatever this ends up being should go back to latest. + - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:e4c85e74bf0592c0efa71476c3246675ec77fc28 + id: filter_go with: - include_deps: true - github_token: ${{ secrets.WORKFLOW_PAT }} - timeout: '10m' - - - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:latest - id: filter_go_nodeps - with: - include_deps: false github_token: ${{ secrets.WORKFLOW_PAT }} timeout: '10m' @@ -65,11 +65,15 @@ jobs: export FILTER_LENGTH_DEPS=$(echo $FILTERED_PATHS_DEPS | jq '. | length') echo "##[set-output name=FILTER_LENGTH_DEPS;]$(echo $FILTER_LENGTH_DEPS)" + export FILTER_LENGTH_UNCHANGED_DEPS=$(echo $UNCHANGED_DEPS | jq '. | length') + echo "##[set-output name=FILTER_LENGTH_UNCHANGED_DEPS;]$(echo $FILTER_LENGTH_UNCHANGED_DEPS)" + export FILTER_LENGTH_NODEPS=$(echo $FILTERED_PATHS_NODEPS | jq '. | length') echo "##[set-output name=FILTER_LENGTH_NODEPS;]$(echo $FILTER_LENGTH_NODEPS)" env: - FILTERED_PATHS_DEPS: ${{ steps.filter_go_deps.outputs.changed_modules }} - FILTERED_PATHS_NODEPS: ${{ steps.filter_go_nodeps.outputs.changed_modules }} + FILTERED_PATHS_DEPS: ${{ steps.filter_go.outputs.changed_modules_deps }} + UNCHANGED_DEPS: ${{ steps.filter_go.outputs.unchanged_modules_deps }} + FILTERED_PATHS_NODEPS: ${{ steps.filter_go.outputs.changed_modules }} test: name: Go Coverage @@ -106,7 +110,7 @@ jobs: submodules: 'recursive' - name: Cache Docker images. - uses: ScribeMD/docker-cache@0.2.6 + uses: ScribeMD/docker-cache@0.3.6 with: key: docker-test-${{ runner.os }}-${{ matrix.package }} @@ -114,6 +118,7 @@ jobs: uses: actions/cache@v3 with: # see https://github.com/mvdan/github-actions-golang + # also: https://glebbahmutov.com/blog/do-not-let-npm-cache-snowball/ w/ go build (workaround now is having a cache that just gets expired at night) path: | ~/go/pkg/mod ~/.cache/go-build @@ -124,7 +129,7 @@ jobs: ${{ runner.os }}-test-${{matrix.package}} - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} @@ -259,7 +264,7 @@ jobs: name: Build needs: changes runs-on: ${{ matrix.platform }} - if: ${{ needs.changes.outputs.package_count_deps > 0 }} + if: ${{ needs.changes.outputs.package_count_deps > 0 && github.event_name != 'pull_request' }} strategy: fail-fast: false matrix: @@ -272,7 +277,7 @@ jobs: with: fetch-depth: 1 - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} @@ -280,6 +285,7 @@ jobs: uses: actions/cache@v3 with: # see https://github.com/mvdan/github-actions-golang + # also: https://glebbahmutov.com/blog/do-not-let-npm-cache-snowball/ w/ go build (workaround now is having a cache that just gets expired at night) path: | ~/go/pkg/mod ~/.cache/go-build @@ -305,12 +311,13 @@ jobs: if: ${{ needs.changes.outputs.package_count_nodeps > 0 }} strategy: fail-fast: false + max-parallel: 8 matrix: # Parse JSON array containing names of all filters matching any of changed files # e.g. ['package1', 'package2'] if both package folders contains changes package: ${{ fromJSON(needs.changes.outputs.packages_nodeps) }} steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: # see: https://github.com/golangci/golangci-lint/issues/3420, moving to go 1.20 requires a new golangci-lint version # TODO: with this being 3 behind this should be done sooner rather than later. @@ -338,26 +345,48 @@ jobs: GOMEMLIMIT: 6GiB GOGC: -1 - issue_number: + pr_metadata: # this is needed to prevent us from hitting the github api rate limit - name: Get The Issue Number - needs: changes + name: Get PR Metadata runs-on: ubuntu-latest + # not stricly true, but this job is fast enough to not block and we want to prioritize canceling outdated because downstream jobs can use many workers + needs: cancel-outdated # currently, this matches the logic in the go generate check. If we ever add more checks that run on all packages, we should # change this to run on those pushes - if: ${{ github.event_name != 'pull_request' && (needs.changes.outputs.solidity_changes == 'true' || needs.changes.outputs.package_count_deps > 0 ) }} + if: ${{ github.event_name != 'pull_request' }} outputs: issue_number: ${{ steps.find_pr.outputs.pr }} + metadata: ${{ steps.metadata.outputs.METADATA }} + labels: ${{ steps.metadata.outputs.LABELS }} steps: - uses: jwalton/gh-find-current-pr@v1 id: find_pr + # TODO: https://stackoverflow.com/a/75429845 consider splitting w/ gql to reduce limit hit + - run: | + # Fetch the metadata + metadata="$(gh api repos/$OWNER/$REPO_NAME/pulls/$PULL_REQUEST_NUMBER)" + + # Extract the labels in JSON format from the metadata + labels_json="$(echo "$metadata" | jq -r -c '[.labels[].name]')" + + # Set the full metadata including the labels as the GitHub Actions step output + echo "::set-output name=METADATA::$metadata" + echo "::set-output name=LABELS::$labels_json" + + id: metadata + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.event.repository.name }} + PULL_REQUEST_NUMBER: ${{ steps.find_pr.outputs.pr }} # check if we need to rerun go generate as a result of solidity changes. Note, this will only run on solidity changes. # TODO: consolidate w/ go change check. This will run twice on agents check-generation-solidity: name: Go Generate (Solidity Only) runs-on: ubuntu-latest - needs: [changes, issue_number] + needs: [changes, pr_metadata] if: ${{ github.event_name != 'pull_request' && needs.changes.outputs.solidity_changes == 'true' }} strategy: fail-fast: false @@ -371,7 +400,7 @@ jobs: submodules: 'recursive' - name: Cache Docker images. - uses: ScribeMD/docker-cache@0.2.6 + uses: ScribeMD/docker-cache@0.3.6 with: key: docker-generate-${{ runner.os }}-${{ matrix.package }} @@ -389,7 +418,7 @@ jobs: run: npx lerna exec npm run build:go --parallel # Setup Go - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: 1.21.x @@ -397,6 +426,7 @@ jobs: uses: actions/cache@v3 with: # see https://github.com/mvdan/github-actions-golang + # also: https://glebbahmutov.com/blog/do-not-let-npm-cache-snowball/ w/ go build (workaround now is having a cache that just gets expired at night) path: | ~/go/pkg/mod ~/.cache/go-build @@ -438,26 +468,50 @@ jobs: echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" # Fail if files need regeneration + # TODO: this can run into a bit of a race condition if any other label is removed/added while this is run, look into fixing this by dispatching another workflow - name: Add Label - if: steps.verify-changed-files.outputs.files_changed == 'true' + if: ${{ !contains(fromJson(needs.pr_metadata.outputs.labels), format('needs-go-generate-{0}', matrix.package)) && steps.verify-changed-files.outputs.files_changed == 'true' }} uses: andymckay/labeler@3a4296e9dcdf9576b0456050db78cfd34853f260 with: add-labels: 'needs-go-generate-${{matrix.package}}' repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-number: ${{ needs.issue_number.outputs.issue_number }} + issue-number: ${{ needs.pr_metadata.outputs.issue_number }} + + - name: Remove Label + if: ${{ contains(fromJson(needs.pr_metadata.outputs.labels), format('needs-go-generate-{0}', matrix.package)) && steps.verify-changed-files.outputs.files_changed != 'true' }} + uses: andymckay/labeler@3a4296e9dcdf9576b0456050db78cfd34853f260 + with: + remove-labels: 'needs-go-generate-${{matrix.package}}' + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ needs.pr_metadata.outputs.issue_number }} + remove-label-generation: + name: Remove Generate Label From Unused Jobs + runs-on: ubuntu-latest + needs: [changes, pr_metadata] + if: ${{ github.event_name != 'pull_request' && needs.changes.outputs.unchanged_package_count_deps > 0 && contains(needs.pr_metadata.outputs.labels, 'needs-go-generate') }} + strategy: + fail-fast: false + max-parallel: 1 + matrix: + # only do on agents for now. Anything that relies on solidity in a package should do this + package: ${{ fromJSON(needs.changes.outputs.unchanged_deps) }} + steps: - name: Remove Label - if: steps.verify-changed-files.outputs.files_changed != 'true' + if: ${{ contains(fromJson(needs.pr_metadata.outputs.labels), format('needs-go-generate-{0}', matrix.package)) }} + # labels can't be removed in parallel uses: andymckay/labeler@3a4296e9dcdf9576b0456050db78cfd34853f260 with: remove-labels: 'needs-go-generate-${{matrix.package}}' repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-number: ${{ needs.issue_number.outputs.issue_number }} + issue-number: ${{ needs.pr_metadata.outputs.issue_number }} + + check-generation: name: Go Generate (Module Changes) runs-on: ubuntu-latest - needs: [changes, issue_number] + needs: [changes, pr_metadata] if: ${{ github.event_name != 'pull_request' && needs.changes.outputs.package_count_deps > 0 }} strategy: fail-fast: false @@ -471,7 +525,7 @@ jobs: submodules: 'recursive' - name: Cache Docker images. - uses: ScribeMD/docker-cache@0.2.6 + uses: ScribeMD/docker-cache@0.3.6 with: key: docker-generate-${{ runner.os }}-${{ matrix.package }} @@ -495,7 +549,7 @@ jobs: if: ${{ contains(matrix.package, 'agents') }} # Setup Go - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: 1.21.x @@ -504,6 +558,7 @@ jobs: if: ${{ !contains(matrix.package, 'services/cctp-relayer') }} with: # see https://github.com/mvdan/github-actions-golang + # also: https://glebbahmutov.com/blog/do-not-let-npm-cache-snowball/ w/ go build (workaround now is having a cache that just gets expired at night) path: | ~/go/pkg/mod ~/.cache/go-build @@ -576,18 +631,19 @@ jobs: echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" # Fail if files need regeneration + # TODO: this can run into a bit of a race condition if any other label is removed/added while this is run, look into fixing this by dispatching another workflow - name: Add Label - if: steps.verify-changed-files.outputs.files_changed == 'true' + if: ${{ !contains(fromJson(needs.pr_metadata.outputs.labels), format('needs-go-generate-{0}', matrix.package)) && steps.verify-changed-files.outputs.files_changed == 'true' }} uses: andymckay/labeler@3a4296e9dcdf9576b0456050db78cfd34853f260 with: add-labels: 'needs-go-generate-${{matrix.package}}' repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-number: ${{ needs.issue_number.outputs.issue_number }} + issue-number: ${{ needs.pr_metadata.outputs.issue_number }} - name: Remove Label - if: steps.verify-changed-files.outputs.files_changed != 'true' + if: ${{ contains(fromJson(needs.pr_metadata.outputs.labels), format('needs-go-generate-{0}', matrix.package)) && steps.verify-changed-files.outputs.files_changed != 'true' }} uses: andymckay/labeler@3a4296e9dcdf9576b0456050db78cfd34853f260 with: remove-labels: 'needs-go-generate-${{matrix.package}}' repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-number: ${{ needs.issue_number.outputs.issue_number }} + issue-number: ${{ needs.pr_metadata.outputs.issue_number }} diff --git a/.github/workflows/goreleaser-actions.yml b/.github/workflows/goreleaser-actions.yml index 446ec1b2a6..b6563e4c53 100644 --- a/.github/workflows/goreleaser-actions.yml +++ b/.github/workflows/goreleaser-actions.yml @@ -24,7 +24,7 @@ jobs: - name: Cache Docker images. - uses: ScribeMD/docker-cache@0.2.6 + uses: ScribeMD/docker-cache@0.3.6 with: key: docker-release-${{ runner.os }}-${{ matrix.package }} @@ -87,7 +87,7 @@ jobs: if: ${{ format('refs/heads/{0}', github.event.repository.default_branch) == github.ref || contains(github.event.head_commit.message, '[goreleaser]') }} outputs: # Expose matched filters as job 'packages' output variable - packages: ${{ steps.filter_go.outputs.changed_modules }} + packages: ${{ steps.filter_go.outputs.changed_modules_deps }} package_count: ${{ steps.length.outputs.FILTER_LENGTH }} steps: - uses: actions/checkout@v4 @@ -95,10 +95,9 @@ jobs: fetch-depth: 0 submodules: 'recursive' - - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:latest + - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:e4c85e74bf0592c0efa71476c3246675ec77fc28 id: filter_go with: - include_deps: true github_token: ${{ secrets.WORKFLOW_PAT }} - id: length @@ -106,7 +105,7 @@ jobs: export FILTER_LENGTH=$(echo $FILTERED_PATHS | jq '. | length') echo "##[set-output name=FILTER_LENGTH;]$(echo $FILTER_LENGTH)" env: - FILTERED_PATHS: ${{ steps.filter_go.outputs.changed_modules }} + FILTERED_PATHS: ${{ steps.filter_go.outputs.changed_modules_deps }} # TODO: we may want to dry run this on prs run-goreleaser: @@ -168,7 +167,7 @@ jobs: passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: 1.20.x diff --git a/.github/workflows/helm-test.yml b/.github/workflows/helm-test.yml index 640ffd4b46..92f37ec9b8 100644 --- a/.github/workflows/helm-test.yml +++ b/.github/workflows/helm-test.yml @@ -7,6 +7,7 @@ on: push: paths: - 'charts/**' + - '.github/workflows/helm-test.yml' # TODO: it'd be nice to eventually work this into release process on new images # definitely not a right now thing @@ -38,7 +39,7 @@ jobs: with: version: v3.9.2 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: 3.7 diff --git a/.github/workflows/jaeger-ui.yml b/.github/workflows/jaeger-ui.yml index a11d13d127..5daf747662 100644 --- a/.github/workflows/jaeger-ui.yml +++ b/.github/workflows/jaeger-ui.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Cache Docker images. - uses: ScribeMD/docker-cache@0.2.6 + uses: ScribeMD/docker-cache@0.3.6 with: key: docker-release-jaeger diff --git a/contrib/git-changes-action/README.md b/contrib/git-changes-action/README.md index 7fd78bc2db..6cacbbe1dd 100644 --- a/contrib/git-changes-action/README.md +++ b/contrib/git-changes-action/README.md @@ -14,7 +14,7 @@ This GitHub Action exports a variable that contains the list of Go modules chang ```yaml steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 submodules: 'recursive' @@ -26,14 +26,12 @@ This GitHub Action exports a variable that contains the list of Go modules chang - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:latest id: filter_go with: - include_deps: true github_token: ${{ secrets.github_token }} timeout: "1m" # optional, defaults to 1m ``` You can customize the behavior of the git-changes script by using the following inputs: - - `include_deps`: A boolean that controls whether dependent modules are included in the list of changed modules. Set to true by default. - `github_token`: The token to use for authentication with the GitHub API. This is required to fetch information about the current pull request. - `timeout`: The maximum time to wait for the GitHub API to respond. Defaults to 1 minute. @@ -41,6 +39,9 @@ The output of the git-changes script is a comma-separated list of Go module path ```yaml - run: echo "Changed modules: ${{ steps.filter_go.outputs.changed_modules }}" + - run: echo "Unchanged modules: ${{ steps.filter_go.outputs.unchanged_modules }}" + - run: echo "Changed modules (including dependencies): ${{ steps.filter_go.outputs.changed_modules_deps }}" + - run: echo "Unchanged modules (including dependencies): ${{ steps.filter_go.outputs.unchanged_modules_deps }}" ``` ## Example @@ -67,7 +68,6 @@ jobs: - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:latest id: filter_go with: - include_deps: true github_token: ${{ secrets.github_token }} timeout: "1m" @@ -97,30 +97,31 @@ Each module in the `go.work` is visited. If any changes were detected by the pre ```mermaid sequenceDiagram - participant GW as go.work - participant M as Module - participant CML as Changed_Module_List - participant ID as include_dependencies - participant D as Dependency - - GW->>M: Visit Module - Note over M: Check for changes - M-->>GW: Changes Detected? - alt Changes Detected - GW->>CML: Add Module to Changed_Module_List - else No Changes Detected - GW-->>M: Skip Module + participant GW as go.work + participant M as Module + participant CML as Changed_Module_List + participant UML as Unchanged_Module_List + participant D as Dependency + + GW->>M: Visit Module + Note over M: Check for changes + M-->>GW: Changes Detected? + alt Changes Detected + GW->>CML: Add Module to Changed_Module_List + M->>D: Has Dependency in go.work? + alt Has Dependency + GW->>CML: Add Dependency to Changed_Module_List + else No Dependency + M-->>GW: No Dependency to Add end - GW->>ID: include_dependencies On? - alt include_dependencies On - M->>D: Has Dependency in go.work? - alt Has Dependency - GW->>CML: Add Dependency to Changed_Module_List - else No Dependency - M-->>GW: No Dependency to Add - end - else include_dependencies Off - GW-->>M: Skip Dependency Check + else No Changes Detected + GW->>UML: Add Module to Unchanged_Module_List + M->>D: Has Dependency in go.work? + alt Has Dependency + GW->>UML: Add Dependency to Unchanged_Module_List + else No Dependency + M-->>GW: No Dependency to Add end - GW->>GW: Continue Until All Modules Visited + end + GW->>GW: Continue Until All Modules Visited ``` diff --git a/contrib/git-changes-action/main.go b/contrib/git-changes-action/main.go index 01cbf82280..60e2ea6883 100644 --- a/contrib/git-changes-action/main.go +++ b/contrib/git-changes-action/main.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/synapsecns/sanguine/contrib/git-changes-action/detector/tree" "os" "sort" "strings" @@ -32,8 +33,6 @@ func main() { panic(fmt.Errorf("failed to parse timeout: %w", err)) } - includeDeps := getIncludeDeps() - ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -42,44 +41,57 @@ func main() { panic(err) } - modules, err := detector.DetectChangedModules(workingDirectory, ct, includeDeps) + noDepChanged, noDepUnchanged, err := outputModuleChanges(workingDirectory, ct, false) + if err != nil { + panic(err) + } + + depChanged, depUnchanged, err := outputModuleChanges(workingDirectory, ct, true) if err != nil { panic(err) } - var changedModules []string + githubactions.SetOutput("changed_modules", noDepChanged) + githubactions.SetOutput("unchanged_modules", noDepUnchanged) + + githubactions.SetOutput("changed_modules_deps", depChanged) + githubactions.SetOutput("unchanged_modules_deps", depUnchanged) +} + +// outputModuleChanges outputs the changed modules. +// this wraps detector.DetectChangedModules and handles the output formatting to be parsable by github actions. +// the final output is a json array of strings. +func outputModuleChanges(workingDirectory string, ct tree.Tree, includeDeps bool) (changedJSON string, unchangedJson string, err error) { + modules, err := detector.DetectChangedModules(workingDirectory, ct, includeDeps) + if err != nil { + return changedJSON, unchangedJson, fmt.Errorf("failed to detect changed modules w/ include deps set to %v: %w", includeDeps, err) + } + + var changedModules, unchangedModules []string for module, changed := range modules { - if !changed { - continue - } + modName := strings.TrimPrefix(module, "./") - changedModules = append(changedModules, strings.TrimPrefix(module, "./")) + if changed { + changedModules = append(changedModules, modName) + } else { + unchangedModules = append(unchangedModules, modName) + } } sort.Strings(changedModules) - marshalledJSON, err := json.Marshal(changedModules) + sort.Strings(unchangedModules) + + marshalledChanged, err := json.Marshal(changedModules) if err != nil { - panic(err) + return changedJSON, unchangedJson, fmt.Errorf("failed to marshall changed module json w/ include deps set to %v: %w", includeDeps, err) } - if len(changedModules) == 0 { - fmt.Println("no modules changed") - } else { - fmt.Printf("setting output to %s\n", marshalledJSON) + marshalledUnchanged, err := json.Marshal(unchangedModules) + if err != nil { + return changedJSON, unchangedJson, fmt.Errorf("failed to marshall unchanged module json w/ include deps set to %v: %w", includeDeps, err) } - githubactions.SetOutput("changed_modules", string(marshalledJSON)) -} -// getIncludeDeps gets the include deps setting. -// If it is not set, it defaults to false. -func getIncludeDeps() (includeDeps bool) { - rawIncludeDeps := githubactions.GetInput("include_deps") - - includeDeps = false - if rawIncludeDeps == "true" { - includeDeps = true - } - return + return string(marshalledChanged), string(marshalledUnchanged), nil } // getTimeout gets the timeout setting. If it is not set, it defaults to 1 minute. From f8149afb0c8523f8554a6c872c57b37c6062f04d Mon Sep 17 00:00:00 2001 From: trajan0x <83933037+trajan0x@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:34:38 -0400 Subject: [PATCH 2/8] Use latest git-changes-action Co-authored-by: Trajan0x Can be done now that #1399 is merged --- .github/workflows/go.yml | 2 +- .github/workflows/goreleaser-actions.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9d27c84ed5..8a756cfbf3 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -43,7 +43,7 @@ jobs: submodules: 'recursive' # note: after this action is pushed, whatever this ends up being should go back to latest. - - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:e4c85e74bf0592c0efa71476c3246675ec77fc28 + - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:latest id: filter_go with: github_token: ${{ secrets.WORKFLOW_PAT }} diff --git a/.github/workflows/goreleaser-actions.yml b/.github/workflows/goreleaser-actions.yml index b6563e4c53..816e2125b4 100644 --- a/.github/workflows/goreleaser-actions.yml +++ b/.github/workflows/goreleaser-actions.yml @@ -95,7 +95,7 @@ jobs: fetch-depth: 0 submodules: 'recursive' - - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:e4c85e74bf0592c0efa71476c3246675ec77fc28 + - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:latest id: filter_go with: github_token: ${{ secrets.WORKFLOW_PAT }} From 568a7e8cd40d69f12f9253b0778df01e7b4469f0 Mon Sep 17 00:00:00 2001 From: trajan0x <83933037+trajan0x@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:41:20 -0400 Subject: [PATCH 3/8] don't run on master (#1403) * don't run on master * rename --------- Co-authored-by: Trajan0x --- .../{clean-up-pr-caches.yml => clean-up-pr.yml} | 9 ++++++--- .github/workflows/go.yml | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) rename .github/workflows/{clean-up-pr-caches.yml => clean-up-pr.yml} (81%) diff --git a/.github/workflows/clean-up-pr-caches.yml b/.github/workflows/clean-up-pr.yml similarity index 81% rename from .github/workflows/clean-up-pr-caches.yml rename to .github/workflows/clean-up-pr.yml index b31bacc14a..053e8e6cf7 100644 --- a/.github/workflows/clean-up-pr-caches.yml +++ b/.github/workflows/clean-up-pr.yml @@ -13,9 +13,6 @@ jobs: cleanup: runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Cleanup caches run: | gh extension install actions/gh-actions-cache @@ -36,3 +33,9 @@ jobs: echo "Done" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Delete closed PR branch + uses: dawidd6/action-delete-branch@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + numbers: ${{github.event.pull_request.number}} diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8a756cfbf3..5daa354963 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -353,7 +353,7 @@ jobs: needs: cancel-outdated # currently, this matches the logic in the go generate check. If we ever add more checks that run on all packages, we should # change this to run on those pushes - if: ${{ github.event_name != 'pull_request' }} + if: ${{ github.event_name != 'pull_request' && format('refs/heads/{0}', github.event.repository.default_branch) != github.ref }} outputs: issue_number: ${{ steps.find_pr.outputs.pr }} metadata: ${{ steps.metadata.outputs.METADATA }} From 759085e544961a297ee9b9b967d353dccd5d26cf Mon Sep 17 00:00:00 2001 From: trajan0x <83933037+trajan0x@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:47:50 -0400 Subject: [PATCH 4/8] softfail cleanup (#1404) * softfail cleanup * continue (see future work of #1399 since this is getting annoying --------- Co-authored-by: Trajan0x --- .github/workflows/clean-up-pr.yml | 1 + .github/workflows/go.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/clean-up-pr.yml b/.github/workflows/clean-up-pr.yml index 053e8e6cf7..003bb35714 100644 --- a/.github/workflows/clean-up-pr.yml +++ b/.github/workflows/clean-up-pr.yml @@ -39,3 +39,4 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} numbers: ${{github.event.pull_request.number}} + soft_fail: true diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5daa354963..5252a0f98d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -375,6 +375,7 @@ jobs: id: metadata shell: bash + continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OWNER: ${{ github.repository_owner }} From 8ce39642a360040af41b3b690ee25b44c90bf08f Mon Sep 17 00:00:00 2001 From: abtestingalpha <104046418+abtestingalpha@users.noreply.github.com> Date: Mon, 25 Sep 2023 19:38:55 -0400 Subject: [PATCH 5/8] Adds balance chain tokens for chain dropdown (#1396) --- .../SelectSpecificNetworkButton.tsx | 111 ++++++++++++++++-- .../constants/chains/master.tsx | 2 +- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/packages/synapse-interface/components/StateManagedBridge/components/SelectSpecificNetworkButton.tsx b/packages/synapse-interface/components/StateManagedBridge/components/SelectSpecificNetworkButton.tsx index f12b7c8b4a..1852838824 100644 --- a/packages/synapse-interface/components/StateManagedBridge/components/SelectSpecificNetworkButton.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/components/SelectSpecificNetworkButton.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { CHAINS_BY_ID } from '@constants/chains' import Image from 'next/image' import { @@ -10,6 +10,11 @@ import { getNetworkButtonBorderActive, getMenuItemStyleForChain, } from '@/styles/chains' +import { usePortfolioState } from '@/slices/portfolio/hooks' +import { + TokenWithBalanceAndAllowances, + sortTokensByBalanceDescending, +} from '@/utils/actions/fetchPortfolioBalances' export const SelectSpecificNetworkButton = ({ itemChainId, @@ -50,7 +55,7 @@ export const SelectSpecificNetworkButton = ({ ref={ref} tabIndex={active ? 1 : 0} className={` - flex items-center + flex items-center justify-between transition-all duration-75 w-full px-2 py-4 @@ -74,17 +79,105 @@ export const SelectSpecificNetworkButton = ({ function ButtonContent({ chainId }: { chainId: number }) { const chain = CHAINS_BY_ID[chainId] + const { balancesAndAllowances } = usePortfolioState() + + const balanceTokens = + balancesAndAllowances && + balancesAndAllowances[chainId] && + sortTokensByBalanceDescending( + balancesAndAllowances[chainId].filter((bt) => bt.balance > 0n) + ) return chain ? ( <> - Switch Network -
-
{chain.name}
+
+ Switch Network +
+
{chain.name}
+
+ {balanceTokens && balanceTokens.length > 0 ? ( + + ) : null} ) : null } + +const ChainTokens = ({ + balanceTokens = [], +}: { + balanceTokens: TokenWithBalanceAndAllowances[] +}) => { + const [isHovered, setIsHovered] = useState(false) + const hasOneToken = useMemo( + () => balanceTokens && balanceTokens.length > 0, + [balanceTokens] + ) + const hasTwoTokens = useMemo( + () => balanceTokens && balanceTokens.length > 1, + [balanceTokens] + ) + const numOverTwoTokens = useMemo( + () => + balanceTokens && balanceTokens.length - 2 > 0 + ? balanceTokens.length - 2 + : 0, + [balanceTokens] + ) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {hasOneToken && ( + {`${balanceTokens[0].token.symbol} + )} + {hasTwoTokens && ( + {`${balanceTokens[1].token.symbol} + )} + {numOverTwoTokens > 0 && ( +
+ {numOverTwoTokens}
+ )} +
+ {isHovered && ( +
+ {balanceTokens.map( + (token: TokenWithBalanceAndAllowances, key: number) => { + const tokenSymbol = token.token.symbol + const balance = token.parsedBalance + return ( +
+ {balance} {tokenSymbol} +
+ ) + } + )} +
+ )} +
+
+ ) +} diff --git a/packages/synapse-interface/constants/chains/master.tsx b/packages/synapse-interface/constants/chains/master.tsx index c01268700c..55f7f24ce0 100644 --- a/packages/synapse-interface/constants/chains/master.tsx +++ b/packages/synapse-interface/constants/chains/master.tsx @@ -406,7 +406,7 @@ export const DOGE: Chain = { } export const BASE: Chain = { - priorityRank: 1, + priorityRank: 90, id: 8453, chainSymbol: 'ETH', name: 'Base', From 8174003e86c0e797ed21715ee72edd330edebd91 Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Mon, 25 Sep 2023 23:42:51 +0000 Subject: [PATCH 6/8] Publish - @synapsecns/synapse-interface@0.1.133 --- packages/synapse-interface/CHANGELOG.md | 8 ++++++++ packages/synapse-interface/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/synapse-interface/CHANGELOG.md b/packages/synapse-interface/CHANGELOG.md index c12a86fd8f..ecd81fdc17 100644 --- a/packages/synapse-interface/CHANGELOG.md +++ b/packages/synapse-interface/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.133](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.1.132...@synapsecns/synapse-interface@0.1.133) (2023-09-25) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + ## [0.1.132](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.1.131...@synapsecns/synapse-interface@0.1.132) (2023-09-15) **Note:** Version bump only for package @synapsecns/synapse-interface diff --git a/packages/synapse-interface/package.json b/packages/synapse-interface/package.json index 10356be875..3900c3d328 100644 --- a/packages/synapse-interface/package.json +++ b/packages/synapse-interface/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/synapse-interface", - "version": "0.1.132", + "version": "0.1.133", "private": true, "engines": { "node": ">=16.0.0" From 64aadea522426ad288e6494a538098232094edc8 Mon Sep 17 00:00:00 2001 From: abtestingalpha <104046418+abtestingalpha@users.noreply.github.com> Date: Mon, 25 Sep 2023 20:05:56 -0400 Subject: [PATCH 7/8] Swap redux refactor (#1391) * Adds swap reducer * State managed swap & components * Adds swapFinder * Generating swap routes * Fleshes out swap reducer * Sets swapChainId on connect / network switch * Code to swap page, deprecates unused helpers * Swap txn button * Deprecates unused components * Removes deprecated Bridgewatcher code * Maps toChainId to fromChainId * Linting * Addresses coderabbit comments * UserProvider * Address more coderabbit comments * Handles paused tokens in reducer * Resets portfolio state on disconnect --- .../components/ActionCardFooter.tsx | 25 - .../components/ChainLabel.tsx | 150 ---- .../ConnectionIndicators.tsx | 0 .../synapse-interface/components/Popup.tsx | 34 - .../StateManagedBridge/FromChainSelector.tsx | 2 +- .../StateManagedBridge/FromTokenSelector.tsx | 2 +- .../StateManagedBridge/InputContainer.tsx | 2 +- .../StateManagedBridge/ToChainSelector.tsx | 2 +- .../StateManagedBridge/ToTokenSelector.tsx | 2 +- .../helpers/sortByBalance.ts | 3 +- .../StateManagedSwap/SwapChainListOverlay.tsx | 194 ++++ .../StateManagedSwap/SwapChainSelector.tsx | 63 ++ .../SwapExchangeRateInfo.tsx | 0 .../SwapFromTokenListOverlay.tsx | 223 +++++ .../SwapFromTokenSelector.tsx | 57 ++ .../StateManagedSwap/SwapInputContainer.tsx | 172 ++++ .../StateManagedSwap/SwapOutputContainer.tsx | 64 ++ .../SwapToTokenListOverlay.tsx | 254 ++++++ .../StateManagedSwap/SwapToTokenSelector.tsx | 57 ++ .../SwapTransactionButton.tsx | 127 +++ .../components/CloseButton.tsx | 17 + .../components/SearchResults.tsx | 21 + .../SelectSpecificNetworkButton.tsx | 90 ++ .../components/SelectSpecificTokenButton.tsx | 207 +++++ .../StateManagedSwap/helpers/sortByBalance.ts | 25 + .../helpers/sortByPriorityRank.ts | 11 + .../components/TransactionItems.tsx | 106 --- .../components => icons}/DropDownArrowSvg.tsx | 0 .../TokenAmountInput/SelectTokenDropdown.tsx | 84 -- .../input/TokenAmountInput/index.tsx | 172 ---- .../components/pairTxKappa.tsx | 57 -- .../components/ui/tailwind/Modal.tsx | 44 - .../components/ui/tailwind/ModalHeadline.tsx | 38 - .../constants/existingSwapRoutes.ts | 52 ++ packages/synapse-interface/constants/index.ts | 2 + .../constants/tokens/deprecated.ts | 4 +- .../constants/tokens/index.ts | 128 +-- ...AnalyticsProvider.tsx => UserProvider.tsx} | 34 +- packages/synapse-interface/pages/_app.tsx | 14 +- .../bridge/BridgeWatcher/BlockCountdown.tsx | 158 ---- .../bridge/BridgeWatcher/BridgeEvent.tsx | 31 - .../bridge/BridgeWatcher/DestinationTx.tsx | 243 ----- .../pages/bridge/BridgeWatcher/EventCard.tsx | 97 -- .../bridge/BridgeWatcher/ExplorerLink.tsx | 28 - .../pages/bridge/BridgeWatcher/index.tsx | 306 ------- .../pages/state-managed-bridge/index.tsx | 9 - .../pages/swap/NoSwapCard.tsx | 26 - .../synapse-interface/pages/swap/SwapCard.tsx | 844 ------------------ .../synapse-interface/pages/swap/index.tsx | 461 +++++++++- .../slices/bridge/reducer.ts | 12 +- .../synapse-interface/slices/swap/hooks.ts | 6 + .../synapse-interface/slices/swap/reducer.ts | 295 ++++++ .../slices/swapDisplaySlice.ts | 40 + packages/synapse-interface/store/store.ts | 4 + .../synapse-interface/utils/bridgeWatcher.ts | 206 ----- .../utils/findTokenByAddressAndChainId.ts | 21 + .../synapse-interface/utils/findValidToken.ts | 12 + .../utils/generateChainIdAddressMapping.ts | 10 +- packages/synapse-interface/utils/getSymbol.ts | 3 + .../swapFinder/generateSwapPossibilities.ts | 82 ++ .../utils/swapFinder/getSwapFromChainIds.ts | 283 ++++++ .../utils/swapFinder/getSwapFromTokens.ts | 235 +++++ .../utils/swapFinder/getSwapToTokens.ts | 213 +++++ .../utils/swapFinder/getTokenAndChainId.ts | 5 + 64 files changed, 3302 insertions(+), 2867 deletions(-) delete mode 100644 packages/synapse-interface/components/ActionCardFooter.tsx delete mode 100644 packages/synapse-interface/components/ChainLabel.tsx rename packages/synapse-interface/components/{StateManagedBridge => }/ConnectionIndicators.tsx (100%) delete mode 100644 packages/synapse-interface/components/Popup.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapChainListOverlay.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapChainSelector.tsx rename packages/synapse-interface/components/{ => StateManagedSwap}/SwapExchangeRateInfo.tsx (100%) create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapFromTokenListOverlay.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapFromTokenSelector.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapOutputContainer.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapToTokenListOverlay.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapToTokenSelector.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/SwapTransactionButton.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/components/CloseButton.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/components/SearchResults.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/components/SelectSpecificNetworkButton.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/components/SelectSpecificTokenButton.tsx create mode 100644 packages/synapse-interface/components/StateManagedSwap/helpers/sortByBalance.ts create mode 100644 packages/synapse-interface/components/StateManagedSwap/helpers/sortByPriorityRank.ts delete mode 100644 packages/synapse-interface/components/TransactionItems.tsx rename packages/synapse-interface/components/{StateManagedBridge/components => icons}/DropDownArrowSvg.tsx (100%) delete mode 100644 packages/synapse-interface/components/input/TokenAmountInput/SelectTokenDropdown.tsx delete mode 100644 packages/synapse-interface/components/input/TokenAmountInput/index.tsx delete mode 100644 packages/synapse-interface/components/pairTxKappa.tsx delete mode 100644 packages/synapse-interface/components/ui/tailwind/Modal.tsx delete mode 100644 packages/synapse-interface/components/ui/tailwind/ModalHeadline.tsx create mode 100644 packages/synapse-interface/constants/existingSwapRoutes.ts rename packages/synapse-interface/contexts/{WalletAnalyticsProvider.tsx => UserProvider.tsx} (59%) delete mode 100644 packages/synapse-interface/pages/bridge/BridgeWatcher/BlockCountdown.tsx delete mode 100644 packages/synapse-interface/pages/bridge/BridgeWatcher/BridgeEvent.tsx delete mode 100644 packages/synapse-interface/pages/bridge/BridgeWatcher/DestinationTx.tsx delete mode 100644 packages/synapse-interface/pages/bridge/BridgeWatcher/EventCard.tsx delete mode 100644 packages/synapse-interface/pages/bridge/BridgeWatcher/ExplorerLink.tsx delete mode 100644 packages/synapse-interface/pages/bridge/BridgeWatcher/index.tsx delete mode 100644 packages/synapse-interface/pages/swap/NoSwapCard.tsx delete mode 100644 packages/synapse-interface/pages/swap/SwapCard.tsx create mode 100644 packages/synapse-interface/slices/swap/hooks.ts create mode 100644 packages/synapse-interface/slices/swap/reducer.ts create mode 100644 packages/synapse-interface/slices/swapDisplaySlice.ts delete mode 100644 packages/synapse-interface/utils/bridgeWatcher.ts create mode 100644 packages/synapse-interface/utils/findTokenByAddressAndChainId.ts create mode 100644 packages/synapse-interface/utils/findValidToken.ts create mode 100644 packages/synapse-interface/utils/getSymbol.ts create mode 100644 packages/synapse-interface/utils/swapFinder/generateSwapPossibilities.ts create mode 100644 packages/synapse-interface/utils/swapFinder/getSwapFromChainIds.ts create mode 100644 packages/synapse-interface/utils/swapFinder/getSwapFromTokens.ts create mode 100644 packages/synapse-interface/utils/swapFinder/getSwapToTokens.ts create mode 100644 packages/synapse-interface/utils/swapFinder/getTokenAndChainId.ts diff --git a/packages/synapse-interface/components/ActionCardFooter.tsx b/packages/synapse-interface/components/ActionCardFooter.tsx deleted file mode 100644 index fc3f509ecc..0000000000 --- a/packages/synapse-interface/components/ActionCardFooter.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ArrowSmRightIcon, ChartSquareBarIcon } from '@heroicons/react/outline' - -import { EXPLORER_PATH } from '@/constants/urls' - -export const ActionCardFooter = ({ link }: { link: string }) => { - return ( - - ) -} diff --git a/packages/synapse-interface/components/ChainLabel.tsx b/packages/synapse-interface/components/ChainLabel.tsx deleted file mode 100644 index 7b15a532d7..0000000000 --- a/packages/synapse-interface/components/ChainLabel.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { ChevronDownIcon } from '@heroicons/react/outline' -import { CHAINS_BY_ID, ORDERED_CHAINS_BY_ID } from '@constants/chains' -import * as CHAINS from '@constants/chains/master' -import { getNetworkButtonBorder } from '@/styles/chains' -import { getOrderedChains } from '@utils/getOrderedChains' -import Image from 'next/image' -import Tooltip from '@tw/Tooltip' -import { useEffect, useState } from 'react' -import { DisplayType } from '@/pages/bridge/DisplayType' - -export const ChainLabel = ({ - isOrigin, - chains, - chainId, - titleText, - connectedChainId, - labelClassNameOverride, - onChangeChain, - setDisplayType, -}: { - isOrigin: boolean - chains: string[] | undefined - chainId: number - titleText?: string - connectedChainId: number - labelClassNameOverride?: string - onChangeChain: (chainId: number, flip: boolean, type: 'from' | 'to') => void - setDisplayType: (v: string) => void -}) => { - const labelClassName = 'text-sm' - const displayType = isOrigin ? DisplayType.FROM_CHAIN : DisplayType.TO_CHAIN - const dataId = isOrigin ? 'bridge-origin-chain' : 'bridge-destination-chain' - const title = titleText ?? (isOrigin ? 'Origin' : 'Dest.') - const [orderedChains, setOrderedChains] = useState([]) - useEffect(() => { - setOrderedChains( - chainOrderBySwapSide(connectedChainId, isOrigin, chainId, chains) - ) - }, [chainId, connectedChainId, chains]) - - return ( -
- -
- {orderedChains.map((id) => - Number(id) === chainId ? ( - - ) : ( - - ) - )} - -
-
- ) -} - -const PossibleChain = ({ - chainId, - onChangeChain, - isOrigin, -}: { - chainId: number - onChangeChain: (chainId: number, flip: boolean, type: 'from' | 'to') => void - isOrigin: boolean -}) => { - const chain = CHAINS_BY_ID[chainId] - return chain ? ( - - ) : null -} - -const SelectedChain = ({ chainId }: { chainId: number }) => { - const chain = CHAINS_BY_ID[chainId] - return chain ? ( -
- chain image -
-
- {chain.name === 'Boba Network' ? 'Boba' : chain.name} -
-
-
- ) : null -} - -const chainOrderBySwapSide = ( - connectedChain: number, - isOrigin: boolean, - chainId: number, - chains: string[] | undefined -) => { - let orderedChains - if (isOrigin) { - orderedChains = ORDERED_CHAINS_BY_ID.filter((e) => e !== String(chainId)) - orderedChains = orderedChains.slice(0, 5) - orderedChains.unshift(chainId) - return orderedChains - } else { - return getOrderedChains(connectedChain, chainId, chains) - } -} diff --git a/packages/synapse-interface/components/StateManagedBridge/ConnectionIndicators.tsx b/packages/synapse-interface/components/ConnectionIndicators.tsx similarity index 100% rename from packages/synapse-interface/components/StateManagedBridge/ConnectionIndicators.tsx rename to packages/synapse-interface/components/ConnectionIndicators.tsx diff --git a/packages/synapse-interface/components/Popup.tsx b/packages/synapse-interface/components/Popup.tsx deleted file mode 100644 index bef8575530..0000000000 --- a/packages/synapse-interface/components/Popup.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect } from 'react' -import { IMPAIRED_CHAINS } from '@/constants/impairedChains' -const Popup = ({ chainId }) => { - const [active, setActive] = useState(false) - - useEffect(() => { - if (chainId && IMPAIRED_CHAINS[chainId]) { - setActive(true) - } else { - setActive(false) - } - }, [chainId]) - - if (!active) return null - return ( -
-
-
- {IMPAIRED_CHAINS[chainId] && IMPAIRED_CHAINS[chainId].content()} -
-
- -
-
-
- ) -} - -export default Popup diff --git a/packages/synapse-interface/components/StateManagedBridge/FromChainSelector.tsx b/packages/synapse-interface/components/StateManagedBridge/FromChainSelector.tsx index a55118bf7b..61a377a876 100644 --- a/packages/synapse-interface/components/StateManagedBridge/FromChainSelector.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/FromChainSelector.tsx @@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux' import { setShowFromChainListOverlay } from '@/slices/bridgeDisplaySlice' import { useBridgeState } from '@/slices/bridge/hooks' import { CHAINS_BY_ID } from '@/constants/chains' -import { DropDownArrowSvg } from './components/DropDownArrowSvg' +import { DropDownArrowSvg } from '../icons/DropDownArrowSvg' import { getNetworkButtonBgClassNameActive, getNetworkButtonBorderActive, diff --git a/packages/synapse-interface/components/StateManagedBridge/FromTokenSelector.tsx b/packages/synapse-interface/components/StateManagedBridge/FromTokenSelector.tsx index bed5ec2f94..20ce214938 100644 --- a/packages/synapse-interface/components/StateManagedBridge/FromTokenSelector.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/FromTokenSelector.tsx @@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux' import { setShowFromTokenListOverlay } from '@/slices/bridgeDisplaySlice' import { useBridgeState } from '@/slices/bridge/hooks' -import { DropDownArrowSvg } from './components/DropDownArrowSvg' +import { DropDownArrowSvg } from '../icons/DropDownArrowSvg' import { getBorderStyleForCoinHover, getMenuItemHoverBgForCoin, diff --git a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx index 3b31daced6..ae7f1294a6 100644 --- a/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/InputContainer.tsx @@ -10,7 +10,7 @@ import { ConnectToNetworkButton, ConnectWalletButton, ConnectedIndicator, -} from './ConnectionIndicators' +} from '@/components/ConnectionIndicators' import { FromChainSelector } from './FromChainSelector' import { FromTokenSelector } from './FromTokenSelector' import { useBridgeState } from '@/slices/bridge/hooks' diff --git a/packages/synapse-interface/components/StateManagedBridge/ToChainSelector.tsx b/packages/synapse-interface/components/StateManagedBridge/ToChainSelector.tsx index 0038eb3f3c..3934fe8d17 100644 --- a/packages/synapse-interface/components/StateManagedBridge/ToChainSelector.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/ToChainSelector.tsx @@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux' import { setShowToChainListOverlay } from '@/slices/bridgeDisplaySlice' import { useBridgeState } from '@/slices/bridge/hooks' import { CHAINS_BY_ID } from '@/constants/chains' -import { DropDownArrowSvg } from './components/DropDownArrowSvg' +import { DropDownArrowSvg } from '../icons/DropDownArrowSvg' import { getNetworkButtonBgClassNameActive, getNetworkButtonBorderActive, diff --git a/packages/synapse-interface/components/StateManagedBridge/ToTokenSelector.tsx b/packages/synapse-interface/components/StateManagedBridge/ToTokenSelector.tsx index 6d2159d908..47166d0b78 100644 --- a/packages/synapse-interface/components/StateManagedBridge/ToTokenSelector.tsx +++ b/packages/synapse-interface/components/StateManagedBridge/ToTokenSelector.tsx @@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux' import { setShowToTokenListOverlay } from '@/slices/bridgeDisplaySlice' import { useBridgeState } from '@/slices/bridge/hooks' -import { DropDownArrowSvg } from './components/DropDownArrowSvg' +import { DropDownArrowSvg } from '../icons/DropDownArrowSvg' import { getBorderStyleForCoinHover, getMenuItemHoverBgForCoin, diff --git a/packages/synapse-interface/components/StateManagedBridge/helpers/sortByBalance.ts b/packages/synapse-interface/components/StateManagedBridge/helpers/sortByBalance.ts index c7f1fc31f3..e8447b81f3 100644 --- a/packages/synapse-interface/components/StateManagedBridge/helpers/sortByBalance.ts +++ b/packages/synapse-interface/components/StateManagedBridge/helpers/sortByBalance.ts @@ -1,11 +1,12 @@ import _ from 'lodash' import { Token } from '@/utils/types' +import { NetworkTokenBalancesAndAllowances } from '@/utils/actions/fetchPortfolioBalances' export const hasBalance = ( t: Token, chainId: number, - portfolioBalances: any + portfolioBalances: NetworkTokenBalancesAndAllowances ) => { if (!chainId) { return false diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapChainListOverlay.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapChainListOverlay.tsx new file mode 100644 index 0000000000..bb98d7d435 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapChainListOverlay.tsx @@ -0,0 +1,194 @@ +import _ from 'lodash' +import { useCallback, useEffect, useRef, useState } from 'react' +import Fuse from 'fuse.js' +import { useKeyPress } from '@hooks/useKeyPress' +import * as ALL_CHAINS from '@constants/chains/master' +import SlideSearchBox from '@pages/bridge/SlideSearchBox' +import { CHAINS_BY_ID, sortChains } from '@constants/chains' +import { useDispatch } from 'react-redux' +import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' +import { setShowFromChainListOverlay } from '@/slices/bridgeDisplaySlice' +import { SelectSpecificNetworkButton } from './components/SelectSpecificNetworkButton' +import useCloseOnOutsideClick from '@/utils/hooks/useCloseOnOutsideClick' +import { CloseButton } from './components/CloseButton' +import { SearchResults } from './components/SearchResults' +import { setShowSwapChainListOverlay } from '@/slices/swapDisplaySlice' +import { setSwapChainId } from '@/slices/swap/reducer' +import { useSwapState } from '@/slices/swap/hooks' + +export const SwapChainListOverlay = () => { + const { swapChainId, swapFromChainIds } = useSwapState() + const [currentIdx, setCurrentIdx] = useState(-1) + const [searchStr, setSearchStr] = useState('') + const dispatch = useDispatch() + const dataId = 'swap-origin-chain-list' + const overlayRef = useRef(null) + + let possibleChains = sortChains( + _(ALL_CHAINS) + .pickBy((value) => _.includes(swapFromChainIds, value.id)) + .values() + .value() + ) + + let remainingChains = swapFromChainIds + ? sortChains( + _.difference( + Object.keys(CHAINS_BY_ID).map((id) => CHAINS_BY_ID[id]), + swapFromChainIds.map((id) => CHAINS_BY_ID[id]) + ) + ) + : [] + + const possibleChainsWithSource = possibleChains.map((chain) => ({ + ...chain, + source: 'possibleChains', + })) + + const remainingChainsWithSource = remainingChains.map((chain) => ({ + ...chain, + source: 'remainingChains', + })) + + const masterList = [...possibleChainsWithSource, ...remainingChainsWithSource] + + const fuseOptions = { + includeScore: true, + threshold: 0.0, + keys: [ + { + name: 'name', + weight: 2, + }, + 'id', + 'nativeCurrency.symbol', + ], + } + + const fuse = new Fuse(masterList, fuseOptions) + + if (searchStr?.length > 0) { + const results = fuse.search(searchStr).map((i) => i.item) + + possibleChains = results.filter((item) => item.source === 'possibleChains') + remainingChains = results.filter( + (item) => item.source === 'remainingChains' + ) + } + + const escPressed = useKeyPress('Escape') + const arrowUp = useKeyPress('ArrowUp') + const arrowDown = useKeyPress('ArrowDown') + + const onClose = useCallback(() => { + setCurrentIdx(-1) + setSearchStr('') + dispatch(setShowSwapChainListOverlay(false)) + }, [dispatch]) + + const escFunc = () => { + if (escPressed) { + onClose() + } + } + const arrowDownFunc = () => { + const nextIdx = currentIdx + 1 + if (arrowDown && nextIdx < masterList.length) { + setCurrentIdx(nextIdx) + } + } + + const arrowUpFunc = () => { + const nextIdx = currentIdx - 1 + if (arrowUp && -1 < nextIdx) { + setCurrentIdx(nextIdx) + } + } + + const onSearch = (str: string) => { + setSearchStr(str) + setCurrentIdx(-1) + } + + useEffect(arrowDownFunc, [arrowDown]) + useEffect(escFunc, [escPressed]) + useEffect(arrowUpFunc, [arrowUp]) + useCloseOnOutsideClick(overlayRef, onClose) + + const handleSetSwapChainId = (chainId) => { + const eventTitle = `[Swap User Action] Sets new fromChainId` + const eventData = { + previousFromChainId: swapChainId, + newFromChainId: chainId, + } + + segmentAnalyticsEvent(eventTitle, eventData) + dispatch(setSwapChainId(chainId)) + onClose() + } + + return ( +
+
+
+ + +
+
+
+ {possibleChains && possibleChains.length > 0 && ( + <> +
From…
+ {possibleChains.map(({ id: mapChainId }, idx) => { + return ( + { + if (swapChainId === mapChainId) { + onClose() + } else { + handleSetSwapChainId(mapChainId) + } + }} + dataId={dataId} + /> + ) + })} + + )} + {remainingChains && remainingChains.length > 0 && ( + <> +
+ All chains +
+ {remainingChains.map(({ id: mapChainId }, idx) => { + return ( + handleSetSwapChainId(mapChainId)} + dataId={dataId} + alternateBackground={true} + /> + ) + })} + + )} + +
+
+ ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapChainSelector.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapChainSelector.tsx new file mode 100644 index 0000000000..b0204ecbaf --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapChainSelector.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { useDispatch } from 'react-redux' + +import { CHAINS_BY_ID } from '@/constants/chains' +import { DropDownArrowSvg } from '@/components/icons/DropDownArrowSvg' +import { + getNetworkButtonBgClassNameActive, + getNetworkButtonBorderActive, + getNetworkButtonBorderHover, + getNetworkHover, +} from '@/styles/chains' +import { useSwapState } from '@/slices/swap/hooks' +import { setShowSwapChainListOverlay } from '@/slices/swapDisplaySlice' + +export const SwapChainSelector = () => { + const dispatch = useDispatch() + const { swapChainId } = useSwapState() + const chain = CHAINS_BY_ID[swapChainId] + + const buttonContent = swapChainId ? ( +
+
+ {chain?.name} +
+
+
From
+
{chain.name}
+
+ +
+ ) : ( +
+
+
From
+
Network
+
+ +
+ ) + + return ( + + ) +} diff --git a/packages/synapse-interface/components/SwapExchangeRateInfo.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapExchangeRateInfo.tsx similarity index 100% rename from packages/synapse-interface/components/SwapExchangeRateInfo.tsx rename to packages/synapse-interface/components/StateManagedSwap/SwapExchangeRateInfo.tsx diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapFromTokenListOverlay.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapFromTokenListOverlay.tsx new file mode 100644 index 0000000000..d97b6318a1 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapFromTokenListOverlay.tsx @@ -0,0 +1,223 @@ +import _ from 'lodash' + +import { useEffect, useRef, useState } from 'react' +import { useDispatch } from 'react-redux' +import Fuse from 'fuse.js' + +import { useKeyPress } from '@hooks/useKeyPress' +import SlideSearchBox from '@pages/bridge/SlideSearchBox' +import { Token } from '@/utils/types' +import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' +import { usePortfolioBalances } from '@/slices/portfolio/hooks' +import SelectSpecificTokenButton from './components/SelectSpecificTokenButton' + +import { hasBalance } from './helpers/sortByBalance' +import { sortByPriorityRank } from './helpers/sortByPriorityRank' +import useCloseOnOutsideClick from '@/utils/hooks/useCloseOnOutsideClick' +import { CloseButton } from './components/CloseButton' +import { SearchResults } from './components/SearchResults' +import { useSwapState } from '@/slices/swap/hooks' +import { setShowSwapFromTokenListOverlay } from '@/slices/swapDisplaySlice' +import { setSwapFromToken } from '@/slices/swap/reducer' +import { getSwapPossibilities } from '@/utils/swapFinder/generateSwapPossibilities' +import { CHAINS_BY_ID } from '@/constants/chains' + +export const SwapFromTokenListOverlay = () => { + const [currentIdx, setCurrentIdx] = useState(-1) + const [searchStr, setSearchStr] = useState('') + const dispatch = useDispatch() + const overlayRef = useRef(null) + + const { swapFromTokens, swapChainId, swapFromToken } = useSwapState() + const portfolioBalances = usePortfolioBalances() + + let possibleTokens = sortByPriorityRank(swapFromTokens) + + possibleTokens = [ + ...possibleTokens.filter((t) => + hasBalance(t, swapChainId, portfolioBalances) + ), + ...possibleTokens.filter( + (t) => !hasBalance(t, swapChainId, portfolioBalances) + ), + ] + + const { fromTokens: allSwapChainTokens } = getSwapPossibilities({ + fromChainId: swapChainId, + fromToken: null, + toChainId: swapChainId, + toToken: null, + }) + + let remainingTokens = sortByPriorityRank( + _.difference(allSwapChainTokens, swapFromTokens) + ) + + remainingTokens = [ + ...remainingTokens.filter((t) => + hasBalance(t, swapChainId, portfolioBalances) + ), + ...remainingTokens.filter( + (t) => !hasBalance(t, swapChainId, portfolioBalances) + ), + ] + + const possibleTokensWithSource = possibleTokens.map((token) => ({ + ...token, + source: 'possibleTokens', + })) + const remainingTokensWithSource = remainingTokens.map((token) => ({ + ...token, + source: 'remainingTokens', + })) + + const masterList = [...possibleTokensWithSource, ...remainingTokensWithSource] + + const fuseOptions = { + ignoreLocation: true, + includeScore: true, + threshold: 0.0, + keys: [ + { + name: 'symbol', + weight: 2, + }, + 'routeSymbol', + `addresses.${swapChainId}`, + 'name', + ], + } + + const fuse = new Fuse(masterList, fuseOptions) + + if (searchStr?.length > 0) { + const results = fuse.search(searchStr).map((i) => i.item) + possibleTokens = results.filter((item) => item.source === 'possibleTokens') + remainingTokens = results.filter( + (item) => item.source === 'remainingTokens' + ) + } + + const escPressed = useKeyPress('Escape') + const arrowUp = useKeyPress('ArrowUp') + const arrowDown = useKeyPress('ArrowDown') + + function onClose() { + setCurrentIdx(-1) + setSearchStr('') + dispatch(setShowSwapFromTokenListOverlay(false)) + } + + function escFunc() { + if (escPressed) { + onClose() + } + } + + function arrowDownFunc() { + const nextIdx = currentIdx + 1 + if (arrowDown && nextIdx < masterList.length) { + setCurrentIdx(nextIdx) + } + } + + function arrowUpFunc() { + const nextIdx = currentIdx - 1 + if (arrowUp && -1 < nextIdx) { + setCurrentIdx(nextIdx) + } + } + + function onSearch(str: string) { + setSearchStr(str) + setCurrentIdx(-1) + } + + useEffect(escFunc, [escPressed]) + useEffect(arrowDownFunc, [arrowDown]) + useEffect(arrowUpFunc, [arrowUp]) + useCloseOnOutsideClick(overlayRef, onClose) + + const handleSetFromToken = (oldToken: Token, newToken: Token) => { + const eventTitle = '[Swap User Action] Sets new fromToken' + const eventData = { + previousFromToken: oldToken?.symbol, + newFromToken: newToken?.symbol, + } + segmentAnalyticsEvent(eventTitle, eventData) + dispatch(setSwapFromToken(newToken)) + onClose() + } + + return ( +
+
+
+ + +
+
+ {possibleTokens && possibleTokens.length > 0 && ( + <> +
+ Swap… +
+
+ {possibleTokens.map((token, idx) => { + return ( + { + if (token === swapFromToken) { + onClose() + } else { + handleSetFromToken(swapFromToken, token) + } + }} + /> + ) + })} +
+ + )} + {remainingTokens && remainingTokens.length > 0 && ( + <> +
+ {swapChainId + ? `More on ${CHAINS_BY_ID[swapChainId]?.name}` + : 'All swappable tokens'} +
+
+ {remainingTokens.map((token, idx) => { + return ( + handleSetFromToken(swapFromToken, token)} + /> + ) + })} +
+ + )} + +
+ ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapFromTokenSelector.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapFromTokenSelector.tsx new file mode 100644 index 0000000000..1dcca30170 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapFromTokenSelector.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import { useDispatch } from 'react-redux' + +import { DropDownArrowSvg } from '../icons/DropDownArrowSvg' +import { + getBorderStyleForCoinHover, + getMenuItemHoverBgForCoin, +} from '@/styles/tokens' +import { useSwapState } from '@/slices/swap/hooks' +import { setShowSwapFromTokenListOverlay } from '@/slices/swapDisplaySlice' + +export const SwapFromTokenSelector = () => { + const dispatch = useDispatch() + + const { swapFromToken } = useSwapState() + + const buttonContent = swapFromToken ? ( +
+
+ {`Icon +
+
+
+ {swapFromToken?.symbol} +
+
+ +
+ ) : ( +
+
+
In
+
+ +
+ ) + + return ( + + ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx new file mode 100644 index 0000000000..82eb3a047c --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx @@ -0,0 +1,172 @@ +import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react' +import { useDispatch } from 'react-redux' +import { useAccount, useNetwork } from 'wagmi' + +import MiniMaxButton from '../buttons/MiniMaxButton' +import { formatBigIntToString, stringToBigInt } from '@/utils/bigint/format' +import { cleanNumberInput } from '@/utils/cleanNumberInput' +import { + ConnectToNetworkButton, + ConnectWalletButton, + ConnectedIndicator, +} from '@/components/ConnectionIndicators' +import { SwapChainSelector } from './SwapChainSelector' +import { SwapFromTokenSelector } from './SwapFromTokenSelector' +import { usePortfolioState } from '@/slices/portfolio/hooks' +import { updateSwapFromValue } from '@/slices/swap/reducer' +import { useSwapState } from '@/slices/swap/hooks' + +export const SwapInputContainer = () => { + const inputRef = useRef(null) + const { swapChainId, swapFromToken, swapToToken, swapFromValue } = + useSwapState() + const [showValue, setShowValue] = useState('') + + const [hasMounted, setHasMounted] = useState(false) + + const { balancesAndAllowances } = usePortfolioState() + + useEffect(() => { + setHasMounted(true) + }, []) + + const { isConnected } = useAccount() + const { chain } = useNetwork() + + const dispatch = useDispatch() + + const tokenData = balancesAndAllowances[swapChainId]?.find( + (token) => token.tokenAddress === swapFromToken?.addresses[swapChainId] + ) + + const parsedBalance = tokenData?.parsedBalance + + const balance = tokenData?.balance + + useEffect(() => { + if ( + swapFromToken && + swapFromToken.decimals[swapChainId] && + stringToBigInt(swapFromValue, swapFromToken.decimals[swapChainId]) !== 0n + ) { + setShowValue(swapFromValue) + } + }, [swapFromValue, swapChainId, swapFromToken]) + + const handleFromValueChange = ( + event: React.ChangeEvent + ) => { + const swapFromValueString: string = cleanNumberInput(event.target.value) + try { + dispatch(updateSwapFromValue(swapFromValueString)) + setShowValue(swapFromValueString) + } catch (error) { + console.error('Invalid value for conversion to BigInteger') + const inputValue = event.target.value + const regex = /^[0-9]*[.,]?[0-9]*$/ + + if (regex.test(inputValue) || inputValue === '') { + dispatch(updateSwapFromValue('')) + setShowValue(inputValue) + } + } + } + + const onMaxBalance = useCallback(() => { + dispatch( + updateSwapFromValue( + formatBigIntToString(balance, swapFromToken.decimals[swapChainId]) + ) + ) + }, [balance, swapChainId, swapFromToken]) + + const connectedStatus = useMemo(() => { + if (hasMounted && isConnected) { + if (swapChainId === chain.id) { + return + } else if (swapChainId !== chain.id) { + return + } + } else if (hasMounted && !isConnected) { + return + } + }, [chain, swapChainId, isConnected, hasMounted]) + + return ( +
+
+ + {connectedStatus} +
+
+
+
+ +
+
+ +
+ {hasMounted && isConnected && ( + + )} +
+
+
+ {hasMounted && isConnected && ( +
+ +
+ )} +
+
+
+
+ ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapOutputContainer.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapOutputContainer.tsx new file mode 100644 index 0000000000..ee35082cf7 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapOutputContainer.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react' +import { Address, useAccount } from 'wagmi' + +import LoadingSpinner from '../ui/tailwind/LoadingSpinner' +import { SwapToTokenSelector } from './SwapToTokenSelector' +import { useSwapState } from '@/slices/swap/hooks' + +export const SwapOutputContainer = ({}) => { + const { swapQuote, isLoading, swapToToken } = useSwapState() + + const { address: isConnectedAddress } = useAccount() + const [address, setAddress] = useState
() + + useEffect(() => { + setAddress(isConnectedAddress) + }, [isConnectedAddress]) + + return ( +
+
+
+ +
+ {isLoading ? ( + + ) : ( + + )} +
+
+
+
+ ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapToTokenListOverlay.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapToTokenListOverlay.tsx new file mode 100644 index 0000000000..77bf22b49e --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapToTokenListOverlay.tsx @@ -0,0 +1,254 @@ +import _ from 'lodash' +import { useEffect, useRef, useState } from 'react' +import { useDispatch } from 'react-redux' +import Fuse from 'fuse.js' + +import { useKeyPress } from '@hooks/useKeyPress' +import SlideSearchBox from '@pages/bridge/SlideSearchBox' +import { Token } from '@/utils/types' +import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' +import SelectSpecificTokenButton from './components/SelectSpecificTokenButton' +import { getRoutePossibilities } from '@/utils/routeMaker/generateRoutePossibilities' + +import { sortByPriorityRank } from './helpers/sortByPriorityRank' +import { CHAINS_BY_ID } from '@/constants/chains' +import useCloseOnOutsideClick from '@/utils/hooks/useCloseOnOutsideClick' +import { CloseButton } from './components/CloseButton' +import { SearchResults } from './components/SearchResults' +import { setShowSwapToTokenListOverlay } from '@/slices/swapDisplaySlice' +import { setSwapToToken } from '@/slices/swap/reducer' +import { useSwapState } from '@/slices/swap/hooks' +import { getSwapPossibilities } from '@/utils/swapFinder/generateSwapPossibilities' + +export const SwapToTokenListOverlay = () => { + const { swapChainId, swapToTokens, swapToToken } = useSwapState() + + const [currentIdx, setCurrentIdx] = useState(-1) + const [searchStr, setSearchStr] = useState('') + const dispatch = useDispatch() + const overlayRef = useRef(null) + + let possibleTokens = sortByPriorityRank(swapToTokens) + + const { toTokens: allToChainTokens } = getSwapPossibilities({ + fromChainId: swapChainId, + fromToken: null, + toChainId: swapChainId, + toToken: null, + }) + + let remainingChainTokens = swapChainId + ? sortByPriorityRank(_.difference(allToChainTokens, swapToTokens)) + : [] + + const { toTokens: allTokens } = getSwapPossibilities({ + fromChainId: null, + fromToken: null, + toChainId: null, + toToken: null, + }) + + let allOtherToTokens = swapChainId + ? sortByPriorityRank(_.difference(allTokens, allToChainTokens)) + : sortByPriorityRank(allTokens) + + const possibleTokenswithSource = possibleTokens.map((token) => ({ + ...token, + source: 'possibleTokens', + })) + + const remainingChainTokensWithSource = remainingChainTokens.map((token) => ({ + ...token, + source: 'remainingChainTokens', + })) + + const allOtherToTokensWithSource = allOtherToTokens.map((token) => ({ + ...token, + source: 'allOtherToTokens', + })) + + const masterList = [ + ...possibleTokenswithSource, + ...remainingChainTokensWithSource, + ...allOtherToTokensWithSource, + ] + + const fuseOptions = { + ignoreLocation: true, + includeScore: true, + threshold: 0.0, + keys: [ + { + name: 'symbol', + weight: 2, + }, + 'routeSymbol', + `addresses.${swapChainId}`, + 'name', + ], + } + const fuse = new Fuse(masterList, fuseOptions) + + if (searchStr?.length > 0) { + const results = fuse.search(searchStr).map((i) => i.item) + + possibleTokens = results.filter((item) => item.source === 'possibleTokens') + remainingChainTokens = results.filter( + (item) => item.source === 'remainingChainTokens' + ) + allOtherToTokens = results.filter( + (item) => item.source === 'allOtherToTokens' + ) + } + + const escPressed = useKeyPress('Escape') + const arrowUp = useKeyPress('ArrowUp') + const arrowDown = useKeyPress('ArrowDown') + + function onClose() { + setCurrentIdx(-1) + setSearchStr('') + dispatch(setShowSwapToTokenListOverlay(false)) + } + + function escFunc() { + if (escPressed) { + onClose() + } + } + + function arrowDownFunc() { + const nextIdx = currentIdx + 1 + if (arrowDown && nextIdx < masterList.length) { + setCurrentIdx(nextIdx) + } + } + + function arrowUpFunc() { + const nextIdx = currentIdx - 1 + if (arrowUp && -1 < nextIdx) { + setCurrentIdx(nextIdx) + } + } + + function onSearch(str: string) { + setSearchStr(str) + setCurrentIdx(-1) + } + + useEffect(escFunc, [escPressed]) + useEffect(arrowDownFunc, [arrowDown]) + useEffect(arrowUpFunc, [arrowUp]) + useCloseOnOutsideClick(overlayRef, onClose) + + const handleSetToToken = (oldToken: Token, newToken: Token) => { + const eventTitle = `[Swap User Action] Sets new toToken` + const eventData = { + previousToToken: oldToken?.symbol, + newToToken: newToken?.symbol, + } + segmentAnalyticsEvent(eventTitle, eventData) + dispatch(setSwapToToken(newToken)) + onClose() + } + + return ( +
+
+
+ + +
+
+ {possibleTokens && possibleTokens.length > 0 && ( + <> +
+ Receive… +
+
+ {possibleTokens.map((token, idx) => { + return ( + { + if (token === swapToToken) { + onClose() + } else { + handleSetToToken(swapToToken, token) + } + }} + /> + ) + })} +
+ + )} + {remainingChainTokens && remainingChainTokens.length > 0 && ( + <> +
+ {swapChainId + ? `More on ${CHAINS_BY_ID[swapChainId]?.name}` + : 'All swapable tokens'} +
+
+ {remainingChainTokens.map((token, idx) => { + return ( + handleSetToToken(swapToToken, token)} + /> + ) + })} +
+ + )} + {allOtherToTokens && allOtherToTokens.length > 0 && ( + <> +
+ All swapable tokens +
+
+ {allOtherToTokens.map((token, idx) => { + return ( + handleSetToToken(swapToToken, token)} + alternateBackground={true} + /> + ) + })} +
+ + )} + +
+ ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapToTokenSelector.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapToTokenSelector.tsx new file mode 100644 index 0000000000..e0fe621320 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapToTokenSelector.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import { useDispatch } from 'react-redux' + +import { DropDownArrowSvg } from '../icons/DropDownArrowSvg' +import { + getBorderStyleForCoinHover, + getMenuItemHoverBgForCoin, +} from '@/styles/tokens' +import { setShowSwapToTokenListOverlay } from '@/slices/swapDisplaySlice' +import { useSwapState } from '@/slices/swap/hooks' + +export const SwapToTokenSelector = () => { + const dispatch = useDispatch() + + const { swapToToken } = useSwapState() + + const buttonContent = swapToToken ? ( +
+
+ {swapToToken?.symbol +
+
+
+ {swapToToken?.symbol} +
+
+ +
+ ) : ( +
+
+
Out
+
+ +
+ ) + + return ( + + ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/SwapTransactionButton.tsx b/packages/synapse-interface/components/StateManagedSwap/SwapTransactionButton.tsx new file mode 100644 index 0000000000..baa2654f04 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/SwapTransactionButton.tsx @@ -0,0 +1,127 @@ +import { useEffect, useMemo, useState } from 'react' +import { useAccount, useNetwork, useSwitchNetwork } from 'wagmi' +import { useConnectModal } from '@rainbow-me/rainbowkit' + +import { TransactionButton } from '@/components/buttons/TransactionButton' +import { EMPTY_SWAP_QUOTE, EMPTY_SWAP_QUOTE_ZERO } from '@/constants/swap' +import { stringToBigInt } from '@/utils/bigint/format' +import { usePortfolioBalances } from '@/slices/portfolio/hooks' +import { useSwapState } from '@/slices/swap/hooks' +import { SWAP_CHAIN_IDS } from '@/constants/existingSwapRoutes' + +export const SwapTransactionButton = ({ + approveTxn, + executeSwap, + isApproved, +}) => { + const [isConnected, setIsConnected] = useState(false) + const { openConnectModal } = useConnectModal() + + const { chain } = useNetwork() + const { chains, switchNetwork } = useSwitchNetwork() + + const { isConnected: isConnectedInit } = useAccount({ + onDisconnect() { + setIsConnected(false) + }, + }) + + useEffect(() => { + setIsConnected(isConnectedInit) + }, [isConnectedInit]) + + const { + swapChainId, + swapFromToken, + swapToToken, + swapFromValue, + isLoading, + swapQuote, + } = useSwapState() + + const balances = usePortfolioBalances() + const balancesForChain = balances[swapChainId] + const balanceForToken = balancesForChain?.find( + (t) => t.tokenAddress === swapFromToken?.addresses[swapChainId] + )?.balance + + const sufficientBalance = useMemo(() => { + if (!swapChainId || !swapFromToken || !swapToToken) return false + return ( + stringToBigInt(swapFromValue, swapFromToken?.decimals[swapChainId]) <= + balanceForToken + ) + }, [balanceForToken, swapFromValue, swapChainId, swapFromToken, swapToToken]) + + const isButtonDisabled = + isLoading || + swapQuote === EMPTY_SWAP_QUOTE_ZERO || + swapQuote === EMPTY_SWAP_QUOTE || + (isConnected && !sufficientBalance) + + let buttonProperties + + const fromTokenDecimals: number | undefined = + swapFromToken && swapFromToken.decimals[swapChainId] + + const fromValueBigInt = useMemo(() => { + return fromTokenDecimals + ? stringToBigInt(swapFromValue, fromTokenDecimals) + : 0 + }, [swapFromValue, fromTokenDecimals, swapChainId, swapFromToken]) + + if (!swapChainId) { + buttonProperties = { + label: 'Please select Origin network', + onClick: null, + } + } else if (!SWAP_CHAIN_IDS.includes(swapChainId)) { + buttonProperties = { + label: 'Swaps are not available on this network', + onClick: null, + } + } else if (!swapFromToken) { + buttonProperties = { + label: `Please select token`, + onClick: null, + } + } else if (!isConnected && fromValueBigInt > 0) { + buttonProperties = { + label: `Connect Wallet to Swap`, + onClick: openConnectModal, + } + } else if (isConnected && !sufficientBalance) { + buttonProperties = { + label: 'Insufficient balance', + onClick: null, + } + } else if (chain?.id != swapChainId && fromValueBigInt > 0) { + buttonProperties = { + label: `Switch to ${chains.find((c) => c.id === swapChainId).name}`, + onClick: () => switchNetwork(swapChainId), + pendingLabel: 'Switching chains', + } + } else if (!isApproved) { + buttonProperties = { + onClick: approveTxn, + label: `Approve ${swapFromToken?.symbol}`, + pendingLabel: 'Approving', + } + } else { + buttonProperties = { + onClick: executeSwap, + label: `Swap ${swapFromToken?.symbol} for ${swapToToken?.symbol}`, + pendingLabel: 'Swapping', + } + } + + return ( + buttonProperties && ( + + ) + ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/components/CloseButton.tsx b/packages/synapse-interface/components/StateManagedSwap/components/CloseButton.tsx new file mode 100644 index 0000000000..c9a0c25099 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/components/CloseButton.tsx @@ -0,0 +1,17 @@ +import { XIcon } from '@heroicons/react/outline' + +export const CloseButton = ({ onClick }: { onClick: () => void }) => { + return ( + + ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/components/SearchResults.tsx b/packages/synapse-interface/components/StateManagedSwap/components/SearchResults.tsx new file mode 100644 index 0000000000..1f52ec4356 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/components/SearchResults.tsx @@ -0,0 +1,21 @@ +export const SearchResults = ({ + searchStr, + type, +}: { + searchStr: string + type: string +}) => { + return ( +
+ {searchStr ? ( +
+ No other results found for{' '} + {searchStr}. +
+ Want to see a {type} supported on Synapse? Let us know! +
+
+ ) : null} +
+ ) +} diff --git a/packages/synapse-interface/components/StateManagedSwap/components/SelectSpecificNetworkButton.tsx b/packages/synapse-interface/components/StateManagedSwap/components/SelectSpecificNetworkButton.tsx new file mode 100644 index 0000000000..f12b7c8b4a --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/components/SelectSpecificNetworkButton.tsx @@ -0,0 +1,90 @@ +import { useEffect, useRef } from 'react' +import { CHAINS_BY_ID } from '@constants/chains' +import Image from 'next/image' +import { + getNetworkHover, + getNetworkButtonBorder, + getNetworkButtonBorderHover, + getNetworkButtonBgClassName, + getNetworkButtonBgClassNameActive, + getNetworkButtonBorderActive, + getMenuItemStyleForChain, +} from '@/styles/chains' + +export const SelectSpecificNetworkButton = ({ + itemChainId, + isCurrentChain, + active, + onClick, + dataId, + alternateBackground = false, +}: { + itemChainId: number + isCurrentChain: boolean + active: boolean + onClick: () => void + dataId: string + alternateBackground?: boolean +}) => { + const ref = useRef(null) + const chain = CHAINS_BY_ID[itemChainId] + + useEffect(() => { + if (active) { + ref?.current?.focus() + } + }, [active]) + + let bgClassName + + if (isCurrentChain) { + bgClassName = ` + ${getNetworkButtonBgClassName(chain.color)} + ${getNetworkButtonBorder(chain.color)} + bg-opacity-30 + ` + } + + return ( + + ) +} + +function ButtonContent({ chainId }: { chainId: number }) { + const chain = CHAINS_BY_ID[chainId] + + return chain ? ( + <> + Switch Network +
+
{chain.name}
+
+ + ) : null +} diff --git a/packages/synapse-interface/components/StateManagedSwap/components/SelectSpecificTokenButton.tsx b/packages/synapse-interface/components/StateManagedSwap/components/SelectSpecificTokenButton.tsx new file mode 100644 index 0000000000..9f56f08a3e --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/components/SelectSpecificTokenButton.tsx @@ -0,0 +1,207 @@ +import { + getBorderStyleForCoin, + getBorderStyleForCoinHover, + getMenuItemBgForCoin, + getMenuItemStyleForCoin, +} from '@styles/tokens' +import { memo, useEffect, useRef, useState } from 'react' +import { Token } from '@/utils/types' +import { usePortfolioBalances } from '@/slices/portfolio/hooks' +import { CHAINS_BY_ID } from '@/constants/chains' +import { useSwapState } from '@/slices/swap/hooks' + +const SelectSpecificTokenButton = ({ + showAllChains, + isOrigin, + token, + active, + selectedToken, + onClick, + alternateBackground = false, +}: { + showAllChains?: boolean + isOrigin: boolean + token: Token + active: boolean + selectedToken: Token + onClick: () => void + alternateBackground?: boolean +}) => { + const ref = useRef(null) + const isCurrentlySelected = selectedToken?.routeSymbol === token?.routeSymbol + const { swapChainId, swapFromToken, swapToToken } = useSwapState() + + useEffect(() => { + if (active) { + ref?.current?.focus() + } + }, [active]) + + const chainId = swapChainId + + let bgClassName + + const classNameForMenuItemStyle = getMenuItemStyleForCoin(token?.color) + + if (isCurrentlySelected) { + bgClassName = `${getMenuItemBgForCoin( + token?.color + )} ${getBorderStyleForCoin(token?.color)}` + } else { + bgClassName = getBorderStyleForCoinHover(token?.color) + } + + return ( + + ) +} + +const ButtonContent = memo( + ({ + token, + chainId, + isOrigin, + showAllChains, + }: { + token: Token + chainId: number + isOrigin: boolean + showAllChains: boolean + }) => { + const portfolioBalances = usePortfolioBalances() + + const parsedBalance = portfolioBalances[chainId]?.find( + (tb) => tb.token.addresses[chainId] === token.addresses[chainId] + )?.parsedBalance + + return ( +
+ token image + + {isOrigin && ( + + )} +
+ ) + } +) + +const Coin = ({ token, showAllChains }: { token; showAllChains: boolean }) => { + return ( +
+
{token?.symbol}
+
+
{token?.name}
+ {showAllChains && } +
+
+ ) +} + +const TokenBalance = ({ + token, + chainId, + parsedBalance, +}: { + token: Token + chainId: number + parsedBalance?: string +}) => { + return ( +
+ {parsedBalance && parsedBalance !== '0.0' && ( +
+ {parsedBalance} + + {' '} + {token ? token.symbol : ''} + +
+ )} +
+ ) +} + +const AvailableChains = ({ token }: { token: Token }) => { + const [isHovered, setIsHovered] = useState(false) + const chainIds = Object.keys(token.addresses) + const hasOneChain = chainIds.length > 0 + const hasMultipleChains = chainIds.length > 1 + const numOverTwoChains = chainIds.length - 2 > 0 ? chainIds.length - 2 : 0 + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {hasOneChain && ( + {`${CHAINS_BY_ID[chainIds[0]].name} + )} + {hasMultipleChains && ( + {`${CHAINS_BY_ID[chainIds[1]].name} + )} + {numOverTwoChains > 0 && ( +
+ {numOverTwoChains}
+ )} +
+ {isHovered && ( +
+ {chainIds.map((chainId) => { + const chainName = CHAINS_BY_ID[chainId].name + return
{chainName}
+ })} +
+ )} +
+
+ ) +} + +export default SelectSpecificTokenButton diff --git a/packages/synapse-interface/components/StateManagedSwap/helpers/sortByBalance.ts b/packages/synapse-interface/components/StateManagedSwap/helpers/sortByBalance.ts new file mode 100644 index 0000000000..8f03dfd6b2 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/helpers/sortByBalance.ts @@ -0,0 +1,25 @@ +import _ from 'lodash' + +import { Token } from '@/utils/types' +import { NetworkTokenBalancesAndAllowances } from '@/utils/actions/fetchPortfolioBalances' + +export const hasBalance = ( + t: Token, + chainId: number, + portfolioBalances: NetworkTokenBalancesAndAllowances +) => { + if (!chainId) { + return false + } + const pb = portfolioBalances[chainId] + if (!pb) { + return false + } + const token = _(pb) + .pickBy((value, _key) => value.token === t) + .value() + + const tokenWithPb = Object.values(token)[0] + + return tokenWithPb && tokenWithPb.balance !== 0n +} diff --git a/packages/synapse-interface/components/StateManagedSwap/helpers/sortByPriorityRank.ts b/packages/synapse-interface/components/StateManagedSwap/helpers/sortByPriorityRank.ts new file mode 100644 index 0000000000..57925085b7 --- /dev/null +++ b/packages/synapse-interface/components/StateManagedSwap/helpers/sortByPriorityRank.ts @@ -0,0 +1,11 @@ +import _ from 'lodash' + +import { Token } from '@/utils/types' + +export const sortByPriorityRank = (tokens: Token[]) => { + return _.orderBy( + tokens, + [(token) => token.priorityRank, (token) => token.symbol.toLowerCase()], + ['asc', 'asc'] + ) +} diff --git a/packages/synapse-interface/components/TransactionItems.tsx b/packages/synapse-interface/components/TransactionItems.tsx deleted file mode 100644 index 05c43d3988..0000000000 --- a/packages/synapse-interface/components/TransactionItems.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { CHAINS_BY_ID } from '@constants/chains' -import { getNetworkLinkTextColor } from '@styles/chains' -import { Chain } from '@types' - -export const CheckingConfPlaceholder = ({ chain }: { chain: Chain }) => { - return ( -
-
-
-
-
- Confirmations left on {chain?.name} -
-
-
-
-
-
- ) -} - -export const PendingCreditTransactionItem = ({ - chainId, -}: { - chainId: number -}) => { - const chain = CHAINS_BY_ID[chainId] - - return ( -
-
- -
-
-
-
-
- Waiting to be credited on -
- {chain?.name} -
-
-
-
-
-
- ) -} - -export const EmptySubTransactionItem = ({ chainId }: { chainId: number }) => { - const chain = CHAINS_BY_ID[chainId] - return ( -
-
- -
-
- ) -} - -export const CreditedTransactionItem = ({ chainId }: { chainId: number }) => { - const chain = CHAINS_BY_ID[chainId] - return ( -
-
- -
-
-
-
-
- Bridging Completed on -
- {chain?.name} -
-
-
-
-
-
- ) -} diff --git a/packages/synapse-interface/components/StateManagedBridge/components/DropDownArrowSvg.tsx b/packages/synapse-interface/components/icons/DropDownArrowSvg.tsx similarity index 100% rename from packages/synapse-interface/components/StateManagedBridge/components/DropDownArrowSvg.tsx rename to packages/synapse-interface/components/icons/DropDownArrowSvg.tsx diff --git a/packages/synapse-interface/components/input/TokenAmountInput/SelectTokenDropdown.tsx b/packages/synapse-interface/components/input/TokenAmountInput/SelectTokenDropdown.tsx deleted file mode 100644 index 1c38bcf012..0000000000 --- a/packages/synapse-interface/components/input/TokenAmountInput/SelectTokenDropdown.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { ChevronDownIcon } from '@heroicons/react/outline' -import React from 'react' -import Image from 'next/image' -import { - getBorderStyleForCoinHover, - getMenuItemHoverBgForCoin, -} from '@styles/tokens' -import { Token } from '@/utils/types' -import { QuestionMarkCircleIcon } from '@heroicons/react/outline' -import { CHAINS_BY_ID } from '@/constants/chains' -import { Chain } from '@/utils/types' - -const SelectTokenDropdown = ({ - chainId, - selectedToken, - onClick, - isOrigin, -}: { - chainId: number - selectedToken: Token - onClick: () => void - isOrigin: boolean -}) => { - const currentChain: Chain = CHAINS_BY_ID[chainId] - const isUnsupportedChain: boolean = currentChain ? false : true - const symbol = selectedToken ? selectedToken.symbol : '' - const dataId = isOrigin ? 'bridge-origin-token' : 'bridge-destination-token' - - return ( - - ) -} -export default SelectTokenDropdown diff --git a/packages/synapse-interface/components/input/TokenAmountInput/index.tsx b/packages/synapse-interface/components/input/TokenAmountInput/index.tsx deleted file mode 100644 index 399f557e0f..0000000000 --- a/packages/synapse-interface/components/input/TokenAmountInput/index.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { formatBigIntToString } from '@/utils/bigint/format' -import React, { useMemo } from 'react' -import SwitchButton from '@components/buttons/SwitchButton' -import MiniMaxButton from '@components/buttons/MiniMaxButton' -import Spinner from '@/components/icons/Spinner' -import { cleanNumberInput } from '@utils/cleanNumberInput' - -import { Token } from '@/utils/types' -import { ChainLabel } from '@components/ChainLabel' -import { DisplayType } from '@/pages/bridge/DisplayType' -import SelectTokenDropdown from './SelectTokenDropdown' - -const BridgeInputContainer = ({ - address, - isOrigin, - isSwap, - chains, - chainId, - inputString, - selectedToken, - connectedChainId, - onChangeChain, - onChangeAmount, - setDisplayType, - fromTokenBalance, - isQuoteLoading = false, -}: { - address: `0x${string}` - isOrigin: boolean - isSwap: boolean - chains: string[] - chainId: number - inputString: string - selectedToken: Token - connectedChainId: number - setDisplayType: (v: DisplayType) => void - onChangeAmount?: (v: string) => void - onChangeChain: (chainId: number, flip: boolean, type: 'from' | 'to') => void - fromTokenBalance?: bigint - isQuoteLoading?: boolean -}) => { - const formattedBalance = useMemo(() => { - if (!fromTokenBalance) return '0.0' - return formatBigIntToString( - fromTokenBalance, - selectedToken?.decimals[chainId as keyof Token['decimals']], - 3 - ) - }, [fromTokenBalance]) - - const isConnected = address !== null - const isMaxDisabled = formattedBalance === '0.0' - - const onClickBalance = () => { - onChangeAmount( - formatBigIntToString( - fromTokenBalance, - selectedToken?.decimals[chainId as keyof Token['decimals']] - ) - ) - } - - return ( -
-
-
- {!isOrigin && !isSwap && ( -
-
- - onChangeChain(chainId, true, isOrigin ? 'from' : 'to') - } - /> -
-
- )} - {!(isSwap && !isOrigin) && ( - - )} -
-
-
-
- { - setDisplayType(isOrigin ? DisplayType.FROM : DisplayType.TO) - }} - /> -
- onChangeAmount(cleanNumberInput(e.target.value)) - : () => null - } - value={inputString === '0' ? null : inputString} - name="inputRow" - autoComplete="off" - /> - {isOrigin && isConnected && ( - - )} -
- {isOrigin && isConnected && ( -
- -
- )} -
-
-
- ) -} - -export default BridgeInputContainer diff --git a/packages/synapse-interface/components/pairTxKappa.tsx b/packages/synapse-interface/components/pairTxKappa.tsx deleted file mode 100644 index 868a3a8c21..0000000000 --- a/packages/synapse-interface/components/pairTxKappa.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import _ from 'lodash' - -function checkTxIn(tx) { - if (tx.args?.chainId) { - // if (tx.args?.chainId ?? false) { // - return true - } else { - return false - } -} - -/** - * @param {Transaction[]} transactions - * @return {Transaction[][]} - */ -export function pairTxKappa(transactions) { - const transactionsByHash = {} - for (const tx of transactions) { - transactionsByHash[tx.transactionHash] = tx - } - - const inputTxns = transactions.filter((tx) => tx.kekTxSig).filter(checkTxIn) - const outputTxns = transactions - .filter((tx) => tx.kekTxSig) - .filter((tx) => !checkTxIn(tx)) - - const outputTxnsDict = {} - - for (const tx of outputTxns) { - outputTxnsDict[tx.args?.kappa] = tx - } - - const pairSetsByChain = [] - - for (const inTx of inputTxns) { - const outTx = outputTxnsDict[inTx.kekTxSig] - if (outTx) { - pairSetsByChain.push([inTx, outTx]) - } else { - pairSetsByChain.push([inTx, undefined]) - } - } - const outTxnKeys = pairSetsByChain.map( - ([inTx, outTx]) => outTx?.transactionHash - ) - const remainingOuts = outputTxns.filter( - (tx) => !outTxnKeys.includes(tx.transactionHash) - ) - - for (const outTx of remainingOuts) { - pairSetsByChain.push([undefined, outTx]) - } - - return _.sortBy(pairSetsByChain, ([inTx, outTx]) => { - return -(outTx?.timestamp ?? inTx?.timestamp) - }) -} diff --git a/packages/synapse-interface/components/ui/tailwind/Modal.tsx b/packages/synapse-interface/components/ui/tailwind/Modal.tsx deleted file mode 100644 index 34bb136ba1..0000000000 --- a/packages/synapse-interface/components/ui/tailwind/Modal.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useEffect } from 'react' -import { useKeyPress } from '@hooks/useKeyPress' - -export default function Modal({ - isOpen, - onClose, - children, -}: { - isOpen: boolean - onClose: () => void - children: any -}) { - const escPressed = useKeyPress('Escape') - - function escEffect() { - if (escPressed) { - onClose() - } - } - - useEffect(escEffect, [escPressed]) - - if (isOpen) { - return ( - <> -
-
-
- {children} -
-
-
-
- - ) - } else { - return null - } -} diff --git a/packages/synapse-interface/components/ui/tailwind/ModalHeadline.tsx b/packages/synapse-interface/components/ui/tailwind/ModalHeadline.tsx deleted file mode 100644 index 20207f107f..0000000000 --- a/packages/synapse-interface/components/ui/tailwind/ModalHeadline.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { twMerge } from 'tailwind-merge' - -const baseClassname = ` - mb-3 text-sm text-secondaryTextColor text-opacity-50 -` - -export default function ModalHeadline({ - title, - subtitle, - onClose, - titleClassName, -}: { - title: string - subtitle: string - onClose: any - titleClassName?: string -}) { - const mergedTitleClassName = twMerge(`${baseClassname} ${titleClassName}`) - - return ( -
-
- -
-
- Clear -
-
-
-

{subtitle}

-
- ) -} diff --git a/packages/synapse-interface/constants/existingSwapRoutes.ts b/packages/synapse-interface/constants/existingSwapRoutes.ts new file mode 100644 index 0000000000..e7f6a7e7a3 --- /dev/null +++ b/packages/synapse-interface/constants/existingSwapRoutes.ts @@ -0,0 +1,52 @@ +import _ from 'lodash' +import { zeroAddress } from 'viem' + +import { BRIDGE_MAP } from './bridgeMap' +import { findTokenByAddressAndChain } from '@/utils/findTokenByAddressAndChainId' +import { ETHEREUM_ADDRESS } from '.' + +export const FILTERED = _(BRIDGE_MAP) + .mapValues((chainObj) => { + return _(chainObj) + .pickBy( + (tokenObj: any) => + Array.isArray(tokenObj.swappable) && tokenObj.swappable.length > 0 + ) + .value() + }) + .pickBy((value, _key) => Object.values(value).length > 0) + .value() + +export const SWAP_CHAIN_IDS = Object.keys(FILTERED).map(Number) + +export const EXISTING_SWAP_ROUTES = _(FILTERED) + .map((tokens, chainId) => { + return _(tokens) + .map((info, tokenAddress) => { + if (tokenAddress.toLowerCase() === ETHEREUM_ADDRESS.toLowerCase()) { + tokenAddress = zeroAddress + } + + const symbol = findTokenByAddressAndChain( + tokenAddress, + chainId + )?.routeSymbol + const key = `${symbol}-${chainId}` + const swappable = info.swappable.map((address) => { + if (address.toLowerCase() === ETHEREUM_ADDRESS.toLowerCase()) { + address = zeroAddress + } + + const symbol = findTokenByAddressAndChain( + address, + chainId + )?.routeSymbol + return `${symbol}-${chainId}` + }) + return [key, swappable] + }) + .value() + }) + .flatten() + .fromPairs() + .value() diff --git a/packages/synapse-interface/constants/index.ts b/packages/synapse-interface/constants/index.ts index 6fcfb54de0..85e1dee41c 100644 --- a/packages/synapse-interface/constants/index.ts +++ b/packages/synapse-interface/constants/index.ts @@ -1,2 +1,4 @@ export const MAX_UINT256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935n + +export const ETHEREUM_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' diff --git a/packages/synapse-interface/constants/tokens/deprecated.ts b/packages/synapse-interface/constants/tokens/deprecated.ts index b5ab9eae64..e807d1e8e5 100644 --- a/packages/synapse-interface/constants/tokens/deprecated.ts +++ b/packages/synapse-interface/constants/tokens/deprecated.ts @@ -1,5 +1,5 @@ -import usdbLogo from '@assets/icons/usdb.png' -import fusdtLogo from '@assets/icons/fusdt.svg' +import usdbLogo from '@assets/icons/usdc.svg' +import fusdtLogo from '@assets/icons/usdt.svg' import { Token } from '@/utils/types' import * as CHAINS from '@/constants/chains/master' diff --git a/packages/synapse-interface/constants/tokens/index.ts b/packages/synapse-interface/constants/tokens/index.ts index 5fe0d98f28..73e70c077a 100644 --- a/packages/synapse-interface/constants/tokens/index.ts +++ b/packages/synapse-interface/constants/tokens/index.ts @@ -16,19 +16,7 @@ interface TokensByChain { interface TokenByKey { [cID: string]: Token } -interface BridgeChainsByType { - [swapableType: string]: string[] -} - -interface BridgeTypeByChain { - [cID: string]: string[] -} -interface SwapableTokensByType { - [cID: string]: { - [swapableType: string]: Token[] - } -} export const sortTokens = (tokens: Token[]) => Object.values(tokens).sort((a, b) => b.visibilityRank - a.visibilityRank) @@ -81,83 +69,6 @@ const getBridgeableTokens = (): TokensByChain => { return bridgeableTokens } -const getBridgeChainsByType = (): BridgeChainsByType => { - const bridgeChainsByType: BridgeChainsByType = {} - Object.entries(all).map(([key, token]) => { - const swapableType = String(token?.swapableType) - const keys = Object.keys(token.addresses).filter((cID) => { - // Skip if the token is paused on the current chain - if (PAUSED_TOKENS_BY_CHAIN[cID]?.includes(key)) return false - - return !bridgeChainsByType[swapableType]?.includes(cID) - }) - - if (bridgeChainsByType[swapableType]) { - bridgeChainsByType[swapableType] = [ - ...bridgeChainsByType[swapableType], - ...keys, - ] - } else { - bridgeChainsByType[swapableType] = keys - } - }) - return bridgeChainsByType -} - -const getBridgeTypeByChain = (): BridgeTypeByChain => { - const bridgeChainByType = getBridgeChainsByType() - const bridgeTypeByChain: BridgeTypeByChain = {} - Object.keys(bridgeChainByType).forEach((key) => { - bridgeChainByType[key].forEach((value) => { - if (bridgeTypeByChain[value]) { - bridgeTypeByChain[value].push(key) - } else { - bridgeTypeByChain[value] = [key] - } - }) - }) - return bridgeTypeByChain -} - -const convertArrayToObject = (array: any) => { - return array.reduce((obj: any, value: any) => { - obj[value] = [] - return obj - }, {}) -} - -const getBridgeableTokensByType = (): SwapableTokensByType => { - const bridgeTypeByChain = getBridgeTypeByChain() - const bridgeSwapableTokensByType = Object.fromEntries( - Object.entries(bridgeTypeByChain).map(([k, v]) => [ - k, - convertArrayToObject(v), - ]) - ) - - Object.entries(all).map(([key, token]) => { - const swapableType = String(token?.swapableType) - - for (const cID of Object.keys(token.addresses)) { - // Skip if the token is paused on the current chain - if (PAUSED_TOKENS_BY_CHAIN[cID]?.includes(key)) continue - - if (bridgeSwapableTokensByType[cID][swapableType].length === 0) { - bridgeSwapableTokensByType[cID][swapableType] = [token] - } else if ( - !bridgeSwapableTokensByType[cID][swapableType]?.includes(token) - ) { - bridgeSwapableTokensByType[cID][swapableType] = [ - ...bridgeSwapableTokensByType[cID][swapableType], - token, - ] - } - } - }) - - return bridgeSwapableTokensByType -} - const getTokenHashMap = () => { const tokenHashMap = {} @@ -182,9 +93,7 @@ export const TOKENS_SORTED_BY_SYMBOL = Array.from( new Set(sortedTokens.map((token) => token.symbol)) ) export const BRIDGABLE_TOKENS = getBridgeableTokens() -export const BRIDGE_CHAINS_BY_TYPE = getBridgeChainsByType() -export const BRIDGE_TYPES_BY_CHAIN = getBridgeTypeByChain() -export const BRIDGE_SWAPABLE_TOKENS_BY_TYPE = getBridgeableTokensByType() + export const tokenSymbolToToken = (chainId: number, symbol: string) => { if (chainId) { const token = BRIDGABLE_TOKENS[chainId].find((token) => { @@ -213,40 +122,7 @@ export const TOKEN_HASH_MAP = getTokenHashMap() // SWAPS const allTokensWithSwap = [...Object.values(all), ...Object.values(allSwap)] -const getSwapableTokens = (): TokensByChain => { - const swapTokens: TokensByChain = {} - allTokensWithSwap.map((token) => { - if (!(token?.swapableOn?.length > 0)) return - for (const cID of token.swapableOn) { - if (!swapTokens[cID]) { - swapTokens[cID] = [token] - } else if (!swapTokens[cID]?.includes(token)) { - swapTokens[cID] = [...swapTokens[cID], token] - } - } - }) - return swapTokens -} -const getSwapableTokensByType = (): SwapableTokensByType => { - const swapTokens: SwapableTokensByType = {} - allTokensWithSwap.map((token) => { - if (!(token?.swapableOn?.length > 0)) return - for (const cID of token.swapableOn) { - if (!swapTokens[cID]) { - swapTokens[cID] = { [token.swapableType]: [token] } - } else if (!swapTokens[cID][token.swapableType]) { - swapTokens[cID][token.swapableType] = [token] - } else if (!swapTokens[cID][token.swapableType].includes(token)) { - swapTokens[cID][token.swapableType] = [ - ...swapTokens[cID][token.swapableType], - token, - ] - } - } - }) - return swapTokens -} const getSwapPriorityRanking = () => { const swapPriorityRanking = {} allTokensWithSwap.map((token) => { @@ -262,8 +138,6 @@ const getSwapPriorityRanking = () => { }) return swapPriorityRanking } -export const SWAPABLE_TOKENS = getSwapableTokens() -export const SWAPABLE_TOKENS_BY_TYPE = getSwapableTokensByType() export const POOL_PRIORITY_RANKING = getSwapPriorityRanking() // POOLS diff --git a/packages/synapse-interface/contexts/WalletAnalyticsProvider.tsx b/packages/synapse-interface/contexts/UserProvider.tsx similarity index 59% rename from packages/synapse-interface/contexts/WalletAnalyticsProvider.tsx rename to packages/synapse-interface/contexts/UserProvider.tsx index f32e9f6a2f..a610ffae42 100644 --- a/packages/synapse-interface/contexts/WalletAnalyticsProvider.tsx +++ b/packages/synapse-interface/contexts/UserProvider.tsx @@ -2,14 +2,20 @@ import { createContext, useContext, useEffect, useRef } from 'react' import { Chain, useAccount, useNetwork } from 'wagmi' import { segmentAnalyticsEvent } from './SegmentAnalyticsProvider' import { useRouter } from 'next/router' +import { setSwapChainId } from '@/slices/swap/reducer' + +import { fetchAndStorePortfolioBalances } from '@/slices/portfolio/hooks' +import { useAppDispatch } from '@/store/hooks' +import { resetPortfolioState } from '@/slices/portfolio/actions' const WalletStatusContext = createContext(undefined) -export const WalletAnalyticsProvider = ({ children }) => { +export const UserProvider = ({ children }) => { + const dispatch = useAppDispatch() const { chain } = useNetwork() const router = useRouter() const { query, pathname } = router - const { connector } = useAccount({ + const { address, connector } = useAccount({ onConnect() { segmentAnalyticsEvent(`[Wallet Analytics] connects`, { walletId: connector?.id, @@ -30,10 +36,16 @@ export const WalletAnalyticsProvider = ({ children }) => { const prevChain = prevChainRef.current useEffect(() => { + if (chain) { + dispatch(setSwapChainId(chain.id)) + } + if (!chain) { return } if (prevChain && chain !== prevChain) { + dispatch(setSwapChainId(chain.id)) + segmentAnalyticsEvent(`[Wallet Analytics] connected to new chain`, { previousNetworkName: prevChain.name, previousChainId: prevChain.id, @@ -46,6 +58,22 @@ export const WalletAnalyticsProvider = ({ children }) => { } }, [chain]) + useEffect(() => { + ;(async () => { + if (address && chain?.id) { + try { + await dispatch(fetchAndStorePortfolioBalances(address)) + } catch (error) { + console.error('Failed to fetch and store portfolio balances:', error) + } + } + + if (!address) { + dispatch(resetPortfolioState()) + } + })() + }, [chain, address]) + return ( {children} @@ -53,4 +81,4 @@ export const WalletAnalyticsProvider = ({ children }) => { ) } -export const useWalletStatus = () => useContext(WalletStatusContext) +export const useUserStatus = () => useContext(WalletStatusContext) diff --git a/packages/synapse-interface/pages/_app.tsx b/packages/synapse-interface/pages/_app.tsx index 19cef6e443..bb2844fed9 100644 --- a/packages/synapse-interface/pages/_app.tsx +++ b/packages/synapse-interface/pages/_app.tsx @@ -46,7 +46,7 @@ import { SegmentAnalyticsProvider } from '@/contexts/SegmentAnalyticsProvider' import { Provider } from 'react-redux' import { store } from '@/store/store' -import { WalletAnalyticsProvider } from '@/contexts/WalletAnalyticsProvider' +import { UserProvider } from '@/contexts/UserProvider' import PortfolioUpdater from '@/slices/portfolio/updater' import TransactionsUpdater from '@/slices/transactions/updater' @@ -146,16 +146,16 @@ const App = ({ Component, pageProps }: AppProps) => { - - - + + + - - - + + + diff --git a/packages/synapse-interface/pages/bridge/BridgeWatcher/BlockCountdown.tsx b/packages/synapse-interface/pages/bridge/BridgeWatcher/BlockCountdown.tsx deleted file mode 100644 index fe604a6721..0000000000 --- a/packages/synapse-interface/pages/bridge/BridgeWatcher/BlockCountdown.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import _ from 'lodash' -import { useEffect, useState, memo } from 'react' -import { fetchBlockNumber } from '@wagmi/core' -import { - ChevronRightIcon, - ChevronDoubleRightIcon, -} from '@heroicons/react/outline' -import { Arc } from '@visx/shape' -import { Chord } from '@visx/chord' -import { BridgeWatcherTx } from '@types' -import { getNetworkTextColor } from '@styles/chains' -import { CHAINS_BY_ID } from '@/constants/chains' -import { BRIDGE_REQUIRED_CONFIRMATIONS } from '@constants/bridge' -import { - EmptySubTransactionItem, - CheckingConfPlaceholder, -} from '@components/TransactionItems' - -const BlockCountdown = memo( - ({ - fromEvent, - toEvent, - setCompletedConf, - }: { - fromEvent: BridgeWatcherTx - toEvent?: BridgeWatcherTx - setCompletedConf: (bool: boolean) => void - }) => { - const chain = fromEvent?.chainId ? CHAINS_BY_ID[fromEvent.chainId] : null - const [confirmationDelta, setConfirmationDelta] = useState(-1) - const [time, setTime] = useState(Date.now()) - - useEffect(() => { - const interval = setInterval(() => { - setTime(Date.now()) - }, 5000) - - return () => { - clearInterval(interval) - } - }, []) - - useEffect(() => { - if (confirmationDelta === 0 || toEvent) { - return - } - fetchBlockNumber({ - chainId: fromEvent?.chainId, - }).then((newestBlockNumber) => { - // if error with rpc getting block number, don't run the following code - if (!newestBlockNumber) { - return - } - // get number of blocks since from event blocknumber - const blockDifference = newestBlockNumber - BigInt(fromEvent.blockNumber) - - // get number of blocks since event block number - required confirmations - const blocksSinceConfirmed = - blockDifference - BigInt(BRIDGE_REQUIRED_CONFIRMATIONS[fromEvent?.chainId]) - - // if blocks since confirmed is less than 0, thats how many blocks left to confirm - setConfirmationDelta( - blocksSinceConfirmed >= 0 ? 0 : Number(blocksSinceConfirmed) * -1 - ) - if (blocksSinceConfirmed >= 0) { - setCompletedConf(true) - } - }) - }, [time]) - - return ( - <> -
-
- {fromEvent?.toChainId && confirmationDelta > 0 && ( - <> - - - - - - - )} -
-
- - ) - } -) - -const BlockCountdownCircle = ({ - clampedDiff, - fromChainConfirmations, - coloring, -}) => { - const dataMatrix = [ - [fromChainConfirmations - clampedDiff, 0, 0, 0], - [clampedDiff, 0, 0, 0], - ] - return ( - - - {clampedDiff} - - - - {({ chords }) => ( - - {chords.groups - .filter((group) => group.value != 0) - .map((group, i) => ( - - ))} - - )} - - - - ) -} - -export default BlockCountdown diff --git a/packages/synapse-interface/pages/bridge/BridgeWatcher/BridgeEvent.tsx b/packages/synapse-interface/pages/bridge/BridgeWatcher/BridgeEvent.tsx deleted file mode 100644 index 87cb0bb049..0000000000 --- a/packages/synapse-interface/pages/bridge/BridgeWatcher/BridgeEvent.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import EventCard from './EventCard' -import DestinationTx from './DestinationTx' -import { BridgeWatcherTx } from '@types' -import Link from 'next/link' -import { EXPLORER_KAPPA } from '@urls' -import { memo } from 'react' - -const BridgeEvent = memo((fromEvent: BridgeWatcherTx) => { - // Saving Event Link for when indexing occurs faster - const EventLink = ( - - - View on Explorer - {' '} - - ) - return ( -
-
-
- {fromEvent && } -
-
- {fromEvent && } -
-
- {/* {EventLink} */} -
- ) -}) -export default BridgeEvent diff --git a/packages/synapse-interface/pages/bridge/BridgeWatcher/DestinationTx.tsx b/packages/synapse-interface/pages/bridge/BridgeWatcher/DestinationTx.tsx deleted file mode 100644 index de6f5087d8..0000000000 --- a/packages/synapse-interface/pages/bridge/BridgeWatcher/DestinationTx.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import _ from 'lodash' -import { useEffect, useState, memo, useMemo } from 'react' -import { useWalletClient } from 'wagmi' -import { fetchBlockNumber } from '@wagmi/core' -import { Contract, Signer } from 'ethers' -import { Interface } from '@ethersproject/abi' -import { - ChevronRightIcon, - ChevronDoubleRightIcon, -} from '@heroicons/react/outline' -import { BridgeWatcherTx } from '@types' -import { getNetworkTextColor } from '@/styles/chains' -import SYNAPSE_BRIDGE_ABI from '@abis/synapseBridge.json' -import { BRIDGE_CONTRACTS } from '@constants/bridge' -import { CHAINS_BY_ID } from '@/constants/chains' -import { GETLOGS_SIZE } from '@constants/bridgeWatcher' -import { useSynapseContext } from '@/utils/providers/SynapseProvider' -import { - getLogs, - getBlock, - getTransactionReceipt, - generateBridgeTx, - checkTxIn, -} from '@utils/bridgeWatcher' -import { remove0xPrefix } from '@/utils/remove0xPrefix' -import EventCard from './EventCard' -import BlockCountdown from './BlockCountdown' -import { CreditedTransactionItem } from '@components/TransactionItems' -import { Address } from 'viem' -import SYNAPSE_CCTP_ABI from '@abis/synapseCCTP.json' -import { SYNAPSE_CCTP_CONTRACTS } from '@constants/bridge' - -const DestinationTx = (fromEvent: BridgeWatcherTx) => { - const [toEvent, setToEvent] = useState(undefined) - const [toSynapseContract, setToSynapseContract] = - useState(undefined) - const [toCCTPContract, setToCCTPContract] = useState(undefined) - const [toSigner, setToSigner] = useState
() - const { data: toSignerRaw } = useWalletClient({ - chainId: fromEvent.toChainId, - }) - const [completedConf, setCompletedConf] = useState(false) - const [attempted, setAttempted] = useState(false) - const { providerMap } = useSynapseContext() - - const networkTextColorClass: string = useMemo(() => { - const networkChainById = CHAINS_BY_ID[fromEvent.chainId] - return getNetworkTextColor(networkChainById?.color) - }, [fromEvent.toChainId]) - - const getToBridgeEvent = async (): Promise => { - const headOnDestination = await fetchBlockNumber({ - chainId: fromEvent.toChainId, - }) - const provider = providerMap[fromEvent.toChainId] - - const isCCTP = - typeof fromEvent.contractEmittedFrom === 'string' && - typeof SYNAPSE_CCTP_CONTRACTS[fromEvent.chainId] === 'string' && - fromEvent.contractEmittedFrom.toLowerCase() === - SYNAPSE_CCTP_CONTRACTS[fromEvent.chainId].toLowerCase() - ? true - : false - - const iface = new Interface(isCCTP ? SYNAPSE_CCTP_ABI : SYNAPSE_BRIDGE_ABI) - - let allToEvents = [] - let i = 0 - let afterOrginTx = true - while (afterOrginTx) { - const startBlock = Number(headOnDestination) - GETLOGS_SIZE * i - - // get timestamp from from block - const blockRaw = await getBlock(startBlock - (GETLOGS_SIZE + 1), provider) - const blockTimestamp = blockRaw?.timestamp - - // Exit loop if destination block was mined before the block for the origin tx - if (blockTimestamp < fromEvent.timestamp) { - afterOrginTx = false - } - - const fromEventsBridge = await getLogs( - startBlock, - provider, - toSynapseContract, - fromEvent.toAddress - ) - - const fromEventsCCTP = await getLogs( - startBlock, - provider, - toCCTPContract, - fromEvent.toAddress - ) - - allToEvents.push(fromEventsBridge, fromEventsCCTP) - i++ - - // Break if cannot find tx - if (i > 30) { - afterOrginTx = false - } - } - const flattendEvents = _.flatten(allToEvents) - let parsedLogs - if (!isCCTP) { - parsedLogs = flattendEvents - .map((log) => { - return { - ...iface.parseLog(log).args, - transactionHash: log.transactionHash, - blockNumber: Number(log.blockNumber), - } - }) - .filter((log: any) => { - const convertedKappa = remove0xPrefix(log.kappa) - return !checkTxIn(log) && convertedKappa === fromEvent.kappa - }) - } else { - parsedLogs = flattendEvents - .map((log) => { - return { - ...iface.parseLog(log).args, - transactionHash: log.transactionHash, - blockNumber: Number(log.blockNumber), - } - }) - .filter((log: any) => { - return log.requestID === fromEvent.kappa - }) - } - - const parsedLog = parsedLogs?.[0] - if (parsedLog) { - const [inputTimestamp, transactionReceipt] = await Promise.all([ - getBlock(parsedLog.blockNumber, provider), - getTransactionReceipt(parsedLog.transactionHash, provider), - ]) - - const destBridgeTx = generateBridgeTx( - false, - fromEvent.toAddress, - fromEvent.toChainId, - parsedLog, - inputTimestamp, - transactionReceipt, - fromEvent.toAddress - ) - setAttempted(true) - return destBridgeTx - } - - setAttempted(true) - return null - } - - useEffect(() => { - if (toSigner && fromEvent) { - const toSynapseContract = new Contract( - BRIDGE_CONTRACTS[fromEvent.toChainId], - SYNAPSE_BRIDGE_ABI, - providerMap[fromEvent.toChainId] - ) - setToSynapseContract(toSynapseContract) - - // Initialize CCTP Contract when signer and fromEvent are available - if (SYNAPSE_CCTP_CONTRACTS[fromEvent.toChainId]) { - const toCCTPContract = new Contract( - SYNAPSE_CCTP_CONTRACTS[fromEvent.toChainId], - SYNAPSE_CCTP_ABI, - providerMap[fromEvent.toChainId] - ) - setToCCTPContract(toCCTPContract) - } - } - }, [fromEvent, toSigner]) - - // Listens for confirmations to complete and if so, recheck destination chain for logs - useEffect(() => { - if (completedConf && (toSynapseContract || toCCTPContract) && attempted) { - getToBridgeEvent().then((tx) => { - setToEvent(tx) - }) - } - }, [completedConf, toEvent, fromEvent, toSynapseContract, toCCTPContract]) - - // Listens for SynapseContract to be set and if so, will check destination chain for logs if there is no toEvent - useEffect(() => { - if ((toSynapseContract || toCCTPContract) && !toEvent) { - getToBridgeEvent().then((tx) => { - setToEvent(tx) - return - }) - } - return - }, [toSynapseContract, toCCTPContract]) - - useEffect(() => { - if (toSignerRaw != undefined) { - setToSigner(toSignerRaw.account.address) - } - }, [toSignerRaw]) - - return ( -
-
- {toEvent ? ( - - ) : ( - - )} -
- - {toEvent ? ( -
- -
- ) : ( -
- -
- )} -
- ) -} -export default DestinationTx diff --git a/packages/synapse-interface/pages/bridge/BridgeWatcher/EventCard.tsx b/packages/synapse-interface/pages/bridge/BridgeWatcher/EventCard.tsx deleted file mode 100644 index 75e9f0eaa7..0000000000 --- a/packages/synapse-interface/pages/bridge/BridgeWatcher/EventCard.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { BridgeWatcherTx } from '@types' -import { CHAINS_BY_ID } from '@constants/chains' -import { ETH } from '@constants/tokens/bridgeable' -import ExplorerLink from './ExplorerLink' -import { getNetworkLinkTextColor } from '@styles/chains' -import { AddToWalletMiniButton } from '@components/buttons/AddToWalletButton' -import { getCoinTextColorCombined } from '@styles/tokens' -import { memo } from 'react' -import { formatTimestampToDate } from '@utils/time' -import { commify, formatBigIntToString } from '@/utils/bigint/format' - -const EventCard = memo((event: BridgeWatcherTx) => { - const chain = CHAINS_BY_ID[event.chainId] - let showAddBtn - if (event.token?.symbol == ETH.symbol) { - showAddBtn = false - } else { - showAddBtn = true - } - - return ( - <> -
- {event?.timestamp && formatTimestampToDate(event.timestamp)} -
-
-
- -
-
-
-
- {event && ( - - )} -
-
-
-
- - {event?.amount - ? commify( - formatBigIntToString( - BigInt(event.amount.toString()), - event.token?.decimals[event.chainId], - 8 - ) - ) - : '0'} - - - {event.token && ( - <> - - {' '} - {event.token.symbol}{' '} - - - - )} -
-
-
-
- {showAddBtn && event.isFrom && ( - - )} -
-
- - ) -}) - -export default EventCard diff --git a/packages/synapse-interface/pages/bridge/BridgeWatcher/ExplorerLink.tsx b/packages/synapse-interface/pages/bridge/BridgeWatcher/ExplorerLink.tsx deleted file mode 100644 index 2e610abc26..0000000000 --- a/packages/synapse-interface/pages/bridge/BridgeWatcher/ExplorerLink.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ExternalLinkIcon } from '@heroicons/react/outline' -import { getExplorerTxUrl } from '@urls' - -const ExplorerLink = ({ - transactionHash, - chainId, - className, - overrideExistingClassname = false, - showIcon = false, -}) => { - const explorerTxUrl = getExplorerTxUrl({ hash: transactionHash, chainId }) - const len = transactionHash?.length - - return ( - - {transactionHash?.slice(0, 6)}...{transactionHash?.slice(len - 4, len)} - {showIcon && } - - ) -} -export default ExplorerLink diff --git a/packages/synapse-interface/pages/bridge/BridgeWatcher/index.tsx b/packages/synapse-interface/pages/bridge/BridgeWatcher/index.tsx deleted file mode 100644 index 4979acb4e6..0000000000 --- a/packages/synapse-interface/pages/bridge/BridgeWatcher/index.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { fetchBlockNumber } from '@wagmi/core' -import { useWalletClient, useAccount } from 'wagmi' -import SYNAPSE_BRIDGE_ABI from '@abis/synapseBridge.json' -import { Contract, Signer } from 'ethers' -import { BRIDGE_CONTRACTS, SYNAPSE_CCTP_CONTRACTS } from '@constants/bridge' -import { useEffect, useState } from 'react' -import { Interface } from '@ethersproject/abi' -import _ from 'lodash' -import Grid from '@tw/Grid' -import Card from '@tw/Card' -import BridgeEvent from './BridgeEvent' -import { BridgeWatcherTx } from '@types' -import { GETLOGS_SIZE, GETLOGS_REQUEST_COUNT } from '@constants/bridgeWatcher' -import { useSynapseContext } from '@/utils/providers/SynapseProvider' -import { useSelector } from 'react-redux' -import { RootState } from '@/store/store' -import { Address } from 'viem' -import { walletClientToSigner } from '@/ethers' -import SYNAPSE_CCTP_ABI from '@abis/synapseCCTP.json' -import * as CHAINS from '@constants/chains/master' -import { ChainId } from '@/constants/chains' - -import { - getLogs, - getBlock, - getTransactionReceipt, - generateBridgeTx, - checkTxIn, -} from '@utils/bridgeWatcher' - -const BridgeWatcher = ({ - fromChainId, - toChainId, - address, - destinationAddress, -}: { - fromChainId: number - toChainId: number - address: string - destinationAddress: string -}) => { - const bridgeTxHashes = useSelector((state: RootState) => state.bridge) - const [fromTransactions, setFromTransactions] = useState([]) - const [fromSynapseContract, setFromSynapseContract] = useState() - const [fromCCTPContract, setFromCCTPContract] = useState() - - const [fromSigner, setFromSigner] = useState() - const { address: fromSignerRaw, isConnecting, isDisconnected } = useAccount() - const { providerMap } = useSynapseContext() - - const createContractsAndInterfaces = (chainId, provider) => { - const bridgeAddress = BRIDGE_CONTRACTS[chainId] - const synapseCCTPAddress = SYNAPSE_CCTP_CONTRACTS[chainId] - - const validBridgeContract = BRIDGE_CONTRACTS[fromChainId] - ? BRIDGE_CONTRACTS[fromChainId] - : BRIDGE_CONTRACTS[CHAINS.ETH.id] - const bridgeContract = new Contract( - validBridgeContract, - SYNAPSE_BRIDGE_ABI, - provider - ) - - const bridgeInterface = new Interface(SYNAPSE_BRIDGE_ABI) - - const synapseCCTPContract = synapseCCTPAddress - ? new Contract(synapseCCTPAddress, SYNAPSE_CCTP_ABI, provider) - : null - - const synapseCCTPInterface = synapseCCTPAddress - ? new Interface(SYNAPSE_CCTP_ABI) - : null - - return { - bridgeContract, - bridgeInterface, - synapseCCTPContract, - synapseCCTPInterface, - } - } - - const fetchFromBridgeEvents = async ( - currentFromBlock: number, - provider: any, - adjustedAddress: string - ) => { - const { - bridgeContract, - bridgeInterface, - synapseCCTPContract, - synapseCCTPInterface, - } = createContractsAndInterfaces(provider.network.chainId, provider) - let allFromEvents = [] - let retryCount = 0 - const maxRetries = 5 // Adjust this as needed - - // fetch bridge logs - for (let i = 0; i < GETLOGS_REQUEST_COUNT; i++) { - let successful = false - while (!successful && retryCount < maxRetries) { - try { - const fromEvents = await getLogs( - currentFromBlock - GETLOGS_SIZE * i, - provider, - bridgeContract, - adjustedAddress - ) - allFromEvents.push(fromEvents) - successful = true - } catch (error) { - retryCount++ - console.log( - `getLogs failed, retrying in ${Math.pow(2, retryCount)} seconds...` - ) - await new Promise((resolve) => - setTimeout(resolve, Math.pow(2, retryCount) * 1000) - ) - } - } - if (retryCount === maxRetries) { - console.error('getLogs failed after maximum retries') - break - } - } - // fetch synapseCCTP logs if the contract exists for the chain - if (synapseCCTPContract) { - for (let i = 0; i < GETLOGS_REQUEST_COUNT; i++) { - let successful = false - while (!successful && retryCount < maxRetries) { - try { - const fromEvents = await getLogs( - currentFromBlock - GETLOGS_SIZE * i, - provider, - synapseCCTPContract, - adjustedAddress - ) - allFromEvents.push(fromEvents) - successful = true - } catch (error) { - retryCount++ - console.log( - `getLogs failed, retrying in ${Math.pow( - 2, - retryCount - )} seconds...` - ) - await new Promise((resolve) => - setTimeout(resolve, Math.pow(2, retryCount) * 1000) - ) - } - } - if (retryCount === maxRetries) { - console.error('getLogs failed after maximum retries') - break - } - } - } - - return _.flatten(allFromEvents) - } - - const parseLogs = ( - fromEvents: any[], - bridgeInterface: Interface, - synapseCCTPInterface: Interface, - bridgeAddress: string, - synapseCCTPAddress?: string - ) => { - return fromEvents - .map((log) => { - // Select the correct interface based on the contract address - const iface = - log.address.toLowerCase() === bridgeAddress.toLowerCase() - ? bridgeInterface - : synapseCCTPInterface - - return { - ...iface.parseLog(log).args, - transactionHash: log.transactionHash, - blockNumber: Number(log.blockNumber), - contractEmittedFrom: log.address.toLowerCase(), - } - }) - .filter((log) => checkTxIn(log)) - } - - const fetchTimestampsAndReceipts = (parsedLogs: any[], provider: any) => { - return Promise.all([ - Promise.all(parsedLogs.map((log) => getBlock(log.blockNumber, provider))), - Promise.all( - parsedLogs.map((log) => - getTransactionReceipt(log.transactionHash, provider) - ) - ), - ]) - } - - const generateBridgeTransactions = ( - parsedLogs: any[], - inputTimestamps: any[], - transactionReceipts: any[], - address: string, - fromChainId: number, - destinationAddress: string - ) => { - return _.zip(parsedLogs, inputTimestamps, transactionReceipts).map( - ([parsedLog, timestampObj, txReceipt]) => { - return generateBridgeTx( - true, - address, - fromChainId, - parsedLog, - timestampObj, - txReceipt, - destinationAddress - ) - } - ) - } - - const getFromBridgeEvents = async (): Promise => { - const currentFromBlock = await fetchBlockNumber({ chainId: fromChainId }) - const provider = providerMap[fromChainId] ?? providerMap[ChainId.ETH] - const iface = new Interface(SYNAPSE_BRIDGE_ABI) - const adjustedAddress = destinationAddress ? destinationAddress : address - - // Define the contracts and interfaces here - const { - bridgeContract, - bridgeInterface, - synapseCCTPContract, - synapseCCTPInterface, - } = createContractsAndInterfaces(provider?.network?.chainId, provider) - - const fromEvents = await fetchFromBridgeEvents( - Number(currentFromBlock), - provider, - adjustedAddress - ) - // Use the correct interfaces and addresses when parsing the logs - const parsedLogs = parseLogs( - fromEvents, - bridgeInterface, - synapseCCTPInterface, - bridgeContract.address, - synapseCCTPContract?.address - ) - - const [inputTimestamps, transactionReceipts] = - await fetchTimestampsAndReceipts(parsedLogs, provider) - const txObjects = generateBridgeTransactions( - parsedLogs, - inputTimestamps, - transactionReceipts, - address, - fromChainId, - destinationAddress - ) - - return txObjects - } - - useEffect(() => { - if (fromSigner && fromChainId && toChainId && address) { - const validBridgeContract = BRIDGE_CONTRACTS[fromChainId] - ? BRIDGE_CONTRACTS[fromChainId] - : BRIDGE_CONTRACTS[1] - const fromSynapseContract = new Contract( - validBridgeContract, - SYNAPSE_BRIDGE_ABI, - providerMap[fromChainId] - ) - setFromSynapseContract(fromSynapseContract) - } - }, [fromChainId, fromSigner]) - - useEffect(() => { - if (fromSynapseContract) { - getFromBridgeEvents().then((txs) => { - setFromTransactions(txs) - }) - } - - return () => setFromTransactions([...fromTransactions]) - }, [fromSynapseContract, bridgeTxHashes]) - - useEffect(() => { - setFromSigner(fromSignerRaw) - }, [fromSignerRaw]) - - return ( -
- {fromTransactions?.length > 0 && ( - - - {fromTransactions.map((fromEvent, i) => { - return - })} - - - )} -
- ) -} - -export default BridgeWatcher diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx index f3a0aa165a..b5da17b74c 100644 --- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx +++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx @@ -3,7 +3,6 @@ import { useSelector } from 'react-redux' import { RootState } from '../../store/store' import toast from 'react-hot-toast' import { animated } from 'react-spring' -import BridgeWatcher from '@/pages/bridge/BridgeWatcher' import { useRouter } from 'next/router' import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' @@ -562,14 +561,6 @@ const StateManagedBridge = () => {
- {/*
- -
*/} ) } diff --git a/packages/synapse-interface/pages/swap/NoSwapCard.tsx b/packages/synapse-interface/pages/swap/NoSwapCard.tsx deleted file mode 100644 index 0fcb7a59d9..0000000000 --- a/packages/synapse-interface/pages/swap/NoSwapCard.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import Card from '@tw/Card' -import { CHAINS_BY_ID } from '@constants/chains' -import { getNetworkTextColor } from '@styles/chains' -import { useMemo } from 'react' - -const NoSwapCard = ({ chainId }: { chainId: number }) => { - const chain = useMemo(() => CHAINS_BY_ID[chainId], [chainId]) - return ( - -
- No swaps available on{' '} - - {chain?.name ?? 'current network'} - -
-
- ) -} - -export default NoSwapCard diff --git a/packages/synapse-interface/pages/swap/SwapCard.tsx b/packages/synapse-interface/pages/swap/SwapCard.tsx deleted file mode 100644 index a0d87cc686..0000000000 --- a/packages/synapse-interface/pages/swap/SwapCard.tsx +++ /dev/null @@ -1,844 +0,0 @@ -import Grid from '@tw/Grid' -import { useEffect, useState, useMemo, useCallback } from 'react' -import { useRouter } from 'next/router' -import { getWalletClient, switchNetwork } from '@wagmi/core' -import { Address } from 'wagmi' -import { sortByTokenBalance, sortByVisibilityRank } from '@utils/sortTokens' -import { calculateExchangeRate } from '@utils/calculateExchangeRate' -import SwapExchangeRateInfo from '@components/SwapExchangeRateInfo' -import { TransactionButton } from '@/components/buttons/TransactionButton' -import BridgeInputContainer from '../../components/input/TokenAmountInput/index' -import { approveToken } from '@/utils/approveToken' -import { validateAndParseAddress } from '@utils/validateAndParseAddress' -import { commify } from '@ethersproject/units' -import { subtractSlippage } from '@utils/slippage' -import { ChainSlideOver } from '@/components/misc/ChainSlideOver' -import { TokenSlideOver } from '@/components/misc/TokenSlideOver' -import { Token } from '@/utils/types' -import { SWAP_PATH } from '@/constants/urls' -import { stringToBigInt } from '@/utils/bigint/format' -import { useSynapseContext } from '@/utils/providers/SynapseProvider' -import { checkStringIfOnlyZeroes } from '@/utils/regex' -import { timeout } from '@/utils/timeout' -import { Transition } from '@headlessui/react' -import { COIN_SLIDE_OVER_PROPS } from '@styles/transitions' -import Card from '@tw/Card' -import { SwapQuote, Query } from '@types' -import { IMPAIRED_CHAINS } from '@/constants/impairedChains' -import { CHAINS_BY_ID } from '@constants/chains' -import { toast } from 'react-hot-toast' -import { txErrorHandler } from '@/utils/txErrorHandler' -import ExplorerToastLink from '@/components/ExplorerToastLink' -import { zeroAddress } from 'viem' - -import { - DEFAULT_FROM_TOKEN, - DEFAULT_TO_TOKEN, - EMPTY_SWAP_QUOTE, - QUOTE_POLLING_INTERVAL, - EMPTY_SWAP_QUOTE_ZERO, -} from '@/constants/swap' -import { - SWAPABLE_TOKENS, - BRIDGE_SWAPABLE_TOKENS_BY_TYPE, - SWAPABLE_TOKENS_BY_TYPE, - tokenSymbolToToken, -} from '@constants/tokens' -import { formatBigIntToString } from '@/utils/bigint/format' -import { getErc20TokenAllowance } from '@/actions/getErc20TokenAllowance' - -const SwapCard = ({ - address, - connectedChainId, -}: { - address: Address | undefined - connectedChainId: number -}) => { - const router = useRouter() - const { synapseSDK } = useSynapseContext() - const [time, setTime] = useState(Date.now()) - const [fromToken, setFromToken] = useState(DEFAULT_FROM_TOKEN) - const [fromTokens, setFromTokens] = useState([]) - const [fromInput, setFromInput] = useState({ string: '', bigInt: 0n }) - const [toToken, setToToken] = useState(DEFAULT_TO_TOKEN) - const [toTokens, setToTokens] = useState([]) //add default - const [isQuoteLoading, setIsQuoteLoading] = useState(false) - const [error, setError] = useState(undefined) - const [destinationAddress, setDestinationAddress] = useState('') - const [swapQuote, setSwapQuote] = useState(EMPTY_SWAP_QUOTE) - const [displayType, setDisplayType] = useState(undefined) - const [fromTokenBalance, setFromTokenBalance] = useState(0n) - const [validChainId, setValidChainId] = useState(true) - const [swapTxnHash, setSwapTxnHash] = useState('') - const [approveTx, setApproveTx] = useState(null) - - let pendingPopup: any - let successPopup: any - let errorPopup: string - - /* - useEffect Trigger: onMount - - Gets current network connected and sets it as the state. - - Initializes polling (setInterval) func to re-retrieve quotes. - */ - useEffect(() => { - const interval = setInterval( - () => setTime(Date.now()), - QUOTE_POLLING_INTERVAL - ) - return () => { - clearInterval(interval) - } - }, []) - - /* - useEffect Trigger: fromInput - - Resets approve txn status if user input changes after amount is approved - */ - - useEffect(() => { - if (approveTx) { - setApproveTx(null) - } - }, [fromInput]) - - /* - useEffect Trigger: fromToken, fromTokens - - When either the from token or list of from tokens are mutated, the selected token's balance is set in state - this is for checking max bridge possible as well as for producing the option to select max bridge - */ - useEffect(() => { - if (fromTokens && fromToken) { - setFromTokenBalance( - fromTokens.filter((token) => token.token === fromToken)[0]?.balance - ? fromTokens.filter((token) => token.token === fromToken)[0]?.balance - : 0n - ) - } - }, [fromToken, fromTokens]) - - useEffect(() => { - if (!router.isReady || !SWAPABLE_TOKENS[connectedChainId]) { - return - } - const { - inputCurrency: fromTokenSymbolUrl, - outputCurrency: toTokenSymbolUrl, - } = router.query - - let tempFromToken: Token = getMostCommonSwapableType(connectedChainId) - - if (fromTokenSymbolUrl) { - let token = tokenSymbolToToken( - connectedChainId, - String(fromTokenSymbolUrl) - ) - if (token) { - tempFromToken = token - } - } - const { swapableToken, swapableTokens } = handleNewFromToken( - tempFromToken, - toTokenSymbolUrl ? String(toTokenSymbolUrl) : undefined, - connectedChainId - ) - resetTokenPermutation( - tempFromToken, - swapableToken, - swapableTokens, - tempFromToken.symbol, - swapableToken.symbol - ) - updateUrlParams({ - inputCurrency: fromToken.symbol, - outputCurrency: swapableToken.symbol, - }) - }, [router.isReady]) - - /* - useEffect Trigger: connectedChain - - when the connected chain changes (wagmi hook), update the state - */ - useEffect(() => { - if (address === undefined) { - return - } - handleChainChange(connectedChainId, undefined, undefined) - - sortByTokenBalance( - SWAPABLE_TOKENS[connectedChainId], - connectedChainId, - address - ).then((tokens) => { - setFromTokens(tokens) - }) - return - }, [connectedChainId, swapTxnHash, address]) - - /* - useEffect Triggers: toToken, fromInput, toChainId, time - - Gets a quote when the polling function is executed or any of the bridge attributes are altered. - - Debounce quote call by calling quote price AFTER user has stopped typing for 1s or 1000ms - */ - useEffect(() => { - let isCancelled = false - - const handleChange = async () => { - // await timeout(1000) - if ( - connectedChainId && - String(fromToken.addresses[connectedChainId]) && - fromInput && - fromInput.bigInt > 0n - ) { - // TODO this needs to be debounced or throttled somehow to prevent spam and lag in the ui - getQuote() - } else { - setSwapQuote(EMPTY_SWAP_QUOTE) - } - } - handleChange() - - return () => { - isCancelled = true - } - }, [toToken, fromInput, time, connectedChainId]) - - /* - Helper Function: resetTokenPermutation - - Handles when theres a new from token/chain and all other parts of the bridge arrangement needs to be updated - - Updates url params. - */ - const resetTokenPermutation = ( - newFromToken: Token, - newToToken: Token, - newSwapableTokens: Token[], - newFromTokenSymbol: string, - newSwapableTokenSymbol: string - ) => { - setFromToken(newFromToken) - setToToken(newToToken) - setToTokens(newSwapableTokens) - // resetRates() - updateUrlParams({ - inputCurrency: newFromTokenSymbol, - outputCurrency: newSwapableTokenSymbol, - }) - } - - /* - Helper Function: resetRates - - Called when switching from chain/token so that the from input isn't populated with stale data. - */ - const resetRates = () => { - setSwapQuote(EMPTY_SWAP_QUOTE) - setFromInput({ string: '', bigInt: 0n }) - } - - /* - Helper Function: onChangeFromAmount - - Ensures inputted data isn't too long and then sets state with the input. - - Calculates BigNum from the input and stores in state as well (for quotes) - */ - const onChangeFromAmount = (value: string) => { - if ( - !( - value.split('.')[1]?.length > - fromToken[connectedChainId as keyof Token['decimals']] - ) - ) { - let bigInt = - stringToBigInt(value, fromToken.decimals[connectedChainId]) ?? 0n - setFromInput({ - string: value, - bigInt: bigInt, - }) - } - } - - /* - Helper Function: getMostCommonSwapableType - - Returns the default token to display when switching chains. Usually returns stables or eth/wrapped eth. - */ - const getMostCommonSwapableType = (chainId: number) => { - const fromChainTokensByType = Object.values( - SWAPABLE_TOKENS_BY_TYPE[chainId] - ) - let maxTokenLength = 0 - let mostCommonSwapableType: Token[] = fromChainTokensByType[0] - fromChainTokensByType.map((tokenArr, i) => { - if (tokenArr.length > maxTokenLength) { - maxTokenLength = tokenArr.length - mostCommonSwapableType = tokenArr - } - }) - - return sortByVisibilityRank(mostCommonSwapableType)[0] - } - - /* - Helper Function: updateUrlParams - - Pushes chain and token changes to url - NOTE: did not alter any variable names in case previous users have saved links of different bridging permutations. - */ - const updateUrlParams = ({ - inputCurrency, - outputCurrency, - }: { - inputCurrency: string - outputCurrency: string - }) => { - router.replace( - { - pathname: SWAP_PATH, - query: { - inputCurrency, - outputCurrency, - }, - }, - undefined, - { shallow: true } - ) - } - - /* - Function: handleNewFromToken - - Handles all the changes that occur when selecting a new "from token", such as generating lists of potential chains/tokens - to bridge to and handling if the current "to chain/token" are incompatible. - */ - const handleNewFromToken = ( - token: Token, - positedToSymbol: string | undefined, - fromChainId: number - ) => { - const swapExceptionsArr: number[] = - token?.swapExceptions?.[fromChainId as keyof Token['swapExceptions']] - - const positedToToken = positedToSymbol - ? tokenSymbolToToken(fromChainId, positedToSymbol) - : tokenSymbolToToken(fromChainId, token.symbol) - - let swapableTokens: Token[] = sortByVisibilityRank( - BRIDGE_SWAPABLE_TOKENS_BY_TYPE[fromChainId][String(token.swapableType)] - ).filter((toToken) => toToken !== token) - if (swapExceptionsArr?.length > 0) { - swapableTokens = swapableTokens.filter( - (toToken) => toToken.symbol === token.symbol - ) - } - let swapableToken: Token = positedToToken - if (!swapableTokens.includes(positedToToken)) { - swapableToken = swapableTokens[0] - } - - return { - swapableToken, - swapableTokens, - } - } - - /* - useEffect triggers: address, popup - - will dismiss toast asking user to connect wallet once wallet has been connected - */ - useEffect(() => { - if (address && errorPopup) { - toast.dismiss(errorPopup) - } - }, [address, errorPopup]) - - /* - Function: handleChainChange - - Produces and alert if chain not connected (upgrade to toaster) - - Handles flipping to and from chains if flag is set to true - - Handles altering the chain state for origin or destination depending on the type specified. - */ - const handleChainChange = useCallback( - async (chainId: number, flip: boolean, type: 'from' | 'to') => { - if (address === undefined) { - errorPopup = toast.error('Please connect your wallet', { - id: 'bridge-connect-wallet', - duration: 20000, - }) - return errorPopup - } - const desiredChainId = Number(chainId) - - const res = await switchNetwork({ chainId: desiredChainId }) - .then((res) => { - if (fromInput.string !== '') { - setIsQuoteLoading(true) - } - return res - }) - .catch((error) => { - return error && undefined - }) - - if (res === undefined) { - console.log("can't switch network, chainId: ", chainId) - return - } - if (!SWAPABLE_TOKENS[desiredChainId]) { - return - } - setValidChainId(true) - - const swapableFromTokens: Token[] = sortByVisibilityRank( - BRIDGE_SWAPABLE_TOKENS_BY_TYPE[chainId][String(fromToken.swapableType)] - ) - let tempFromToken: Token = fromToken - - if (swapableFromTokens?.length > 0) { - tempFromToken = getMostCommonSwapableType(chainId) - } - const { swapableToken, swapableTokens } = handleNewFromToken( - tempFromToken, - toToken.symbol, - desiredChainId - ) - resetTokenPermutation( - tempFromToken, - swapableToken, - swapableTokens, - tempFromToken.symbol, - swapableToken?.symbol - ) - return - }, - [ - fromToken, - toToken, - connectedChainId, - address, - isQuoteLoading, - handleNewFromToken, - switchNetwork, - ] - ) - - /* - Function:handleTokenChange - - Handles when the user selects a new token from either the origin or destination - */ - const handleTokenChange = (token: Token, type: 'from' | 'to') => { - switch (type) { - case 'from': - const { swapableToken, swapableTokens } = handleNewFromToken( - token, - toToken.symbol, - connectedChainId - ) - resetTokenPermutation( - token, - swapableToken, - swapableTokens, - token.symbol, - swapableToken.symbol - ) - if (fromInput.string !== '') { - setIsQuoteLoading(true) - } - return - case 'to': - setToToken(token) - if (fromInput.string !== '') { - setIsQuoteLoading(true) - } - updateUrlParams({ - inputCurrency: fromToken.symbol, - outputCurrency: token.symbol, - }) - return - } - } - - /* - Function: getQuote - - Gets quote data from the Synapse SDK (from the imported provider) - - Calculates slippage by subtracting fee from input amount (checks to ensure proper num of decimals are in use - ask someone about stable swaps if you want to learn more) - */ - const getQuote = async () => { - try { - if (swapQuote === EMPTY_SWAP_QUOTE) { - setIsQuoteLoading(true) - } - const { routerAddress, maxAmountOut, query } = await synapseSDK.swapQuote( - connectedChainId, - fromToken.addresses[connectedChainId], - toToken.addresses[connectedChainId], - fromInput.bigInt - ) - if (!(query && maxAmountOut)) { - setSwapQuote(EMPTY_SWAP_QUOTE_ZERO) - setIsQuoteLoading(false) - return - } - - const toValueBigInt = BigInt(maxAmountOut.toString()) ?? 0n - - const allowance = - fromToken.addresses[connectedChainId] === zeroAddress || - address === undefined - ? 0n - : await getErc20TokenAllowance({ - address, - chainId: connectedChainId, - tokenAddress: fromToken.addresses[connectedChainId] as Address, - spender: routerAddress, - }) - - const minWithSlippage = subtractSlippage( - query?.minAmountOut ?? 0n, - 'ONE_TENTH', - null - ) - // TODO 1) make dynamic 2) clean up - let newOriginQuery = { ...query } - newOriginQuery.minAmountOut = minWithSlippage - - setSwapQuote({ - outputAmount: toValueBigInt, - outputAmountString: commify( - formatBigIntToString( - toValueBigInt, - toToken.decimals[connectedChainId], - 8 - ) - ), - routerAddress, - allowance: BigInt(allowance.toString()), - exchangeRate: calculateExchangeRate( - fromInput.bigInt - 0n, // this needs to be changed once we can get fee data from router. - fromToken.decimals[connectedChainId], - toValueBigInt, - toToken.decimals[connectedChainId] - ), - delta: toValueBigInt, - quote: newOriginQuery, - }) - setIsQuoteLoading(false) - return - } catch (error) { - setIsQuoteLoading(false) - console.log(`Quote failed with error: ${error}`) - return - } - } - - /* - Function: approveToken - - Gets raw unsigned tx data from sdk and then execute it with ethers. - - Only executes if token has already been approved. - */ - const executeSwap = async () => { - const currentChainName = CHAINS_BY_ID[connectedChainId]?.name - pendingPopup = toast( - `Initiating swap from ${fromToken.symbol} to ${toToken.symbol} on ${currentChainName}`, - { id: 'swap-in-progress-popup', duration: Infinity } - ) - - try { - const wallet = await getWalletClient({ - chainId: connectedChainId, - }) - - const data = await synapseSDK.swap( - connectedChainId, - address, - fromToken.addresses[connectedChainId], - fromInput.bigInt, - swapQuote.quote - ) - - const payload = - fromToken.addresses[connectedChainId as keyof Token['addresses']] === - zeroAddress || - fromToken.addresses[connectedChainId as keyof Token['addresses']] === '' - ? { data: data.data, to: data.to, value: fromInput.bigInt } - : data - - const tx = await wallet.sendTransaction(payload) - - try { - const successTx = await tx - - setSwapTxnHash(successTx) - - toast.dismiss(pendingPopup) - - console.log(`Transaction mined successfully: ${tx}`) - - const successToastContent = ( -
-
- Successfully swapped from {fromToken.symbol} to {toToken.symbol}{' '} - on {currentChainName} -
- -
- ) - - successPopup = toast.success(successToastContent, { - id: 'swap-successful-popup', - duration: 10000, - }) - - resetRates() - return tx - } catch (error) { - toast.dismiss(pendingPopup) - console.log(`Transaction failed with error: ${error}`) - } - } catch (error) { - console.log(`Swap Execution failed with error: ${error}`) - toast.dismiss(pendingPopup) - txErrorHandler(error) - } - } - - const transitionProps = { - ...COIN_SLIDE_OVER_PROPS, - className: ` - origin-bottom absolute - w-full h-full - md:w-[95%] md:h-[95%] - -ml-0 md:-ml-3 - bg-bgBase - z-20 rounded-lg - `, - } - - const isFromBalanceEnough = fromTokenBalance >= fromInput.bigInt - let destAddrNotValid: boolean - - const getButtonProperties = () => { - let properties = { - label: `Enter amount to swap`, - pendingLabel: 'Swapping funds...', - className: '', - disabled: true, - buttonAction: () => executeSwap(), - postButtonAction: () => resetRates(), - } - - const isInputZero = checkStringIfOnlyZeroes(fromInput?.string) - - if (error) { - properties.label = error - properties.disabled = true - return properties - } - - if (isInputZero || fromInput?.bigInt === 0n) { - properties.label = `Enter amount to swap` - properties.disabled = true - return properties - } - - if (!isFromBalanceEnough) { - properties.label = `Insufficient ${fromToken.symbol} Balance` - properties.disabled = true - return properties - } - - if (IMPAIRED_CHAINS[connectedChainId]?.disabled) { - properties.label = `${CHAINS_BY_ID[connectedChainId]?.name} is currently paused` - properties.disabled = true - return properties - } - - if (fromInput.bigInt === 0n) { - properties.label = `Amount must be greater than fee` - properties.disabled = true - return properties - } - - if ( - fromInput?.bigInt !== 0n && - fromToken?.addresses[connectedChainId] !== '' && - fromToken?.addresses[connectedChainId] !== zeroAddress && - !swapQuote?.allowance && - swapQuote?.allowance < fromInput.bigInt && - !approveTx - ) { - properties.buttonAction = () => - approveToken( - swapQuote.routerAddress, - connectedChainId, - fromToken.addresses[connectedChainId], - fromInput.bigInt - ) - properties.label = `Approve ${fromToken.symbol}` - properties.pendingLabel = `Approving ${fromToken.symbol}` - properties.className = 'from-[#feba06] to-[#FEC737]' - properties.disabled = false - properties.postButtonAction = () => { - setApproveTx('approved') - } - return properties - } - - if (destinationAddress && !validateAndParseAddress(destinationAddress)) { - destAddrNotValid = true - properties.label = 'Invalid Destination Address' - properties.disabled = true - return properties - } - - // default case - properties.label = 'Swap your funds' - properties.disabled = false - - const numExchangeRate = swapQuote?.exchangeRate - ? Number(formatBigIntToString(swapQuote.exchangeRate, 18, 4)) - : 0 - - if ( - fromInput.bigInt !== 0n && - numExchangeRate !== 0 && - (numExchangeRate < 0.95 || numExchangeRate > 1.05) - ) { - properties.className = 'from-[#fe064a] to-[#fe5281]' - properties.label = 'Slippage High - Swap Anyway?' - } - - return properties - } - - const { - label: btnLabel, - pendingLabel, - className: btnClassName, - buttonAction, - postButtonAction, - disabled, - } = useMemo(getButtonProperties, [ - isFromBalanceEnough, - address, - fromInput, - fromToken, - swapQuote, - isQuoteLoading, - destinationAddress, - error, - approveTx, - ]) - - const ActionButton = useMemo(() => { - return ( - buttonAction()} - disabled={disabled || destAddrNotValid} - className={btnClassName} - label={btnLabel} - pendingLabel={pendingLabel} - chainId={connectedChainId} - onSuccess={() => { - postButtonAction() - }} - /> - ) - }, [fromInput, time, swapQuote, error, approveTx]) - - /* - useEffect Triggers: fromInput - - Checks that user input is not zero. When input changes, - - isQuoteLoading state is set to true for loading state interactions - */ - useEffect(() => { - const { string, bigInt } = fromInput - const isInvalid = checkStringIfOnlyZeroes(string) - isInvalid ? () => null : setIsQuoteLoading(true) - - return () => { - setIsQuoteLoading(false) - } - }, [fromInput]) - - return ( - -
- - - - - - - - - - - -
- -
- - -
{ActionButton}
-
-
- ) -} - -export default SwapCard diff --git a/packages/synapse-interface/pages/swap/index.tsx b/packages/synapse-interface/pages/swap/index.tsx index cb1c8c450d..9f0870ae0c 100644 --- a/packages/synapse-interface/pages/swap/index.tsx +++ b/packages/synapse-interface/pages/swap/index.tsx @@ -1,73 +1,438 @@ -import { useEffect, useState } from 'react' import { useAccount, useNetwork } from 'wagmi' -import { PageHeader } from '@components/PageHeader' -import { SWAPABLE_TOKENS } from '@constants/tokens' -import { DEFAULT_FROM_CHAIN } from '@/constants/swap' -import { LandingPageWrapper } from '@layouts/LandingPageWrapper' -import StandardPageContainer from '@layouts/StandardPageContainer' -import Grid from '@tw/Grid' -import SwapCard from './SwapCard' -import NoSwapCard from './NoSwapCard' +import { useSelector } from 'react-redux' +import { RootState } from '../../store/store' +import toast from 'react-hot-toast' +import { animated } from 'react-spring' import { useRouter } from 'next/router' import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' -import { Address } from 'wagmi' -const SwapPage = () => { - const { address: currentAddress } = useAccount() +import { setIsLoading } from '@/slices/swap/reducer' + +import { useSynapseContext } from '@/utils/providers/SynapseProvider' +import { getErc20TokenAllowance } from '@/actions/getErc20TokenAllowance' +import { subtractSlippage } from '@/utils/slippage' +import { commify } from '@ethersproject/units' +import { formatBigIntToString } from '@/utils/bigint/format' +import { calculateExchangeRate } from '@/utils/calculateExchangeRate' +import { useEffect, useRef, useState } from 'react' +import { Token } from '@/utils/types' +import { getWalletClient } from '@wagmi/core' +import { txErrorHandler } from '@/utils/txErrorHandler' +import { AcceptedChainId, CHAINS_ARR, CHAINS_BY_ID } from '@/constants/chains' +import { approveToken } from '@/utils/approveToken' +import { PageHeader } from '@/components/PageHeader' +import Card from '@/components/ui/tailwind/Card' +import { Transition } from '@headlessui/react' +import { + SECTION_TRANSITION_PROPS, + TRANSITION_PROPS, +} from '@/styles/transitions' +import ExplorerToastLink from '@/components/ExplorerToastLink' +import { Address, zeroAddress } from 'viem' +import { stringToBigInt } from '@/utils/bigint/format' +import { useAppDispatch } from '@/store/hooks' +import { + fetchAndStoreSingleTokenAllowance, + fetchAndStoreSingleTokenBalance, +} from '@/slices/portfolio/hooks' +import { + usePortfolioBalances, + useFetchPortfolioBalances, +} from '@/slices/portfolio/hooks' +import { FetchState } from '@/slices/portfolio/actions' +import { updateSingleTokenAllowance } from '@/slices/portfolio/actions' +import { SwapTransactionButton } from '@/components/StateManagedSwap/SwapTransactionButton' +import SwapExchangeRateInfo from '@/components/StateManagedSwap/SwapExchangeRateInfo' +import { useSwapState } from '@/slices/swap/hooks' +import { SwapChainListOverlay } from '@/components/StateManagedSwap/SwapChainListOverlay' +import { SwapFromTokenListOverlay } from '@/components/StateManagedSwap/SwapFromTokenListOverlay' +import { SwapInputContainer } from '@/components/StateManagedSwap/SwapInputContainer' +import { SwapOutputContainer } from '@/components/StateManagedSwap/SwapOutputContainer' +import { setSwapQuote, updateSwapFromValue } from '@/slices/swap/reducer' +import { DEFAULT_FROM_CHAIN, EMPTY_SWAP_QUOTE_ZERO } from '@/constants/swap' +import { SwapToTokenListOverlay } from '@/components/StateManagedSwap/SwapToTokenListOverlay' +import { LandingPageWrapper } from '@/components/layouts/LandingPageWrapper' + +const StateManagedSwap = () => { + const { address } = useAccount() const { chain } = useNetwork() - const [connectedChainId, setConnectedChainId] = useState(0) - const [address, setAddress] = useState
(undefined) + const { synapseSDK } = useSynapseContext() + const swapDisplayRef = useRef(null) + const currentSDKRequestID = useRef(0) const router = useRouter() + const { query, pathname } = router + + const { balancesAndAllowances: portfolioBalances, status: portfolioStatus } = + useFetchPortfolioBalances() + + const { swapChainId, swapFromToken, swapToToken, swapFromValue, swapQuote } = + useSwapState() + + const { + showSwapFromTokenListOverlay, + showSwapChainListOverlay, + showSwapToTokenListOverlay, + } = useSelector((state: RootState) => state.swapDisplay) + + let pendingPopup + let successPopup + + const [isApproved, setIsApproved] = useState(false) + + const dispatch = useAppDispatch() useEffect(() => { - segmentAnalyticsEvent(`[Swap] arrives`, { - fromChainId: chain?.id, - query: router.query, - pathname: router.pathname, + segmentAnalyticsEvent(`[Swap page] arrives`, { + swapChainId, + query, + pathname, }) - }, []) + }, [query]) useEffect(() => { - setConnectedChainId(chain?.id ?? DEFAULT_FROM_CHAIN) - }, [chain]) + if ( + swapFromToken && + swapToToken && + swapFromToken?.decimals[swapChainId] && + stringToBigInt(swapFromValue, swapFromToken.decimals[swapChainId]) > 0n + ) { + console.log('trying to set swap quote') + getAndSetSwapQuote() + } else { + dispatch(setSwapQuote(EMPTY_SWAP_QUOTE_ZERO)) + dispatch(setIsLoading(false)) + } + }, [ + swapChainId, + swapFromToken, + swapToToken, + swapFromValue, + address, + portfolioBalances, + ]) useEffect(() => { - setAddress(currentAddress) - }, [currentAddress]) + if ( + swapFromToken && + swapFromToken?.addresses[swapChainId] === zeroAddress + ) { + setIsApproved(true) + } else { + if ( + swapFromToken && + swapQuote?.allowance && + stringToBigInt(swapFromValue, swapFromToken.decimals[swapChainId]) <= + swapQuote.allowance + ) { + setIsApproved(true) + } else { + setIsApproved(false) + } + } + }, [swapQuote, swapFromToken, swapFromValue, swapChainId]) + + let quoteToast + + const getAndSetSwapQuote = async () => { + currentSDKRequestID.current += 1 + const thisRequestId = currentSDKRequestID.current + try { + dispatch(setIsLoading(true)) + + const { routerAddress, maxAmountOut, query } = await synapseSDK.swapQuote( + swapChainId, + swapFromToken.addresses[swapChainId], + swapToToken.addresses[swapChainId], + stringToBigInt(swapFromValue, swapFromToken.decimals[swapChainId]) + ) + + if (!(query && maxAmountOut)) { + dispatch(setSwapQuote(EMPTY_SWAP_QUOTE_ZERO)) + dispatch(setIsLoading(true)) + return + } + + const toValueBigInt = BigInt(maxAmountOut.toString()) ?? 0n + + const allowance = + swapFromToken.addresses[swapChainId] === zeroAddress || + address === undefined + ? 0n + : await getErc20TokenAllowance({ + address, + chainId: swapChainId, + tokenAddress: swapFromToken.addresses[swapChainId] as Address, + spender: routerAddress, + }) + + const minWithSlippage = subtractSlippage( + query?.minAmountOut ?? 0n, + 'ONE_TENTH', + null + ) + + let newOriginQuery = { ...query } + newOriginQuery.minAmountOut = minWithSlippage + + if (thisRequestId === currentSDKRequestID.current) { + dispatch( + setSwapQuote({ + outputAmount: toValueBigInt, + outputAmountString: commify( + formatBigIntToString( + toValueBigInt, + swapToToken.decimals[swapChainId], + 8 + ) + ), + routerAddress, + allowance: BigInt(allowance.toString()), + exchangeRate: calculateExchangeRate( + stringToBigInt( + swapFromValue, + swapFromToken.decimals[swapChainId] + ), + swapFromToken.decimals[swapChainId], + toValueBigInt, + swapToToken.decimals[swapChainId] + ), + delta: toValueBigInt, + quote: newOriginQuery, + }) + ) + + dispatch(setIsLoading(false)) + toast.dismiss(quoteToast) + const message = `Route found for swapping ${swapFromValue} ${swapFromToken.symbol} on ${CHAINS_BY_ID[swapChainId]?.name} to ${swapToToken.symbol}` + console.log(message) + quoteToast = toast(message, { duration: 3000 }) + } + } catch (err) { + console.log(err) + if (thisRequestId === currentSDKRequestID.current) { + toast.dismiss(quoteToast) + let message + if (!swapChainId) { + message = 'Please select an origin chain' + } else if (!swapFromToken) { + message = 'Please select an origin token' + } else if (!swapToToken) { + message = 'Please select a destination token' + } else { + message = `No route found for swapping ${swapFromValue} ${swapFromToken.symbol} on ${CHAINS_BY_ID[swapChainId]?.name} to ${swapToToken.symbol}` + } + console.log(message) + quoteToast = toast(message, { duration: 3000 }) + + dispatch(setSwapQuote(EMPTY_SWAP_QUOTE_ZERO)) + return + } + } finally { + if (thisRequestId === currentSDKRequestID.current) { + dispatch(setIsLoading(false)) + } + } + } + + const approveTxn = async () => { + try { + const tx = approveToken( + swapQuote?.routerAddress, + swapChainId, + swapFromToken?.addresses[swapChainId] + ).then(() => { + dispatch( + fetchAndStoreSingleTokenAllowance({ + routerAddress: swapQuote?.routerAddress as Address, + tokenAddress: swapFromToken?.addresses[swapChainId] as Address, + address: address, + chainId: swapChainId, + }) + ) + }) + + try { + await tx + setIsApproved(true) + } catch (error) { + return txErrorHandler(error) + } + } catch (error) { + return txErrorHandler(error) + } + } + + const executeSwap = async () => { + const currentChainName = CHAINS_BY_ID[swapChainId]?.name + + pendingPopup = toast( + `Initiating swap from ${swapFromToken.symbol} to ${swapToToken.symbol} on ${currentChainName}`, + { id: 'swap-in-progress-popup', duration: Infinity } + ) + segmentAnalyticsEvent(`[Swap] initiates swap`, { + address, + chainId: swapChainId, + swapFromToken: swapFromToken.symbol, + swapToToken: swapToToken.symbol, + inputAmount: swapFromValue, + expectedReceivedAmount: swapQuote.outputAmountString, + exchangeRate: swapQuote.exchangeRate, + }) + try { + const wallet = await getWalletClient({ + chainId: swapChainId, + }) + + const data = await synapseSDK.swap( + swapChainId, + address, + swapFromToken.addresses[swapChainId], + stringToBigInt(swapFromValue, swapFromToken.decimals[swapChainId]), + swapQuote.quote + ) + + const payload = + swapFromToken.addresses[swapChainId as keyof Token['addresses']] === + zeroAddress || + swapFromToken.addresses[swapChainId as keyof Token['addresses']] === '' + ? { + data: data.data, + to: data.to, + value: stringToBigInt( + swapFromValue, + swapFromToken.decimals[swapChainId] + ), + } + : data + + const tx = await wallet.sendTransaction(payload) + + const originChainName = CHAINS_BY_ID[swapChainId]?.name + pendingPopup = toast( + `Swapping ${swapFromToken.symbol} on ${originChainName} to ${swapToToken.symbol}`, + { id: 'swap-in-progress-popup', duration: Infinity } + ) + + try { + const successTx = await tx + + segmentAnalyticsEvent(`[Swap] swaps successfully`, { + address, + originChainId: swapChainId, + inputAmount: swapFromValue, + expectedReceivedAmount: swapQuote.outputAmountString, + exchangeRate: swapQuote.exchangeRate, + }) + + toast.dismiss(pendingPopup) + + const successToastContent = ( +
+
+ Successfully swapped from {swapFromToken.symbol} to{' '} + {swapToToken.symbol} on {currentChainName} +
+ +
+ ) + + successPopup = toast.success(successToastContent, { + id: 'swap-successful-popup', + duration: 10000, + }) + + dispatch(setSwapQuote(EMPTY_SWAP_QUOTE_ZERO)) + dispatch(updateSwapFromValue()) + return tx + } catch (error) { + toast.dismiss(pendingPopup) + console.log(`Transaction failed with error: ${error}`) + } + } catch (error) { + console.log(`Swap Execution failed with error: ${error}`) + toast.dismiss(pendingPopup) + txErrorHandler(error) + } + } + + const springClass = + '-mt-4 fixed z-50 w-full h-full bg-opacity-50 bg-[#343036]' return ( - -
- +
+
+ +
+ -
-
- + + + + + + + + + + + + + + + + + + + -
- {SWAPABLE_TOKENS[connectedChainId]?.length > 0 ? ( - +
+ - ) : ( - - )} +
- +
- +
) } -export default SwapPage +export default StateManagedSwap diff --git a/packages/synapse-interface/slices/bridge/reducer.ts b/packages/synapse-interface/slices/bridge/reducer.ts index 798cb57a14..ec6b367527 100644 --- a/packages/synapse-interface/slices/bridge/reducer.ts +++ b/packages/synapse-interface/slices/bridge/reducer.ts @@ -21,6 +21,7 @@ import { updatePendingBridgeTransaction, updatePendingBridgeTransactions, } from './actions' +import { findValidToken } from '@/utils/findValidToken' export interface BridgeState { fromChainId: number @@ -525,14 +526,3 @@ export const { } = bridgeSlice.actions export default bridgeSlice.reducer - -const findValidToken = ( - tokens: Token[], - routeSymbol: string, - swapableType: string -) => { - const matchingToken = tokens?.find((t) => t.routeSymbol === routeSymbol) - const swapableToken = tokens?.find((t) => t.swapableType === swapableType) - - return matchingToken ? matchingToken : swapableToken ? swapableToken : null -} diff --git a/packages/synapse-interface/slices/swap/hooks.ts b/packages/synapse-interface/slices/swap/hooks.ts new file mode 100644 index 0000000000..13168dc2f4 --- /dev/null +++ b/packages/synapse-interface/slices/swap/hooks.ts @@ -0,0 +1,6 @@ +import { RootState } from '@/store/store' +import { useAppSelector } from '@/store/hooks' + +export const useSwapState = (): RootState['swap'] => { + return useAppSelector((state) => state.swap) +} diff --git a/packages/synapse-interface/slices/swap/reducer.ts b/packages/synapse-interface/slices/swap/reducer.ts new file mode 100644 index 0000000000..13599a0a76 --- /dev/null +++ b/packages/synapse-interface/slices/swap/reducer.ts @@ -0,0 +1,295 @@ +import _ from 'lodash' +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +import { DAI, USDC } from '@/constants/tokens/bridgeable' +import { EMPTY_SWAP_QUOTE } from '@/constants/swap' +import { ETH as ETHEREUM } from '@/constants/chains/master' +import { getSwapPossibilities } from '@/utils/swapFinder/generateSwapPossibilities' +import { SwapQuote, Token } from '@/utils/types' +import { getSwapFromTokens } from '@/utils/swapFinder/getSwapFromTokens' +import { getSymbol } from '@/utils/getSymbol' +import { findTokenByRouteSymbol } from '@/utils/findTokenByRouteSymbol' +import { getSwapToTokens } from '@/utils/swapFinder/getSwapToTokens' +import { getSwapFromChainIds } from '@/utils/swapFinder/getSwapFromChainIds' +import { findValidToken } from '@/utils/findValidToken' +import { flattenPausedTokens } from '@/utils/flattenPausedTokens' + +export interface SwapState { + swapChainId: number + swapFromToken: Token + swapToToken: Token + swapFromChainIds: number[] + swapFromTokens: Token[] + swapToTokens: Token[] + + swapFromValue: string + swapQuote: SwapQuote + isLoading: boolean +} + +const { fromChainId, fromToken, toToken, fromChainIds, fromTokens, toTokens } = + getSwapPossibilities({ + fromChainId: ETHEREUM.id, + fromToken: USDC, + toChainId: ETHEREUM.id, + toToken: DAI, + }) + +export const initialState: SwapState = { + swapChainId: fromChainId, + swapFromToken: fromToken, + swapToToken: toToken, + swapFromChainIds: fromChainIds, + swapFromTokens: fromTokens, + swapToTokens: toTokens, + + swapFromValue: '', + swapQuote: EMPTY_SWAP_QUOTE, + isLoading: false, +} + +export const swapSlice = createSlice({ + name: 'swap', + initialState, + reducers: { + setIsLoading: (state, action: PayloadAction) => { + state.isLoading = action.payload + }, + setSwapChainId: (state, action: PayloadAction) => { + const incomingFromChainId = action.payload + + const validFromTokens = _( + getSwapFromTokens({ + fromChainId: incomingFromChainId ?? null, + fromTokenRouteSymbol: state.swapFromToken?.routeSymbol ?? null, + toChainId: incomingFromChainId ?? null, + toTokenRouteSymbol: null, + }) + ) + .difference(flattenPausedTokens()) + ?.map(getSymbol) + .map((s) => findTokenByRouteSymbol(s)) + .filter(Boolean) + .value() + + const validToTokens = _( + getSwapToTokens({ + fromChainId: incomingFromChainId ?? null, + fromTokenRouteSymbol: state.swapFromToken?.routeSymbol ?? null, + toChainId: incomingFromChainId ?? null, + toTokenRouteSymbol: null, + }) + ) + .difference(flattenPausedTokens()) + ?.map(getSymbol) + .map((s) => findTokenByRouteSymbol(s)) + .filter(Boolean) + .value() + + let validFromToken + let validToToken + + if ( + validFromTokens?.some( + (token) => token?.routeSymbol === state.swapFromToken?.routeSymbol + ) + ) { + validFromToken = state.swapFromToken + } else { + validFromToken = findValidToken( + validFromTokens, + state.swapToToken?.routeSymbol, + state.swapToToken?.swapableType + ) + } + + if ( + validToTokens?.some( + (token) => token?.routeSymbol === state.swapToToken?.routeSymbol + ) + ) { + validToToken = state.swapToToken + } else { + validToToken = findValidToken( + validToTokens, + state.swapFromToken?.routeSymbol, + state.swapFromToken?.swapableType + ) + } + + const { + fromChainId, + fromToken, + toToken, + fromChainIds, + fromTokens, + toTokens, + } = getSwapPossibilities({ + fromChainId: incomingFromChainId, + fromToken: validFromToken, + toChainId: incomingFromChainId, + toToken: validToToken, + }) + + state.swapChainId = fromChainId + state.swapFromToken = fromToken + state.swapToToken = toToken + state.swapFromChainIds = fromChainIds + state.swapFromTokens = fromTokens + state.swapToTokens = toTokens + }, + setSwapFromToken: (state, action: PayloadAction) => { + const incomingFromToken = action.payload + + const validFromChainIds = getSwapFromChainIds({ + fromChainId: state.swapChainId ?? null, + fromTokenRouteSymbol: incomingFromToken?.routeSymbol ?? null, + toChainId: null, + toTokenRouteSymbol: null, + }) + + const validToTokens = _( + getSwapToTokens({ + fromChainId: state.swapChainId ?? null, + fromTokenRouteSymbol: incomingFromToken?.routeSymbol ?? null, + toChainId: state.swapChainId ?? null, + toTokenRouteSymbol: null, + }) + ) + .difference(flattenPausedTokens()) + ?.map(getSymbol) + .map((s) => findTokenByRouteSymbol(s)) + .filter(Boolean) + .value() + + let validFromChainId + let validToToken + + if (validFromChainIds?.includes(state.swapChainId)) { + validFromChainId = state.swapChainId + } else { + validFromChainId = null + } + + if ( + validToTokens?.some( + (token) => token?.routeSymbol === state.swapToToken?.routeSymbol + ) + ) { + validToToken = state.swapToToken + } else { + validToToken = findValidToken( + validToTokens, + incomingFromToken?.routeSymbol, + incomingFromToken?.swapableType + ) + } + + const { + fromChainId, + fromToken, + toToken, + fromChainIds, + fromTokens, + toTokens, + } = getSwapPossibilities({ + fromChainId: validFromChainId, + fromToken: incomingFromToken, + toChainId: validFromChainId, + toToken: validToToken, + }) + + state.swapChainId = fromChainId + state.swapFromToken = fromToken + state.swapToToken = toToken + state.swapFromChainIds = fromChainIds + state.swapFromTokens = fromTokens + state.swapToTokens = toTokens + }, + setSwapToToken: (state, action: PayloadAction) => { + const incomingToToken = action.payload + + const validFromChainIds = getSwapFromChainIds({ + fromChainId: state.swapChainId ?? null, + fromTokenRouteSymbol: null, + toChainId: state.swapChainId ?? null, + toTokenRouteSymbol: incomingToToken?.routeSymbol ?? null, + }) + + const validFromTokens = _( + getSwapFromTokens({ + fromChainId: state.swapChainId ?? null, + fromTokenRouteSymbol: state.swapFromToken?.routeSymbol ?? null, + toChainId: state.swapChainId ?? null, + toTokenRouteSymbol: incomingToToken?.routeSymbol ?? null, + }) + ) + .difference(flattenPausedTokens()) + ?.map(getSymbol) + .map((s) => findTokenByRouteSymbol(s)) + .filter(Boolean) + .value() + + let validFromChainId + let validFromToken + + if (validFromChainIds?.includes(state.swapChainId)) { + validFromChainId = state.swapChainId + } else { + validFromChainId = null + } + + if ( + validFromTokens?.some( + (token) => token?.routeSymbol === state.swapFromToken?.routeSymbol + ) + ) { + validFromToken = state.swapFromToken + } else { + validFromToken = findValidToken( + validFromTokens, + incomingToToken?.routeSymbol, + incomingToToken?.swapableType + ) + } + + const { + fromChainId, + fromToken, + toToken, + fromChainIds, + fromTokens, + toTokens, + } = getSwapPossibilities({ + fromChainId: validFromChainId, + fromToken: validFromToken, + toChainId: validFromChainId, + toToken: incomingToToken, + }) + + state.swapChainId = fromChainId + state.swapFromToken = fromToken + state.swapToToken = toToken + state.swapFromChainIds = fromChainIds + state.swapFromTokens = fromTokens + state.swapToTokens = toTokens + }, + setSwapQuote: (state, action: PayloadAction) => { + state.swapQuote = action.payload + }, + updateSwapFromValue: (state, action: PayloadAction) => { + state.swapFromValue = action.payload + }, + }, +}) + +export const { + setSwapChainId, + setSwapFromToken, + setSwapToToken, + updateSwapFromValue, + setSwapQuote, + setIsLoading, +} = swapSlice.actions + +export default swapSlice.reducer diff --git a/packages/synapse-interface/slices/swapDisplaySlice.ts b/packages/synapse-interface/slices/swapDisplaySlice.ts new file mode 100644 index 0000000000..7984f94c97 --- /dev/null +++ b/packages/synapse-interface/slices/swapDisplaySlice.ts @@ -0,0 +1,40 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export interface SwapDisplayState { + showSwapChainListOverlay: boolean + showSwapFromTokenListOverlay: boolean + showSwapToTokenListOverlay: boolean +} + +const initialState: SwapDisplayState = { + showSwapChainListOverlay: false, + showSwapFromTokenListOverlay: false, + showSwapToTokenListOverlay: false, +} + +export const swapDisplaySlice = createSlice({ + name: 'swapDisplay', + initialState, + reducers: { + setShowSwapFromTokenListOverlay: ( + state, + action: PayloadAction + ) => { + state.showSwapFromTokenListOverlay = action.payload + }, + setShowSwapToTokenListOverlay: (state, action: PayloadAction) => { + state.showSwapToTokenListOverlay = action.payload + }, + setShowSwapChainListOverlay: (state, action: PayloadAction) => { + state.showSwapChainListOverlay = action.payload + }, + }, +}) + +export const { + setShowSwapChainListOverlay, + setShowSwapFromTokenListOverlay, + setShowSwapToTokenListOverlay, +} = swapDisplaySlice.actions + +export default swapDisplaySlice.reducer diff --git a/packages/synapse-interface/store/store.ts b/packages/synapse-interface/store/store.ts index 986fa041cd..0beaa7318e 100644 --- a/packages/synapse-interface/store/store.ts +++ b/packages/synapse-interface/store/store.ts @@ -9,6 +9,8 @@ import poolUserDataReducer from '@/slices/poolUserDataSlice' import poolDepositReducer from '@/slices/poolDepositSlice' import poolWithdrawReducer from '@/slices/poolWithdrawSlice' import portfolioReducer from '@/slices/portfolio/reducer' +import swapReducer from '@/slices/swap/reducer' +import swapDisplayReducer from '@/slices/swapDisplaySlice' import transactionsReducer from '@/slices/transactions/reducer' import { api } from '@/slices/api/slice' import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider' @@ -22,6 +24,8 @@ export const store = configureStore({ poolDeposit: poolDepositReducer, poolWithdraw: poolWithdrawReducer, portfolio: portfolioReducer, + swap: swapReducer, + swapDisplay: swapDisplayReducer, transactions: transactionsReducer, [api.reducerPath]: api.reducer, }, diff --git a/packages/synapse-interface/utils/bridgeWatcher.ts b/packages/synapse-interface/utils/bridgeWatcher.ts deleted file mode 100644 index ad8af31509..0000000000 --- a/packages/synapse-interface/utils/bridgeWatcher.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { JsonRpcProvider } from '@ethersproject/providers' -import { hexZeroPad } from '@ethersproject/bytes' -import { Contract } from '@ethersproject/contracts' -import { getAddress, isAddress } from '@ethersproject/address' -import { id } from '@ethersproject/hash' -import { toHexStr } from '@utils/toHexStr' -import { BridgeWatcherTx } from '@types' -import { GETLOGS_SIZE } from '@constants/bridgeWatcher' -import { TOKEN_HASH_MAP } from '@constants/tokens' -import * as CHAINS from '@constants/chains/master' -import { - SYN, - NUSD, - NETH, - FRAX, - SYNFRAX, - WBTC, - DOG, - LINK, - GOHM, - HIGH, - JUMP, - NFD, - NEWO, - VSTA, - GMX, - SDT, - UNIDX, - SFI, - H2O, - L2DAO, - PLS, - AGEUR, - NOTE, - USDC, - SUSD, - WETH, -} from '@constants/tokens/bridgeable' - -export const getTransactionReceipt = async ( - txHash: string, - provider: JsonRpcProvider -) => { - const receipt = await provider.getTransactionReceipt(txHash) - return receipt -} -export const getBlock = async ( - blockNumber: number, - provider: JsonRpcProvider -) => { - const block = await provider.getBlock(blockNumber) - return block -} -export const getLogs = async ( - currentBlock: number, - provider: JsonRpcProvider, - contract: Contract, - address: string -) => { - const filter = { - address: contract?.address, - topics: [null, hexZeroPad(address, 32)], - fromBlock: toHexStr(currentBlock - GETLOGS_SIZE), - toBlock: toHexStr(currentBlock), - } - try { - const logs = await provider.send('eth_getLogs', [filter]) - return logs - } catch (e) { - console.log('getLogs error', e) - return [] - } -} - -export const checkTxIn = (tx) => { - return tx?.chainId ? true : false -} - -export const generateBridgeTx = ( - isFrom, - address, - chainId, - parsedLog, - timestampObj, - txReceipt, - destinationAddress -): BridgeWatcherTx => { - const swapTokenAddr = getAddress(parsedLog.token) - - let tokenAddr - if (isFrom) { - if (txReceipt.logs[1].address === GMX.addresses[CHAINS.AVALANCHE.id]) { - tokenAddr = GMX.addresses[CHAINS.AVALANCHE.id] - } else { - tokenAddr = txReceipt.logs[0].address - } - } else { - if ( - [ - SYN, - LINK, - HIGH, - DOG, - JUMP, - FRAX, - NFD, - GOHM, - AGEUR, - H2O, - L2DAO, - PLS, - NEWO, - VSTA, - SFI, - SDT, - UNIDX, - GMX, - WBTC, - NOTE, - SUSD, - ] - .map((t) => t.addresses[chainId]) - .includes(swapTokenAddr) - ) { - tokenAddr = TOKEN_HASH_MAP[chainId][swapTokenAddr].addresses[chainId] - } else if (swapTokenAddr === SYNFRAX.addresses[chainId]) { - tokenAddr = FRAX.addresses[chainId] - } else if (swapTokenAddr === GMX.wrapperAddresses[chainId]) { - tokenAddr = GMX.addresses[chainId] - } else if (swapTokenAddr === NETH.addresses[chainId]) { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 2].address - } else if (swapTokenAddr === WETH.addresses[chainId]) { - if (chainId === CHAINS.ETH.id) { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 2].address - } else { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 1].address - } - } else if (swapTokenAddr === NUSD.addresses[chainId]) { - if (chainId === CHAINS.ETH.id) { - if (parsedLog.event === 'TokenWithdraw') { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 1].address - } else { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 2].address - } - } else if (chainId === CHAINS.POLYGON.id) { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 3].address - } else { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 2].address - } - } else if ( - !isFrom && - swapTokenAddr === USDC.addresses[chainId] && - [CHAINS.ARBITRUM.id, CHAINS.ETH.id, CHAINS.AVALANCHE.id].includes(chainId) - ) { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 3].address - } else { - tokenAddr = txReceipt.logs[txReceipt.logs.length - 2].address - } - } - const token = TOKEN_HASH_MAP[chainId][tokenAddr] - - let inputTokenAmount - if ( - getAddress(txReceipt.logs[0]?.address) === - GMX.addresses[CHAINS.ARBITRUM.id] || - getAddress(txReceipt.logs[1]?.address) === - GMX.addresses[CHAINS.AVALANCHE.id] - ) { - inputTokenAmount = txReceipt.logs[1].data - } else { - inputTokenAmount = txReceipt.logs[0].data - } - - return { - isFrom, - amount: isFrom ? inputTokenAmount : parsedLog.amount, - timestamp: timestampObj.timestamp, - blockNumber: parsedLog.blockNumber, - chainId, - address, - txHash: txReceipt.transactionHash, - txReceipt, - token, - kappa: parsedLog.requestID - ? parsedLog.requestID - : removePrefix(id(parsedLog.transactionHash)), - toChainId: isFrom ? Number(parsedLog.chainId.toString()) : chainId, - toAddress: isAddress(destinationAddress) ? destinationAddress : address, - contractEmittedFrom: parsedLog.contractEmittedFrom, - } -} - -export const getHighestBlock = async ( - chainId: number, - provider: JsonRpcProvider -) => { - const highestBlock = await provider.getBlockNumber() - return highestBlock -} - -const removePrefix = (str: string): string => { - if (str.startsWith('0x')) { - return str.substring(2) - } - return str -} diff --git a/packages/synapse-interface/utils/findTokenByAddressAndChainId.ts b/packages/synapse-interface/utils/findTokenByAddressAndChainId.ts new file mode 100644 index 0000000000..d6f18f5713 --- /dev/null +++ b/packages/synapse-interface/utils/findTokenByAddressAndChainId.ts @@ -0,0 +1,21 @@ +import { Address } from 'viem' + +import { ALL_TOKENS } from '@/constants/tokens/master' + +export const findTokenByAddressAndChain = ( + address: Address | string, + chainId: string +) => { + for (const [, token] of Object.entries(ALL_TOKENS)) { + const chainAddresses = token.addresses + if ( + chainAddresses && + Object.keys(chainAddresses).length > 0 && + chainAddresses[chainId] && + chainAddresses[chainId].toLowerCase() === address.toLowerCase() + ) { + return token + } + } + return null +} diff --git a/packages/synapse-interface/utils/findValidToken.ts b/packages/synapse-interface/utils/findValidToken.ts new file mode 100644 index 0000000000..12c6fe6d25 --- /dev/null +++ b/packages/synapse-interface/utils/findValidToken.ts @@ -0,0 +1,12 @@ +import { Token } from './types' + +export const findValidToken = ( + tokens: Token[], + routeSymbol: string, + swapableType: string +): Token | null => { + const matchingToken = tokens?.find((t) => t.routeSymbol === routeSymbol) + const swapableToken = tokens?.find((t) => t.swapableType === swapableType) + + return matchingToken ? matchingToken : swapableToken ? swapableToken : null +} diff --git a/packages/synapse-interface/utils/generateChainIdAddressMapping.ts b/packages/synapse-interface/utils/generateChainIdAddressMapping.ts index c698d541d8..6788bbd479 100644 --- a/packages/synapse-interface/utils/generateChainIdAddressMapping.ts +++ b/packages/synapse-interface/utils/generateChainIdAddressMapping.ts @@ -1,7 +1,9 @@ import { zeroAddress } from 'viem' import _ from 'lodash' -import * as BRIDGEABLE from '@constants/tokens/bridgeable' -import { BRIDGE_MAP } from '@constants/bridgeMap' + +import * as BRIDGEABLE from '@/constants/tokens/bridgeable' +import { BRIDGE_MAP } from '@/constants/bridgeMap' +import { ETHEREUM_ADDRESS } from '@/constants' export const generateChainIdAddressMapping = (routeSymbol: string) => { const result: { [key: number]: string } = {} @@ -10,9 +12,7 @@ export const generateChainIdAddressMapping = (routeSymbol: string) => { Object.entries(tokens).forEach(([address, token]) => { if (token.symbol === routeSymbol) { result[Number(chainId)] = - address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' - ? zeroAddress - : address + address === ETHEREUM_ADDRESS ? zeroAddress : address } }) }) diff --git a/packages/synapse-interface/utils/getSymbol.ts b/packages/synapse-interface/utils/getSymbol.ts new file mode 100644 index 0000000000..ff6057d86e --- /dev/null +++ b/packages/synapse-interface/utils/getSymbol.ts @@ -0,0 +1,3 @@ +export const getSymbol = (tokenAndChainId: string): string => { + return tokenAndChainId.split('-')[0] +} diff --git a/packages/synapse-interface/utils/swapFinder/generateSwapPossibilities.ts b/packages/synapse-interface/utils/swapFinder/generateSwapPossibilities.ts new file mode 100644 index 0000000000..0c80ee0c7c --- /dev/null +++ b/packages/synapse-interface/utils/swapFinder/generateSwapPossibilities.ts @@ -0,0 +1,82 @@ +import _ from 'lodash' + +import { flattenPausedTokens } from '../flattenPausedTokens' +import { Token } from '../types' +import { getSwapFromChainIds } from './getSwapFromChainIds' +import { getSwapFromTokens } from './getSwapFromTokens' +import { getSwapToTokens } from './getSwapToTokens' +import { PAUSED_TO_CHAIN_IDS } from '@/constants/chains' +import { findTokenByRouteSymbol } from '../findTokenByRouteSymbol' +import { getSymbol } from '@/utils/getSymbol' + +export interface RouteQueryFields { + fromChainId?: number + fromTokenRouteSymbol?: string + toChainId?: number + toTokenRouteSymbol?: string +} + +export const getSwapPossibilities = ({ + fromChainId, + fromToken, + toChainId, + toToken, +}: { + fromChainId?: number + fromToken?: Token + toChainId?: number + toToken?: Token +}) => { + const fromTokenRouteSymbol = fromToken && fromToken.routeSymbol + const toTokenRouteSymbol = toToken && toToken.routeSymbol + + const fromChainIds: number[] = getSwapFromChainIds({ + fromChainId, + fromTokenRouteSymbol, + toChainId, + toTokenRouteSymbol, + }) + + const fromTokens: Token[] = _( + getSwapFromTokens({ + fromChainId, + fromTokenRouteSymbol, + toChainId, + toTokenRouteSymbol, + }) + ) + .difference(flattenPausedTokens()) + .map(getSymbol) + .uniq() + .map((symbol) => findTokenByRouteSymbol(symbol)) + .compact() + .value() + + const toTokens: Token[] = _( + getSwapToTokens({ + fromChainId, + fromTokenRouteSymbol, + toChainId, + toTokenRouteSymbol, + }) + ) + .difference(flattenPausedTokens()) + .filter((token) => { + return !PAUSED_TO_CHAIN_IDS.some((value) => token.endsWith(`-${value}`)) + }) + .map(getSymbol) + .uniq() + .map((symbol) => findTokenByRouteSymbol(symbol)) + .compact() + .value() + + return { + fromChainId, + fromToken, + toChainId, + toToken, + fromChainIds, + fromTokens, + toTokens, + } +} diff --git a/packages/synapse-interface/utils/swapFinder/getSwapFromChainIds.ts b/packages/synapse-interface/utils/swapFinder/getSwapFromChainIds.ts new file mode 100644 index 0000000000..8012eb044e --- /dev/null +++ b/packages/synapse-interface/utils/swapFinder/getSwapFromChainIds.ts @@ -0,0 +1,283 @@ +import _ from 'lodash' + +import { + EXISTING_SWAP_ROUTES, + SWAP_CHAIN_IDS, +} from '@/constants/existingSwapRoutes' +import { RouteQueryFields } from './generateSwapPossibilities' +import { getTokenAndChainId } from './getTokenAndChainId' + +export const getAllFromChainIds = () => SWAP_CHAIN_IDS + +export const getSwapFromChainIds = ({ + fromChainId, + fromTokenRouteSymbol, + toChainId, + toTokenRouteSymbol, +}: RouteQueryFields) => { + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .keys() + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .keys() + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .keys() + .filter((key) => { + const { symbol } = getTokenAndChainId(key) + return symbol === fromTokenRouteSymbol + }) + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .keys() + .filter((key) => { + const { symbol } = getTokenAndChainId(key) + return symbol === fromTokenRouteSymbol + }) + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .entries() + .filter(([_key, values]) => + values.some((v) => v.endsWith(`-${toChainId}`)) + ) + .map(([key]) => getTokenAndChainId(key).chainId) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .entries() + .filter(([_key, values]) => + values.some((v) => v.endsWith(`-${toChainId}`)) + ) + .map(([key]) => getTokenAndChainId(key).chainId) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .entries() + .filter(([key, _values]) => key.startsWith(`${fromTokenRouteSymbol}-`)) + .filter(([_key, values]) => + values.some((v) => getTokenAndChainId(v).chainId === toChainId) + ) + .map(([key, _values]) => key) + .filter((token) => token.endsWith(`-${toChainId}`)) + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((_values, key) => key.startsWith(`${fromTokenRouteSymbol}-`)) + .keys() + .map((token) => getTokenAndChainId(token).chainId) + .filter((chainId) => chainId !== toChainId) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .chain() + .filter((values, _key) => { + return values.some((v) => { + const { symbol } = getTokenAndChainId(v) + return symbol === toTokenRouteSymbol + }) + }) + .flatten() + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .chain() + .filter((values, _key) => { + return values.some((v) => { + const { symbol } = getTokenAndChainId(v) + return symbol === toTokenRouteSymbol + }) + }) + .flatten() + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .entries() + .filter(([key, _values]) => key.startsWith(`${fromTokenRouteSymbol}-`)) + .map(([key, _values]) => key) + .flatten() + .filter((token) => token.startsWith(`${toTokenRouteSymbol}-`)) + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .chain() + .filter((values, key) => { + return ( + values.some((v) => { + const { symbol } = getTokenAndChainId(v) + return symbol === toTokenRouteSymbol + }) && key.startsWith(`${fromTokenRouteSymbol}-`) + ) + }) + .flatten() + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => { + return _.includes(values, `${toTokenRouteSymbol}-${toChainId}`) + }) + .keys() + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => { + return _.includes(values, `${toTokenRouteSymbol}-${toChainId}`) + }) + .keys() + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => { + return _.includes(values, `${toTokenRouteSymbol}-${toChainId}`) + }) + .keys() + .filter((k) => k.startsWith(`${fromTokenRouteSymbol}-`)) + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } + + if (fromChainId && fromTokenRouteSymbol && toChainId && toTokenRouteSymbol) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => { + return _.includes(values, `${toTokenRouteSymbol}-${toChainId}`) + }) + .keys() + .filter((k) => k.startsWith(`${fromTokenRouteSymbol}-`)) + .map((token) => getTokenAndChainId(token).chainId) + .uniq() + .value() + } +} diff --git a/packages/synapse-interface/utils/swapFinder/getSwapFromTokens.ts b/packages/synapse-interface/utils/swapFinder/getSwapFromTokens.ts new file mode 100644 index 0000000000..fc755330ec --- /dev/null +++ b/packages/synapse-interface/utils/swapFinder/getSwapFromTokens.ts @@ -0,0 +1,235 @@ +import _ from 'lodash' + +import { EXISTING_SWAP_ROUTES } from '@/constants/existingSwapRoutes' +import { RouteQueryFields } from './generateSwapPossibilities' +import { getTokenAndChainId } from './getTokenAndChainId' + +export const getSwapFromTokens = ({ + fromChainId, + fromTokenRouteSymbol, + toChainId, + toTokenRouteSymbol, +}: RouteQueryFields) => { + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES).keys().uniq().value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .keys() + .filter((token) => token.endsWith(`-${fromChainId}`)) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES).keys().uniq().value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .keys() + .filter((key) => getTokenAndChainId(key).chainId === fromChainId) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => values.some((v) => v.endsWith(`-${toChainId}`))) + .keys() + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => values.some((v) => v.endsWith(`-${toChainId}`))) + .keys() + .filter((key) => key.endsWith(`-${fromChainId}`)) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .entries() + .filter(([key, _values]) => key.startsWith(`${fromTokenRouteSymbol}-`)) + .filter(([_key, values]) => + values.some((v) => getTokenAndChainId(v).chainId === toChainId) + ) + .map(([key, _values]) => key) + .filter((token) => token.endsWith(`-${toChainId}`)) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => values.some((v) => v.endsWith(`-${toChainId}`))) + .pickBy((_values, key) => key.endsWith(`-${fromChainId}`)) + .keys() + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .chain() + .filter((values, _key) => + values.some((v) => getTokenAndChainId(v).symbol === toTokenRouteSymbol) + ) + .flatten() + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => + values.some((v) => getTokenAndChainId(v).symbol === toTokenRouteSymbol) + ) + .keys() + .filter((k) => k.endsWith(`-${fromChainId}`)) + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => + values.some((v) => v.startsWith(`${toTokenRouteSymbol}-`)) + ) + .keys() + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .chain() + .filter((values, _key) => { + return values.some((v) => { + const { symbol } = getTokenAndChainId(v) + return symbol === toTokenRouteSymbol + }) + }) + .flatten() + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => { + return _.includes(values, `${toTokenRouteSymbol}-${toChainId}`) + }) + .keys() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((_values, key) => key.endsWith(`-${fromChainId}`)) + .pickBy((values, _key) => { + return _.includes(values, `${toTokenRouteSymbol}-${toChainId}`) + }) + .keys() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => { + return _.includes(values, `${toTokenRouteSymbol}-${toChainId}`) + }) + .keys() + .value() + } + + if (fromChainId && fromTokenRouteSymbol && toChainId && toTokenRouteSymbol) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((values, _key) => + values.some((v) => { + return v === `${toTokenRouteSymbol}-${toChainId}` + }) + ) + .keys() + .filter((key) => key.endsWith(`-${fromChainId}`)) + .value() + } +} diff --git a/packages/synapse-interface/utils/swapFinder/getSwapToTokens.ts b/packages/synapse-interface/utils/swapFinder/getSwapToTokens.ts new file mode 100644 index 0000000000..cc773f7078 --- /dev/null +++ b/packages/synapse-interface/utils/swapFinder/getSwapToTokens.ts @@ -0,0 +1,213 @@ +import _ from 'lodash' + +import { EXISTING_SWAP_ROUTES } from '@/constants/existingSwapRoutes' +import { RouteQueryFields } from './generateSwapPossibilities' + +export const getSwapToTokens = ({ + fromChainId, + fromTokenRouteSymbol, + toChainId, + toTokenRouteSymbol, +}: RouteQueryFields) => { + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES).values().flatten().uniq().value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((_values, key) => key.endsWith(`-${fromChainId}`)) + .values() + .flatten() + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((_values, key) => key.startsWith(`${fromTokenRouteSymbol}-`)) + .values() + .flatten() + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol === null + ) { + return EXISTING_SWAP_ROUTES[`${fromTokenRouteSymbol}-${fromChainId}`] + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .values() + .flatten() + .filter((token) => token.endsWith(`-${toChainId}`)) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((_values, key) => key.endsWith(`-${fromChainId}`)) + .values() + .flatten() + .filter((value) => value.endsWith(`-${toChainId}`)) + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol === null + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((_values, key) => key.startsWith(`${fromTokenRouteSymbol}-`)) + .values() + .flatten() + .filter((token) => token.endsWith(`-${toChainId}`)) + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol === null + ) { + return EXISTING_SWAP_ROUTES[ + `${fromTokenRouteSymbol}-${fromChainId}` + ]?.filter((token) => token.endsWith(`-${toChainId}`)) + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES).values().flatten().uniq().value() + } + + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((_values, key) => key.endsWith(`-${fromChainId}`)) + .values() + .flatten() + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .pickBy((_values, key) => key.startsWith(`${fromTokenRouteSymbol}-`)) + .values() + .flatten() + .uniq() + .value() + } + + if ( + fromChainId && + fromTokenRouteSymbol && + toChainId === null && + toTokenRouteSymbol + ) { + return EXISTING_SWAP_ROUTES[`${fromTokenRouteSymbol}-${fromChainId}`] + } + + if ( + fromChainId === null && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .mapValues((values) => + values.filter((token) => token === `${toTokenRouteSymbol}-${toChainId}`) + ) + .values() + .flatten() + .uniq() + .value() + } + if ( + fromChainId && + fromTokenRouteSymbol === null && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .mapValues((values) => + values.filter((token) => token === `${toTokenRouteSymbol}-${toChainId}`) + ) + .pickBy((_values, key) => key.endsWith(`-${fromChainId}`)) + .values() + .flatten() + .uniq() + .value() + } + + if ( + fromChainId === null && + fromTokenRouteSymbol && + toChainId && + toTokenRouteSymbol + ) { + return _(EXISTING_SWAP_ROUTES) + .mapValues((values) => + values.filter((token) => token === `${toTokenRouteSymbol}-${toChainId}`) + ) + .pickBy((_values, key) => key.startsWith(`${fromTokenRouteSymbol}-`)) + .values() + .flatten() + .uniq() + .value() + } + + if (fromChainId && fromTokenRouteSymbol && toChainId && toTokenRouteSymbol) { + return EXISTING_SWAP_ROUTES[ + `${fromTokenRouteSymbol}-${fromChainId}` + ]?.filter((value) => value.endsWith(`-${toChainId}`)) + } +} diff --git a/packages/synapse-interface/utils/swapFinder/getTokenAndChainId.ts b/packages/synapse-interface/utils/swapFinder/getTokenAndChainId.ts new file mode 100644 index 0000000000..2be2f57df9 --- /dev/null +++ b/packages/synapse-interface/utils/swapFinder/getTokenAndChainId.ts @@ -0,0 +1,5 @@ +export const getTokenAndChainId = (tokenAndChainId: string) => { + const [symbol, chainId] = tokenAndChainId.split('-') + + return { symbol, chainId: Number(chainId) } +} From 699117dc02b377ed4e1515af6ff0cf6a578d2f3e Mon Sep 17 00:00:00 2001 From: abtestingalpha Date: Tue, 26 Sep 2023 00:09:46 +0000 Subject: [PATCH 8/8] Publish - @synapsecns/synapse-interface@0.1.134 --- packages/synapse-interface/CHANGELOG.md | 8 ++++++++ packages/synapse-interface/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/synapse-interface/CHANGELOG.md b/packages/synapse-interface/CHANGELOG.md index ecd81fdc17..e7e5afd7ac 100644 --- a/packages/synapse-interface/CHANGELOG.md +++ b/packages/synapse-interface/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.134](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.1.133...@synapsecns/synapse-interface@0.1.134) (2023-09-26) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + ## [0.1.133](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.1.132...@synapsecns/synapse-interface@0.1.133) (2023-09-25) **Note:** Version bump only for package @synapsecns/synapse-interface diff --git a/packages/synapse-interface/package.json b/packages/synapse-interface/package.json index 3900c3d328..3c4df98ab0 100644 --- a/packages/synapse-interface/package.json +++ b/packages/synapse-interface/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/synapse-interface", - "version": "0.1.133", + "version": "0.1.134", "private": true, "engines": { "node": ">=16.0.0"