diff --git a/.github/workflows/clean-up-pr-caches.yml b/.github/workflows/clean-up-pr.yml similarity index 79% rename from .github/workflows/clean-up-pr-caches.yml rename to .github/workflows/clean-up-pr.yml index b31bacc14a..003bb35714 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,10 @@ 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}} + soft_fail: true 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..5252a0f98d 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' + # 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:latest - id: filter_go_deps - 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 + id: filter_go 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,49 @@ 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' && format('refs/heads/{0}', github.event.repository.default_branch) != github.ref }} 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 + continue-on-error: true + 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 +401,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 +419,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 +427,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 +469,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: 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 }} + + 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: ${{ 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.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 +526,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 +550,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 +559,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 +632,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..816e2125b4 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 @@ -98,7 +98,6 @@ jobs: - uses: docker://ghcr.io/synapsecns/sanguine/git-changes-action:latest 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. diff --git a/packages/synapse-interface/CHANGELOG.md b/packages/synapse-interface/CHANGELOG.md index c12a86fd8f..e7e5afd7ac 100644 --- a/packages/synapse-interface/CHANGELOG.md +++ b/packages/synapse-interface/CHANGELOG.md @@ -3,6 +3,22 @@ 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 + + + + + ## [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/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 ( -
-
- Need help? Read - - - this guide{' '} - - - -
- -
- - Explorer -
-
-
- ) -} 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/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/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/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', 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/package.json b/packages/synapse-interface/package.json index 14535c1c28..e8c72c8078 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.134", "private": true, "engines": { "node": ">=16.0.0" 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) } +}