Repository: coinbase/wallet-mobile-sdk Branch: main Commit: 87da8726e82e Files: 273 Total size: 467.0 KB Directory structure: gitextract_l82khy5j/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── publish.yml │ ├── run_tests.yml │ └── version_update.yml ├── .gitignore ├── CODEOWNERS ├── CoinbaseWalletSDK.podspec ├── Gemfile ├── LICENSE ├── Package.swift ├── README.md ├── android/ │ ├── README.md │ ├── build.gradle │ ├── example/ │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── coinbase/ │ │ │ └── android/ │ │ │ └── beta/ │ │ │ ├── ActionsManager.kt │ │ │ ├── MainActivity.kt │ │ │ └── SecondActivity.java │ │ └── res/ │ │ ├── drawable/ │ │ │ └── ic_launcher_background.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ ├── values-night/ │ │ │ └── themes.xml │ │ └── xml/ │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ ├── gradle/ │ │ ├── libs.versions.toml │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── scripts/ │ │ └── publish-root.gradle │ ├── settings.gradle │ └── walletsdk/ │ ├── .editorconfig │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── coinbase/ │ │ └── android/ │ │ └── nativesdk/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── coinbase/ │ │ └── android/ │ │ └── nativesdk/ │ │ ├── CoinbaseWalletSDK.kt │ │ ├── CoinbaseWalletSDKError.kt │ │ ├── OpenIntentCallback.kt │ │ ├── key/ │ │ │ ├── KeyManager.kt │ │ │ ├── KeyStore.kt │ │ │ └── PublicKeySerializer.kt │ │ ├── message/ │ │ │ ├── Cipher.kt │ │ │ ├── DateSerializer.kt │ │ │ ├── JSON.kt │ │ │ ├── Message.kt │ │ │ ├── MessageConverter.kt │ │ │ ├── request/ │ │ │ │ ├── Account.kt │ │ │ │ ├── Action.kt │ │ │ │ ├── EncryptedRequestMessage.kt │ │ │ │ ├── RequestConverter.kt │ │ │ │ ├── UnencryptedRequestMessage.kt │ │ │ │ └── Web3JsonRPC.kt │ │ │ └── response/ │ │ │ ├── ActionResult.kt │ │ │ ├── EncryptedResponseMessage.kt │ │ │ ├── Response.kt │ │ │ ├── ResponseCallback.kt │ │ │ ├── ResponseConverter.kt │ │ │ └── UnencryptedResponseMessage.kt │ │ └── task/ │ │ ├── Task.kt │ │ └── TaskManager.kt │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── coinbase/ │ │ └── android/ │ │ └── nativesdk/ │ │ ├── ExampleUnitTest.kt │ │ ├── helper/ │ │ │ └── InputStreamExtensions.kt │ │ └── message/ │ │ └── request/ │ │ └── Web3JsonRPCTest.kt │ └── resources/ │ ├── add_chain.json │ ├── personal_sign.json │ ├── send_transaction.json │ ├── sign_transaction.json │ ├── sign_typed_data_v3.json │ ├── sign_typed_data_v4.json │ └── watch_asset.json ├── docs/ │ ├── .gitignore │ ├── babel.config.js │ ├── docs/ │ │ ├── client-sdk/ │ │ │ ├── android-api-reference.md │ │ │ ├── android-establishing-a-connection.md │ │ │ ├── android-install.md │ │ │ ├── android-making-requests.md │ │ │ ├── android-setup.md │ │ │ ├── ios-api-reference.md │ │ │ ├── ios-establishing-a-connection.md │ │ │ ├── ios-install.md │ │ │ ├── ios-making-requests.md │ │ │ ├── ios-setup.md │ │ │ └── mobile-sdk-overview.md │ │ ├── spec/ │ │ │ ├── batch.md │ │ │ ├── encryption.md │ │ │ ├── handshake.md │ │ │ ├── messages-example.md │ │ │ ├── messages-request.md │ │ │ ├── messages-response.md │ │ │ ├── messages.md │ │ │ ├── multi-chain.md │ │ │ ├── network.md │ │ │ └── verification.md │ │ └── spec-overview.md │ ├── docusaurus.config.js │ ├── package.json │ ├── sidebars.js │ ├── spec.md │ ├── src/ │ │ ├── components/ │ │ │ └── HomepageFeatures/ │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── css/ │ │ │ └── custom.css │ │ └── pages/ │ │ ├── index.module.css │ │ └── index.tsx │ ├── static/ │ │ └── .nojekyll │ └── tsconfig.json ├── flutter/ │ ├── .gitignore │ ├── .metadata │ ├── .pubignore │ ├── .vscode/ │ │ └── settings.json │ ├── CHANGELOG.md │ ├── README.md │ ├── analysis_options.yaml │ ├── android/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ └── gradle-wrapper.properties │ │ ├── settings.gradle │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── coinbase/ │ │ └── flutter/ │ │ └── wallet_sdk/ │ │ └── CoinbaseWalletSdkFlutterPlugin.kt │ ├── example/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── android/ │ │ │ ├── .gitignore │ │ │ ├── app/ │ │ │ │ ├── build.gradle │ │ │ │ └── src/ │ │ │ │ ├── debug/ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── main/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ ├── kotlin/ │ │ │ │ │ │ └── xyz/ │ │ │ │ │ │ └── tribes/ │ │ │ │ │ │ └── coinbase/ │ │ │ │ │ │ └── coinbase_wallet_sdk_flutter_example/ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ └── res/ │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21/ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values/ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night/ │ │ │ │ │ └── styles.xml │ │ │ │ └── profile/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── build.gradle │ │ │ ├── gradle/ │ │ │ │ └── wrapper/ │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradle.properties │ │ │ └── settings.gradle │ │ ├── ios/ │ │ │ ├── .gitignore │ │ │ ├── Flutter/ │ │ │ │ ├── AppFrameworkInfo.plist │ │ │ │ ├── Debug.xcconfig │ │ │ │ └── Release.xcconfig │ │ │ ├── Podfile │ │ │ ├── Runner/ │ │ │ │ ├── AppDelegate.swift │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── README.md │ │ │ │ ├── Base.lproj/ │ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ │ └── Main.storyboard │ │ │ │ ├── Info.plist │ │ │ │ └── Runner-Bridging-Header.h │ │ │ └── Runner.xcodeproj/ │ │ │ └── project.pbxproj │ │ ├── lib/ │ │ │ └── main.dart │ │ ├── pubspec.yaml │ │ └── test/ │ │ └── widget_test.dart │ ├── ios/ │ │ ├── .gitignore │ │ ├── Assets/ │ │ │ └── .gitkeep │ │ ├── Classes/ │ │ │ ├── CoinbaseWalletSdkFlutterPlugin.h │ │ │ ├── CoinbaseWalletSdkFlutterPlugin.m │ │ │ └── SwiftCoinbaseWalletSdkFlutterPlugin.swift │ │ └── coinbase_wallet_sdk.podspec │ ├── lib/ │ │ ├── account.dart │ │ ├── action.dart │ │ ├── coinbase_wallet_sdk.dart │ │ ├── coinbase_wallet_sdk_method_channel.dart │ │ ├── coinbase_wallet_sdk_platform_interface.dart │ │ ├── configuration.dart │ │ ├── eth_web3_rpc.dart │ │ ├── request.dart │ │ └── return_value.dart │ ├── pubspec.yaml │ └── test/ │ ├── coinbase_wallet_sdk_method_channel_test.dart │ └── coinbase_wallet_sdk_test.dart ├── ios/ │ ├── CoinbaseWalletSDK/ │ │ ├── CoinbaseWalletSDK.swift │ │ ├── Error.swift │ │ ├── Host/ │ │ │ ├── CoinbaseWalletHostSDK.swift │ │ │ └── ResponseMessage+init.swift │ │ ├── Key/ │ │ │ ├── Key+RawRepresentable.swift │ │ │ ├── KeyManager.swift │ │ │ ├── KeyStorage.swift │ │ │ └── KeyStorageItem.swift │ │ ├── Message/ │ │ │ ├── Cipher.swift │ │ │ ├── EncryptedMessage.swift │ │ │ ├── JSONString.swift │ │ │ ├── Message.swift │ │ │ ├── MessageConverter.swift │ │ │ ├── Request/ │ │ │ │ ├── Account.swift │ │ │ │ ├── Action.swift │ │ │ │ ├── EncryptedRequestContent.swift │ │ │ │ ├── Request.swift │ │ │ │ ├── RequestMessage.swift │ │ │ │ └── Web3JSONRPC.swift │ │ │ ├── Response/ │ │ │ │ ├── ActionResult.swift │ │ │ │ ├── EncryptedResponseContent.swift │ │ │ │ └── ResponseMessage.swift │ │ │ └── URL+extension.swift │ │ ├── Resources/ │ │ │ └── CoinbaseWalletSDK+version.swift │ │ ├── Task/ │ │ │ ├── Task.swift │ │ │ └── TaskManager.swift │ │ └── Test/ │ │ └── ExampleTest.swift │ ├── README.md │ └── example/ │ ├── Podfile │ ├── README.md │ ├── SampleClient/ │ │ ├── AppDelegate.swift │ │ ├── Base.lproj/ │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── SampleClient.xcodeproj/ │ │ └── project.pbxproj │ ├── SampleWallet/ │ │ ├── AppDelegate.swift │ │ ├── Base.lproj/ │ │ │ └── Main.storyboard │ │ └── Info.plist │ └── SampleWallet.xcodeproj/ │ └── project.pbxproj └── react-native/ ├── .eslintrc.js ├── .npmignore ├── README.md ├── android/ │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── expo/ │ └── modules/ │ └── coinbasewalletsdkexpo/ │ ├── ActivityLifecycleListener.kt │ ├── CoinbaseWalletSDKModule.kt │ ├── CoinbaseWalletSDKPackage.kt │ └── records/ │ ├── AccountRecord.kt │ ├── ActionRecord.kt │ ├── ActionResultRecord.kt │ ├── ConfigParamsRecord.kt │ ├── HandshakeParamsRecord.kt │ └── RequestParamsRecord.kt ├── example/ │ ├── .buckconfig │ ├── .bundle/ │ │ └── config │ ├── .eslintrc.js │ ├── .flowconfig │ ├── .gitignore │ ├── .prettierrc.js │ ├── .watchmanconfig │ ├── App.js │ ├── __tests__/ │ │ └── App-test.js │ ├── _node-version │ ├── app.json │ ├── babel.config.js │ ├── index.js │ ├── metro.config.js │ └── package.json ├── expo-module.config.json ├── ios/ │ ├── CoinbaseWalletSDKExpo.podspec │ ├── CoinbaseWalletSDKModule.swift │ └── Records/ │ ├── AccountRecord.swift │ ├── ActionRecord.swift │ ├── ActionResultRecord.swift │ ├── ConfigParamsRecord.swift │ ├── HandshakeParamsRecord.swift │ └── RequestParamsRecord.swift ├── package.json ├── src/ │ ├── CoinbaseWalletSDK.ts │ ├── CoinbaseWalletSDK.types.ts │ ├── CoinbaseWalletSDKModule.ts │ ├── WalletMobileSDKEVMProvider.ts │ └── types/ │ ├── core/ │ │ ├── type.ts │ │ └── util.ts │ └── provider/ │ ├── JSONRPC.ts │ └── Web3Provider.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Create a report to help us improve title: "Bug: " labels: ["type: bug"] body: - type: input id: description attributes: label: Describe the bug description: A clear and concise description of what the bug is. placeholder: Tell us what you see! validations: required: true - type: textarea id: steps attributes: label: Steps description: Steps to reproduce the behavior. placeholder: | 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. validations: required: true - type: input id: version attributes: label: Version description: Which version of Coinbase Wallet SDK - type: textarea id: additional attributes: label: Additional info description: If applicable, include links to screenshots or error logs - type: textarea id: desktop attributes: label: Desktop description: Please fill in details for bugs reported on desktop placeholder: | - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] - type: textarea id: smartphone attributes: label: Smartphone description: Please fill in details for bugs reported on smartphone devices placeholder: | - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Coinbase Wallet Developer Docs url: https://docs.cloud.coinbase.com/wallet-sdk/docs about: Coinbase Wallet's developer documentation ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request description: Suggest an idea for this project title: "Feature Request: " labels: ["type: enhancement"] body: - type: markdown attributes: value: | This issue form is for feature requests only! If you've found a bug, please use [bug_report](/new?template=bug_report.yml) - type: textarea id: problem attributes: label: Is your feature request related to a problem? Please describe. description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - type: textarea id: solution attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. validations: required: true - type: textarea id: alternatives attributes: label: Describe alternatives you've considered description: Describe any alternative solutions or features you've considered. - type: textarea id: other attributes: label: Additional context description: Add any other context or screenshots about the feature request here. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### _Summary_ ### _How did you test your changes?_ ================================================ FILE: .github/workflows/publish.yml ================================================ name: Tag & Publish [auto] on: push: branches: - master jobs: environment: release permissions: contents: write pull-requests: write id-token: write authorize: name: Authorize runs-on: ubuntu-latest steps: - name: ${{ github.actor }} permission check to update release version uses: "lannonbr/repo-permission-check-action@2.0.2" with: permission: "write" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} autotag: name: Auto Tag needs: authorize runs-on: ubuntu-latest outputs: tagcreated: ${{ steps.autotag.outputs.tagcreated }} steps: - name: Checkout uses: actions/checkout@v3 - id: autotag uses: ButlerLogic/action-autotag@1.1.1 with: strategy: regex root: "CoinbaseWalletSDK.podspec" regex_pattern: "s.version\\s*=\\s*'(\\d+\\.\\d+\\.\\d+)'" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish_ios: name: Publish iOS SDK needs: autotag if: ${{ needs.autotag.outputs.tagcreated == 'yes' }} runs-on: macOS-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Setup ruby uses: ruby/setup-ruby@v1 with: ruby-version: '2.7.5' bundler-cache: true - name: Publish to Cocoapods run: | set -eo pipefail pod trunk push CoinbaseWalletSDK.podspec --allow-warnings env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} publish_android: name: Publish Android SDK needs: autotag if: ${{ needs.autotag.outputs.tagcreated == 'yes' }} runs-on: macOS-latest defaults: run: working-directory: android steps: - name: Checkout uses: actions/checkout@v3 - name: Setup java uses: actions/setup-java@v3 with: distribution: temurin java-version: 11 - name: Setup Gradle uses: gradle/gradle-build-action@v2 - name: Execute Gradle build run: ./gradlew publishReleasePublicationToSonatypeRepository closeAndReleaseSonatypeStagingRepository env: SIGNING_KEY_ID: ${{ secrets.ANDROID_SIGNING_KEYID }} SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} SIGNING_KEY: ${{ secrets.ANDROID_SIGNING_KEY }} OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} publish_rn: name: Publish React Native SDK needs: [publish_ios, publish_android] runs-on: macOS-latest defaults: run: working-directory: react-native steps: - name: Checkout uses: actions/checkout@v3 - name: Setup node uses: actions/setup-node@v3 with: node-version: '16.x' registry-url: 'https://registry.npmjs.org' - name: Publish to npm run: | npm install npm run prepare npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }} publish_flutter: name: Publish Flutter SDK needs: [publish_ios, publish_android] runs-on: macOS-latest defaults: run: working-directory: flutter steps: - name: Checkout uses: actions/checkout@v3 - name: Setup Dart uses: dart-lang/setup-dart@v1.3 - name: Authenticate run: | mkdir -p $PUB_CACHE cd $PUB_CACHE echo "$PUBDEV_CREDENTIALSJSON" > credentials.json env: PUBDEV_CREDENTIALSJSON: ${{ secrets.PUBDEV_CREDENTIALSJSON }} - name: Publish to pub.dev run: dart pub publish --force # TODO: automatically update CHANGELOG to run this without --force - name: Clean up if: ${{ always() }} run: rm -r $PUB_CACHE ================================================ FILE: .github/workflows/run_tests.yml ================================================ name: Run Tests on: pull_request: branches: - master jobs: android: name: Android runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 # config java sdk version - name: Setup java uses: actions/setup-java@v3 with: distribution: temurin java-version: 11 - name: Setup Gradle uses: gradle/gradle-build-action@v2 # Run Android unit tests - name: Run unit testing run: | cd android ./gradlew :walletsdk:testDebugUnitTest ios: name: iOS runs-on: macOS-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Setup ruby uses: ruby/setup-ruby@v1 with: ruby-version: '2.7.5' bundler-cache: true - name: Run unit test run: bundle exec pod lib lint --verbose ================================================ FILE: .github/workflows/version_update.yml ================================================ name: Version Update PR on: workflow_dispatch: inputs: packageVersion: description: "The version to publish in MAJOR.MINOR.PATCH format" required: true default: "" jobs: environment: release permissions: contents: write pull-requests: write id-token: write authorize: name: Authorize runs-on: ubuntu-latest steps: - name: ${{ github.actor }} permission check to update release version uses: "lannonbr/repo-permission-check-action@2.0.2" with: permission: "write" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} version: name: Update package version runs-on: macOS-latest needs: authorize env: V_REGEX: '[0-9]+\.[0-9]+\.[0-9]+' NEW_VERSION: ${{ github.event.inputs.packageVersion }} steps: - name: Checkout uses: actions/checkout@v3 - name: Update iOS version run: | sed -i '' -E "s/= '${{env.V_REGEX}}/= '${{env.NEW_VERSION}}/" CoinbaseWalletSDK.podspec sed -i '' -E 's/= "${{env.V_REGEX}}/= "${{env.NEW_VERSION}}/' ios/CoinbaseWalletSDK/Resources/CoinbaseWalletSDK+version.swift - name: Update Android version run: | sed -i '' -E 's/(sdk-version =) "${{env.V_REGEX}}/\1 "${{env.NEW_VERSION}}/' android/gradle/libs.versions.toml - name: Update React Native version run: | sed -i '' -E "s/(CoinbaseWalletSDK\/CrossPlatform',) '${{env.V_REGEX}}/\1 '${{env.NEW_VERSION}}/" react-native/ios/CoinbaseWalletSDKExpo.podspec sed -i '' -E 's/(com.coinbase:coinbase-wallet-sdk):${{env.V_REGEX}}/\1:${{env.NEW_VERSION}}/' react-native/android/build.gradle sed -i '' -E 's/(version":) "${{env.V_REGEX}}/\1 "${{env.NEW_VERSION}}/' react-native/package.json - name: Update Flutter version run: | sed -i '' -E "s/(CoinbaseWalletSDK\/CrossPlatform',) '${{env.V_REGEX}}/\1 '${{env.NEW_VERSION}}/" flutter/ios/coinbase_wallet_sdk.podspec sed -i '' -E 's/(com.coinbase:coinbase-wallet-sdk):${{env.V_REGEX}}/\1:${{env.NEW_VERSION}}/' flutter/android/build.gradle sed -i '' -E 's/(version:) ${{env.V_REGEX}}/\1 ${{env.NEW_VERSION}}/' flutter/pubspec.yaml - name: Setup ruby uses: ruby/setup-ruby@v1 with: ruby-version: '2.7.5' bundler-cache: true - name: Update iOS example run: bundle exec pod install working-directory: ios/example # - name: Update React Native example # run: bundle exec pod install # working-directory: react-native/example/ios # - name: Update Flutter example # run: bundle exec pod install # working-directory: flutter/example/ios - name: Open pull request uses: peter-evans/create-pull-request@v4 with: add-paths: | CoinbaseWalletSDK.podspec ios/CoinbaseWalletSDK/Resources/CoinbaseWalletSDK+version.swift ios/example/Podfile.lock android/gradle/libs.versions.toml react-native/ios/CoinbaseWalletSDKExpo.podspec react-native/android/build.gradle react-native/package.json flutter/ios/coinbase_wallet_sdk.podspec flutter/android/build.gradle flutter/pubspec.yaml title: "[Version update] v${{env.NEW_VERSION}}" body: "Automated workflow: version update" branch: version-update-v${{env.NEW_VERSION}} reviewers: | bangtoven vishnumad AMITGOELNY commit-message: "[Version update] v${{env.NEW_VERSION}}" labels: version-update delete-branch: true ================================================ FILE: .gitignore ================================================ # misc .DS_Store # Native mobile ## User settings xcuserdata/ ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ *.moved-aside *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 ## Obj-C/Swift specific *.hmap ## App packaging *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins # Package.resolved # *.xcodeproj # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project # .swiftpm .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # Pods/ # # Add this line if you want to avoid checking in source code from the Xcode workspace *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build/ # Accio dependency management Dependencies/ .accio/ # fastlane # # It is recommended to not store the screenshots in the git repo. # Instead, use fastlane to re-generate the screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output # Code Injection # # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ .DS_Store xcshareddata # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Log/OS Files *.log # Android Studio generated files and folders captures/ .externalNativeBuild/ .cxx/ *.apk output.json # IntelliJ *.iml .idea/ misc.xml deploymentTargetDropDown.xml render.experimental.xml # Keystore files *.jks *.keystore # Google Services (e.g. APIs or Firebase) google-services.json # Android Profiling *.hprof node_modules/ ================================================ FILE: CODEOWNERS ================================================ * @coinbase/wallet-mobile-sdk .github/workflows/* @bangtoven ================================================ FILE: CoinbaseWalletSDK.podspec ================================================ Pod::Spec.new do |s| s.name = 'CoinbaseWalletSDK' s.version = '1.1.2' s.summary = 'Swift implementation of WalletSegue protocol to interact with Coinbase Wallet iOS app' s.source = { :git => 'https://github.com/MobileWalletProtocol/wallet-mobile-sdk.git', :tag => s.version } s.author = 'Coinbase Wallet' s.social_media_url = 'https://twitter.com/CoinbaseWallet' s.homepage = 'https://github.com/MobileWalletProtocol/wallet-mobile-sdk' s.license = { :type => 'Apache', :file => 'LICENSE' } s.ios.deployment_target = '13.0' s.swift_version = '5.0' SDK_PATH = 'ios/CoinbaseWalletSDK' s.subspec 'Client' do |ss| ss.source_files = "#{SDK_PATH}/**/*.swift" ss.exclude_files = [ "#{SDK_PATH}/Host/**/*.swift", "#{SDK_PATH}/Test/**/*.swift" ] end s.subspec 'Host' do |ss| ss.dependency 'CoinbaseWalletSDK/Client' ss.source_files = "#{SDK_PATH}/Host/**/*.swift" end s.subspec 'CrossPlatform' do |ss| ss.dependency 'CoinbaseWalletSDK/Client' ss.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-DCROSS_PLATFORM' } end s.test_spec 'Test' do |ts| ts.ios.deployment_target = '13.0' ts.source_files = "#{SDK_PATH}/Test/**/*.swift" end s.default_subspec = 'Client' end ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' gem 'cocoapods' ================================================ FILE: LICENSE ================================================ Copyright (c) 2022 Coinbase, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Package.swift ================================================ // swift-tools-version: 5.4 import PackageDescription let package = Package( name: "CoinbaseWalletSDK", platforms: [.iOS(.v13)], products: [ .library( name: "CoinbaseWalletSDK", targets: ["CoinbaseWalletSDK"] ) ], targets: [ .target( name: "CoinbaseWalletSDK", path: "ios/CoinbaseWalletSDK", exclude: ["Host", "Test"] ), .testTarget( name: "CoinbaseWalletSDKTests", dependencies: ["CoinbaseWalletSDK"], path: "ios/CoinbaseWalletSDK/Test" ) ] ) ================================================ FILE: README.md ================================================ # Coinbase Wallet Mobile SDK [![Cocoapods](https://img.shields.io/cocoapods/v/CoinbaseWalletSDK)](https://cocoapods.org/pods/CoinbaseWalletSDK) [![Maven](https://img.shields.io/maven-central/v/com.coinbase/coinbase-wallet-sdk?label=maven)](https://mavenlibs.com/maven/dependency/com.coinbase/coinbase-wallet-sdk) [![npm](https://img.shields.io/npm/v/@coinbase/wallet-mobile-sdk)](https://www.npmjs.com/package/@coinbase/wallet-mobile-sdk) [![pub.dev](https://img.shields.io/pub/v/coinbase_wallet_sdk)](https://pub.dev/packages/coinbase_wallet_sdk) Coinbase Wallet Mobile SDK is an open source SDK which allows you to connect your native mobile applications to millions of Coinbase Wallet users. ## Getting Started The SDK is available for the following platforms: - [iOS](https://github.com/coinbase/wallet-mobile-sdk/tree/master/ios) - [Android](https://github.com/coinbase/wallet-mobile-sdk/tree/master/android) Wrapper libraries and modules are also available for the following platforms: - [React Native](https://github.com/coinbase/wallet-mobile-sdk/tree/master/react-native) - [Flutter](https://github.com/coinbase/wallet-mobile-sdk/tree/master/flutter) For **Install** and **Usage** instructions of each platform, visit the links above or our [developer documentation](https://docs.cloud.coinbase.com/wallet-sdk/docs/mobile-sdk-overview). ## References - Mobile Wallet Protocol [Specification](https://mobilewalletprotocol.github.io/wallet-mobile-sdk/docs/spec) - Coinbase Wallet [Developer Documentation](https://docs.cloud.coinbase.com/wallet-sdk/docs) - Questions? Visit our [Developer Forums](https://forums.coinbasecloud.dev/). - For bugs, please report an issue on Github. ## License ``` Copyright © 2022 Coinbase, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: android/README.md ================================================ # Coinbase Wallet Mobile SDK [![Maven](https://img.shields.io/maven-central/v/com.coinbase/coinbase-wallet-sdk?label=maven)](https://mavenlibs.com/maven/dependency/com.coinbase/coinbase-wallet-sdk) Coinbase Wallet Mobile SDK is an open source SDK which allows you to connect your native mobile applications to millions of Coinbase Wallet users. ## Install The Coinbase Wallet Mobile SDK is available on [Maven Central](https://search.maven.org/artifact/com.coinbase/coinbase-wallet-sdk/0.1.0/aar). ### Gradle Add Coinbase Wallet SDK to your `build.gradle` file. ```groovy repositories { mavenCentral() } dependencies { implementation "com.coinbase:coinbase-wallet-sdk:1.0.3" } ``` ### Maven Add Coinbase Wallet SDK to your `pom.xml` file. ```xml com.coinbase coinbase-wallet-sdk 1.0.3 ``` ## Usage ### Setup In order for your app to interact with Coinbase Wallet, you must add a [queries element](https://developer.android.com/guide/topics/manifest/queries-element) to your `AndroidManifest.xml` file, specifying the package name for Coinbase Wallet, `org.toshi`. ```xml ``` Before the SDK can be used, it needs to be configured with an App Link to your application. This callback URL will be used by the Coinbase Wallet application to navigate back to your application. ```kotlin CoinbaseWalletSDK( appContext = applicationContext, domain = Uri.parse("https://www.myappxyz.com"), openIntent = { intent -> launcher.launch(intent) } ) ``` When your application receives a response from Coinbase Wallet via App Links, this URL needs to be handed off to the SDK via the handleResponse function. ```kotlin launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val uri = result.data?.data ?: return@registerForActivityResult client.handleResponse(uri) } ``` An example is provided in the sample [application](https://github.com/coinbase/wallet-mobile-sdk/blob/master/android/example/src/main/java/com/coinbase/android/beta/MainActivity.kt#L27). ### Establishing a connection A connection to Coinbase Wallet can be initiated by calling the `initiateHandshake` function provided by the SDK. The function also takes in an optional `initialActions` parameter which apps can use to take certain actions along with the initial handshake request. ```kotlin val requestAccount = Web3JsonRPC.RequestAccounts().action() val handShakeActions = listOf(requestAccount) client.initiateHandshake( initialActions = handShakeActions ) { result: Result>, account: Account? -> result.onSuccess { actionResults: List -> actionResults.handleSuccess("Handshake", handShakeActions, account) } result.onFailure { err -> err.handleError("HandShake") } } ``` An example handshake request is provided in the sample [application](https://github.com/coinbase/wallet-mobile-sdk/blob/master/android/example/src/main/java/com/coinbase/android/beta/MainActivity.kt#L52). ### Making requests Requests to Coinbase Wallet can be made by calling the `makeRequest` function provided by the SDK. This function also accepts a list of `actions` that can be taken in as a single batch request. ```kotlin val signTypedDataV3 = Web3JsonRPC.SignTypedDataV3( "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", // address typedDataJson // typed data JSON ).action() val requestActions = listOf(signTypedDataV3) client.makeRequest(request = RequestContent.Request(actions = requestActions)) { result -> result.fold( onSuccess = { returnValues -> returnValues.handleSuccess("Request", requestActions) }, onFailure = { err -> err.handleError("Request") } ) } ``` An example request is provided in the sample [application](https://github.com/coinbase/wallet-mobile-sdk/blob/master/android/example/src/main/java/com/coinbase/android/beta/MainActivity.kt#L68). For more information on the types of requests you can make, visit our [developer documentation](https://docs.cloud.coinbase.com/wallet-sdk/docs/mobile-sdk-overview). ## References - Coinbase Wallet [Developer Documentation](https://docs.cloud.coinbase.com/wallet-sdk/docs) - Questions? Visit our [Developer Forums](https://forums.coinbasecloud.dev/). - For bugs, please report an issue on Github. ## License ``` Copyright © 2022 Coinbase, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: android/build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias libs.plugins.android.application apply false alias libs.plugins.android.library apply false alias libs.plugins.kotlin.android apply false alias libs.plugins.maven.publish } apply from: "${rootDir}/scripts/publish-root.gradle" task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: android/example/.editorconfig ================================================ [{*.kt, *.kts}] max_line_length = 120 ================================================ FILE: android/example/.gitignore ================================================ /build ================================================ FILE: android/example/build.gradle ================================================ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { compileSdk 33 signingConfigs { development { storeFile file('debug.keystore') storePassword 'android' keyAlias 'androiddebugkey' keyPassword 'android' } } defaultConfig { applicationId "com.coinbase.android.beta" minSdk 23 targetSdk 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.development } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { viewBinding true } namespace 'com.coinbase.android.beta' } dependencies { implementation libs.android.ktx implementation libs.android.appcompat implementation libs.android.constraintlayout implementation libs.android.material testImplementation libs.junit4 androidTestImplementation libs.androidx.junit4 implementation project(':walletsdk') } ================================================ FILE: android/example/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: android/example/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/example/src/main/java/com/coinbase/android/beta/ActionsManager.kt ================================================ package com.coinbase.android.beta import android.content.Context import android.content.SharedPreferences import com.coinbase.android.nativesdk.message.request.Web3JsonRPC object ActionsManager { private val requestAccount = Web3JsonRPC.RequestAccounts().action() private val personalSign = Web3JsonRPC.PersonalSign("", "Hello world").action() private val switchEthereumChain = Web3JsonRPC.SwitchEthereumChain("137").action() val addEthereumChain = Web3JsonRPC.AddEthereumChain("172222").action() private fun getSignTransaction(fromAddress: String, toAddress: String) = Web3JsonRPC.SignTransaction( fromAddress = fromAddress, toAddress = toAddress, weiValue = "10000000000000", data = "0x", nonce = null, gasPriceInWei = null, maxFeePerGas = null, maxPriorityFeePerGas = null, gasLimit = "1000", chainId = "1", ).action() private fun getSendTransaction(fromAddress: String, toAddress: String) = Web3JsonRPC.SendTransaction( fromAddress = fromAddress, toAddress = toAddress, weiValue = "10000000000000", data = "0x", nonce = null, gasPriceInWei = null, maxFeePerGas = null, maxPriorityFeePerGas = null, gasLimit = "1000", chainId = "1", ).action() val signTypedDataV3 = Web3JsonRPC.SignTypedDataV3("0xabcdefabcdefabcdefabcdefabcdefabcdef", "").action() val handShakeActions = listOf(requestAccount, personalSign) fun getRequestActions( fromAddress: String = SharedPrefsManager.account, toAddress: String ) = listOf( requestAccount, personalSign, switchEthereumChain, getSendTransaction(fromAddress, toAddress) ) } object SharedPrefsManager { lateinit var sharedPrefs: SharedPreferences var account: String get() = sharedPrefs.getString("eth_account", "") ?: "" set(value) { with(sharedPrefs.edit()) { putString("eth_account", value) apply() } } fun init(context: Context) { sharedPrefs = context.getSharedPreferences("DEMO_APP", Context.MODE_PRIVATE) } fun removeAccount() { with(sharedPrefs.edit()) { remove("eth_account") apply() } } } ================================================ FILE: android/example/src/main/java/com/coinbase/android/beta/MainActivity.kt ================================================ package com.coinbase.android.beta import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.coinbase.android.beta.databinding.ActivityMainBinding import com.coinbase.android.nativesdk.CoinbaseWalletSDK import com.coinbase.android.nativesdk.message.request.Account import com.coinbase.android.nativesdk.message.request.Action import com.coinbase.android.nativesdk.message.request.RequestContent import com.coinbase.android.nativesdk.message.response.ActionResult import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var launcher: ActivityResultLauncher private val client by lazy { CoinbaseWalletSDK( appContext = applicationContext, domain = Uri.parse("https://myappxyz.com"), openIntent = { intent -> launcher.launch(intent) } ) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) SharedPrefsManager.init(this) launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val uri = result.data?.data ?: return@registerForActivityResult client.handleResponse(uri) } } override fun onStart() = with(binding) { super.onStart() val mwpVersion = CoinbaseWalletSDK.getCoinbaseWalletMWPVersion(this@MainActivity) textArea.text = "Wallet MWP Version: $mwpVersion" setVisibility() connectWalletButton.setOnClickListener { val handShakeActions = ActionsManager.handShakeActions client.initiateHandshake( initialActions = handShakeActions ) { result: Result>, account: Account? -> result.onSuccess { actionResults: List -> actionResults.handleSuccess("Handshake", handShakeActions, account) } result.onFailure { err -> err.handleError("HandShake") } } } personalSign.setOnClickListener { val requestActions = ActionsManager.getRequestActions( toAddress = "0x571a6a108adb08f9ca54fe8605280f9ee0ed4af6" ) client.makeRequest(request = RequestContent.Request(actions = requestActions)) { result -> result.fold( onSuccess = { returnValues -> returnValues.handleSuccess("Request", requestActions) }, onFailure = { err -> err.handleError("Request") } ) } } removeAccount.setOnClickListener { client.resetSession() SharedPrefsManager.removeAccount() textArea.setText(R.string.connection_removed) setVisibility() } } private fun List.handleSuccess( requestType: String, actions: List, account: Account? = null ) = with(binding) { textArea.text = buildString { if (actions.isEmpty()) { append("Handshake successful") } else { this@handleSuccess.forEachIndexed { index, returnValue -> append( "${actions[index].method} Result: " + "${if (returnValue is ActionResult.Result) "Success" else "Error"}\n" ) if (account != null) { SharedPrefsManager.account = account.address } val result = when (returnValue) { is ActionResult.Result -> returnValue.value is ActionResult.Error -> returnValue.message } append("${result}\n\n") } } } setVisibility() } private fun Throwable.handleError(requestType: String) = with(binding) { textArea.text = buildString { append("$requestType Error: \n\n") append(message) } } private fun setVisibility() = with(binding) { val isConnected = client.isConnected connectContainer.isVisible = !isConnected requestContainer.isVisible = isConnected with(connectedStatus) { val radius = 28f val shapeAppearanceModel = ShapeAppearanceModel() .toBuilder() .setAllCorners(CornerFamily.ROUNDED, radius) .build() val shapeDrawable = MaterialShapeDrawable(shapeAppearanceModel) ViewCompat.setBackground(connectedStatus, shapeDrawable) if (isConnected) { text = getString( R.string.connected_state, "${SharedPrefsManager.account.take(5)}...${SharedPrefsManager.account.takeLast(4)}" ) setTextColor(getColor(R.color.teal_200)) shapeDrawable.setStroke(2.0f, getColor(R.color.teal_200)) } else { setText(R.string.unconnected_state) setTextColor(getColor(R.color.red_error)) shapeDrawable.setStroke(2.0f, getColor(R.color.red_error)) } } } } ================================================ FILE: android/example/src/main/java/com/coinbase/android/beta/SecondActivity.java ================================================ package com.coinbase.android.beta; import static com.coinbase.android.nativesdk.CoinbaseWalletSDKKt.CBW_PACKAGE_NAME; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.coinbase.android.nativesdk.CoinbaseWalletSDK; import com.coinbase.android.nativesdk.message.request.Action; import com.coinbase.android.nativesdk.message.request.Web3JsonRPC; import com.coinbase.android.nativesdk.message.response.ActionResult; import java.util.ArrayList; public class SecondActivity extends AppCompatActivity { final int CBW_ACTIVITY_RESULT_CODE = 9182736; CoinbaseWalletSDK client; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); client = new CoinbaseWalletSDK( Uri.parse("https://myappxyz.com"), getApplicationContext(), CBW_PACKAGE_NAME, intent -> { startActivityForResult(intent, CBW_ACTIVITY_RESULT_CODE); } ); } @Override protected void onStart() { super.onStart(); ArrayList actions = new ArrayList<>(); actions.add( new Web3JsonRPC.RequestAccounts().action(false) ); client.initiateHandshake( actions, (results, account) -> { for (ActionResult result : results) { if (result instanceof ActionResult.Result) { ((ActionResult.Result) result).getValue(); } if (result instanceof ActionResult.Error) { ((ActionResult.Error) result).getCode(); ((ActionResult.Error) result).getMessage(); } } }, error -> { } ); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode != CBW_ACTIVITY_RESULT_CODE) { return; } if (data == null) { return; } Uri url = data.getData(); client.handleResponse(url); } } ================================================ FILE: android/example/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: android/example/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: android/example/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: android/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: android/example/src/main/res/values/colors.xml ================================================ #FFBB86FC #FF6200EE #FF3700B3 #FF03DAC5 #FF018786 #FF000000 #FFFFFFFF #F44336 ================================================ FILE: android/example/src/main/res/values/strings.xml ================================================ WalletSDK ClientApp Connect Wallet Request personal_sign Remove Account Select an option Connection Removed Connected: %s Not Connected ================================================ FILE: android/example/src/main/res/values/themes.xml ================================================ ================================================ FILE: android/example/src/main/res/values-night/themes.xml ================================================ ================================================ FILE: android/example/src/main/res/xml/backup_rules.xml ================================================ ================================================ FILE: android/example/src/main/res/xml/data_extraction_rules.xml ================================================ ================================================ FILE: android/gradle/libs.versions.toml ================================================ [versions] gradleplugin = "7.2.2" kotlin = "1.6.21" kotlinSerialization = "1.3.3" nexus-publish = "1.1.0" sdk-version = "1.2.0" [libraries] android-ktx = { module = "androidx.core:core-ktx", version = "1.9.0" } android-appcompat = { module = "androidx.appcompat:appcompat", version = "1.5.1" } android-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.4" } android-material = { module = "com.google.android.material:material", version = "1.6.1" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } security-crypto = { module = "androidx.security:security-crypto", version = "1.0.0" } security-tink = { module = "com.google.crypto.tink:tink-android", version = "1.6.1" } # Test Libs androidx-junit4 = { module = "androidx.test.ext:junit", version = "1.1.3" } junit4 = { module = "junit:junit", version = "4.13.2" } kotest = { module = "io.kotest:kotest-assertions-core", version = "5.5.0" } [plugins] android-application = { id = "com.android.application", version.ref = "gradleplugin" } android-library = { id = "com.android.library", version.ref = "gradleplugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } maven-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus-publish" } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: android/gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true ================================================ FILE: android/gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: android/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: android/scripts/publish-root.gradle ================================================ // Create variables with empty default values ext["signing.keyId"] = '' ext["signing.password"] = '' ext["signing.key"] = '' ext["ossrhUsername"] = '' ext["ossrhPassword"] = '' ext["sonatypeStagingProfileId"] = '' ext["snapshot"] = false File secretPropsFile = project.rootProject.file('local.properties') if (secretPropsFile.exists()) { // Read local.properties file first if it exists Properties p = new Properties() new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } p.each { name, value -> ext[name] = value } } else { // Use system environment variables ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') ext["sonatypeStagingProfileId"] = '61a7f2c94aea57' ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') ext["signing.password"] = System.getenv('SIGNING_PASSWORD') ext["signing.key"] = System.getenv('SIGNING_KEY') ext["snapshot"] = System.getenv('SNAPSHOT') } def versionName = libs.versions.sdk.version def snapshotVersionName = "${versionName}-SNAPSHOT" if (snapshot) { ext["rootVersionName"] = snapshotVersionName } else { ext["rootVersionName"] = versionName } // Set up Sonatype repository nexusPublishing { repositories { sonatype { stagingProfileId = sonatypeStagingProfileId username = ossrhUsername password = ossrhPassword version = versionName } } } ================================================ FILE: android/settings.gradle ================================================ pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "Wallet SDK" include ':example', ':walletsdk' ================================================ FILE: android/walletsdk/.editorconfig ================================================ [{*.kt, *.kts}] max_line_length = 120 ================================================ FILE: android/walletsdk/.gitignore ================================================ /build ================================================ FILE: android/walletsdk/build.gradle ================================================ plugins { alias libs.plugins.android.library alias libs.plugins.kotlin.android id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.21' id 'maven-publish' id 'signing' } android { compileSdk 34 defaultConfig { minSdk 23 targetSdk 34 versionCode 3 versionName libs.versions.sdk.version.get() buildConfigField "int", "LIBRARY_VERSION_CODE", "${versionCode}" buildConfigField "String", "LIBRARY_VERSION_NAME", "\"${versionName}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } publishing { singleVariant("release") { withSourcesJar() withJavadocJar() } } testOptions { unitTests { includeAndroidResources = true } } } dependencies { implementation libs.kotlin.serialization implementation libs.security.crypto implementation libs.security.tink testImplementation libs.junit4 testImplementation libs.kotest androidTestImplementation libs.androidx.junit4 } afterEvaluate { publishing { publications { release(MavenPublication) { from components.release groupId 'com.coinbase' artifactId 'coinbase-wallet-sdk' version = android.defaultConfig.versionName pom { name = 'coinbase-wallet-sdk' description = 'Coinbase Wallet Mobile SDK' url = 'https://github.com/MobileWalletProtocol/wallet-mobile-sdk' licenses { license { name = 'Coinbase License' url = 'https://github.com/MobileWalletProtocol/wallet-mobile-sdk/blob/master/LICENSE' } } developers { developer { id = 'bangtoven' name = 'Jungho Bang' email = 'me@bangtoven.com' } developer { id = 'vishnumad' name = 'Vishnu Madhusoodanan' email = 'vishnu.madhusoodanan@coinbase.com' } } scm { developerConnection = 'scm:git@github.com:MobileWalletProtocol/wallet-mobile-sdk.git' url = 'https://github.com/MobileWalletProtocol/wallet-mobile-sdk/tree/main' } } } } } } signing { useInMemoryPgpKeys( rootProject.ext["signing.keyId"], rootProject.ext["signing.key"], rootProject.ext["signing.password"], ) sign publishing.publications } ================================================ FILE: android/walletsdk/consumer-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile # Keep `Companion` object fields of serializable classes. # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. -if @kotlinx.serialization.Serializable class ** -keepclassmembers class <1> { static <1>$Companion Companion; } # Keep `serializer()` on companion objects (both default and named) of serializable classes. -if @kotlinx.serialization.Serializable class ** { static **$* *; } -keepclassmembers class <2>$<3> { kotlinx.serialization.KSerializer serializer(...); } # Keep `INSTANCE.serializer()` of serializable objects. -if @kotlinx.serialization.Serializable class ** { public static ** INSTANCE; } -keepclassmembers class <1> { public static <1> INSTANCE; kotlinx.serialization.KSerializer serializer(...); } # @Serializable and @Polymorphic are used at runtime for polymorphic serialization. -keepattributes RuntimeVisibleAnnotations,AnnotationDefault # Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`. # If you have any, uncomment and replace classes with those containing named companion objects. #-keepattributes InnerClasses # Needed for `getDeclaredClasses`. #-if @kotlinx.serialization.Serializable class #com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions. #com.example.myapplication.HasNamedCompanion2 #{ # static **$* *; #} #-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept. # static <1>$$serializer INSTANCE; #} ================================================ FILE: android/walletsdk/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile # Keep `Companion` object fields of serializable classes. # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. -if @kotlinx.serialization.Serializable class ** -keepclassmembers class <1> { static <1>$Companion Companion; } # Keep `serializer()` on companion objects (both default and named) of serializable classes. -if @kotlinx.serialization.Serializable class ** { static **$* *; } -keepclassmembers class <2>$<3> { kotlinx.serialization.KSerializer serializer(...); } # Keep `INSTANCE.serializer()` of serializable objects. -if @kotlinx.serialization.Serializable class ** { public static ** INSTANCE; } -keepclassmembers class <1> { public static <1> INSTANCE; kotlinx.serialization.KSerializer serializer(...); } # @Serializable and @Polymorphic are used at runtime for polymorphic serialization. -keepattributes RuntimeVisibleAnnotations,AnnotationDefault # Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`. # If you have any, uncomment and replace classes with those containing named companion objects. #-keepattributes InnerClasses # Needed for `getDeclaredClasses`. #-if @kotlinx.serialization.Serializable class #com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions. #com.example.myapplication.HasNamedCompanion2 #{ # static **$* *; #} #-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept. # static <1>$$serializer INSTANCE; #} ================================================ FILE: android/walletsdk/src/androidTest/java/com/coinbase/android/nativesdk/ExampleInstrumentedTest.kt ================================================ package com.coinbase.android.nativesdk import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.* /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.coinbase.android.walletsegue.test", appContext.packageName) } } ================================================ FILE: android/walletsdk/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/CoinbaseWalletSDK.kt ================================================ package com.coinbase.android.nativesdk import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.net.Uri import com.coinbase.android.nativesdk.key.KeyManager import com.coinbase.android.nativesdk.message.JSON import com.coinbase.android.nativesdk.message.request.Account import com.coinbase.android.nativesdk.message.request.Action import com.coinbase.android.nativesdk.message.request.ETH_REQUEST_ACCOUNTS import com.coinbase.android.nativesdk.message.request.RequestContent import com.coinbase.android.nativesdk.message.request.RequestConverter import com.coinbase.android.nativesdk.message.request.UnencryptedRequestContent import com.coinbase.android.nativesdk.message.request.UnencryptedRequestMessage import com.coinbase.android.nativesdk.message.request.nonHandshakeActions import com.coinbase.android.nativesdk.message.response.FailureResponseCallback import com.coinbase.android.nativesdk.message.response.ResponseHandler import com.coinbase.android.nativesdk.message.response.ResponseResult import com.coinbase.android.nativesdk.message.response.ActionResult import com.coinbase.android.nativesdk.message.response.ResponseConverter import com.coinbase.android.nativesdk.message.response.SuccessHandshakeResponseCallback import com.coinbase.android.nativesdk.message.response.SuccessRequestResponseCallback import com.coinbase.android.nativesdk.task.TaskManager import kotlinx.serialization.decodeFromString import java.security.interfaces.ECPublicKey import java.util.Date import java.util.UUID const val CBW_PACKAGE_NAME = "org.toshi" private const val CBW_SCHEME = "cbwallet://wsegue" class CoinbaseWalletSDK( domain: Uri, private val appContext: Context, private val hostPackageName: String = CBW_PACKAGE_NAME, private val openIntent: (Intent) -> Unit ) { private val domain: Uri private var sdkVersion = BuildConfig.LIBRARY_VERSION_NAME private val keyManager by lazy { KeyManager(appContext, hostPackageName) } private val taskManager by lazy { TaskManager() } private val launchWalletIntent: Intent? get() = appContext.packageManager.getLaunchIntentForPackage(hostPackageName) val isCoinbaseWalletInstalled get() = launchWalletIntent != null val isConnected: Boolean get() = keyManager.peerPublicKey != null val ownPublicKey: ECPublicKey get() = keyManager.ownPublicKey val peerPublicKey: ECPublicKey? get() = keyManager.peerPublicKey companion object { fun getCoinbaseWalletMWPVersion(context: Context): String? { val intents = listOf( "1.1" to Intent(Intent.ACTION_VIEW, Uri.parse("mwp+1.1://")), "1.0" to Intent(Intent.ACTION_VIEW, Uri.parse("cbwallet://")) ) for ((version, intent) in intents) { if (intent.resolveActivity(context.packageManager) != null) { return version } } return null } } init { this.domain = if (domain.pathSegments.size < 2) { domain.buildUpon() .appendPath("wsegue") .build() } else { domain } } constructor( domain: Uri, appContext: Context, hostPackageName: String, openIntent: OpenIntentCallback ) : this( domain, appContext, hostPackageName, { intent -> openIntent.call(intent) } ) fun appendVersionTag(tag: String) { sdkVersion += "/$tag" } /** * Make handshake request to get session key from wallet * @param initialActions Batch of actions that you'd want to execute after successful handshake. `eth_requestAccounts` by default. * @param onResponse Response callback with regular response result and optional parsed [Account] object. */ fun initiateHandshake( initialActions: List? = null, onResponse: (ResponseResult, Account?) -> Unit ) { resetSession() val hasIllegalAction = initialActions?.any { nonHandshakeActions.contains(it.method) } == true if (hasIllegalAction) { onResponse(Result.failure(CoinbaseWalletSDKError.InvalidHandshakeRequest), null) return } val message = UnencryptedRequestMessage( uuid = UUID.randomUUID().toString(), version = sdkVersion, timestamp = Date(), sender = keyManager.ownPublicKey, content = UnencryptedRequestContent( handshake = RequestContent.Handshake( appId = appContext.packageName, callback = domain.toString(), initialActions = initialActions ) ), callbackUrl = domain.toString() ) send(message) { result -> // Get index of eth_requestAccounts action val requestAccountsIndex = initialActions?.indexOfFirst { it.method == ETH_REQUEST_ACCOUNTS } ?: -1 if (requestAccountsIndex == -1) { onResponse(result, null) return@send } // Get response from Wallet at index val requestAccountsResult = result.getOrNull()?.getOrNull(requestAccountsIndex) if (requestAccountsResult !is ActionResult.Result) { onResponse(result, null) return@send } val account = try { JSON.decodeFromString(requestAccountsResult.value) } catch (e: Exception) { null } onResponse(result, account) } } fun initiateHandshake( initialActions: List? = null, onSuccess: SuccessHandshakeResponseCallback, onFailure: FailureResponseCallback ) { initiateHandshake(initialActions) { result, account -> result .onSuccess { onSuccess.call(it, account) } .onFailure { onFailure.call(it) } } } /** * Make regular requests. It requires session key you get after successful handshake. */ fun makeRequest( request: RequestContent.Request, onResponse: ResponseHandler ) { val message = UnencryptedRequestMessage( uuid = UUID.randomUUID().toString(), version = sdkVersion, timestamp = Date(), sender = keyManager.ownPublicKey, content = UnencryptedRequestContent(request = request), callbackUrl = domain.toString() ) send(message, onResponse) } fun makeRequest( request: RequestContent.Request, onSuccess: SuccessRequestResponseCallback, onFailure: FailureResponseCallback ) { makeRequest(request) { result -> result .onSuccess { onSuccess.call(it) } .onFailure { onFailure.call(it) } } } /** * Handle incoming deep links * @param url deep link url * @return `false` if the input was not response message type, or `true` if SDK handled the input */ fun handleResponse(url: Uri): Boolean { if (!isWalletSegueResponseURL(url)) { return false } val ownPublicKey = keyManager.ownPublicKey val peerPublicKey = keyManager.peerPublicKey val message = ResponseConverter.decode( url = url, ownPublicKey = ownPublicKey, ownPrivateKey = keyManager.ownPrivateKey, peerPublicKey = peerPublicKey ) if (peerPublicKey == null && message.sender != ownPublicKey) { keyManager.storePeerPublicKey(message.sender as ECPublicKey) } return taskManager.handleResponse(message) } fun resetSession() { taskManager.reset() keyManager.resetKeys() } private fun send(message: UnencryptedRequestMessage, onResponse: ResponseHandler) { val uri: Uri try { uri = RequestConverter.encode( message = message, recipient = Uri.parse(CBW_SCHEME), ownPrivateKey = keyManager.ownPrivateKey, peerPublicKey = keyManager.peerPublicKey ) } catch (e: Throwable) { when (e) { is CoinbaseWalletSDKError -> onResponse(Result.failure(e)) else -> onResponse(Result.failure(CoinbaseWalletSDKError.EncodingFailed)) } return } val intent = launchWalletIntent if (intent == null) { onResponse(Result.failure(CoinbaseWalletSDKError.OpenWalletFailed)) return } // Prevent intent from launching app in new window intent.type = Intent.ACTION_VIEW if (intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK > 0) { intent.flags = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv() } intent.data = uri taskManager.registerResponseHandler(message, onResponse) openIntent(intent) } private fun isWalletSegueResponseURL(uri: Uri): Boolean { return uri.host == domain.host && uri.path == domain.path && uri.getQueryParameter("p") != null } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/CoinbaseWalletSDKError.kt ================================================ package com.coinbase.android.nativesdk sealed class CoinbaseWalletSDKError( errorMessage: String? = null, cause: Throwable? = null ) : Exception(errorMessage, cause) { object EncodingFailed : CoinbaseWalletSDKError("Encoding failed") object DecodingFailed : CoinbaseWalletSDKError("Decoding failed") object MissingSharedSecret : CoinbaseWalletSDKError("Missing shared secret") object OpenWalletFailed : CoinbaseWalletSDKError("Could not open wallet") object InvalidHandshakeRequest : CoinbaseWalletSDKError("Could not process this request in handshake") class WalletReturnedError(error: String) : CoinbaseWalletSDKError(error) class UserNotAuthenticated(message: String, cause: Throwable) : CoinbaseWalletSDKError(message, cause) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/OpenIntentCallback.kt ================================================ package com.coinbase.android.nativesdk import android.content.Intent interface OpenIntentCallback { fun call(intent: Intent) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/key/KeyManager.kt ================================================ package com.coinbase.android.nativesdk.key import KeyStore import android.content.Context import com.google.crypto.tink.subtle.EllipticCurves import java.security.KeyPair import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey private const val OWN_KEY_PAIR_ALIAS = "own_key_pair" internal class KeyManager(appContext: Context, host: String) { private val keyStore: KeyStore private var ownKeyPair: KeyPair? = null val ownPublicKey: ECPublicKey get() { val own = ownKeyPair ?: getOrCreateKeyPair(OWN_KEY_PAIR_ALIAS) if (ownKeyPair == null) ownKeyPair = own return own.public as ECPublicKey } val ownPrivateKey: ECPrivateKey get() { val own = ownKeyPair ?: getOrCreateKeyPair(OWN_KEY_PAIR_ALIAS) if (ownKeyPair == null) ownKeyPair = own return own.private as ECPrivateKey } val peerPublicKey: ECPublicKey? get() = keyStore.peerPublicKey init { keyStore = KeyStore( fileName = "${host}_wallet_segue_key_store", context = appContext ) } fun storePeerPublicKey(key: ECPublicKey) { keyStore.peerPublicKey = key } fun resetKeys() { keyStore.reset() // Create new KeyPair ownKeyPair = getOrCreateKeyPair(OWN_KEY_PAIR_ALIAS) } private fun deleteKeyPair(alias: String) { keyStore.deleteKeyPair(alias) } private fun getOrCreateKeyPair(alias: String): KeyPair { val keyPair = keyStore.getKeyPair(alias) return if (keyPair != null) { // Already have keys in encrypted storage keyPair } else { // Generate new key pair and save to encrypted storage val kp = EllipticCurves.generateKeyPair(EllipticCurves.CurveType.NIST_P256) keyStore.saveKeyPair(alias, kp) kp } } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/key/KeyStore.kt ================================================ import android.app.KeyguardManager import android.content.Context import android.content.SharedPreferences import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.security.keystore.UserNotAuthenticatedException import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import com.google.crypto.tink.subtle.Base64 import com.google.crypto.tink.subtle.EllipticCurves import java.security.KeyPair import java.security.KeyStore import java.security.interfaces.ECPublicKey private const val DEPRECATED_MAIN_KEY_ALIAS = "wallet_segue_main_key" private const val HAS_MIGRATED_TO_VERSION_120_ALIAS = "has_migrated_to_version_1.2.0" private const val PUBLIC_KEY_ALIAS = "public_key" private const val PRIVATE_KEY_ALIAS = "private_key" private const val PEER_PUBLIC_KEY_ALIAS = "peer_public_key" private const val OWN_KEY_PAIR_ALIAS = "own_key_pair" class KeyStore( private val fileName: String, private val context: Context ) { private val storage: SharedPreferences var peerPublicKey: ECPublicKey? get() { val encoded = storage.getString(PEER_PUBLIC_KEY_ALIAS, null) ?: return null val bytes = Base64.decode(encoded) return EllipticCurves.getEcPublicKey(bytes) } set(value) { if (value != null) { val encoded = Base64.encode(value.encoded) storage.edit() .putString(PEER_PUBLIC_KEY_ALIAS, encoded) .commit() } } init { storage = getSharedPrefs() } fun getKeyPair(alias: String): KeyPair? { val publicKeyAlias = "${alias}-${PUBLIC_KEY_ALIAS}" val privateKeyAlias = "${alias}-${PRIVATE_KEY_ALIAS}" val publicKeyB64 = storage.getString(publicKeyAlias, null) val privateKeyB64 = storage.getString(privateKeyAlias, null) return if (publicKeyB64 != null && privateKeyB64 != null) { // Already have keys in encrypted storage val publicKeyBytes = Base64.decode(publicKeyB64) val privateKeyBytes = Base64.decode(privateKeyB64) KeyPair( EllipticCurves.getEcPublicKey(publicKeyBytes), EllipticCurves.getEcPrivateKey(privateKeyBytes) ) } else { null } } fun saveKeyPair(alias: String, keyPair: KeyPair) { val publicKeyAlias = "${alias}-${PUBLIC_KEY_ALIAS}" val privateKeyAlias = "${alias}-${PRIVATE_KEY_ALIAS}" storage .edit() .putString(publicKeyAlias, Base64.encode(keyPair.public.encoded)) .putString(privateKeyAlias, Base64.encode(keyPair.private.encoded)) .commit() } fun deleteKeyPair(alias: String) { val publicKeyAlias = "${alias}-${PUBLIC_KEY_ALIAS}" val privateKeyAlias = "${alias}-${PRIVATE_KEY_ALIAS}" storage.edit() .remove(publicKeyAlias) .remove(privateKeyAlias) .commit() } fun reset() { val publicKeyAlias = "${OWN_KEY_PAIR_ALIAS}-${PUBLIC_KEY_ALIAS}" val privateKeyAlias = "${OWN_KEY_PAIR_ALIAS}-${PRIVATE_KEY_ALIAS}" storage.edit() .remove(publicKeyAlias) .remove(privateKeyAlias) .remove(PEER_PUBLIC_KEY_ALIAS) .commit() } private fun getSharedPrefs(): SharedPreferences { val sharedPrefs = context.getSharedPreferences("${fileName}_raw", Context.MODE_PRIVATE) val hasMigratedToVersion120 = sharedPrefs.getBoolean(HAS_MIGRATED_TO_VERSION_120_ALIAS, false) if (!hasMigratedToVersion120) { try { // Perform the migration migrateEncryptedPrefs(sharedPrefs) } catch (e: Exception) { // Clear Shared Prefs State sharedPrefs.edit().clear().commit() } finally { // Mark as migrated sharedPrefs.edit().putBoolean(HAS_MIGRATED_TO_VERSION_120_ALIAS, true).commit() } } return sharedPrefs } private fun migrateEncryptedPrefs(newPrefs: SharedPreferences) { // 1. Open the old EncryptedSharedPreferences val encryptedPrefs = getEncryptedPrefs() // 2. Copy all string typed key-value pairs from encrypted prefs to new prefs in plain text val allEntries = encryptedPrefs.all for ((key, value) in allEntries) { if (value != null) { when (value) { is String -> newPrefs.edit().putString(key, value).commit() } } } // 3. Clear the old EncryptedSharedPreferences encryptedPrefs.edit().clear().commit() } private fun getEncryptedPrefs(): SharedPreferences { // Create main key that secures encrypted storage val purposes = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT val keyGenSpec = with(KeyGenParameterSpec.Builder(DEPRECATED_MAIN_KEY_ALIAS, purposes)) { val keyguard = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager if (keyguard.isDeviceSecure) { setUserAuthenticationRequired(true) setUserAuthenticationValidityDurationSeconds(172_800) // 2 days } setBlockModes(KeyProperties.BLOCK_MODE_GCM) setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) setKeySize(256) build() } val mainKey = MasterKeys.getOrCreate(keyGenSpec) return EncryptedSharedPreferences.create( fileName, mainKey, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/key/PublicKeySerializer.kt ================================================ package com.coinbase.android.nativesdk.key import com.google.crypto.tink.subtle.Base64 import com.google.crypto.tink.subtle.EllipticCurves import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import java.security.PublicKey internal object PublicKeySerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PublicKey", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: PublicKey) = encoder.encodeString(Base64.encode(value.encoded)) override fun deserialize(decoder: Decoder): PublicKey = EllipticCurves.getEcPublicKey(Base64.decode(decoder.decodeString())) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/Cipher.kt ================================================ package com.coinbase.android.nativesdk.message import com.google.crypto.tink.subtle.AesGcmJce import com.google.crypto.tink.subtle.Base64 internal object Cipher { fun encrypt(secret: ByteArray, message: String): String { val associatedData = "encrypted data" val encrypted = AesGcmJce(secret).encrypt(message.toByteArray(), associatedData.toByteArray()) return Base64.encode(encrypted) } fun decrypt(secret: ByteArray, encryptedMessage: String): String { val encryptedBytes = Base64.decode(encryptedMessage) val associatedData = "encrypted data" val decryptedBytes = AesGcmJce(secret).decrypt(encryptedBytes, associatedData.toByteArray()) return String(decryptedBytes) } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/DateSerializer.kt ================================================ package com.coinbase.android.nativesdk.message import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import java.util.Date internal object DateSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/JSON.kt ================================================ package com.coinbase.android.nativesdk.message import kotlinx.serialization.json.Json internal val JSON = Json { encodeDefaults = true classDiscriminator = "#wsegue_type" } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/Message.kt ================================================ package com.coinbase.android.nativesdk.message import com.coinbase.android.nativesdk.key.PublicKeySerializer import kotlinx.serialization.Serializable import java.security.PublicKey import java.util.Date @Serializable data class Message( val uuid: String, val version: String, @Serializable(with = PublicKeySerializer::class) val sender: PublicKey, val content: Content, @Serializable(with = DateSerializer::class) val timestamp: Date, val callbackUrl: String? ) { fun copy(newContent: T): Message { return Message( uuid = uuid, version = version, sender = sender, content = newContent, timestamp = timestamp, callbackUrl = callbackUrl ) } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/MessageConverter.kt ================================================ package com.coinbase.android.nativesdk.message import android.net.Uri import com.google.crypto.tink.subtle.EllipticCurves import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey interface MessageConverter { fun encode( message: UnencryptedMessageType, recipient: Uri, ownPrivateKey: ECPrivateKey? = null, peerPublicKey: ECPublicKey? = null ): Uri fun decode( url: Uri, ownPublicKey: ECPublicKey? = null, ownPrivateKey: ECPrivateKey? = null, peerPublicKey: ECPublicKey? = null ): UnencryptedMessageType fun decodeWithoutDecryption(url: Uri): EncryptedMessageType fun getSharedSecret( ownPublicKey: ECPublicKey? = null, ownPrivateKey: ECPrivateKey? = null, peerPublicKey: ECPublicKey? = null, messageSender: ECPublicKey? = null, ): ByteArray? { val myPrivateKey = ownPrivateKey ?: return null val otherPublicKey = (peerPublicKey ?: messageSender) ?: return null if (otherPublicKey == ownPublicKey) { return null } return EllipticCurves.computeSharedSecret(myPrivateKey, otherPublicKey) } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/request/Account.kt ================================================ package com.coinbase.android.nativesdk.message.request import kotlinx.serialization.Serializable @Serializable class Account( val chain: String, val networkId: Long, val address: String ) ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/request/Action.kt ================================================ package com.coinbase.android.nativesdk.message.request import kotlinx.serialization.Serializable @Serializable class Action { val method: String private val paramsJson: String private val optional: Boolean constructor(method: String, paramsJson: String, optional: Boolean = false) { this.method = method this.paramsJson = paramsJson this.optional = optional } constructor(rpc: Web3JsonRPC, optional: Boolean = false) { val (method, paramsJson) = rpc.asJson this.method = method this.paramsJson = paramsJson this.optional = optional } override fun equals(other: Any?): Boolean { return if (other !is Action) { false } else { // Compare the data members and return accordingly this.method == other.method && this.paramsJson == other.paramsJson && this.optional == other.optional } } override fun hashCode(): Int { var result = method.hashCode() result = 31 * result + paramsJson.hashCode() result = 31 * result + optional.hashCode() return result } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/request/EncryptedRequestMessage.kt ================================================ package com.coinbase.android.nativesdk.message.request import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import com.coinbase.android.nativesdk.message.Cipher import com.coinbase.android.nativesdk.message.JSON import com.coinbase.android.nativesdk.message.Message import kotlinx.serialization.EncodeDefault import kotlinx.serialization.EncodeDefault.Mode.NEVER import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString typealias EncryptedRequestMessage = Message @Serializable data class EncryptedRequest(val data: String) @OptIn(ExperimentalSerializationApi::class) @Serializable data class EncryptedRequestContent( @EncodeDefault(NEVER) val handshake: RequestContent.Handshake? = null, @EncodeDefault(NEVER) val request: EncryptedRequest? = null ) fun EncryptedRequestMessage.decrypt(secret: ByteArray?): UnencryptedRequestMessage { val content = when { this.content.handshake != null -> { UnencryptedRequestContent(handshake = this.content.handshake) } this.content.request != null -> { if (secret == null) throw CoinbaseWalletSDKError.MissingSharedSecret val requestJson = Cipher.decrypt(secret, this.content.request.data) val request: RequestContent.Request = JSON.decodeFromString(requestJson) UnencryptedRequestContent(request = request) } else -> throw CoinbaseWalletSDKError.DecodingFailed } return copy(newContent = content) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/request/RequestConverter.kt ================================================ package com.coinbase.android.nativesdk.message.request import android.net.Uri import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import com.coinbase.android.nativesdk.message.MessageConverter import com.coinbase.android.nativesdk.message.JSON import com.google.crypto.tink.subtle.Base64 import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey object RequestConverter : MessageConverter { override fun encode( message: UnencryptedRequestMessage, recipient: Uri, ownPrivateKey: ECPrivateKey?, peerPublicKey: ECPublicKey? ): Uri { val secret = getSharedSecret(ownPrivateKey = ownPrivateKey, peerPublicKey = peerPublicKey) val encrypted = message.encrypt(secret) val json = JSON.encodeToString(encrypted) return recipient.buildUpon() .appendQueryParameter("p", Base64.encode(json.toByteArray())) .build() } override fun decode( url: Uri, ownPublicKey: ECPublicKey?, ownPrivateKey: ECPrivateKey?, peerPublicKey: ECPublicKey? ): UnencryptedRequestMessage { val decoded = decodeWithoutDecryption(url) val secret = getSharedSecret( ownPublicKey = ownPublicKey, ownPrivateKey = ownPrivateKey, peerPublicKey = peerPublicKey, messageSender = decoded.sender as ECPublicKey ) return decoded.decrypt(secret) } override fun decodeWithoutDecryption(url: Uri): EncryptedRequestMessage { val encoded = url.getQueryParameter("p") ?: throw CoinbaseWalletSDKError.DecodingFailed val messageJsonString = String(Base64.decode(encoded)) return JSON.decodeFromString(messageJsonString) } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/request/UnencryptedRequestMessage.kt ================================================ package com.coinbase.android.nativesdk.message.request import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import com.coinbase.android.nativesdk.message.Cipher import com.coinbase.android.nativesdk.message.JSON import com.coinbase.android.nativesdk.message.Message import kotlinx.serialization.EncodeDefault import kotlinx.serialization.EncodeDefault.Mode.NEVER import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString typealias UnencryptedRequestMessage = Message sealed interface RequestContent { @Serializable data class Handshake( val appId: String, val callback: String, val appName: String? = null, val appIconUrl: String? = null, val initialActions: List? = null ) : RequestContent @Serializable data class Request( val actions: List, val account: Account? = null ) : RequestContent } @OptIn(ExperimentalSerializationApi::class) @Serializable data class UnencryptedRequestContent( @EncodeDefault(NEVER) val handshake: RequestContent.Handshake? = null, @EncodeDefault(NEVER) val request: RequestContent.Request? = null ) { val sealed get() = handshake ?: request ?: throw IllegalStateException() } fun UnencryptedRequestMessage.encrypt(secret: ByteArray?): EncryptedRequestMessage { val encryptedContent = when (val content = this.content.sealed) { is RequestContent.Handshake -> { EncryptedRequestContent(handshake = content) } is RequestContent.Request -> { if (secret == null) throw CoinbaseWalletSDKError.MissingSharedSecret val requestJson = JSON.encodeToString(content) val encrypted = Cipher.encrypt(secret, requestJson) EncryptedRequestContent(request = EncryptedRequest(encrypted)) } } return copy(newContent = encryptedContent) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/request/Web3JsonRPC.kt ================================================ package com.coinbase.android.nativesdk.message.request import com.coinbase.android.nativesdk.message.JSON import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString private typealias BigInt = String const val ETH_REQUEST_ACCOUNTS = "eth_requestAccounts" const val PERSONAL_SIGN = "personal_sign" const val ETH_SIGN_TYPED_DATA_V3 = "eth_signTypedData_v3" const val ETH_SIGN_TYPED_DATA_V4 = "eth_signTypedData_v4" const val ETH_SIGN_TRANSACTION = "eth_signTransaction" const val ETH_SEND_TRANSACTION = "eth_sendTransaction" const val WALLET_SWITCH_ETHEREUM_CHAIN = "wallet_switchEthereumChain" const val WALLET_ADD_ETHEREUM_CHAIN = "wallet_addEthereumChain" const val WALLET_WATCH_ASSET = "wallet_watchAsset" val nonHandshakeActions = listOf(ETH_SEND_TRANSACTION, ETH_SIGN_TRANSACTION) @Suppress("unused") @Serializable sealed class Web3JsonRPC { @Serializable @SerialName(ETH_REQUEST_ACCOUNTS) class RequestAccounts : Web3JsonRPC() @Serializable @SerialName(PERSONAL_SIGN) class PersonalSign( val address: String, val message: String ) : Web3JsonRPC() @Serializable @SerialName(ETH_SIGN_TYPED_DATA_V3) class SignTypedDataV3( val address: String, val typedDataJson: String ) : Web3JsonRPC() @Serializable @SerialName(ETH_SIGN_TYPED_DATA_V4) class SignTypedDataV4( val address: String, val typedDataJson: String ) : Web3JsonRPC() @Serializable @SerialName(ETH_SIGN_TRANSACTION) class SignTransaction( val fromAddress: String, val toAddress: String?, val weiValue: BigInt, val data: String, val nonce: Int?, val gasPriceInWei: BigInt?, val maxFeePerGas: BigInt?, val maxPriorityFeePerGas: BigInt?, val gasLimit: BigInt?, val chainId: String ) : Web3JsonRPC() @Serializable @SerialName(ETH_SEND_TRANSACTION) class SendTransaction( val fromAddress: String, val toAddress: String?, val weiValue: BigInt, val data: String, val nonce: Int?, val gasPriceInWei: BigInt?, val maxFeePerGas: BigInt?, val maxPriorityFeePerGas: BigInt?, val gasLimit: BigInt?, val chainId: String, val actionSource: ActionSource? = null ) : Web3JsonRPC() @Serializable @SerialName(WALLET_SWITCH_ETHEREUM_CHAIN) class SwitchEthereumChain(val chainId: String) : Web3JsonRPC() @Serializable @SerialName(WALLET_ADD_ETHEREUM_CHAIN) class AddEthereumChain( val chainId: String, val blockExplorerUrls: List? = null, val chainName: String? = null, val iconUrls: List? = null, val nativeCurrency: AddChainNativeCurrency? = null, val rpcUrls: List = emptyList() ) : Web3JsonRPC() @Serializable @SerialName(WALLET_WATCH_ASSET) class WatchAsset( val type: String, val options: WatchAssetOptions ) : Web3JsonRPC() internal val asJson: Pair get() { val json = JSON.encodeToString(this) val method = when (this) { is RequestAccounts -> ETH_REQUEST_ACCOUNTS is SendTransaction -> ETH_SEND_TRANSACTION is SignTransaction -> ETH_SIGN_TRANSACTION is PersonalSign -> PERSONAL_SIGN is SignTypedDataV3 -> ETH_SIGN_TYPED_DATA_V3 is SignTypedDataV4 -> ETH_SIGN_TYPED_DATA_V4 is AddEthereumChain -> WALLET_ADD_ETHEREUM_CHAIN is SwitchEthereumChain -> WALLET_SWITCH_ETHEREUM_CHAIN is WatchAsset -> WALLET_WATCH_ASSET } return method to json } fun action(optional: Boolean = false): Action = Action(rpc = this, optional = optional) } @Serializable class AddChainNativeCurrency(val name: String, val symbol: String, val decimals: Int) @Serializable class WatchAssetOptions(val address: String, val symbol: String?, val decimals: Int?, val image: String?) @Serializable class ActionSource(val url: String) ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/response/ActionResult.kt ================================================ package com.coinbase.android.nativesdk.message.response import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.jsonObject @Serializable(with = ActionResultSerializer::class) sealed class ActionResult { @Serializable class Result(val value: String) : ActionResult() @Serializable class Error(val code: Long, val message: String) : ActionResult() } internal object ActionResultSerializer : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ReturnValue") override fun serialize(encoder: Encoder, value: ActionResult) { val output = encoder as? JsonEncoder ?: throw CoinbaseWalletSDKError.EncodingFailed val formatter = output.json val json = buildJsonObject { when (value) { is ActionResult.Result -> put("result", formatter.encodeToJsonElement(value)) is ActionResult.Error -> put("error", formatter.encodeToJsonElement(value)) } } output.encodeJsonElement(json) } override fun deserialize(decoder: Decoder): ActionResult { val input = decoder as? JsonDecoder ?: throw CoinbaseWalletSDKError.DecodingFailed val formatter = input.json val json = input.decodeJsonElement().jsonObject return when (val key = json.keys.firstOrNull()) { "result" -> formatter.decodeFromJsonElement(json.getValue(key)) "error" -> formatter.decodeFromJsonElement(json.getValue(key)) else -> throw CoinbaseWalletSDKError.DecodingFailed } } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/response/EncryptedResponseMessage.kt ================================================ package com.coinbase.android.nativesdk.message.response import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import com.coinbase.android.nativesdk.message.Cipher import com.coinbase.android.nativesdk.message.JSON import com.coinbase.android.nativesdk.message.Message import kotlinx.serialization.EncodeDefault import kotlinx.serialization.EncodeDefault.Mode.NEVER import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString typealias EncryptedResponseMessage = Message @Serializable data class EncryptedResponse( val requestId: String? = null, val data: String ) @OptIn(ExperimentalSerializationApi::class) @Serializable data class EncryptedResponseContent( @EncodeDefault(NEVER) val response: EncryptedResponse? = null, @EncodeDefault(NEVER) val failure: ResponseContent.Failure? = null ) fun EncryptedResponseMessage.decrypt(secret: ByteArray?): UnencryptedResponseMessage { val content: UnencryptedResponseContent = when { this.content.failure != null -> { UnencryptedResponseContent(failure = this.content.failure) } this.content.response != null -> { if (secret == null) throw CoinbaseWalletSDKError.MissingSharedSecret val responseJson = Cipher.decrypt(secret, this.content.response.data) val response: ResponseContent.Response = JSON.decodeFromString(responseJson) UnencryptedResponseContent(response = response) } else -> throw CoinbaseWalletSDKError.DecodingFailed } return copy(newContent = content) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/response/Response.kt ================================================ package com.coinbase.android.nativesdk.message.response typealias ResponseResult = Result> typealias ResponseHandler = (ResponseResult) -> Unit ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/response/ResponseCallback.kt ================================================ package com.coinbase.android.nativesdk.message.response import com.coinbase.android.nativesdk.message.request.Account interface SuccessHandshakeResponseCallback { fun call(result: List, account: Account?) } interface SuccessRequestResponseCallback { fun call(result: List) } interface FailureResponseCallback { fun call(error: Throwable) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/response/ResponseConverter.kt ================================================ package com.coinbase.android.nativesdk.message.response import android.net.Uri import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import com.coinbase.android.nativesdk.message.MessageConverter import com.coinbase.android.nativesdk.message.JSON import com.google.crypto.tink.subtle.Base64 import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey object ResponseConverter : MessageConverter { override fun encode( message: UnencryptedResponseMessage, recipient: Uri, ownPrivateKey: ECPrivateKey?, peerPublicKey: ECPublicKey? ): Uri { val secret = getSharedSecret(ownPrivateKey = ownPrivateKey, peerPublicKey = peerPublicKey) val encrypted = message.encrypt(secret) val json = JSON.encodeToString(encrypted) return recipient.buildUpon() .appendQueryParameter("p", Base64.encode(json.toByteArray())) .build() } override fun decode( url: Uri, ownPublicKey: ECPublicKey?, ownPrivateKey: ECPrivateKey?, peerPublicKey: ECPublicKey? ): UnencryptedResponseMessage { val decoded = decodeWithoutDecryption(url) val secret = getSharedSecret( ownPublicKey = ownPublicKey, ownPrivateKey = ownPrivateKey, peerPublicKey = peerPublicKey, messageSender = decoded.sender as ECPublicKey ) return decoded.decrypt(secret) } override fun decodeWithoutDecryption(url: Uri): EncryptedResponseMessage { val encoded = url.getQueryParameter("p") ?: throw CoinbaseWalletSDKError.DecodingFailed val messageJsonString = String(Base64.decode(encoded)) return JSON.decodeFromString(messageJsonString) } } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/message/response/UnencryptedResponseMessage.kt ================================================ package com.coinbase.android.nativesdk.message.response import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import com.coinbase.android.nativesdk.message.Cipher import com.coinbase.android.nativesdk.message.JSON import com.coinbase.android.nativesdk.message.Message import kotlinx.serialization.EncodeDefault import kotlinx.serialization.EncodeDefault.Mode.NEVER import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString typealias UnencryptedResponseMessage = Message sealed interface ResponseContent { @Serializable data class Response( val requestId: String, val values: List ) : ResponseContent @Serializable data class Failure( val requestId: String, val description: String ) : ResponseContent } @OptIn(ExperimentalSerializationApi::class) @Serializable data class UnencryptedResponseContent( @EncodeDefault(NEVER) val failure: ResponseContent.Failure? = null, @EncodeDefault(NEVER) val response: ResponseContent.Response? = null ) { val sealed get() = failure ?: response ?: throw IllegalStateException() } fun UnencryptedResponseMessage.encrypt(secret: ByteArray?): EncryptedResponseMessage { val encryptedContent: EncryptedResponseContent = when (val content = this.content.sealed) { is ResponseContent.Failure -> { EncryptedResponseContent(failure = content) } is ResponseContent.Response -> { if (secret == null) throw CoinbaseWalletSDKError.MissingSharedSecret val responseJson = JSON.encodeToString(content) val encrypted = Cipher.encrypt(secret, responseJson) EncryptedResponseContent( response = EncryptedResponse( requestId = content.requestId, data = encrypted ) ) } } return copy(newContent = encryptedContent) } ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/task/Task.kt ================================================ package com.coinbase.android.nativesdk.task import com.coinbase.android.nativesdk.message.request.UnencryptedRequestMessage import com.coinbase.android.nativesdk.message.response.ResponseHandler import java.util.Date internal class Task( val request: UnencryptedRequestMessage, val handler: ResponseHandler, val timestamp: Date ) ================================================ FILE: android/walletsdk/src/main/java/com/coinbase/android/nativesdk/task/TaskManager.kt ================================================ package com.coinbase.android.nativesdk.task import com.coinbase.android.nativesdk.CoinbaseWalletSDKError import com.coinbase.android.nativesdk.message.request.UnencryptedRequestMessage import com.coinbase.android.nativesdk.message.response.ResponseContent import com.coinbase.android.nativesdk.message.response.ResponseHandler import com.coinbase.android.nativesdk.message.response.ResponseResult import com.coinbase.android.nativesdk.message.response.UnencryptedResponseMessage internal class TaskManager { private val tasks = HashMap() fun registerResponseHandler(message: UnencryptedRequestMessage, handler: ResponseHandler) { tasks[message.uuid] = Task( request = message, handler = handler, timestamp = message.timestamp ) } fun handleResponse(message: UnencryptedResponseMessage): Boolean { val requestId: String val result: ResponseResult = when (val response = message.content.sealed) { is ResponseContent.Response -> { requestId = response.requestId Result.success(response.values) } is ResponseContent.Failure -> { requestId = response.requestId Result.failure(CoinbaseWalletSDKError.WalletReturnedError(response.description)) } } val task = tasks[requestId] ?: return false task.handler(result) tasks.remove(requestId) return true } fun reset(){ tasks.clear() } } ================================================ FILE: android/walletsdk/src/test/java/com/coinbase/android/nativesdk/ExampleUnitTest.kt ================================================ package com.coinbase.android.nativesdk import org.junit.Test import org.junit.Assert.* /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } ================================================ FILE: android/walletsdk/src/test/java/com/coinbase/android/nativesdk/helper/InputStreamExtensions.kt ================================================ package com.coinbase.android.nativesdk.helper import java.io.IOException import java.io.InputStream @Throws(IOException::class) fun InputStream?.readFileWithNewLineFromResources(): String { return this?.bufferedReader() .use { bufferReader -> val builder = StringBuilder() var str: String? = bufferReader?.readLine() while (str != null) { builder.append(str) str = bufferReader?.readLine() } builder.toString().replace(" ", "") } } ================================================ FILE: android/walletsdk/src/test/java/com/coinbase/android/nativesdk/message/request/Web3JsonRPCTest.kt ================================================ package com.coinbase.android.nativesdk.message.request import com.coinbase.android.nativesdk.helper.readFileWithNewLineFromResources import io.kotest.matchers.shouldBe import org.junit.Test private const val typedData = "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"},{\"name\":\"salt\",\"type\":\"bytes32\"}],\"Bid\":[{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"bidder\",\"type\":\"Identity\"}],\"Identity\":[{\"name\":\"userId\",\"type\":\"uint256\"},{\"name\":\"wallet\",\"type\":\"address\"}]},\"domain\":{\"name\":\"DApp Browser Test DApp\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0x1C56346CD2A2Bf3202F771f50d3D14a367B48070\",\"salt\":\"0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558\"},\"primaryType\":\"Bid\",\"message\":{\"amount\":100,\"bidder\":{\"userId\":323,\"wallet\":\"0x3333333333333333333333333333333333333333\"}}}" class Web3JsonRPCTest { @Test fun action_Test_Eth_Request_Accounts_Action() { val expectedAction = Action( method = "eth_requestAccounts", paramsJson = "{\"#wsegue_type\":\"eth_requestAccounts\"}", optional = false ) val action = Web3JsonRPC.RequestAccounts().action() action shouldBe expectedAction } @Test fun action_Test_Personal_Sign_Action() { val inputStream = javaClass.classLoader?.getResourceAsStream("personal_sign.json") val personalSignJson = inputStream.readFileWithNewLineFromResources() val expectedAction = Action( method = "personal_sign", paramsJson = personalSignJson, optional = false ) val action = Web3JsonRPC.PersonalSign("0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", "hello").action() action shouldBe expectedAction } @Test fun action_Test_Sign_TypedData_V3_Action() { val inputStream = javaClass.classLoader?.getResourceAsStream("sign_typed_data_v3.json") val paramsJson = inputStream.readFileWithNewLineFromResources() val expectedAction = Action( method = "eth_signTypedData_v3", paramsJson = paramsJson.replace("DAppBrowserTestDApp", "DApp Browser Test DApp"), optional = false ) val action = Web3JsonRPC.SignTypedDataV3("0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", typedData).action() action shouldBe expectedAction } @Test fun action_Test_Sign_TypedData_V4_Action() { val inputStream = javaClass.classLoader?.getResourceAsStream("sign_typed_data_v4.json") val paramsJson = inputStream.readFileWithNewLineFromResources() val expectedAction = Action( method = "eth_signTypedData_v4", paramsJson = paramsJson.replace("DAppBrowserTestDApp", "DApp Browser Test DApp"), optional = false ) val action = Web3JsonRPC.SignTypedDataV4("0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", typedData).action() action shouldBe expectedAction } @Test fun action_Test_Sign_Transaction_Action() { val inputStream = javaClass.classLoader?.getResourceAsStream("sign_transaction.json") val paramsJson = inputStream.readFileWithNewLineFromResources() val expectedAction = Action( method = ETH_SIGN_TRANSACTION, paramsJson = paramsJson, optional = false ) val action = Web3JsonRPC.SignTransaction( fromAddress = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", toAddress = "0x571a6a108adb08f9ca54fe8605280f9ee0ed4af6", weiValue = "10000000000000", data = "0x", nonce = null, gasPriceInWei = null, maxFeePerGas = null, maxPriorityFeePerGas = null, gasLimit = "1000", chainId = "1", ).action() action shouldBe expectedAction } @Test fun action_Test_Send_Transaction_Action() { val inputStream = javaClass.classLoader?.getResourceAsStream("send_transaction.json") val paramsJson = inputStream.readFileWithNewLineFromResources() val expectedAction = Action( method = ETH_SEND_TRANSACTION, paramsJson = paramsJson, optional = false ) val action = Web3JsonRPC.SendTransaction( fromAddress = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", toAddress = "0x571a6a108adb08f9ca54fe8605280f9ee0ed4af6", weiValue = "10000000000000", data = "0x", nonce = null, gasPriceInWei = null, maxFeePerGas = null, maxPriorityFeePerGas = null, gasLimit = "1000", chainId = "1", ).action() action shouldBe expectedAction } @Test fun action_Test_Switch_Chain_Action() { val expectedAction = Action( method = WALLET_SWITCH_ETHEREUM_CHAIN, paramsJson = "{\"#wsegue_type\":\"wallet_switchEthereumChain\",\"chainId\":\"1\"}", optional = false ) val action = Web3JsonRPC.SwitchEthereumChain(chainId = "1").action() action shouldBe expectedAction } @Test fun action_Test_Add_Chain_Action() { val inputStream = javaClass.classLoader?.getResourceAsStream("add_chain.json") val paramsJson = inputStream.readFileWithNewLineFromResources() val expectedAction = Action( method = WALLET_ADD_ETHEREUM_CHAIN, paramsJson = paramsJson, optional = false ) val action = Web3JsonRPC.AddEthereumChain(chainId = "137").action() action shouldBe expectedAction } @Test fun action_Test_Watch_Asset_Action() { val inputStream = javaClass.classLoader?.getResourceAsStream("watch_asset.json") val paramsJson = inputStream.readFileWithNewLineFromResources() val expectedAction = Action( method = WALLET_WATCH_ASSET, paramsJson = paramsJson, optional = false ) val action = Web3JsonRPC.WatchAsset( type = "type", options = WatchAssetOptions( address = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", symbol = "APE", decimals = 3, image = "image_site" ) ).action() action shouldBe expectedAction } } ================================================ FILE: android/walletsdk/src/test/resources/add_chain.json ================================================ { "#wsegue_type": "wallet_addEthereumChain", "chainId": "137", "blockExplorerUrls": null, "chainName": null, "iconUrls": null, "nativeCurrency": null, "rpcUrls": [] } ================================================ FILE: android/walletsdk/src/test/resources/personal_sign.json ================================================ { "#wsegue_type": "personal_sign", "address": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", "message": "hello" } ================================================ FILE: android/walletsdk/src/test/resources/send_transaction.json ================================================ { "#wsegue_type": "eth_sendTransaction", "fromAddress": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", "toAddress": "0x571a6a108adb08f9ca54fe8605280f9ee0ed4af6", "weiValue": "10000000000000", "data": "0x", "nonce": null, "gasPriceInWei": null, "maxFeePerGas": null, "maxPriorityFeePerGas": null, "gasLimit": "1000", "chainId": "1" } ================================================ FILE: android/walletsdk/src/test/resources/sign_transaction.json ================================================ { "#wsegue_type": "eth_signTransaction", "fromAddress": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", "toAddress": "0x571a6a108adb08f9ca54fe8605280f9ee0ed4af6", "weiValue": "10000000000000", "data": "0x", "nonce": null, "gasPriceInWei": null, "maxFeePerGas": null, "maxPriorityFeePerGas": null, "gasLimit": "1000", "chainId": "1" } ================================================ FILE: android/walletsdk/src/test/resources/sign_typed_data_v3.json ================================================ { "#wsegue_type": "eth_signTypedData_v3", "address": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", "typedDataJson": "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"},{\"name\":\"salt\",\"type\":\"bytes32\"}],\"Bid\":[{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"bidder\",\"type\":\"Identity\"}],\"Identity\":[{\"name\":\"userId\",\"type\":\"uint256\"},{\"name\":\"wallet\",\"type\":\"address\"}]},\"domain\":{\"name\":\"DApp Browser Test DApp\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0x1C56346CD2A2Bf3202F771f50d3D14a367B48070\",\"salt\":\"0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558\"},\"primaryType\":\"Bid\",\"message\":{\"amount\":100,\"bidder\":{\"userId\":323,\"wallet\":\"0x3333333333333333333333333333333333333333\"}}}" } ================================================ FILE: android/walletsdk/src/test/resources/sign_typed_data_v4.json ================================================ { "#wsegue_type": "eth_signTypedData_v4", "address": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", "typedDataJson": "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"},{\"name\":\"salt\",\"type\":\"bytes32\"}],\"Bid\":[{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"bidder\",\"type\":\"Identity\"}],\"Identity\":[{\"name\":\"userId\",\"type\":\"uint256\"},{\"name\":\"wallet\",\"type\":\"address\"}]},\"domain\":{\"name\":\"DApp Browser Test DApp\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0x1C56346CD2A2Bf3202F771f50d3D14a367B48070\",\"salt\":\"0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558\"},\"primaryType\":\"Bid\",\"message\":{\"amount\":100,\"bidder\":{\"userId\":323,\"wallet\":\"0x3333333333333333333333333333333333333333\"}}}" } ================================================ FILE: android/walletsdk/src/test/resources/watch_asset.json ================================================ { "#wsegue_type": "wallet_watchAsset", "type": "type", "options": { "address": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", "symbol": "APE", "decimals": 3, "image": "image_site" } } ================================================ FILE: docs/.gitignore ================================================ # Dependencies /node_modules # Production /build # Generated files .docusaurus .cache-loader # Misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: docs/babel.config.js ================================================ module.exports = { presets: [require.resolve('@docusaurus/core/lib/babel/preset')], }; ================================================ FILE: docs/docs/client-sdk/android-api-reference.md ================================================ --- title: "API Reference" slug: "android-api-reference" category: "633d1d37bc7103008654c123" --- # Actions The `initiateHandshake` and `makeRequest` methods accept a list of actions to perform. An `Action` can be created using the `Web3JsonRPC` class. Below is a list of supported actions for each method: | Action | RPC method | initiateHandshake | makeRequest | | :--- | :--- | :--- | :--- | | [RequestAccounts](doc:android-api-reference#requestaccounts) | [eth_requestAccounts](https://eips.ethereum.org/EIPS/eip-1102) | ✔️ Supported | ✔️ Supported | | [SignTransaction](doc:android-api-reference#signtransaction) | [eth_signTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_signtransaction) | ❌ Not supported | ✔️ Supported | | [SendTransaction](doc:android-api-reference#sendtransaction) | [eth_sendTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction) | ❌ Not supported | ✔️ Supported | | - | [eth_sign](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign) | ❌ Not supported | ❌ Not supported | | [PersonalSign](doc:android-api-reference#personalsign) | [personal_sign](https://eips.ethereum.org/EIPS/eip-191) | ✔️ Supported | ✔️ Supported | | [SignTypedDataV3](doc:android-api-reference#signtypeddatav3) | [eth_signTypedData_v3](https://eips.ethereum.org/EIPS/eip-712) | ✔️ Supported | ✔️ Supported | | [SignTypedDataV4](doc:android-api-reference#signtypeddatav4) | [eth_signTypedData_v4](https://eips.ethereum.org/EIPS/eip-712) | ✔️ Supported | ✔️ Supported | | [SwitchEthereumChain](doc:android-api-reference#switchethereumchain) | [wallet_switchEthereumChain](https://eips.ethereum.org/EIPS/eip-3326) | ✔️ Supported | ✔️ Supported | | [AddEthereumChain](doc:android-api-reference#addethereumchain) | [wallet_addEthereumChain](https://eips.ethereum.org/EIPS/eip-3085) | ✔️ Supported | ✔️ Supported | | [WatchAsset](doc:android-api-reference#watchasset) | [wallet_watchAsset](https://eips.ethereum.org/EIPS/eip-747) | ✔️ Supported | ✔️ Supported | ## RequestAccounts Request that the user provides an account in the form of an Ethereum address. ### Parameters None. ### Example ```kotlin Kotlin val requestAccounts = Web3JsonRPC.RequestAccounts().action() ``` ```java Java Action requestAccounts = new Web3JsonRPC.RequestAccounts().action(false); ``` ## PersonalSign Sign a message by calculating an Ethereum specific signature with: `sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. Adding a prefix to the message makes the calculated signature recognisable as an Ethereum specific signature. This prevents misuse where a malicious DApp can sign arbitrary data (e.g. transaction) and use the signature to impersonate the victim. See [personal_sign](https://eips.ethereum.org/EIPS/eip-191). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | address | `String` | Address to sign data with. | | message | `String` | Message data to sign. | ### Example ```kotlin Kotlin val personalSign = Web3JsonRPC.PersonalSign( address = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" message = "0xdeadbeaf" ).action() ``` ```java Java Action personalSign = new Web3JsonRPC.PersonalSign( "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", "0xdeadbeaf") .action(false); ``` ## SignTypedDataV3 Sign typed structured data. See [eth_signTypedData_v3](https://eips.ethereum.org/EIPS/eip-712). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | address | `String` | Address to sign data with. | | typedDataJson | `String` | Typed data to sign. Structured according to the JSON-Schema specified in [EIP-712](https://eips.ethereum.org/EIPS/eip-712). | ### Example ```kotlin Kotlin val signTypedDataV3 = Web3JsonRPC.SignTypedDataV3( address = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", typedDataJson = typedData ).action() ``` ```java Java Action signTypedDataV3 = new Web3JsonRPC.SignTypedDataV3( "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", typedData) .action(false); ``` ## SignTypedDataV4 Sign typed structured data. See [eth_signTypedData_v4](https://eips.ethereum.org/EIPS/eip-712). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | address | `String` | Address to sign data with. | | typedDataJson | `String` | Typed data to sign. Structured according to the JSON-Schema specified in [EIP-712](https://eips.ethereum.org/EIPS/eip-712). | ### Example ```kotlin Kotlin val signTypedDataV4 = Web3JsonRPC.SignTypedDataV4( address = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", typedDataJson = typedData ).action() ``` ```java Java Action signTypedDataV4 = new Web3JsonRPC.SignTypedDataV4( "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", typedData) .action(false); ``` ## SignTransaction Sign a transaction that can be submitted to the network at a later time. See [eth_signTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_signtransaction). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | fromAddress | `String` | Address the transaction is sent from. | | toAddress | `String` | **Optional**. Address the transaction is sent to. | | weiValue | `BigInt` | Value for the transaction, in Wei. | | data | `String` | Compiled code of a contract or the hash of the invoked method signature and encoded parameters. | | nonce | `Int` | **Optional**. Nonce of the transaction. Allows for overwriting pending transactions that use an identical nonce. | | gasPriceInWei | `BigInt` | **Optional**. Gas price for the transaction, in Wei. | | maxFeePerGas | `BigInt` | **Optional**. Maximum fee per unit of gas for the transaction. | | maxPriorityFeePerGas | `BigInt` | **Optional**. Maximum priority fee per unit of gas for the transaction. | | gasLimit | `BigInt` | **Optional**. Gas limit for the transaction. | | chainId | `String` | Chain ID for the transaction, as an integer string. | ### Example ```kotlin Kotlin val signTransaction = Web3JsonRPC.SignTransaction( fromAddress = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", toAddress = "0x000000000000000000000000000000000000dEaD", weiValue = "10000000000000", data = "0x", nonce = 1, gasPriceInWei = "30000000000", maxFeePerGas = "60000000000", maxPriorityFeePerGas = "2500000000", gasLimit = "1000", chainId = "1" ).action() ``` ```java Java Action signTransaction = new Web3JsonRPC.SignTransaction( "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", // fromAddress "0x000000000000000000000000000000000000dEaD", // toAddress "10000000000000", // weiValue "0x", // data 1, // nonce "30000000000", // gasPriceInWei "60000000000", // maxFeePerGas "2500000000", // maxPriorityFeePerGas "1000", // gasLimit "1") // chainId .action(false); ``` ## SendTransaction Send a transaction, or create a contract if the `data` field contains code. See [eth_sendTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | fromAddress | `String` | Address the transaction is sent from. | | toAddress | `String` | **Optional**. Address the transaction is sent to. | | weiValue | `BigInt` | Value for the transaction, in Wei. | | data | `String` | Compiled code of a contract or the hash of the invoked method signature and encoded parameters. | | nonce | `Int` | **Optional**. Nonce of the transaction. Allows for overwriting pending transactions that use an identical nonce. | | gasPriceInWei | `BigInt` | **Optional**. Gas price for the transaction, in Wei. | | maxFeePerGas | `BigInt` | **Optional**. Maximum fee per unit of gas for the transaction. | | maxPriorityFeePerGas | `BigInt` | **Optional**. Maximum priority fee per unit of gas for the transaction. | | gasLimit | `BigInt` | **Optional**. Gas limit for the transaction. | | chainId | `String` | Chain ID for the transaction, as an integer string. | ### Example ```kotlin Kotlin val sendTransaction = Web3JsonRPC.SendTransaction( fromAddress = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", toAddress = "0x000000000000000000000000000000000000dEaD", weiValue = "10000000000000", data = "0x", nonce = 1, gasPriceInWei = "30000000000", maxFeePerGas = "60000000000", maxPriorityFeePerGas = "2500000000", gasLimit = "1000", chainId = "1" ).action() ``` ```java Java Action sendTransaction = new Web3JsonRPC.SendTransaction( "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", // fromAddress "0x000000000000000000000000000000000000dEaD", // toAddress "10000000000000", // weiValue "0x", // data 1, // nonce "30000000000", // gasPriceInWei "60000000000", // maxFeePerGas "2500000000", // maxPriorityFeePerGas "1000", // gasLimit "1") // chainId .action(false); ``` ## SwitchEthereumChain Switch a wallet’s currently active chain. See [wallet_switchEthereumChain](https://eips.ethereum.org/EIPS/eip-3326). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | chainId | `String` | ID of the chain to switch to, as an integer string. | ### Example ```kotlin Kotlin val switchEthereumChain = Web3JsonRPC.SwitchEthereumChain( chainId = "1666600000" ).action() ``` ```java Java Action switchEthereumChain = new Web3JsonRPC.SwitchEthereumChain( "1666600000") // chainId .action(false); ``` ## AddEthereumChain Add a chain to a wallet. See [wallet_addEthereumChain](https://eips.ethereum.org/EIPS/eip-3085). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | chainId | `String` | ID of the chain to add, as an integer string. | | blockExplorerUrls | `List` | **Optional**. List of block explorer URL strings. | | chainName | `String` | **Optional**. Name of the chain to add. | | iconUrls | `List` | **Optional**. List of image icons URL strings. | | nativeCurrency | [`AddChainNativeCurrency`](doc:android-api-reference#addchainnativecurrency) | **Optional**. Data for the chain’s native currency. | | rpcUrls | `List` | List of RPC URL strings. Defaults to an empty list. | ### Example ```kotlin Kotlin val addEthereumChain = Web3JsonRPC.AddEthereumChain( chainId = "1666600000", blockExplorerUrls = listOf("https://explorer.harmony.one"), chainName = "Harmony Mainnet", iconUrls = listOf("https://harmonynews.one/wp-content/uploads/2019/11/slfdjs.png"), nativeCurrency = AddChainNativeCurrency("ONE", "ONE", 18) ).action() ``` ```java Java Action addEthereumChain = new Web3JsonRPC.AddEthereumChain( "1666600000", // chainId List.of("https://explorer.harmony.one"), // blockExplorerUrls "Harmony Mainnet", // chainName List.of("https://harmonynews.one/wp-content/uploads/2019/11/slfdjs.png"), // iconUrls new AddChainNativeCurrency("ONE", "ONE", 18)) // nativeCurrency .action(false); ``` ## WatchAsset Add and track a new asset within a wallet. See [wallet_watchAsset](https://eips.ethereum.org/EIPS/eip-747). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | type | `String` | Type of token asset. (i.e. `ERC20`, `ERC721`). | | options | [`WatchAssetOptions`](doc:android-api-reference#watchassetoptions) | Data of the asset to watch (i.e. contract address, name, icon, etc.) | ### Example ```kotlin Kotlin val watchAsset = Web3JsonRPC.WatchAsset( type = "ERC20", options = WatchAssetOptions( "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a", "WONE", 18, "https://s2.coinmarketcap.com/static/img/coins/64x64/11696.png" ) ).action() ``` ```java Java Action watchAsset = new Web3JsonRPC.WatchAsset( "ERC20", // type new WatchAssetOptions( // options "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a", // address "WONE", // symbol 18, // decimals "https://s2.coinmarketcap.com/static/img/coins/64x64/11696.png") // image ).action(false); ``` # Types ## AddChainNativeCurrency Defines a native currency to add when making a request to add a new Ethereum chain. See [AddEthereumChain](doc:android-api-reference#addethereumchain). ### Properties | Name | Type | Description | | :--- | :--- | :--- | | name | `String` | Name of native currency for the chain. | | symbol | `String` | Symbol of native currency for the chain. | | decimals | `Int` | Decimals of precision, as an integer. | ### Example ```kotlin Kotlin val nativeCurrency = AddChainNativeCurrency("ONE", "ONE", 18) ``` ```java Java AddChainNativeCurrency nativeCurrency = new AddChainNativeCurrency("ONE", "ONE", 18); ``` ## WatchAssetOptions Defines options when making a request to watch a new asset. See [WatchAsset](doc:android-api-reference#watchasset). ### Properties | Name | Type | Description | | :--- | :--- | :--- | | address | `String` | Contract address for the token asset. | | symbol | `String` | **Optional**. Symbol for the token asset. | | decimals | `Int` | **Optional**. Decimals of precision, as an integer. | | image | `String` | **Optional**. Logo image for the token asset. | ### Example ```kotlin Kotlin val watchAssetOptions = WatchAssetOptions( address = "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a", symbol = "WONE", decimals = 18, image = "https://s2.coinmarketcap.com/static/img/coins/64x64/11696.png" ) ``` ```java Java WatchAssetOptions watchAssetOptions = new WatchAssetOptions( "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a", // address "WONE", // symbol 18, // decimals "https://s2.coinmarketcap.com/static/img/coins/64x64/11696.png") // image ); ``` ================================================ FILE: docs/docs/client-sdk/android-establishing-a-connection.md ================================================ --- title: "Establishing a connection" slug: "android-establishing-a-connection" category: "633d1d37bc7103008654c123" --- A connection to Coinbase Wallet can be initiated by calling the `initiateHandshake` function provided by the SDK. The function also takes in an optional `initialActions` parameter which apps can use to take certain actions along with the initial handshake request. ```kotlin Kotlin val requestAccount = Web3JsonRPC.RequestAccounts().action() val handShakeActions = listOf(requestAccount) client.initiateHandshake( initialActions = handShakeActions ) { result: Result>, account: Account? -> result.onSuccess { actionResults: List -> actionResults.handleSuccess("Handshake", handShakeActions, account) } result.onFailure { err -> err.handleError("HandShake") } } ``` ```java Java // requestAccounts request ArrayList actions = new ArrayList<>(); actions.add( new Web3JsonRPC.RequestAccounts().action(false) ); // Initiate handshake client.initiateHandshake( actions, (results, account) -> { for (ActionResult result : results) { if (result instanceof ActionResult.Result) { ((ActionResult.Result) result).getValue(); } if (result instanceof ActionResult.Error) { ((ActionResult.Error) result).getCode(); ((ActionResult.Error) result).getMessage(); } } }, error -> { } ); ``` An example handshake request is provided in the [sample application](https://github.com/coinbase/wallet-mobile-sdk/blob/master/android/example/src/main/java/com/coinbase/android/beta/MainActivity.kt#L52). ================================================ FILE: docs/docs/client-sdk/android-install.md ================================================ --- title: "Install" slug: "android-install" category: "633d1d37bc7103008654c123" --- The Coinbase Wallet Mobile SDK is available on [Maven Central](https://search.maven.org/artifact/com.coinbase/coinbase-wallet-sdk/0.1.0/aar). ## Gradle Add Coinbase Wallet SDK to your `build.gradle` file. ```groovy repositories { mavenCentral() } dependencies { implementation "com.coinbase:coinbase-wallet-sdk:1.0.3" } ``` ## Maven Add Coinbase Wallet SDK to your `pom.xml` file. ```xml com.coinbase coinbase-wallet-sdk 1.0.3 ``` ================================================ FILE: docs/docs/client-sdk/android-making-requests.md ================================================ --- title: "Making requests" slug: "android-making-requests" category: "633d1d37bc7103008654c123" --- Requests to Coinbase Wallet can be made by calling the `makeRequest` function provided by the SDK. This function also accepts a list of actions that can be taken in as a single batch request. ```kotlin Kotlin val signTypedDataV3 = Web3JsonRPC.SignTypedDataV3( "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", // address "" // typed data JSON ).action() val requestActions = listOf(signTypedDataV3) client.makeRequest(request = RequestContent.Request(actions = requestActions)) { result -> result.fold( onSuccess = { returnValues -> returnValues.handleSuccess("Request", requestActions) }, onFailure = { err -> err.handleError("Request") } ) } ``` ================================================ FILE: docs/docs/client-sdk/android-setup.md ================================================ --- title: "Setup" slug: "android-setup" category: "633d1d37bc7103008654c123" --- In order for your app to interact with Coinbase Wallet, you must add a [queries element](https://developer.android.com/guide/topics/manifest/queries-element) to your `AndroidManifest.xml` file, specifying the package name for Coinbase Wallet, `org.toshi`. ```xml AndroidManifest.xml ``` Before the SDK can be used, it needs to be configured with an App Link to your application. This callback URL will be used by the Coinbase Wallet application to navigate back to your application. ```kotlin Kotlin CoinbaseWalletSDK( appContext = applicationContext, domain = Uri.parse("https://www.myappxyz.com"), openIntent = { intent -> launcher.launch(intent) } ) ``` ```java Java new CoinbaseWalletSDK( Uri.parse("https://www.myappxyz.com"), getApplicationContext(), CBW_PACKAGE_NAME, intent -> { startActivityForResult(intent, CBW_ACTIVITY_RESULT_CODE); } ); ``` When your application receives a response from Coinbase Wallet via App Links, this URL needs to be handed off to the SDK via the `handleResponse` function. ```kotlin Kotlin launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val uri = result.data?.data ?: return@registerForActivityResult client.handleResponse(uri) } ``` ```java Java @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode != CBW_ACTIVITY_RESULT_CODE) { return; } if (data == null) { return; } Uri url = data.getData(); client.handleResponse(url); } ``` An example is provided in our [sample application](https://github.com/coinbase/wallet-mobile-sdk/blob/master/android/example/src/main/java/com/coinbase/android/beta/MainActivity.kt#L27). ================================================ FILE: docs/docs/client-sdk/ios-api-reference.md ================================================ --- title: "API Reference" slug: "ios-api-reference" category: "633d1d37bc7103008654c123" --- # Actions The `initiateHandshake` and `makeRequest` methods accept a list of actions to perform. An `Action` can be created using the `Web3JsonRPC` class. Below is a list of supported actions for each method: | Action | RPC method | initiateHandshake | makeRequest | | :--- | :--- | :--- | :--- | | [RequestAccounts](doc:ios-api-reference#requestaccounts) | [eth_requestAccounts](https://eips.ethereum.org/EIPS/eip-1102) | ✔️ Supported | ✔️ Supported | | [SignTransaction](doc:ios-api-reference#signtransaction) | [eth_signTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_signtransaction) | ❌ Not supported | ✔️ Supported | | [SendTransaction](doc:ios-api-reference#sendtransaction) | [eth_sendTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction) | ❌ Not supported | ✔️ Supported | | - | [eth_sign](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign) | ❌ Not supported | ❌ Not supported | | [PersonalSign](doc:ios-api-reference#personalsign) | [personal_sign](https://eips.ethereum.org/EIPS/eip-191) | ✔️ Supported | ✔️ Supported | | [SignTypedDataV3](doc:ios-api-reference#signtypeddatav3) | [eth_signTypedData_v3](https://eips.ethereum.org/EIPS/eip-712) | ✔️ Supported | ✔️ Supported | | [SignTypedDataV4](doc:ios-api-reference#signtypeddatav4) | [eth_signTypedData_v4](https://eips.ethereum.org/EIPS/eip-712) | ✔️ Supported | ✔️ Supported | | [SwitchEthereumChain](doc:ios-api-reference#switchethereumchain) | [wallet_switchEthereumChain](https://eips.ethereum.org/EIPS/eip-3326) | ✔️ Supported | ✔️ Supported | | [AddEthereumChain](doc:ios-api-reference#addethereumchain) | [wallet_addEthereumChain](https://eips.ethereum.org/EIPS/eip-3085) | ✔️ Supported | ✔️ Supported | | [WatchAsset](doc:ios-api-reference#watchasset) | [wallet_watchAsset](https://eips.ethereum.org/EIPS/eip-747) | ✔️ Supported | ✔️ Supported | ## RequestAccounts Request that the user provides an account in the form of an Ethereum address. ### Parameters None. ### Example ```swift let requestAccounts = Action(jsonRpc: .eth_requestAccounts) ``` ## PersonalSign Sign a message by calculating an Ethereum specific signature with: `sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. Adding a prefix to the message makes the calculated signature recognisable as an Ethereum specific signature. This prevents misuse where a malicious DApp can sign arbitrary data (e.g. transaction) and use the signature to impersonate the victim. See [personal_sign](https://eips.ethereum.org/EIPS/eip-191). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | address | `String` | Address to sign data with. | | message | `String` | Message data to sign. | ### Example ```swift let personalSign = Action(jsonRpc: .personal_sign( address: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", message: "0xdeadbeaf")) ``` ## SignTypedDataV3 Sign typed structured data. See [eth_signTypedData_v3](https://eips.ethereum.org/EIPS/eip-712). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | address | `String` | Address to sign data with. | | typedDataJson | `String` | Typed data to sign. Structured according to the JSON-Schema specified in [EIP-712](https://eips.ethereum.org/EIPS/eip-712). | ### Example ```swift let signTypedDataV3 = Action(jsonRpc: .eth_signTypedData_v3( address: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", typedDataJson: JSONString(encode: typedData)!)) ``` ## SignTypedDataV4 Sign typed structured data. See [eth_signTypedData_v4](https://eips.ethereum.org/EIPS/eip-712). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | address | `String` | Address to sign data with. | | typedDataJson | `String` | Typed data to sign. Structured according to the JSON-Schema specified in [EIP-712](https://eips.ethereum.org/EIPS/eip-712). | ### Example ```swift let signTypedDataV4 = Action(jsonRpc: .eth_signTypedData_v4( address: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", typedDataJson: JSONString(encode: typedData)!)) ``` ## SignTransaction Sign a transaction that can be submitted to the network at a later time. See [eth_signTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_signtransaction). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | fromAddress | `String` | Address the transaction is sent from. | | toAddress | `String` | **Optional**. Address the transaction is sent to. | | weiValue | `BigInt` | Value for the transaction, in Wei. | | data | `String` | Compiled code of a contract or the hash of the invoked method signature and encoded parameters. | | nonce | `Int` | **Optional**. Nonce of the transaction. Allows for overwriting pending transactions that use an identical nonce. | | gasPriceInWei | `BigInt` | **Optional**. Gas price for the transaction, in Wei. | | maxFeePerGas | `BigInt` | **Optional**. Maximum fee per unit of gas for the transaction. | | maxPriorityFeePerGas | `BigInt` | **Optional**. Maximum priority fee per unit of gas for the transaction. | | gasLimit | `BigInt` | **Optional**. Gas limit for the transaction. | | chainId | `String` | Chain ID for the transaction, as an integer string. | ### Example ```swift let signTransaction = Action(jsonRpc: .eth_signTransaction( fromAddress: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", toAddress: "0x000000000000000000000000000000000000dEaD", weiValue: "10000000000000", data: "0x", nonce: 1, gasPriceInWei: "30000000000", maxFeePerGas: "60000000000", maxPriorityFeePerGas: "2500000000", gasLimit: "1000", chainId: "1")) ``` ## SendTransaction Send a transaction, or create a contract if the `data` field contains code. See [eth_sendTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | fromAddress | `String` | Address the transaction is sent from. | | toAddress | `String` | **Optional**. Address the transaction is sent to. | | weiValue | `BigInt` | Value for the transaction, in Wei. | | data | `String` | Compiled code of a contract or the hash of the invoked method signature and encoded parameters. | | nonce | `Int` | **Optional**. Nonce of the transaction. Allows for overwriting pending transactions that use an identical nonce. | | gasPriceInWei | `BigInt` | **Optional**. Gas price for the transaction, in Wei. | | maxFeePerGas | `BigInt` | **Optional**. Maximum fee per unit of gas for the transaction. | | maxPriorityFeePerGas | `BigInt` | **Optional**. Maximum priority fee per unit of gas for the transaction. | | gasLimit | `BigInt` | **Optional**. Gas limit for the transaction. | | chainId | `String` | Chain ID for the transaction, as an integer string. | ### Example ```swift let sendTransaction = Action(jsonRpc: .eth_sendTransaction( fromAddress: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", toAddress: "0x000000000000000000000000000000000000dEaD", weiValue: "10000000000000", data: "0x", nonce: 1, gasPriceInWei: "30000000000", maxFeePerGas: "60000000000", maxPriorityFeePerGas: "2500000000", gasLimit: "1000", chainId: "1")) ``` ## SwitchEthereumChain Switch a wallet’s currently active chain. See [wallet_switchEthereumChain](https://eips.ethereum.org/EIPS/eip-3326). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | chainId | `String` | ID of the chain to switch to, as an integer string. | ### Example ```swift let switchEthereumChain = Action(jsonRpc: .wallet_switchEthereumChain(chainId: "1666600000")) ``` ## AddEthereumChain Add a chain to a wallet. See [wallet_addEthereumChain](https://eips.ethereum.org/EIPS/eip-3085). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | chainId | `String` | ID of the chain to add, as an integer string. | | blockExplorerUrls | `List` | **Optional**. List of block explorer URL strings. | | chainName | `String` | **Optional**. Name of the chain to add. | | iconUrls | `List` | **Optional**. List of image icons URL strings. | | nativeCurrency | [`AddChainNativeCurrency`](doc:ios-api-reference#addchainnativecurrency) | **Optional**. Data for the chain’s native currency. | | rpcUrls | `List` | List of RPC URL strings. Defaults to an empty list. | ### Example ```swift let addEthereumChain = Action(jsonRpc: .wallet_addEthereumChain( chainId: "1666600000", blockExplorerUrls: ["https://explorer.harmony.one"], chainName: "Harmony Mainnet", iconUrls: ["https://harmonynews.one/wp-content/uploads/2019/11/slfdjs.png"], nativeCurrency: AddChainNativeCurrency( name: "ONE", symbol: "ONE", decimals: 18) )) ``` ## WatchAsset Add and track a new asset within a wallet. See [wallet_watchAsset](https://eips.ethereum.org/EIPS/eip-747). ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | type | `String` | Type of token asset. (i.e. `ERC20`, `ERC721`). | | options | [`WatchAssetOptions`](doc:ios-api-reference#watchassetoptions) | Data of the asset to watch (i.e. contract address, name, icon, etc.) | ### Example ```swift let watchAsset = Action(jsonRpc: .wallet_watchAsset( type: "ERC20", options: WatchAssetOptions( address: "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a", symbol: "WONE", decimals: 18, image: "https://s2.coinmarketcap.com/static/img/coins/64x64/11696.png") )) ``` # Types ## AddChainNativeCurrency Defines a native currency to add when making a request to add a new Ethereum chain. See [AddEthereumChain](doc:ios-api-reference#addethereumchain). ### Properties | Name | Type | Description | | :--- | :--- | :--- | | name | `String` | Name of native currency for the chain. | | symbol | `String` | Symbol of native currency for the chain. | | decimals | `Int` | Decimals of precision, as an integer. | ### Example ```swift let nativeCurrency = AddChainNativeCurrency(name: "ONE", symbol: "ONE", decimals: 18) ``` ## WatchAssetOptions Defines options when making a request to watch a new asset. See [WatchAsset](doc:ios-api-reference#watchasset). ### Properties | Name | Type | Description | | :--- | :--- | :--- | | address | `String` | Contract address for the token asset. | | symbol | `String` | **Optional**. Symbol for the token asset. | | decimals | `Int` | **Optional**. Decimals of precision, as an integer. | | image | `String` | **Optional**. Logo image for the token asset. | ### Example ```swift let watchAssetOptions = WatchAssetOptions( address: "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a", symbol: "WONE", decimals: 18, image: "https://s2.coinmarketcap.com/static/img/coins/64x64/11696.png") ``` ================================================ FILE: docs/docs/client-sdk/ios-establishing-a-connection.md ================================================ --- title: "Establishing a connection" slug: "ios-establishing-a-connection" category: "633d1d37bc7103008654c123" --- A connection to Coinbase Wallet can be initiated by calling the `initiateHandshake` function provided by the SDK. The function also takes in an optional `initialActions` parameter which apps can use to take certain actions along with the initial handshake request. ```swift private let cbwallet = CoinbaseWalletSDK.shared cbwallet.initiateHandshake( initialActions: [ Action(jsonRpc: .eth_requestAccounts) ] ) { result, account in switch result { case .success(let response): self.logObject(label: "Response:\n", response) guard let account = account else { return } self.logObject(label: "Account:\n", account) self.address = account.address case .failure(let error): self.log("\(error)") } self.updateSessionStatus() } ``` An example handshake request is provided in our [sample application](https://github.com/coinbase/wallet-mobile-sdk/blob/master/ios/example/SampleClient/ViewController.swift#L63). ================================================ FILE: docs/docs/client-sdk/ios-install.md ================================================ --- title: "Install" slug: "ios-install" category: "633d1d37bc7103008654c123" --- The Coinbase Wallet Mobile SDK is available on both [CocoaPods](https://cocoapods.org/) and [Swift Package Manager](https://swift.org/package-manager). ## Cocoapods Add Coinbase Wallet SDK to your `Podfile`. ```ruby use_frameworks! target 'YOUR_TARGET_NAME' do pod 'CoinbaseWalletSDK', '1.0.3' end ``` Replace `YOUR_TARGET_NAME`, and then in the `Podfile` directory run: ```bash pod install ``` ## Swift Package Manager Add Coinbase Wallet SDK to your `Package.swift` file. Under **File > Add packages…** enter the package url: [https://github.com/coinbase/wallet-mobile-sdk](https://github.com/coinbase/wallet-mobile-sdk) ```swift import PackageDescription let package = Package( name: "YOUR_PROJECT_NAME", dependencies: [ .package(url: "https://github.com/coinbase/wallet-mobile-sdk.git", from: "1.0.3"), ] ) ``` Replace `YOUR_PROJECT_NAME`, and then run: ```bash swift build ``` ================================================ FILE: docs/docs/client-sdk/ios-making-requests.md ================================================ --- title: "Making requests" slug: "ios-making-requests" category: "633d1d37bc7103008654c123" --- Requests to Coinbase Wallet can be made by calling the `makeRequest` function provided by the SDK. This function also accepts a list of `actions` that can be taken in as a single batch request. ```swift cbwallet.makeRequest( Request(actions: [ Action(jsonRpc: .eth_signTypedData_v3( address: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", message: Data())) ]) ) { result in self.log("\(result)") } ``` An example request is provided in our [sample application](https://github.com/coinbase/coinbase-wallet-sdk/blob/master/examples/native-sdk-ios-client/SampleApp/ViewController.swift#L29). ================================================ FILE: docs/docs/client-sdk/ios-setup.md ================================================ --- title: "Setup" slug: "ios-setup" category: "633d1d37bc7103008654c123" --- Coinbase Wallet Mobile SDK uses [Universal Links](https://developer.apple.com/ios/universal-links/) to communicate between Coinbase Wallet and your application. Before the SDK can be used, it needs to be configured with a Universal Link to your application. This callback URL will be used by the Coinbase Wallet application to navigate back to your application. ```swift CoinbaseWalletSDK.configure( callback: URL(string: "https://myappxyz.com/mycallback")! ) ``` When your application receives a response from Coinbase Wallet via a Universal Link, this URL needs to be handed off to the SDK via the `handleResponse` function. ```swift func application(_ app: UIApplication, open url: URL ...) -> Bool { if (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true { return true } // handle other types of deep links return false } ``` It’s recommended to place this configuration logic in the AppDelegate as shown in this [example](https://github.com/coinbase/wallet-mobile-sdk/blob/master/ios/example/SampleClient/AppDelegate.swift#L19). ================================================ FILE: docs/docs/client-sdk/mobile-sdk-overview.md ================================================ --- title: "Overview" slug: "mobile-sdk-overview" category: "633d1d37bc7103008654c123" --- [Coinbase Wallet Mobile SDK](https://github.com/coinbase/wallet-mobile-sdk) is an open source SDK which allows you to connect your native mobile applications to millions of Coinbase Wallet users. ## Platforms The SDK is available for the following platforms: - [iOS](https://cocoapods.org/pods/CoinbaseWalletSDK) - [Android](https://mavenlibs.com/maven/dependency/com.coinbase/coinbase-wallet-sdk) We also provide wrapper libraries and modules for [React Native](https://www.npmjs.com/package/@coinbase/wallet-mobile-sdk) and [Flutter](https://pub.dev/packages/coinbase_wallet_sdk) applications. ## Features - **Easy**: Simplified and improved wallet integration for native mobile applications via deep-linking, without requiring a web application. - **Decentralized and reliable**: Doesn't depend on external services and relay servers for delivering messages and app-to-wallet communication. - **Secure**: Utilizes end-to-end encryption with secure key exchange and decentralized identity verification using the well-known URI standard for universal links. - **Efficient**: Reduces the number of hops between client applications and wallet via support for batch requests. - **Open-source**: All of the code for Coinbase Wallet Mobile SDK is open source and available on [GitHub](https://github.com/coinbase/wallet-mobile-sdk). ================================================ FILE: docs/docs/spec/batch.md ================================================ # Batch requests To improve UX by minimizing app switches, MWP allows client apps to make requests with multiple actions at once. Wallets should return results in a single response message as well. Client can specify whether each action is required or optional to customize the flow. ## `Request` has `action`s - A `request` message contains an array of `action`s - Each `action` defines a single JSON RPC call with corresponding parameters in JSON format - `optional` boolean property to tell the host wallet to cancel the request if it fails to process non-optional action. ### Example request message with three actions batched ```json { "version": "1.0.3", "sender": "AD6aqQNPr4/NRQymzqr14qjlnO9LN5JaEs/XEwEGTno=", "content": { "request": { "actions": [ { "paramsJson": "{\"chainId\":\"137\"}", "method": "wallet_switchEthereumChain", "optional": false }, { "paramsJson": "{\"address\":\"0x571a6a108adb08f9ca54fe8605280F9EE0eD4AF6\",\"message\":\"message\"}", "method": "personal_sign", "optional": true }, { "paramsJson": "{\"toAddress\":\"0\",\"fromAddress\":\"0x571a6a108adb08f9ca54fe8605280F9EE0eD4AF6\",\"chainId\":\"137\",\"weiValue\":\"0\",\"data\":\"\"}", "method": "eth_sendTransaction", "optional": false } ] } }, "timestamp": 689616588.09238, "callbackUrl": "myappxyz://mycallback/wsegue", "uuid": "84920218-ED92-4DF7-83BC-3CBFD1E5C7E3" } ``` ## `Response` has `value`s - A `response` message contains array of `value`s - `value` can be either - `result`: JSON string - `error`: error code and message ### Example coming soon ================================================ FILE: docs/docs/spec/encryption.md ================================================ # Encryption MWP uses deep links for direct communication between peers. To ensure security, all messages in MWP after secure key exchange process are end-to-end encrypted. ## Key generation Both the client and the wallet generate their own key pair for each session via [Key-agreement protocol](https://en.wikipedia.org/wiki/Key-agreement_protocol) using [Elliptic-curve](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography). Each peer stores its private key in secure persistent storage and share its public key with the other party. ## Key exchange to derive shared secret MWP uses [Diffie–Hellman key exchange](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) method to securely share cryptographic keys over deep links. This method allows the two parties to derive the same shared secret offline using its own private key and the peer's public key. ## Message `content` encryption After successful [handshake process](handshake) to exchange keys, subsequent messages ([`content` data](messages#content)) are encrypted with the common shared secret using a symmetric-key algorithm. It uses [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)-[GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) algorithm for cryptographic operations. ### Example message after encryption ```json { "version": "1.0.3", "sender": "lEC/X3K68rlgOoldMdk0D77738Y7W0mDbMMV5R6VyCE=", "content": { "request": { "data": "bgPGRoCBgH10b0IUhs8ZWRRPhFBGUlS3ESmB+xiGNb7n7oUdU9L9PGABmla/kCdnmES6iWXI7u5xzKm/CzPMGvlvlXLuRGlU4RjGJqbcQ55He3UcBXOgB0q489Vx2cx0fllrISuo87//kfcSolCWNSCHSBuSmojORG8YmO1L20724C8YKQrPxjmfWm5YyZxp/HyTnwX60eRJb819FZI4Zfc4RkTp4h+1A2lzBlERelr+MojfVBBBwxv4qQTAn7QRbUgoU+1CfblqcuB7bHUvfl+OwyZqIoxP68agkU9Na3aKDSM+oK6q9IBKG5X4Vz1+CBVchR5vggY+D7Omdaes6uDL443yV3uf8XHyv5ieWGgIS8bGgkdPvMrAtv78wdBr6iWLlh0pF7RyrPOWq/h8WiYerbAD9RFYhAxo1z3dymFgLamn/rf1LS+xu+sClKDrFx3DYkHEa/75CyQokBQiJFqO/sCTBLnXzV5I9bbjTTGuHi5tE/lKnsMsDz5tBpedavo9BT2bJNRUGh7yPMFybxV1hjyamlobBxyxik2GNjh09bINpZ4HVxe3mpmQWdg0NZiE5HG08HRHWooV+wQavMsWLqmOQtH30dy3+WMRSfURrrVdRXm0TZda6wCdU+sPbLPuvWucfdCTG62P8rGqklavjDQL8kkWAnsgMkCnAQJyoGqFHtGxjNd3rH5Pmkd7RC34fkEOsXxxKMzyOMlZjgr4a1MHO0URbrgA5k3IPdQ83Iq2GRpboshyHRyy27ClH6She9rQXUnbRI7WcGK/YCVu97rTxjWT8AzS8twI7egN6BhyricjMoUXNCewxWUBO4pp316mHtnPyktvFAZxF5Q/hbM2bcFQS2fdWooRUWlB405yv+magBC89GDbJHwBN73q3KTtYFVb5N7vDVIU71eGhL6ehve3NPcGxLKbRXWxWQnKNpIeWZ9W2mNWI5C6mbaasPbgGxW228OEs9FxjNmdA6XCScctgb8b31nM8xPOWeD+q33UIdNpQvkrZicPu7f5lGXGYibjsXnNWdO3tiOg6kuiHevt5Jqp35bO1Vn6y3UzZu6xwbUZQbE4NmR6j9BlGuNGS40Y91O/eCrFzSyayxPl6plRoekn2djBGHDyBZVwXfSKQef6gK9jV1RxQTjKbO6rDGR1hRgqWDvDeDLg03GBqwP93ZwqANK1jxpLOm0wbUycCl8grwAzTrZ0bJ68aUlT/Jmi3VAyV4QAwED8IZ0ipjLz7EmXkQWjuhChIiSwjkR6EEfctQhLK1+xvLsY796OSXG18qTAfjx2vgyGoxxrmEnufTIh4a9A+CTYQzNU137aRLXK/rN7Xs0d51xbis/FVm9ysgBNuWfxlOpVeahkvNSHzAM7Wn4WP/nlWp1yly/R/KrEE5iFi2dVxE9UMQFcbJ/xFDl+dj4hMJDFdCf1tyuP+ZCF6VSOjNUt6TCn2uXNTjIIJj2ZuH3O0YETcESMy+HwR8mmqn4DaFbbzRkZo7szOR3kGzKU8yk0fuW+McZtXfGd9YWG6wZEdMSV210jsmnDHklgHND87Zw0uZBdOWAYc6KzmTU1i2FxOEZaCjCGm9NDaVtgb1+gjrMUQN4KTuEWKaFpW74dZQymnfR9W/SYjH62DRLLEMDBV3xJDXl8fUgusnXMbLvf97fe/qZ7X1fgK70cWDJRTYSvziOC9knXQgOnr94bGaqWbzr7I2x6ODgTC6dczM9HJWOHjNvV8jouhgQIRldOaKA7ALLIEslhA6beJ/hnD2C2JpVwrcur/rCi2RZ5YOC3NKSvOv2sn4PG3++dpqHah1Zi4sdBKRxngSRCCcC1i123zmO3O4zSzI1i/Lo=" } }, "timestamp": 689610765.957546, "callbackUrl": "myappxyz://mycallback/wsegue", "uuid": "71BCE700-D717-4463-B4E4-4ECDCFC179A3" } ``` ### Example message before encryption (internal usage) ```json { "version": "1.0.3", "sender": "lEC/X3K68rlgOoldMdk0D77738Y7W0mDbMMV5R6VyCE=", "content": { "request": { "actions": [ { "paramsJson": "{\"address\":\"0x571a6a108adb08f9ca54fe8605280F9EE0eD4AF6\",\"message\":\"message\"}", "method": "personal_sign", "optional": false }, { "paramsJson": "{\"typedDataJson\":\"{\\\"types\\\":{\\\"Identity\\\":[{\\\"type\\\":\\\"uint256\\\",\\\"name\\\":\\\"userId\\\"},{\\\"type\\\":\\\"address\\\",\\\"name\\\":\\\"wallet\\\"}],\\\"Bid\\\":[{\\\"name\\\":\\\"amount\\\",\\\"type\\\":\\\"uint256\\\"},{\\\"name\\\":\\\"bidder\\\",\\\"type\\\":\\\"Identity\\\"}],\\\"EIP712Domain\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\"},{\\\"name\\\":\\\"version\\\",\\\"type\\\":\\\"string\\\"},{\\\"name\\\":\\\"chainId\\\",\\\"type\\\":\\\"uint256\\\"},{\\\"name\\\":\\\"verifyingContract\\\",\\\"type\\\":\\\"address\\\"},{\\\"type\\\":\\\"bytes32\\\",\\\"name\\\":\\\"salt\\\"}]},\\\"primaryType\\\":\\\"Bid\\\",\\\"message\\\":{\\\"bidder\\\":{\\\"userId\\\":323,\\\"wallet\\\":\\\"0x3333333333333333333333333333333333333333\\\"},\\\"amount\\\":100},\\\"domain\\\":{\\\"salt\\\":\\\"0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558\\\",\\\"name\\\":\\\"DApp Browser Test DApp\\\",\\\"chainId\\\":1,\\\"version\\\":\\\"1\\\",\\\"verifyingContract\\\":\\\"0x1C56346CD2A2Bf3202F771f50d3D14a367B48070\\\"}}\",\"address\":\"0x571a6a108adb08f9ca54fe8605280F9EE0eD4AF6\"}", "method": "eth_signTypedData_v3", "optional": false } ] } }, "timestamp": 689610765.957546, "callbackUrl": "myappxyz://mycallback/wsegue", "uuid": "71BCE700-D717-4463-B4E4-4ECDCFC179A3" } ``` ## Messages without encryption However, there are two exceptions where the content data are not encrypted: 1. Handshake calls from the client in order to exchange keys with the wallet 2. Failure responses from wallet to return errors happening during the handshake processes ### Example handshake request message ```json { "version": "1.0.3", "sender": "lEC/X3K68rlgOoldMdk0D77738Y7W0mDbMMV5R6VyCE=", "content": { "handshake": { "appId": "com.coinbase.SampleClient", "callback": "myappxyz://mycallback/wsegue", "initialActions": [ { "paramsJson": "{}", "method": "eth_requestAccounts", "optional": false } ] } }, "timestamp": 689587969.289106, "callbackUrl": "myappxyz://mycallback/wsegue", "uuid": "451711FA-96B6-4955-B6EA-EFA78CEB89F5" } ``` ### Example failure response message ```json { "version": "1.0.3", "sender": "lEC/X3K68rlgOoldMdk0D77738Y7W0mDbMMV5R6VyCE=", "content": { "failure": { "requestId": "B78F510E-DD5C-4477-8350-7550FAC7452E", "description": "Request denied" } }, "timestamp": 689611333.369556, "uuid": "E485820B-8F5E-4F8C-9A00-CE5B5B9C6F35" } ``` ## Implementation Coinbase's SDK is built with [Apple CryptoKit](https://developer.apple.com/documentation/cryptokit/) for iOS, [Google Tink](https://github.com/google/tink) and [androidx.security](https://developer.android.com/jetpack/androidx/releases/security) for Android. ![](img/diffie-hellman.png) > source: https://commons.wikimedia.org/wiki/File:Public_key_shared_secret.svg ================================================ FILE: docs/docs/spec/handshake.md ================================================ # Handshake For key exchange, client apps make `handshake` calls to ask wallets to generate and share a key to encrypt subsequent messages on the session. ## Handshake request message from client `content` of a handshake message contains following: ### `appId` Bundle ID (iOS) or application/package ID (Android). e.g. "com.coinbase.SampleClient" ### `callback` Sender's URL to receive response from receiver. For the best security measure, [universal links](https://developer.apple.com/ios/universal-links/) (or [app links](https://developer.android.com/training/app-links)) are recommended. ### `initialActions` (optional) Actions to request after successful handshake process. e.g. `eth_requestAccounts` to get user's eth account info along with the handshake response ### Example request message ```json { "version": "1.0.3", "sender": "lEC/X3K68rlgOoldMdk0D77738Y7W0mDbMMV5R6VyCE=", "content": { "handshake": { "appId": "com.coinbase.SampleClient", "callback": "myappxyz://mycallback/wsegue", "initialActions": [ { "paramsJson": "{}", "method": "eth_requestAccounts", "optional": false } ] } }, "timestamp": 689587969.289106, "callbackUrl": "myappxyz://mycallback/wsegue", "uuid": "451711FA-96B6-4955-B6EA-EFA78CEB89F5" } ``` ## Response from wallet Once the host wallet [verifies the client app](verification) and gets approval from the user, it generates a key pair for the session and shares it with the caller. ### Example response message (success) ```json { "version": "1.0.3", "sender": "EnX8x2D6lHzTg8YZCLybHPwivbCBRyFlP8aA235+MBg=", "content": { "response": { "requestId": "5A920962-ECBE-42BC-A956-83A56F0D52F8", "values": [ { "result": { "value": "{\"chain\":\"eth\",\"networkId\":1,\"address\":\"0x571a6a108adb08f9ca54fe8605280F9EE0eD4AF6\"}" } } ] } }, "timestamp": 689615011.030334, "uuid": "7446663A-685A-47E5-89E3-C1D37F085330" } ``` ### Example response message (failure) ```json { "version": "1.0.3", "sender": "lEC/X3K68rlgOoldMdk0D77738Y7W0mDbMMV5R6VyCE=", "content": { "failure": { "requestId": "B78F510E-DD5C-4477-8350-7550FAC7452E", "description": "Request denied" } }, "timestamp": 689611333.369556, "uuid": "E485820B-8F5E-4F8C-9A00-CE5B5B9C6F35" } ``` ================================================ FILE: docs/docs/spec/messages-example.md ================================================ # Example ### handshake - URL `https://go.cb-w.com/wsegue?p=eyJ2ZXJzaW9uIjoiMS4wLjMiLCJzZW5kZXIiOiJsRUNcL1gzSzY4cmxnT29sZE1kazBENzc3MzhZN1cwbURiTU1WNVI2VnlDRT0iLCJjb250ZW50Ijp7ImhhbmRzaGFrZSI6eyJhcHBJZCI6ImNvbS5jb2luYmFzZS5TYW1wbGVDbGllbnQiLCJjYWxsYmFjayI6Im15YXBweHl6OlwvXC9teWNhbGxiYWNrXC93c2VndWUiLCJpbml0aWFsQWN0aW9ucyI6W3sicGFyYW1zSnNvbiI6Int9IiwibWV0aG9kIjoiZXRoX3JlcXVlc3RBY2NvdW50cyIsIm9wdGlvbmFsIjpmYWxzZX1dfX0sInRpbWVzdGFtcCI6MTY2Nzg5NTE2OTI4OS4xMDYsImNhbGxiYWNrVXJsIjoibXlhcHB4eXo6XC9cL215Y2FsbGJhY2tcL3dzZWd1ZSIsInV1aWQiOiI0NTE3MTFGQS05NkI2LTQ5NTUtQjZFQS1FRkE3OENFQjg5RjUifQ%3D%3D` - JSON (decoded the URL above. handshake messages are not encrypted) ```json { "version": "1.0.3", "sender": "lEC/X3K68rlgOoldMdk0D77738Y7W0mDbMMV5R6VyCE=", "content": { "handshake": { "appId": "com.coinbase.SampleClient", "callback": "myappxyz://mycallback/wsegue", "initialActions": [ { "paramsJson": "{}", "method": "eth_requestAccounts", "optional": false } ] } }, "timestamp": 689587969.289106, "callbackUrl": "myappxyz://mycallback/wsegue", "uuid": "451711FA-96B6-4955-B6EA-EFA78CEB89F5" } ``` ### request - URL `https://go.cb-w.com/wsegue?p=eyJ2ZXJzaW9uIjoiMS4yLjMiLCJzZW5kZXIiOiJmbW5IZVh3OTNlY000QnFzSXpRTk5zb2FvS0ZxK3NOMHZRaWdvaVNQZ2dvPSIsImNvbnRlbnQiOnsicmVxdWVzdCI6eyJfMCI6IkdQYUwwTWJjbVhoVWU5eUprY0p5dnRJWVJUM3RXNEhcL1lBUmNKTXZGZlZWOWh6OXcya1h3YmxIMEJPcFN2UXI0RVhoV0FVUlwvZGFwWUVMZ2lvTkFXY3IxVlwvY1JoNDVUaFN2SFV1cHRlY0lJR0tLeGxKTWdwZ2RiMWNJRWhNWEdpVElBYkZTblJTOVlwOFowOHJcL3pkcjlBVytFQjRXSXBLcnhTTmVqQXRRTDJWTDByRWE2YzhvM0lodG5DQ1U1SzRpaVVZcTJkamd0eGRJZm1FbmhrTUFCQVwvSm5INFpBPT0ifX0sInV1aWQiOiJEMzg3MUYwRS03QkNFLTQxMjYtQjY1Ny05QjlGOEQ1NEU3N0YifQ%3D%3D` - encrypted JSON (decoded from the URL above) ```json { "version": "1.2.3", "sender": "oEC2AZndVwTcLs3ixQyxThlHrKBNdBczbWp9OjeglGY=", "content": { "request": { "data": "/LzMCiGCpiYuUHp2vdlQM4V+f7hYygKI2qhX/ZWuFA6/aqZ/bmnWhROK14vtBH3sbrROqfefMXue3rbqLOg1s+xzh6iXoVavhIeCSevugp1ZlERG9q+CSuLXyRR3tou6wdsJ60jOTDjGzLCvcHp2ykglfDUr2qaVRo3i/RXsJRoPrW9CurSM9+TmNZ46aq1Y/K8lBcpq1aFUYSn7+kHHR8xBY+QoPE0yox+dZrvSi7Z16fX3uwZ3NQPmhPqQXpDFEHrZeKEzoIZAPA8NUlrajgY/1mxhbkH9tmM8X5vSG7w=" } }, "uuid": "3E445386-8CB3-4995-99EC-DCF06A60081C" } ``` - decrypted JSON (to pass via RN - native bridge) ```json { "version": "1.2.3", "sender": "qSAE/fvQ1cnZvVnKDjiHRyzK6bVQ/qJ7W29DL2aMjns=", "content": { "request": { "actions": [ { "paramsJson" : "{}", "method" : "eth_requestAccounts", "optional" : false }, { "paramsJson" : "{\"fromAddress\":\"\",\"data\":\"bWVzc2FnZQ==\"}", "method" : "personal_sign", "optional" : false } ], "account": { "chain": "eth", "networkId": 17, "address": "0x12345678ABCD" } } }, "uuid": "853BFBB5-A6F1-4FBA-B8C6-DC2BE3CCF6DF" } ``` ### response - plain response JSON (to pass via RN - native bridge) ```json { "version": "7.13.1", "sender": "u9IB3p8tN8P4U2LvYfaCphd8/bXRN34eTnuQPO4g6zQ=", "content": { "response": { "requestId": "9D34C731-397B-473B-9850-C6F0261BC085", "values": [ { "result": { "value": "return value 1" } }, { "result": { "value": "return value 2" } }, { "error": { "message": "error 1", "code": 1 } }, { "error": { "message": "error 2", "code": 2 } } ] } }, "uuid": "C5CDDCF7-7A31-4185-BB6B-7A3B8F9B19BA" } ``` - encrypted JSON (encrypted the JSON object above) ```json { "version": "7.13.1", "sender": "u9IB3p8tN8P4U2LvYfaCphd8/bXRN34eTnuQPO4g6zQ=", "content": { "response": { "requestId": "9D34C731-397B-473B-9850-C6F0261BC085", "data": "uXqgB78alErYBFXie8gP397igJ18dx+mcQcUV7K44W+96OQxNxJGm3WxEgsGMxeS8Wg1wiiRaXYRghFbwlnJuOLNpIvAiQYMOzJ1IAlFg452hwG7PrCCn6e9/xU5Hc+kzZYio355zSxSuXByo1bItCgNdjp5aocX1kUnZceV7dGjIGv66/z1k93hxGYooH0uCeI2wQ7qaZzJKfUJLgO0NFmj1fbTciv7CoVd8/mQAfHzlgaIhkHB+j4mAw==" } }, "uuid": "C5CDDCF7-7A31-4185-BB6B-7A3B8F9B19BA" } ``` - URL `https://myapp.xyz/native-sdk?p=eyJ2ZXJzaW9uIjoiNy4xMy4xIiwic2VuZGVyIjoidTlJQjNwOHROOFA0VTJMdllmYUNwaGQ4XC9iWFJOMzRlVG51UVBPNGc2elE9IiwiY29udGVudCI6eyJyZXNwb25zZSI6eyJyZXF1ZXN0SWQiOiI5RDM0QzczMS0zOTdCLTQ3M0ItOTg1MC1DNkYwMjYxQkMwODUiLCJkYXRhIjoidVhxZ0I3OGFsRXJZQkZYaWU4Z1AzOTdpZ0oxOGR4K21jUWNVVjdLNDRXKzk2T1F4TnhKR20zV3hFZ3NHTXhlUzhXZzF3aWlSYVhZUmdoRmJ3bG5KdU9MTnBJdkFpUVlNT3pKMUlBbEZnNDUyaHdHN1ByQ0NuNmU5XC94VTVIYytrelpZaW8zNTV6U3hTdVhCeW8xYkl0Q2dOZGpwNWFvY1gxa1VuWmNlVjdkR2pJR3Y2NlwvejFrOTNoeEdZb29IMHVDZUkyd1E3cWFaekpLZlVKTGdPME5GbWoxZmJUY2l2N0NvVmQ4XC9tUUFmSHpsZ2FJaGtIQitqNG1Bdz09In19LCJ1dWlkIjoiQzVDRERDRjctN0EzMS00MTg1LUJCNkItN0EzQjhGOUIxOUJBIn0%3D` ### error - JSON (error messages are not encrypted) ```json { "version": "7.13.1", "sender": "OzXg53x+wwIW1YCEbvP3sya7MmZT5yCQcArK3GbLmDo=", "content": { "failure": { "requestId": "6C7706C2-B17F-4E96-8D52-C6876C09AECB", "description": "error message from host" } }, "uuid": "C6900BD5-25EE-46F4-AB01-EBB8FAB1AC76" } ``` - URL `https://myapp.xyz/native-sdk?p=eyJ2ZXJzaW9uIjoiNy4xMy4xIiwic2VuZGVyIjoiT3pYZzUzeCt3d0lXMVlDRWJ2UDNzeWE3TW1aVDV5Q1FjQXJLM0diTG1Ebz0iLCJjb250ZW50Ijp7ImZhaWx1cmUiOnsicmVxdWVzdElkIjoiNkM3NzA2QzItQjE3Ri00RTk2LThENTItQzY4NzZDMDlBRUNCIiwiZGVzY3JpcHRpb24iOiJlcnJvciBtZXNzYWdlIGZyb20gaG9zdCJ9fSwidXVpZCI6IkM2OTAwQkQ1LTI1RUUtNDZGNC1BQjAxLUVCQjhGQUIxQUM3NiJ9` ================================================ FILE: docs/docs/spec/messages-request.md ================================================ # Request content Request message has `request` as its `content`, which contains following: ## Properties ### `actions` Array of actions. More details on [batch requests](batch). ### `account` Optional property to specify the chain, network id, and/or address to run the `action`s. More details on [multi-chain](multi-chain). ## Example ```json { "version": "1.0.3", "sender": "AD6aqQNPr4/NRQymzqr14qjlnO9LN5JaEs/XEwEGTno=", "content": { "request": { "actions": [ { "paramsJson": "{\"message\":\"message\",\"address\":\"\"}", "method": "personal_sign", "optional": true } ], "account": { "chain": "eth", "networkId": 137, "address": "" } } }, "timestamp": 689618032.540427, "callbackUrl": "myappxyz://mycallback/wsegue", "uuid": "B454B12D-516D-4CDF-979F-B3B50C956DFC" } ``` ================================================ FILE: docs/docs/spec/messages-response.md ================================================ # Response content Response content can be either `response` or `failure`. ## `response` As long as the host wallet can derive a valid shared secret to decrypt the request from the sender and encrypt the response from itself, the return message has `response` as its `content`. The data of `response` content are encrypted. More details on [encryption](encryption). ### Properties #### `requestId` ID of the corresponding request #### `values` Array of `value`s. More details on [batch requests](batch). `Value` can be either: - `result` of JSON string type - `error` with `code` and `message` ### Example coming soon ## `failure` If the wallet fails to derive a valid shared secret, it returns a unencrypted failure message with following properties: ### Properties #### `requestId` ID of the corresponding request #### `description` Error description ### Example failure response message ```json { "version": "1.0.3", "sender": "lEC/X3K68rlgOoldMdk0D77738Y7W0mDbMMV5R6VyCE=", "content": { "failure": { "requestId": "B78F510E-DD5C-4477-8350-7550FAC7452E", "description": "Request denied" } }, "timestamp": 689611333.369556, "uuid": "E485820B-8F5E-4F8C-9A00-CE5B5B9C6F35" } ``` ================================================ FILE: docs/docs/spec/messages.md ================================================ # Messages Communications between client and server (wallet host) in MWP are through exchanging discrete stateless messages. The messages sent by the client app are called requests and the ones returned by the wallet host are called responses. ## Forms MWP messages can be transformed between two forms according to their use cases. ### Data form JSON object or corresponding data type in the language in use (e.g. Swift `struct`, Kotlin `data class`, or JavaScript object) for internal usage within the app ```json { "uuid" : "634A5C15-0316-4FD1-86FB-4818DBD6C12D", "sender" : "bwf9U+VbjmvfBr3p3aoJyOEKS6mq7sSrg56V6FDYMBs=", "content" : {...}, "version" : "1.0.0", "timestamp" : "1667475279" } ``` ### URL form Deep link URL specifying recipient's address and base64 encoded [JSON object](#data-form) as query parameter to send the data to other party `https://wallet.coinbase.com/wsegue?p=eyJ2ZXJzaW...U2RTMyQiJ9` ## Properties ### `uuid` Unique id of the message ### `sender` Public key of the sender in base64 encoded string ### `content` [Message's content](encryption#message-encryption) which might be encrypted by the sender using the derived shared secret ### `timestamp` UNIX millisecond timestamp ### `version` Version of the protocol used by the sender ### `callback` (Optional) sender's callback URL ================================================ FILE: docs/docs/spec/multi-chain.md ================================================ # Multi-chain support MWP is chain-agnostic. As long as the wallet supports the chain and is able to process the requested actions, apps can communicate using MWP. ## `Account` MWP defines a dedicated type to specify `account`s. It contains `chain` and `networkId` along with `address`. ### `chain` e.g. `"eth"` ### `networkId` e.g. `1` ### `address` e.g. `"0x571a6a108adb08f9ca54fe8605280F9EE0eD4AF6"` ## `Request` message with `account` See [request content properties](messages-request#account). ================================================ FILE: docs/docs/spec/network.md ================================================ # Transport layer Deep linking through universal links on iOS. Intent + app links on android. ================================================ FILE: docs/docs/spec/verification.md ================================================ # Verification Decentralized verification of participating apps’ authenticity using [.well-known](https://en.wikipedia.org/wiki/Well-known_URI) data without centralized registry - [apple-app-site-association](https://developer.apple.com/documentation/xcode/supporting-associated-domains) - [assetlinks.json](https://developer.android.com/training/app-links/verify-site-associations ) 3rd party client apps make requests to the wallet through universal links, whose authenticity is verified by the OS. - Wallet sends responses through universal links as well. - Application ID passed by caller should match the information on their domain. MWP host wallets load metadata from the iOS App Store / Android package manager with the client app's `appId`. ![](img/handshake.png) ================================================ FILE: docs/docs/spec-overview.md ================================================ --- title: "Overview" slug: "spec" --- # Overview ## Mobile Wallet Protocol ![](spec/img/overview.png) MWP is a protocol to allow mobile web3 apps to interact with wallet apps and access users' web3 accounts. It creates a direct channel between client and wallet apps, removing the need for an intermediary bridge or relay server. - Simple: The messages sent by the client app are called requests and the ones returned by the wallet host are called responses. - Direct: The protocol uses deep links as its transport layer to let participating apps deliver messages directly to their peer without external entities such as a bridge server. - Secure: It provides an encrypted P2P communication channel between client and server (wallet host) to exchange discrete stateless messages. - Efficient: Reduces the number of hops between client applications and wallet via support for batch requests. - Decentralized and reliable: It defines a decentralized verification procedure to check authenticity of each other using well-known URI standard for univeral link without a centralized registry. ================================================ FILE: docs/docusaurus.config.js ================================================ // @ts-check // Note: type annotations allow type checking and IDEs autocompletion const lightCodeTheme = require('prism-react-renderer/themes/github'); const darkCodeTheme = require('prism-react-renderer/themes/dracula'); /** @type {import('@docusaurus/types').Config} */ const config = { title: 'Mobile Wallet Protocol', tagline: 'An open protocol for mobile web3 apps to interact with wallets', url: 'https://coinbase.github.io', baseUrl: '/wallet-mobile-sdk/', onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', favicon: 'img/favicon.ico', // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. organizationName: 'coinbase', // Usually your GitHub org/user name. projectName: 'wallet-mobile-sdk', // Usually your repo name. // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want // to replace "en" with "zh-Hans". i18n: { defaultLocale: 'en', locales: ['en'], }, presets: [ [ 'classic', /** @type {import('@docusaurus/preset-classic').Options} */ ({ docs: { sidebarPath: require.resolve('./sidebars.js'), // // Please change this to your repo. // // Remove this to remove the "edit this page" links. // editUrl: // 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', }, theme: { customCss: require.resolve('./src/css/custom.css'), }, }), ], ], themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ navbar: { title: 'Mobile Wallet Protocol', logo: { alt: 'My Site Logo', src: 'img/logo.svg', }, items: [ { type: 'doc', docId: 'spec-overview', position: 'left', label: 'Spec', }, { type: 'doc', docId: 'client-sdk/mobile-sdk-overview', position: 'left', label: 'Client SDK', }, { href: 'https://github.com/coinbase/wallet-mobile-sdk', label: 'GitHub', position: 'right', }, ], }, footer: { style: 'dark', links: [ { title: 'Docs', items: [ { label: 'Spec', to: '/docs/spec', }, ], }, { title: 'Community', items: [ { label: 'Stack Overflow', href: 'https://stackoverflow.com/questions/tagged/coinbase-wallet-sdk', }, { label: 'Twitter', href: 'https://twitter.com/CoinbaseWallet', }, ], }, { title: 'More', items: [ { label: 'GitHub', href: 'https://github.com/coinbase/wallet-mobile-sdk', }, ], }, ], copyright: `Copyright © ${new Date().getFullYear()} Coinbase, Inc. Built with Docusaurus.`, }, prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, }, }), }; module.exports = config; ================================================ FILE: docs/package.json ================================================ { "name": "mobile-wallet-protocol", "version": "0.0.0", "private": true, "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc" }, "dependencies": { "@docusaurus/core": "2.2.0", "@docusaurus/preset-classic": "2.2.0", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "prism-react-renderer": "^1.3.5", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.2.0", "@tsconfig/docusaurus": "^1.0.5", "typescript": "^4.7.4" }, "browserslist": { "production": [ ">0.5%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "engines": { "node": ">=16.14" } } ================================================ FILE: docs/sidebars.js ================================================ /** * Creating a sidebar enables you to: - create an ordered group of docs - render a sidebar for each doc of that group - provide next/previous navigation The sidebars can be generated from the filesystem, or explicitly defined here. Create as many sidebars as you want. */ // @ts-check /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { spec: [ 'spec-overview', { type: 'category', label: 'Messages', link: { type: 'doc', id: 'spec/messages', }, items: [ 'spec/messages-request', 'spec/messages-response' ] }, 'spec/batch', 'spec/encryption', 'spec/handshake', 'spec/verification', 'spec/multi-chain', 'spec/network', ], clientSdk: [ "client-sdk/mobile-sdk-overview", { type: "category", label: "iOS", items: [ "client-sdk/ios-install", "client-sdk/ios-setup", "client-sdk/ios-establishing-a-connection", "client-sdk/ios-making-requests", "client-sdk/ios-api-reference" ] }, { type: "category", label: "Android", items: [ "client-sdk/android-install", "client-sdk/android-setup", "client-sdk/android-establishing-a-connection", "client-sdk/android-making-requests", "client-sdk/android-api-reference" ] } ], }; module.exports = sidebars; ================================================ FILE: docs/spec.md ================================================ --- layout: page title: "Spec" permalink: /spec --- # Mobile Wallet Protocol (MWP) MWP is a protocol to allow mobile web3 apps to interact with wallet apps and access users' web3 accounts. ================================================ FILE: docs/src/components/HomepageFeatures/index.tsx ================================================ import React from 'react'; import clsx from 'clsx'; import styles from './styles.module.css'; type FeatureItem = { title: string; description: JSX.Element; }; const FeatureList: FeatureItem[] = [ { title: 'Easy', description: ( <> Simplified and improved wallet integration for native mobile applications via deep-linking, without requiring a web application. ), }, { title: 'Efficient', description: ( <> Reduces the number of hops between client applications and wallet via support for batch requests. ), }, { title: 'Decentralized and reliable', description: ( <> Doesn't depend on external services and relay servers for delivering messages and app-to-wallet communication. ), }, { title: 'Secure', description: ( <> Utilizes end-to-end encryption with secure key exchange and decentralized identity verification using the well-known URI standard for universal links. ), }, ]; function Feature({title, description}: FeatureItem) { return (

{title}

{description}

); } export default function HomepageFeatures(): JSX.Element { return (
{FeatureList.map((props, idx) => ( ))}
); } ================================================ FILE: docs/src/components/HomepageFeatures/styles.module.css ================================================ .features { display: flex; align-items: center; padding: 2rem 0; width: 100%; } .featureSvg { height: 200px; width: 200px; } ================================================ FILE: docs/src/css/custom.css ================================================ /** * Any CSS included here will be global. The classic template * bundles Infima by default. Infima is a CSS framework designed to * work well for content-centric websites. */ /* You can override the default Infima variables here. */ :root { --ifm-color-primary: #1B53E4; --ifm-color-primary-dark: #29784c; --ifm-color-primary-darker: #277148; --ifm-color-primary-darkest: #205d3b; --ifm-color-primary-light: #33925d; --ifm-color-primary-lighter: #359962; --ifm-color-primary-lightest: #3cad6e; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { --ifm-color-primary: #1B53E4; --ifm-color-primary-dark: #21af90; --ifm-color-primary-darker: #1fa588; --ifm-color-primary-darkest: #1a8870; --ifm-color-primary-light: #29d5b0; --ifm-color-primary-lighter: #32d8b4; --ifm-color-primary-lightest: #4fddbf; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } ================================================ FILE: docs/src/pages/index.module.css ================================================ /** * CSS files with the .module.css suffix will be treated as CSS modules * and scoped locally. */ .heroBanner { padding: 4rem 0; text-align: center; position: relative; overflow: hidden; color: #e3e3e3; } @media screen and (max-width: 996px) { .heroBanner { padding: 2rem; } } .buttons { display: flex; align-items: center; justify-content: center; } ================================================ FILE: docs/src/pages/index.tsx ================================================ import React from 'react'; import clsx from 'clsx'; import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; import styles from './index.module.css'; function HomepageHeader() { const {siteConfig} = useDocusaurusContext(); return (

{siteConfig.title}

{siteConfig.tagline}

Protocol Specification
); } export default function Home(): JSX.Element { const {siteConfig} = useDocusaurusContext(); return (
); } ================================================ FILE: docs/static/.nojekyll ================================================ ================================================ FILE: docs/tsconfig.json ================================================ { // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@tsconfig/docusaurus/tsconfig.json", "compilerOptions": { "baseUrl": "." } } ================================================ FILE: flutter/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. /pubspec.lock **/doc/api/ .dart_tool/ .packages build/ ================================================ FILE: flutter/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: f1875d570e39de09040c8f79aa13cc56baab8db1 channel: stable project_type: plugin # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - platform: android create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - platform: ios create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: flutter/.pubignore ================================================ example test ================================================ FILE: flutter/.vscode/settings.json ================================================ { "workbench.colorTheme": "Solarized Light" } ================================================ FILE: flutter/CHANGELOG.md ================================================ ## 1.0.0 - iOS/Android Flutter support ================================================ FILE: flutter/README.md ================================================ # coinbase_wallet_sdk A flutter wrapper for CoinbaseWallet mobile SDK Note: This wrapper only supports iOS and Android. ## Getting Started ```dart import 'package:coinbase_wallet_sdk/coinbase_wallet_sdk.dart'; // Configure SDK for each platform await CoinbaseWalletSDK.shared.configure( Configuration( ios: IOSConfiguration( host: Uri.parse('https://wallet.coinbase.com/wsegue'), callback: Uri.parse('tribesxyz://mycallback'), ), android: AndroidConfiguration( domain: Uri.parse('https://www.myappxyz.com'), ), ), ); ``` ### iOS only ```swift override func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { if (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true { return true } // handle other types of deep links return false } override func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { if let url = userActivity.webpageURL, (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true { return true } // handle other types of deep links return false } ``` ## Usage ```dart // To call web3's eth_requestAccounts final response = await CoinbaseWalletSDK.shared.initiateHandshake([ const RequestAccounts(), ]); final walletAddress = response[0].value; // to call web3's personalSign final response = await CoinbaseWalletSDK.shared.makeRequest( Request( actions: [ PersonalSign(address: address.value, message: message), ], ), ); final signature = response[0].value; ``` ================================================ FILE: flutter/analysis_options.yaml ================================================ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: flutter/android/.gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .cxx ================================================ FILE: flutter/android/build.gradle ================================================ group 'com.coinbase.coinbase_wallet_sdk' version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } rootProject.allprojects { repositories { google() mavenCentral() } } apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { compileSdkVersion 31 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { minSdkVersion 23 consumerProguardFiles "consumer-rules.pro" proguardFiles "proguard-rules.pro" } buildTypes { release { minifyEnabled false } } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.coinbase:coinbase-wallet-sdk:1.0.4" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" } ================================================ FILE: flutter/android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip ================================================ FILE: flutter/android/settings.gradle ================================================ rootProject.name = 'coinbase_wallet_sdk' ================================================ FILE: flutter/android/src/main/AndroidManifest.xml ================================================ ================================================ FILE: flutter/android/src/main/kotlin/com/coinbase/flutter/wallet_sdk/CoinbaseWalletSdkFlutterPlugin.kt ================================================ package com.coinbase.flutter.wallet_sdk import android.content.Context import android.content.Intent import android.net.Uri import androidx.annotation.NonNull import com.coinbase.android.nativesdk.CoinbaseWalletSDK import com.coinbase.android.nativesdk.message.request.Account import com.coinbase.android.nativesdk.message.request.Action import com.coinbase.android.nativesdk.message.request.RequestContent import com.coinbase.android.nativesdk.message.response.ActionResult import com.coinbase.android.nativesdk.message.response.ResponseResult import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.encodeToJsonElement /** CoinbaseWalletSdkFlutterPlugin */ class CoinbaseWalletSdkFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel: MethodChannel private lateinit var coinbase: CoinbaseWalletSDK private lateinit var flutterApplicationContext: Context private var act: android.app.Activity? = null private val successJson = "{ \"success\": true}" override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { flutterApplicationContext = flutterPluginBinding.applicationContext coinbase = CoinbaseWalletSDK( appContext = flutterPluginBinding.applicationContext, // TODO: This should be changed, and passed in from client domain = Uri.parse("https://www.coinbase.com"), openIntent = { intent -> act?.startActivityForResult(intent, 0) } ) coinbase.appendVersionTag("flutter") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "coinbase_wallet_sdk") channel.setMethodCallHandler(this) } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { try { if (call.method == "configure") { return configure(call, result) } if (call.method == "initiateHandshake") { return initiateHandshake(call, result) } if (call.method == "makeRequest") { return makeRequest(call, result) } if (call.method == "resetSession") { return resetSession(result) } if (call.method == "isAppInstalled") { return isAppInstalled(result) } } catch (e: Throwable) { result.error("onMethodCall", e.message, null) } result.notImplemented() } private fun isAppInstalled(@NonNull result: Result) { result.success(coinbase.isCoinbaseWalletInstalled) } private fun configure(@NonNull call: MethodCall, @NonNull result: Result) { val args = call.arguments if (args !is Map<*, *>) { return result.error("configure", "Missing arguments", null) } val domain = args["domain"] as String coinbase = CoinbaseWalletSDK( appContext = flutterApplicationContext, domain = Uri.parse(domain), openIntent = { intent -> act?.startActivityForResult(intent, 0) } ) result.success(successJson) } private fun initiateHandshake(@NonNull call: MethodCall, @NonNull result: Result) { val jsonString = call.arguments if (jsonString !is String) { return result.error("initiateHandshake", "Missing args", null) } val actions = Json.decodeFromString>(jsonString) coinbase.initiateHandshake(initialActions = actions) { responseResult, account -> handleResponse("initiateHandshake", responseResult, account, result) } } private fun makeRequest(@NonNull call: MethodCall, @NonNull result: Result) { val jsonString = call.arguments if (jsonString !is String) { return result.error("makeRequest", "Missing args", null) } val request = Json.decodeFromString(jsonString) coinbase.makeRequest(request) { responseResult -> handleResponse("makeRequest", responseResult, null, result) } } private fun resetSession(@NonNull result: Result) { coinbase.resetSession() result.success(successJson) } private fun handleResponse( code: String, responseResult: ResponseResult, account: Account?, result: Result ) { if (responseResult.isFailure) { return result.error(code, responseResult.exceptionOrNull()?.message, null) } val returnValues = responseResult.getOrNull() ?: emptyList() val toFlutter = returnValues.map { actionResult -> buildJsonObject { if (account != null) { val accountJson = Json.encodeToJsonElement(account) put("account", accountJson) } when (actionResult) { is ActionResult.Result -> { put("result", JsonPrimitive(actionResult.value)) } is ActionResult.Error -> { val errorJson = Json.encodeToJsonElement(actionResult) put("error", errorJson) } } } } val jsonString = Json.encodeToString(toFlutter) result.success(jsonString) } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { act = binding.activity binding.addActivityResultListener(this) } override fun onDetachedFromActivityForConfigChanges() { act = null } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { // no-op } override fun onDetachedFromActivity() { // no-op } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { val uri = data?.data ?: return false return coinbase.handleResponse(uri) } } ================================================ FILE: flutter/example/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: flutter/example/README.md ================================================ # coinbase_wallet_sdk_example Demonstrates how to use the coinbase_wallet_sdk plugin. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) For help getting started with Flutter development, view the [online documentation](https://docs.flutter.dev/), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: flutter/example/analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: flutter/example/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks ================================================ FILE: flutter/example/android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "xyz.tribes.coinbase.coinbase_wallet_sdk_example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. minSdkVersion 23 // flutter.minSdkVersion targetSdkVersion 32 // flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation "com.coinbase:coinbase-wallet-sdk:1.0.1" } ================================================ FILE: flutter/example/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: flutter/example/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: flutter/example/android/app/src/main/kotlin/xyz/tribes/coinbase/coinbase_wallet_sdk_flutter_example/MainActivity.kt ================================================ package xyz.tribes.coinbase.coinbase_wallet_sdk_example import android.content.Intent import android.net.Uri import android.os.Bundle import com.coinbase.android.nativesdk.CoinbaseWalletSDK import io.flutter.embedding.android.FlutterActivity import androidx.activity.result.ActivityResultLauncher class MainActivity: FlutterActivity() { private lateinit var launcher: ActivityResultLauncher private val client by lazy { CoinbaseWalletSDK( appContext = applicationContext, domain = Uri.parse("https://www.myappxyz.com"), openIntent = { intent -> launcher.launch(intent) } ) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ================================================ FILE: flutter/example/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: flutter/example/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: flutter/example/android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: flutter/example/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: flutter/example/android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: flutter/example/android/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip ================================================ FILE: flutter/example/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true ================================================ FILE: flutter/example/android/settings.gradle ================================================ include ':app' def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" ================================================ FILE: flutter/example/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: flutter/example/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 11.0 ================================================ FILE: flutter/example/ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: flutter/example/ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: flutter/example/ios/Podfile ================================================ platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) pod 'CoinbaseWalletSDK/CrossPlatform', :path => '../../../' end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: flutter/example/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter import CoinbaseWalletSDK @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } override func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { if (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true { return true } // handle other types of deep links return false } override func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { if let url = userActivity.webpageURL, (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true { return true } // handle other types of deep links return false } } ================================================ FILE: flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: flutter/example/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: flutter/example/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Coinbase Wallet Sdk Flutter CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName coinbase_wallet_sdk_example CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance CADisableMinimumFrameDurationOnPhone LSApplicationQueriesSchemes cbwallet CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName xyz.myapp CFBundleURLSchemes tribesxyzsample ================================================ FILE: flutter/example/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: flutter/example/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 51; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; A801D991D2C0347E18E774F2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 632977FC36B713376CA20149 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1231562487A9E69F0695A637 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 632977FC36B713376CA20149 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9B9F6ED6A5104AB32086D219 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; CDE608CC9346B2F30C39D2F9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( A801D991D2C0347E18E774F2 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 6E7600A0553582F9BBF745AC /* Frameworks */ = { isa = PBXGroup; children = ( 632977FC36B713376CA20149 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, ACA9B6DF8E133DCF23C189FC /* Pods */, 6E7600A0553582F9BBF745AC /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; ACA9B6DF8E133DCF23C189FC /* Pods */ = { isa = PBXGroup; children = ( 9B9F6ED6A5104AB32086D219 /* Pods-Runner.debug.xcconfig */, 1231562487A9E69F0695A637 /* Pods-Runner.release.xcconfig */, CDE608CC9346B2F30C39D2F9 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( EA14CAA479E7DE3ABA560117 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, C9F10FB848EC932D8A5546BF /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; C9F10FB848EC932D8A5546BF /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; EA14CAA479E7DE3ABA560117 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = K9PRP53827; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = xyz.tribes.coinbase.coinbaseWalletSdkFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = K9PRP53827; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = xyz.tribes.coinbase.coinbaseWalletSdkFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = K9PRP53827; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = xyz.tribes.coinbase.coinbaseWalletSdkFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: flutter/example/lib/main.dart ================================================ import 'dart:async'; import 'package:coinbase_wallet_sdk/coinbase_wallet_sdk.dart'; import 'package:coinbase_wallet_sdk/configuration.dart'; import 'package:coinbase_wallet_sdk/eth_web3_rpc.dart'; import 'package:coinbase_wallet_sdk/request.dart'; import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { String _addy = ""; String _signed = ""; String _sessionCleared = ""; @override void initState() { CoinbaseWalletSDK.shared.configure( Configuration( ios: IOSConfiguration( host: Uri.parse('cbwallet://wsegue'), callback: Uri.parse('tribesxyzsample://mycallback'), ), android: AndroidConfiguration( domain: Uri.parse('https://www.myappxyz.com'), ), ), ); super.initState(); } // Platform messages are asynchronous, so we initialize in an async method. Future _requestAccount() async { String addy; try { final results = await CoinbaseWalletSDK.shared.initiateHandshake([ const RequestAccounts(), ]); addy = results[0].account?.address ?? ""; } catch (e) { addy = 'Failed to get address. => $e'; } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; setState(() { _addy = addy; }); } Future _personalSign() async { String message = "Hello, world!"; String signed; try { final request = Request( actions: [PersonalSign(address: _addy, message: message)], ); final results = await CoinbaseWalletSDK.shared.makeRequest(request); signed = results[0].value ?? ""; } catch (e) { debugPrint('error --> $e'); signed = "Failed to sign message."; } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; setState(() { _signed = signed; }); } Future _resetSession() async { try { await CoinbaseWalletSDK.shared.resetSession(); setState(() { _sessionCleared = "SEssion Cleared!"; }); } catch (e) { setState(() { _sessionCleared = "Failed to reset session"; }); } } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Coinbase Flutter SDK'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FutureBuilder( future: CoinbaseWalletSDK.shared.isAppInstalled(), builder: ((context, snapshot) { return Text( 'Is installed? ${snapshot.data}', ); }), ), TextButton( onPressed: () => _requestAccount(), child: const Text("Request Account"), ), Text('address is\n\n $_addy'), const SizedBox(height: 50), TextButton( onPressed: () => _personalSign(), child: const Text("personalSign"), ), Text('signed message is\n\n $_signed'), const SizedBox(height: 50), TextButton( onPressed: () => _resetSession(), child: const Text("Reset Session"), ), Text('is reset\n\n $_sessionCleared'), ], ), ), ), ); } } ================================================ FILE: flutter/example/pubspec.yaml ================================================ name: coinbase_wallet_sdk_example description: Demonstrates how to use the coinbase_wallet_sdk plugin. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: sdk: ">=2.17.6 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter coinbase_wallet_sdk: # When depending on this package from a real application you should use: # coinbase_wallet_sdk: ^x.y.z # See https://dart.dev/tools/pub/dependencies#version-constraints # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: flutter/example/test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:coinbase_wallet_sdk_example/main.dart'; void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); // Verify that platform version is retrieved. expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data!.startsWith('Running on:'), ), findsOneWidget, ); }); } ================================================ FILE: flutter/ios/.gitignore ================================================ .idea/ .vagrant/ .sconsign.dblite .svn/ .DS_Store *.swp profile DerivedData/ build/ GeneratedPluginRegistrant.h GeneratedPluginRegistrant.m .generated/ *.pbxuser *.mode1v3 *.mode2v3 *.perspectivev3 !default.pbxuser !default.mode1v3 !default.mode2v3 !default.perspectivev3 xcuserdata *.moved-aside *.pyc *sync/ Icon? .tags* /Flutter/Generated.xcconfig /Flutter/ephemeral/ /Flutter/flutter_export_environment.sh ================================================ FILE: flutter/ios/Assets/.gitkeep ================================================ ================================================ FILE: flutter/ios/Classes/CoinbaseWalletSdkFlutterPlugin.h ================================================ #import @interface CoinbaseWalletSdkFlutterPlugin : NSObject @end ================================================ FILE: flutter/ios/Classes/CoinbaseWalletSdkFlutterPlugin.m ================================================ #import "CoinbaseWalletSdkFlutterPlugin.h" #if __has_include() #import #else // Support project import fallback if the generated compatibility header // is not copied when this plugin is created as a library. // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 #import "coinbase_wallet_sdk-Swift.h" #endif @implementation CoinbaseWalletSdkFlutterPlugin + (void)registerWithRegistrar:(NSObject*)registrar { [SwiftCoinbaseWalletSdkFlutterPlugin registerWithRegistrar:registrar]; } @end ================================================ FILE: flutter/ios/Classes/SwiftCoinbaseWalletSdkFlutterPlugin.swift ================================================ import Flutter import UIKit import CoinbaseWalletSDK public class SwiftCoinbaseWalletSdkFlutterPlugin: NSObject, FlutterPlugin { private static let success = "{ \"success\": true}" public override init() {} public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "coinbase_wallet_sdk", binaryMessenger: registrar.messenger()) let instance = SwiftCoinbaseWalletSdkFlutterPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { do { if (call.method == "configure") { return configure(call: call, result: result) } if (call.method == "initiateHandshake") { return try initiateHandshake(call: call, result: result) } if (call.method == "makeRequest") { return try makeRequest(call: call, result: result) } if (call.method == "resetSession") { return resetSession(call: call, result: result) } if (call.method == "isAppInstalled") { return isAppInstalled(result: result) } } catch { result(FlutterError(code: "handle", message: error.localizedDescription, details: nil)) return } result(FlutterMethodNotImplemented) } private func isAppInstalled(result: @escaping FlutterResult) { result(CoinbaseWalletSDK.isCoinbaseWalletInstalled()) } private func configure(call: FlutterMethodCall, result: @escaping FlutterResult) { guard let args = call.arguments as? [String: Any], let host = args["host"] as? String, let callback = args["callback"] as? String, let hostURL = URL(string: host), let callbackURL = URL(string: callback) else { result(FlutterError(code: "configure", message: "Invalid arguments", details: nil)) return } CoinbaseWalletSDK.configure(host: hostURL,callback: callbackURL) CoinbaseWalletSDK.appendVersionTag("flutter") result(SwiftCoinbaseWalletSdkFlutterPlugin.success) } private func initiateHandshake(call: FlutterMethodCall, result: @escaping FlutterResult) throws { var actions = [Action]() if let args = call.arguments as? String, let jsonData = args.data(using: .utf8) { actions = try JSONDecoder().decode([Action].self, from: jsonData) } CoinbaseWalletSDK.shared.initiateHandshake(initialActions: actions) { responseResult, account in self.handleResponse( code: "initiateHandshake", responseResult: responseResult, account: account, result: result ) } } private func makeRequest(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let args = call.arguments as? String, let jsonData = args.data(using: .utf8) else { result(FlutterError(code: "makeRequest", message: "Invalid arguments", details: nil)) return } let request = try JSONDecoder().decode(Request.self, from: jsonData) CoinbaseWalletSDK.shared.makeRequest(request) { responseResult in self.handleResponse( code: "makeRequest", responseResult: responseResult, account: nil, result: result ) } } private func resetSession(call: FlutterMethodCall, result: @escaping FlutterResult) { let responseResult = CoinbaseWalletSDK.shared.resetSession() switch responseResult { case .success: result(SwiftCoinbaseWalletSdkFlutterPlugin.success) case .failure(let error): result(FlutterError(code: "resetSession", message: error.localizedDescription, details: nil)) } } private func handleResponse( code: String, responseResult: ResponseResult, account: Account?, result: @escaping FlutterResult ) { do { switch responseResult { case .success(let returnValues): var toFlutter = [[String: Any]]() returnValues.content.forEach { it in var response = [String: Any]() if let account = account { response["account"] = [ "chain": account.chain, "networkId": account.networkId, "address": account.address ] } switch it { case .success(let value): response["result"] = value.rawValue case .failure(let error): response["error"] = ["code": error.code, "message": error.message] } toFlutter.append(response) } let data = try JSONSerialization.data(withJSONObject: toFlutter, options: []) let jsonString = String(data: data, encoding: .utf8)! result(jsonString) case .failure(let error): result(FlutterError(code: code, message: error.localizedDescription, details: nil)) } } catch { result(FlutterError(code: code, message: error.localizedDescription, details: nil)) } } } ================================================ FILE: flutter/ios/coinbase_wallet_sdk.podspec ================================================ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. # Run `pod lib lint coinbase_wallet_sdk.podspec` to validate before publishing. # Pod::Spec.new do |s| s.name = 'coinbase_wallet_sdk' s.version = '0.0.1' s.summary = 'A new Flutter plugin project.' s.description = <<-DESC A new Flutter plugin project. DESC s.homepage = 'http://example.com' s.license = { :file => '../LICENSE' } s.author = { 'Hish Bouabdallah' => 'hish@tribes.xyz' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.dependency 'CoinbaseWalletSDK/CrossPlatform', '1.0.4' s.ios.deployment_target = '13.0' s.swift_version = '5.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end ================================================ FILE: flutter/lib/account.dart ================================================ class Account { final String chain; final int networkId; final String address; const Account({ required this.chain, required this.networkId, required this.address, }); Map toJson() { return { 'chain': chain, 'networkId': networkId, 'address': address, }; } factory Account.fromJson(Map json) { return Account( chain: json['chain'] as String, networkId: json['networkId'] as int, address: json['address'] as String, ); } } ================================================ FILE: flutter/lib/action.dart ================================================ import 'dart:convert'; class Action { final String method; final String paramsJson; final bool optional; const Action({ required this.method, required this.paramsJson, this.optional = false, }); factory Action.create({ required String method, required Map params, bool optional = false, }) { return Action( method: method, paramsJson: jsonEncode(params), optional: optional, ); } Map toJson() { return { 'method': method, 'paramsJson': paramsJson, 'optional': optional, }; } } ================================================ FILE: flutter/lib/coinbase_wallet_sdk.dart ================================================ import 'dart:convert'; import 'package:coinbase_wallet_sdk/account.dart'; import 'package:coinbase_wallet_sdk/action.dart'; import 'package:coinbase_wallet_sdk/coinbase_wallet_sdk_platform_interface.dart'; import 'package:coinbase_wallet_sdk/configuration.dart'; import 'package:coinbase_wallet_sdk/request.dart'; import 'package:coinbase_wallet_sdk/return_value.dart'; import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform; class CoinbaseWalletSDK { static const CoinbaseWalletSDK shared = CoinbaseWalletSDK._(); const CoinbaseWalletSDK._(); /// Setup the SDK Future configure(Configuration configuration) async { if (defaultTargetPlatform == TargetPlatform.iOS) { await _configureIOS(configuration.ios); } else if (defaultTargetPlatform == TargetPlatform.android) { await _configureAndroid(configuration.android); } else { throw UnsupportedError( 'Unsupported platform: ${defaultTargetPlatform.toString()}', ); } } /// Initiate the handshake with Coinbase Wallet app Future> initiateHandshake( List? initialActions, ) async { final actionsJson = (initialActions ?? []).map((action) => action.toJson()).toList(); final result = await CoinbaseWalletSdkFlutterPlatform.instance .call('initiateHandshake', jsonEncode(actionsJson)); final returnValuesWithAccounts = (result ?? []) .map( (e) { final map = Map.from(e); return ReturnValueWithAccount.fromJson(map); }, ) .cast() .toList(); return returnValuesWithAccounts; } /// Send a web3 request to Coinbase Wallet app Future> makeRequest(Request request) async { final result = await CoinbaseWalletSdkFlutterPlatform.instance .call('makeRequest', jsonEncode(request.toJson())); return (result ?? []) .map((e) => ReturnValue.fromJson(e)) .cast() .toList(); } /// Disconnect any active session Future resetSession() async { await CoinbaseWalletSdkFlutterPlatform.instance.call('resetSession'); } /// Check whether CoinbaseWallet app is installed Future isAppInstalled() async { final result = await CoinbaseWalletSdkFlutterPlatform.instance.call('isAppInstalled'); return result ?? false; } // private helper methods Future _configureIOS(IOSConfiguration? configuration) async { if (configuration == null) { throw ArgumentError('iOS configuration is missing.'); } await CoinbaseWalletSdkFlutterPlatform.instance .call('configure', configuration.toJson()); } Future _configureAndroid(AndroidConfiguration? configuration) async { if (configuration == null) { throw ArgumentError('Android configuration is missing.'); } await CoinbaseWalletSdkFlutterPlatform.instance .call('configure', configuration.toJson()); } } class ReturnValueWithAccount { final String? value; final ReturnValueError? error; final Account? account; ReturnValueWithAccount(ReturnValue returnValue, this.account) : value = returnValue.value, error = returnValue.error; factory ReturnValueWithAccount.fromJson(Map json) { final account = json['account']; return ReturnValueWithAccount( ReturnValue.fromJson(json), account == null ? null : Account.fromJson(Map.from(account)), ); } } ================================================ FILE: flutter/lib/coinbase_wallet_sdk_method_channel.dart ================================================ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'coinbase_wallet_sdk_platform_interface.dart'; /// An implementation of [CoinbaseWalletSdkFlutterPlatform] that uses method channels. class MethodChannelCoinbaseWalletSdkFlutter extends CoinbaseWalletSdkFlutterPlatform { /// The method channel used to interact with the native platform. @visibleForTesting final methodChannel = const MethodChannel('coinbase_wallet_sdk'); @override Future call(String method, [arguments]) async { final result = await methodChannel.invokeMethod(method, arguments); if (result is bool) { return result; } return result != null ? jsonDecode(result as String) : null; } } ================================================ FILE: flutter/lib/coinbase_wallet_sdk_platform_interface.dart ================================================ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'coinbase_wallet_sdk_method_channel.dart'; abstract class CoinbaseWalletSdkFlutterPlatform extends PlatformInterface { /// Constructs a CoinbaseWalletSdkFlutterPlatform. CoinbaseWalletSdkFlutterPlatform() : super(token: _token); static final Object _token = Object(); static CoinbaseWalletSdkFlutterPlatform _instance = MethodChannelCoinbaseWalletSdkFlutter(); /// The default instance of [CoinbaseWalletSdkFlutterPlatform] to use. /// /// Defaults to [MethodChannelCoinbaseWalletSdkFlutter]. static CoinbaseWalletSdkFlutterPlatform get instance => _instance; /// Platform-specific implementations should set this with their own /// platform-specific class that extends [CoinbaseWalletSdkFlutterPlatform] when /// they register themselves. static set instance(CoinbaseWalletSdkFlutterPlatform instance) { PlatformInterface.verifyToken(instance, _token); _instance = instance; } Future call(String method, [dynamic arguments]); } class PlatformError implements Exception { final int code; final String message; PlatformError(this.code, this.message); @override String toString() { return "PlatformError(code: $code, message: $message)"; } } class PlatformResult { final String? value; final PlatformError? error; PlatformResult(this.value, this.error); } ================================================ FILE: flutter/lib/configuration.dart ================================================ class Configuration { final IOSConfiguration? ios; final AndroidConfiguration? android; Configuration({ required this.ios, required this.android, }); } class IOSConfiguration { final Uri host; final Uri callback; const IOSConfiguration({ required this.host, required this.callback, }); Map toJson() { return { 'host': host.toString(), 'callback': callback.toString(), }; } } class AndroidConfiguration { final Uri domain; const AndroidConfiguration({ required this.domain, }); Map toJson() { return { 'domain': domain.toString(), }; } } ================================================ FILE: flutter/lib/eth_web3_rpc.dart ================================================ import 'dart:convert'; import 'package:coinbase_wallet_sdk/action.dart'; class RequestAccounts extends Action { const RequestAccounts() : super( method: 'eth_requestAccounts', paramsJson: '{}', ); } class PersonalSign extends Action { PersonalSign({ required String address, required String message, bool optional = false, }) : super( method: 'personal_sign', paramsJson: jsonEncode({ 'address': address, 'message': message, }), optional: optional, ); } class SignTypedDataV3 extends Action { final String address; final String typedDataJson; SignTypedDataV3({ required this.address, required this.typedDataJson, bool optional = false, }) : super( method: 'eth_signTypedData_v3', paramsJson: jsonEncode({ 'address': address, 'typedDataJson': typedDataJson, }), optional: optional, ); } class SignTypedDataV4 extends Action { final String address; final String typedDataJson; SignTypedDataV4({ required this.address, required this.typedDataJson, bool optional = false, }) : super( method: 'eth_signTypedData_v4', paramsJson: jsonEncode({ 'address': address, 'typedDataJson': typedDataJson, }), optional: optional, ); } class SignTransaction extends Action { SignTransaction({ required String fromAddress, required String? toAddress, required BigInt weiValue, required String data, required int? nonce, required BigInt? gasPriceInWei, required BigInt? maxFeePerGas, required BigInt? maxPriorityFeePerGas, required BigInt? gasLimit, required String chainId, }) : super( method: 'eth_signTransaction', paramsJson: jsonEncode({ 'fromAddress': fromAddress, 'toAddress': toAddress, 'weiValue': weiValue.toString(), 'data': data, 'nonce': nonce, 'gasPriceInWei': gasPriceInWei?.toString(), 'maxFeePerGas': maxFeePerGas?.toString(), 'maxPriorityFeePerGas': maxPriorityFeePerGas?.toString(), 'gasLimit': gasLimit?.toString(), 'chainId': chainId, }), ); } class SendTransaction extends Action { SendTransaction({ required String fromAddress, required String? toAddress, required BigInt weiValue, required String data, required int? nonce, required BigInt? gasPriceInWei, required BigInt? maxFeePerGas, required BigInt? maxPriorityFeePerGas, required BigInt? gasLimit, required String chainId, }) : super( method: 'eth_signTransaction', paramsJson: jsonEncode({ 'fromAddress': fromAddress, 'toAddress': toAddress, 'weiValue': weiValue.toString(), 'data': data, 'nonce': nonce, 'gasPriceInWei': gasPriceInWei?.toString(), 'maxFeePerGas': maxFeePerGas?.toString(), 'maxPriorityFeePerGas': maxPriorityFeePerGas?.toString(), 'gasLimit': gasLimit?.toString(), 'chainId': chainId, }), ); } class SwitchEthereumChain extends Action { SwitchEthereumChain({ required int chainId, }) : super( method: 'wallet_switchEthereumChain', paramsJson: jsonEncode({ 'chainId': chainId, }), ); } ================================================ FILE: flutter/lib/request.dart ================================================ import 'package:coinbase_wallet_sdk/account.dart'; import 'package:coinbase_wallet_sdk/action.dart'; class Request { final List actions; final Account? account; const Request({ required this.actions, this.account, }); Map toJson() { return { 'actions': actions.map((action) => action.toJson()).toList(), 'account': account?.toJson(), }; } } ================================================ FILE: flutter/lib/return_value.dart ================================================ class ReturnValue { final String? value; final ReturnValueError? error; const ReturnValue({ this.value, this.error, }); factory ReturnValue.fromJson(Map json) { final error = json['error']; return ReturnValue( value: json['result'] as String?, error: error == null ? null : ReturnValueError.fromJson(Map.from(error)), ); } } class ReturnValueError { final int code; final String message; const ReturnValueError(this.code, this.message); factory ReturnValueError.fromJson(Map json) { return ReturnValueError( json['code'] as int, json['message'] as String, ); } } ================================================ FILE: flutter/pubspec.yaml ================================================ name: coinbase_wallet_sdk description: Fluter implementation of Coinbase Wallet SDK. version: 1.0.4 homepage: https://wallet.coinbase.com environment: sdk: ">=2.17.6 <3.0.0" flutter: ">=2.5.0" dependencies: flutter: sdk: flutter plugin_platform_interface: ^2.0.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: plugin: platforms: android: package: com.coinbase.flutter.wallet_sdk pluginClass: CoinbaseWalletSdkFlutterPlugin ios: pluginClass: CoinbaseWalletSdkFlutterPlugin ================================================ FILE: flutter/test/coinbase_wallet_sdk_method_channel_test.dart ================================================ import 'package:coinbase_wallet_sdk/coinbase_wallet_sdk_method_channel.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { MethodChannelCoinbaseWalletSdkFlutter platform = MethodChannelCoinbaseWalletSdkFlutter(); const MethodChannel channel = MethodChannel('coinbase_wallet_sdk'); TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { return '42'; }); }); tearDown(() { channel.setMockMethodCallHandler(null); }); test('getPlatformVersion', () async { // expect(await platform.getPlatformVersion(), '42'); }); } ================================================ FILE: flutter/test/coinbase_wallet_sdk_test.dart ================================================ import 'package:coinbase_wallet_sdk/coinbase_wallet_sdk_method_channel.dart'; import 'package:coinbase_wallet_sdk/coinbase_wallet_sdk_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; // class MockCoinbaseWalletSdkFlutterPlatform // with MockPlatformInterfaceMixin // implements CoinbaseWalletSdkFlutterPlatform { // @override // Future getPlatformVersion() => Future.value('42'); // @override // Future initiateHandshake() async { // return _addy; // } // @override // Future personalSign(String address, String message) async { // if (address == _addy) { // return '0x0'; // } else { // throw PlatformError(1, 'Invalid address'); // } // } // } void main() { final CoinbaseWalletSdkFlutterPlatform initialPlatform = CoinbaseWalletSdkFlutterPlatform.instance; test('$MethodChannelCoinbaseWalletSdkFlutter is the default instance', () { expect( initialPlatform, isInstanceOf()); }); test('getPlatformVersion', () async { // CoinbaseWalletSdkFlutter coinbaseWalletSdkFlutterPlugin = // CoinbaseWalletSdkFlutter(); // MockCoinbaseWalletSdkFlutterPlatform fakePlatform = // MockCoinbaseWalletSdkFlutterPlatform(); // CoinbaseWalletSdkFlutterPlatform.instance = fakePlatform; // expect(await coinbaseWalletSdkFlutterPlugin.getPlatformVersion(), '42'); }); } ================================================ FILE: ios/CoinbaseWalletSDK/CoinbaseWalletSDK.swift ================================================ // // CoinbaseWalletSDK.swift // WalletSegue // // Created by Jungho Bang on 5/20/22. // import Foundation import CryptoKit import UIKit public final class CoinbaseWalletSDK { static public func isCoinbaseWalletInstalled() -> Bool { return UIApplication.shared.canOpenURL(URL(string: "cbwallet://")!) } static public func getCoinbaseWalletMWPVersion() -> String? { if UIApplication.shared.canOpenURL(URL(string: "mwp+1.1://")!) { return "1.1" } else if isCoinbaseWalletInstalled() { return "1.0" } else { return nil } } // MARK: - Constructor static private var host: URL? static private var callback: URL? static public var isConfigured: Bool { return host != nil && callback != nil } static public func configure( host: URL = URL(string: "https://wallet.coinbase.com/wsegue")!, callback: URL ) { guard isConfigured == false else { assertionFailure("`CoinbaseWalletSDK.configure` should be called only once.") return } self.host = host if callback.pathComponents.count < 2 { // [] or ["/"] self.callback = callback.appendingPathComponent("wsegue") } else { self.callback = callback } } static public var shared: CoinbaseWalletSDK = { guard let host = CoinbaseWalletSDK.host, let callback = CoinbaseWalletSDK.callback else { preconditionFailure("Missing configuration: call `CoinbaseWalletSDK.configure` before accessing the `shared` instance.") } return CoinbaseWalletSDK(host: host, callback: callback) }() // MARK: - Properties private let appId: String private let host: URL private let callback: URL private lazy var keyManager: KeyManager = { KeyManager(host: self.host) }() private lazy var taskManager: TaskManager = { TaskManager() }() private init( host: URL, callback: URL ) { self.host = host self.callback = callback self.appId = Bundle.main.bundleIdentifier! } // MARK: - Send message /// Make handshake request to get session key from wallet /// - Parameters: /// - initialActions: Batch of actions that you'd want to execute after successful handshake. `eth_requestAccounts` by default. /// - onResponse: Response callback with regular response result and optional parsed `Account` object. public func initiateHandshake( initialActions: [Action]? = [Action(jsonRpc: .eth_requestAccounts)], onResponse: @escaping (ResponseResult, Account?) -> Void ) { let hasUnsupportedAction = initialActions?.contains { action in ["eth_signTransaction", "eth_sendTransaction"].contains(where: { action.method == $0 }) } guard hasUnsupportedAction != true else { onResponse(.failure(Error.invalidHandshakeRequest), nil) return } try? keyManager.resetOwnPrivateKey() let message = RequestMessage( uuid: UUID(), sender: keyManager.ownPublicKey, content: .handshake( appId: appId, callback: callback, initialActions: initialActions ), version: CoinbaseWalletSDK.version, timestamp: Date(), callbackUrl: callback.absoluteString ) self.send(message) { result in guard let requestAccountsIndex = initialActions?.firstIndex(where: { $0.method == "eth_requestAccounts" }), let content = try? result.get().content, content.indices.contains(requestAccountsIndex), case .success(let accountJson) = content[requestAccountsIndex], let account = try? accountJson.decode(as: Account.self) else { onResponse(result, nil) return } onResponse(result, account) } } /// Make regular requests. It requires session key you get after successful handshake. public func makeRequest(_ request: Request, onResponse: @escaping ResponseHandler) { let message = RequestMessage( uuid: UUID(), sender: keyManager.ownPublicKey, content: .request(actions: request.actions, account: request.account), version: CoinbaseWalletSDK.version, timestamp: Date(), callbackUrl: callback.absoluteString ) return self.send(message, onResponse) } private func send(_ request: RequestMessage, _ onResponse: @escaping ResponseHandler) { let url: URL do { url = try MessageConverter.encode(request, to: host, with: keyManager.symmetricKey) } catch { onResponse(.failure(error)) return } UIApplication.shared.open(url) { result in guard result == true else { onResponse(.failure(Error.openUrlFailed)) return } self.taskManager.registerResponseHandler(for: request, onResponse) } } // MARK: - Receive message private func isWalletSegueMessage(_ url: URL) -> Bool { return url.host == callback.host && url.path == callback.path } /// Handle incoming deep links /// - Parameter url: deep link url /// - Returns: `false` if the input was not response message type, `true` if SDK handled the input, or throws error if it failed to decode response. @discardableResult public func handleResponse(_ url: URL) throws -> Bool { guard isWalletSegueMessage(url) else { return false } let response = try decodeResponse(url) taskManager.runResponseHandler(with: response) return true } private func decodeResponse(_ url: URL) throws -> ResponseMessage { if let symmetricKey = keyManager.symmetricKey { return try MessageConverter.decode(url, with: symmetricKey) } // no symmetric key yet let encryptedResponse: EncryptedResponseMessage = try MessageConverter.decodeWithoutDecryption(url) let request = taskManager.findRequest(for: encryptedResponse) guard case .handshake = request?.content else { throw Error.missingSymmetricKey } try handleHandshakeResponse(encryptedResponse) return try encryptedResponse.decrypt(with: keyManager.symmetricKey) } // MARK: - Session public func isConnected() -> Bool { return keyManager.symmetricKey != nil } public var ownPublicKey: PublicKey { return keyManager.ownPublicKey } public var peerPublicKey: PublicKey? { return keyManager.peerPublicKey } @discardableResult public func resetSession() -> Result { do { taskManager.reset() try keyManager.resetOwnPrivateKey() return .success(()) } catch { return .failure(error) } } private func handleHandshakeResponse(_ response: EncryptedResponseMessage) throws { if case .failure = response.content { return } try keyManager.storePeerPublicKey(response.sender) } } ================================================ FILE: ios/CoinbaseWalletSDK/Error.swift ================================================ // // Error.swift // WalletSegue // // Created by Jungho Bang on 6/16/22. // import Foundation extension CoinbaseWalletSDK { public enum Error: Swift.Error { case encodingFailed case decodingFailed case missingSymmetricKey case invalidHandshakeRequest case openUrlFailed case walletReturnedError(String) } } ================================================ FILE: ios/CoinbaseWalletSDK/Host/CoinbaseWalletHostSDK.swift ================================================ // // CoinbaseWalletHostSDK.swift // CoinbaseWalletSDK // // Created by Jungho Bang on 9/2/22. // import Foundation import CryptoKit public final class CoinbaseWalletHostSDK { public static func deriveSymmetricKey( with ownPrivateKey: CoinbaseWalletSDK.PrivateKey, _ peerPublicKey: CoinbaseWalletSDK.PublicKey ) throws -> SymmetricKey { return try Cipher.deriveSymmetricKey(with: ownPrivateKey, peerPublicKey) } public static func encode( _ message: Message, to recipient: URL, with symmetricKey: SymmetricKey? ) throws -> URL { return try MessageConverter.encode(message, to: recipient, with: symmetricKey) } public static func decode( _ url: URL, with symmetricKey: SymmetricKey? ) throws -> Message { return try MessageConverter.decode(url, with: symmetricKey) } } ================================================ FILE: ios/CoinbaseWalletSDK/Host/ResponseMessage+init.swift ================================================ // // ResponseMessage+init.swift // CoinbaseWalletSDK // // Created by Jungho Bang on 9/2/22. // import Foundation extension ResponseMessage { public init( uuid: UUID = UUID(), sender: CoinbaseWalletSDK.PublicKey, content: ResponseContent, timestamp: Date = Date() ) { self.uuid = uuid self.sender = sender self.content = content self.version = CoinbaseWalletSDK.version self.timestamp = timestamp self.callbackUrl = nil } } ================================================ FILE: ios/CoinbaseWalletSDK/Key/Key+RawRepresentable.swift ================================================ // // Key+RawRepresentable.swift // WalletSegue // // Created by Jungho Bang on 6/13/22. // import Foundation public protocol RawRepresentableKey: Codable { init(rawRepresentation data: D) throws where D: ContiguousBytes var rawRepresentation: Data { get } } extension CoinbaseWalletSDK.PrivateKey: RawRepresentableKey {} extension CoinbaseWalletSDK.PublicKey: RawRepresentableKey {} extension RawRepresentableKey { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let data = try container.decode(Data.self) try self.init(rawRepresentation: data) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.rawRepresentation) } } ================================================ FILE: ios/CoinbaseWalletSDK/Key/KeyManager.swift ================================================ // // KeyManager.swift // WalletSegue // // Created by Jungho Bang on 6/9/22. // import Foundation import CryptoKit extension CoinbaseWalletSDK { public typealias PrivateKey = Curve25519.KeyAgreement.PrivateKey public typealias PublicKey = Curve25519.KeyAgreement.PublicKey } final class KeyManager { private(set) var ownPrivateKey: CoinbaseWalletSDK.PrivateKey var ownPublicKey: CoinbaseWalletSDK.PublicKey { return ownPrivateKey.publicKey } private(set) var peerPublicKey: CoinbaseWalletSDK.PublicKey? private(set) var symmetricKey: SymmetricKey? // MARK: - methods private let storage: KeyStorage init(host: URL) { self.storage = KeyStorage(host: host) guard let storedKey = try? storage.read(.ownPrivateKey) else { // generate new private key self.ownPrivateKey = CoinbaseWalletSDK.PrivateKey() try? self.resetOwnPrivateKey(with: ownPrivateKey) return } self.ownPrivateKey = storedKey self.peerPublicKey = try? storage.read(.peerPublicKey) if let peerPublicKey = self.peerPublicKey { self.symmetricKey = try? Cipher.deriveSymmetricKey(with: ownPrivateKey, peerPublicKey) } } func resetOwnPrivateKey(with key: CoinbaseWalletSDK.PrivateKey = CoinbaseWalletSDK.PrivateKey()) throws { self.symmetricKey = nil self.peerPublicKey = nil self.ownPrivateKey = key try storage.store(key, at: .ownPrivateKey) try storage.delete(.peerPublicKey) } func storePeerPublicKey(_ key: CoinbaseWalletSDK.PublicKey) throws { self.peerPublicKey = key self.symmetricKey = try Cipher.deriveSymmetricKey(with: ownPrivateKey, key) try storage.store(key, at: .peerPublicKey) } } ================================================ FILE: ios/CoinbaseWalletSDK/Key/KeyStorage.swift ================================================ // // KeyStorage.swift // WalletSegue // // Created by Jungho Bang on 6/17/22. // import Foundation final class KeyStorage { init(host: URL) { let service = "wsegue.keystorage.\((host.isHttp ? host.host : host.scheme) ?? host.absoluteString)" self.defaultQuery = [ kSecAttrService: service, kSecClass: kSecClassGenericPassword, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecUseDataProtectionKeychain: true ] let isInitializedKey = "\(service)-initialized" if UserDefaults.standard.bool(forKey: isInitializedKey) != true { SecItemDelete(self.defaultQuery as CFDictionary) UserDefaults.standard.set(true, forKey: isInitializedKey) } } private let defaultQuery: [CFString: Any] private func query( for item: KeyStorageItem, with other: [CFString: Any] = [:] ) -> CFDictionary { var query = self.defaultQuery query[kSecAttrAccount] = item.name return query.merging(other, uniquingKeysWith: { $1 }) as CFDictionary } func store(_ data: K, at item: KeyStorageItem) throws { try? self.delete(item) let query = query(for: item, with: [ kSecValueData: data.rawRepresentation ]) let status = SecItemAdd(query, nil) guard status == errSecSuccess else { throw KeyStorage.Error.storeFailed(status.message) } } func read(_ item: KeyStorageItem) throws -> K? { let query = query(for: item, with: [ kSecReturnData: true ]) var item: CFTypeRef? switch SecItemCopyMatching(query, &item) { case errSecSuccess: guard let data = item as? Data else { return nil } return try K(rawRepresentation: data) // Convert back to a key. case errSecItemNotFound: return nil case let status: throw KeyStorage.Error.readFailed(status.message) } } func delete(_ item: KeyStorageItem) throws { let query = query(for: item) switch SecItemDelete(query) { case errSecItemNotFound, errSecSuccess: break // Okay to ignore case let status: throw KeyStorage.Error.deleteFailed(status.message) } } } extension KeyStorage { enum Error: Swift.Error { case storeFailed(String) case readFailed(String) case deleteFailed(String) } } extension OSStatus { var message: String { return (SecCopyErrorMessageString(self, nil) as String?) ?? String(self) } } ================================================ FILE: ios/CoinbaseWalletSDK/Key/KeyStorageItem.swift ================================================ // // KeyStorageItem.swift // WalletSegue // // Created by Jungho Bang on 6/17/22. // import Foundation struct KeyStorageItem { let name: String init(_ name: String) { self.name = name } static var ownPrivateKey: KeyStorageItem { return KeyStorageItem("wsegue.ownPrivateKey") } static var peerPublicKey: KeyStorageItem { return KeyStorageItem("wsegue.peerPublicKey") } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Cipher.swift ================================================ // // Cryptography.swift // WalletSegue // // Created by Jungho Bang on 6/17/22. // import Foundation import CryptoKit final class Cipher { static func encrypt( _ content: C, with symmetricKey: SymmetricKey ) throws -> Data { let jsonData = try JSONEncoder().encode(content) let encrypted = try AES.GCM.seal(jsonData, using: symmetricKey).combined! return encrypted } static func decrypt( _ data: Data, with symmetricKey: SymmetricKey ) throws -> C { let sealedBox = try AES.GCM.SealedBox(combined: data) let decrypted = try AES.GCM.open(sealedBox, using: symmetricKey) return try JSONDecoder().decode(C.self, from: decrypted) } static func deriveSymmetricKey( with ownPrivateKey: CoinbaseWalletSDK.PrivateKey, _ peerPublicKey: CoinbaseWalletSDK.PublicKey ) throws -> SymmetricKey { let sharedSecret = try ownPrivateKey.sharedSecretFromKeyAgreement(with: peerPublicKey) return sharedSecret.hkdfDerivedSymmetricKey( using: SHA256.self, salt: Data(), sharedInfo: Data(), outputByteCount: 32 ) } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/EncryptedMessage.swift ================================================ // // EncryptedMessage.swift // WalletSegue // // Created by Jungho Bang on 6/23/22. // import Foundation import CryptoKit public protocol EncryptedContent: CodableContent { associatedtype Unencrypted: UnencryptedContent where Unencrypted.Encrypted == Self func decrypt(with symmetricKey: SymmetricKey?) throws -> Unencrypted } typealias EncryptedMessage = CodableMessage where C: EncryptedContent extension EncryptedMessage { func decrypt(with symmetricKey: SymmetricKey?) throws -> Message { return Message.copy( self, replaceContentWith: try self.content.decrypt(with: symmetricKey) ) } } extension Message { func encrypt(with symmetricKey: SymmetricKey?) throws -> EncryptedMessage { return EncryptedMessage.copy( self, replaceContentWith: try self.content.encrypt(with: symmetricKey) ) } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/JSONString.swift ================================================ // // JSONString.swift // CoinbaseWalletSDK // // Created by Jungho Bang on 8/19/22. // import Foundation public struct JSONString { public let rawValue: String // MARK: Encode public init?(encode value: T) { guard let encoded = try? JSONEncoder().encode(value) else { return nil } self.init(encodedData: encoded) } public init?(encode value: [String: Any]) { guard let encoded = try? JSONSerialization.data(withJSONObject: value, options: .fragmentsAllowed) else { return nil } self.init(encodedData: encoded) } private init?(encodedData: Data) { guard let string = String(data: encodedData, encoding: .utf8) else { return nil } self.rawValue = string } // MARK: Decode private var data: Data? { self.rawValue.data(using: .utf8) } public func decode() -> Any? { guard let data = self.data, let object = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) else { return nil } return object } public func decode(as type: T.Type) throws -> T? { guard let data = self.data else { return nil } return try JSONDecoder().decode(type, from: data) } } extension JSONString: RawRepresentable, Codable, CustomStringConvertible { public init?(rawValue: String) { self.rawValue = rawValue } public var description: String { self.rawValue } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Message.swift ================================================ // // Message.swift // WalletSegue // // Created by Jungho Bang on 6/13/22. // import Foundation import CryptoKit public typealias Message = CodableMessage where C: UnencryptedContent public protocol UnencryptedContent: CodableContent { associatedtype Encrypted: EncryptedContent where Encrypted.Unencrypted == Self func encrypt(with symmetricKey: SymmetricKey?) throws -> Encrypted } // MARK: - base types public protocol BaseContent {} extension Array: BaseContent where Element: BaseContent {} public struct BaseMessage { public let uuid: UUID public let sender: CoinbaseWalletSDK.PublicKey public let content: C public let version: String public let timestamp: Date public let callbackUrl: String? static func copy(_ orig: BaseMessage, replaceContentWith content: C) -> BaseMessage { return BaseMessage.init( uuid: orig.uuid, sender: orig.sender, content: content, version: orig.version, timestamp: orig.timestamp, callbackUrl: orig.callbackUrl ) } } // MARK: - codable types extension BaseMessage: Codable where C: Codable {} public protocol CodableContent: BaseContent, Codable {} extension Array: CodableContent where Element: CodableContent {} public typealias CodableMessage = BaseMessage where C: CodableContent ================================================ FILE: ios/CoinbaseWalletSDK/Message/MessageConverter.swift ================================================ // // MessageRenderer.swift // WalletSegue // // Created by Jungho Bang on 6/14/22. // import Foundation import CryptoKit final class MessageConverter { static func encode( _ message: Message, to recipient: URL, with symmetricKey: SymmetricKey? ) throws -> URL { let encrypted = try message.encrypt(with: symmetricKey) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .millisecondsSince1970 let data = try encoder.encode(encrypted) let encodedString = data.base64EncodedString() var urlComponents = URLComponents(url: recipient, resolvingAgainstBaseURL: true) urlComponents?.queryItems = [URLQueryItem(name: "p", value: encodedString)] guard let url = urlComponents?.url else { throw CoinbaseWalletSDK.Error.encodingFailed } return url } static func decode( _ url: URL, with symmetricKey: SymmetricKey? ) throws -> Message { let encrypted: EncryptedMessage = try self.decodeWithoutDecryption(url) return try encrypted.decrypt(with: symmetricKey) } static func decodeWithoutDecryption( _ url: URL ) throws -> EncryptedMessage { guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItem = urlComponents.queryItems?.first(where: { $0.name == "p" }), let encodedString = queryItem.value else { throw CoinbaseWalletSDK.Error.decodingFailed } guard let data = Data(base64Encoded: encodedString) else { throw CoinbaseWalletSDK.Error.decodingFailed } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .millisecondsSince1970 return try decoder.decode(EncryptedMessage.self, from: data) } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Request/Account.swift ================================================ // // Account.swift // WalletSegue // // Created by Jungho Bang on 6/13/22. // import Foundation public struct Account: Codable { public let chain: String public let networkId: UInt public let address: String public init(chain: String, networkId: UInt, address: String) { self.chain = chain self.networkId = networkId self.address = address } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Request/Action.swift ================================================ // // Action.swift // WalletSegueHost // // Created by Jungho Bang on 6/21/22. // import Foundation public struct Action: Codable { let method: String let paramsJson: String let optional: Bool public init(method: String, params: [String: Any], optional: Bool = false) { self.method = method self.paramsJson = String(data: try! JSONSerialization.data(withJSONObject: params), encoding: .utf8) ?? "" self.optional = optional } } extension Action { public init(jsonRpc: Web3JSONRPC, optional: Bool = false) { let (method, params) = jsonRpc.rawValues self.init( method: method, params: params, optional: optional ) } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Request/EncryptedRequestContent.swift ================================================ // // EncryptedRequestContent.swift // WalletSegue // // Created by Jungho Bang on 6/23/22. // import Foundation import CryptoKit public enum EncryptedRequestContent: EncryptedContent { case handshake(appId: String, callback: URL, initialActions: [Action]?) case request(data: Data) public func decrypt(with symmetricKey: SymmetricKey?) throws -> RequestContent { switch self { case let .handshake(appId, callback, initialActions): return .handshake(appId: appId, callback: callback, initialActions: initialActions) case let .request(data): guard let symmetricKey = symmetricKey else { throw CoinbaseWalletSDK.Error.missingSymmetricKey } let request: Request = try Cipher.decrypt(data, with: symmetricKey) return .request(actions: request.actions, account: request.account) } } } extension RequestContent { public func encrypt(with symmetricKey: SymmetricKey?) throws -> EncryptedRequestContent { switch self { case let .handshake(appId, callback, initialActions): return .handshake(appId: appId, callback: callback, initialActions: initialActions) case let .request(actions, account): guard let symmetricKey = symmetricKey else { throw CoinbaseWalletSDK.Error.missingSymmetricKey } let request = Request(actions: actions, account: account) return .request(data: try Cipher.encrypt(request, with: symmetricKey)) } } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Request/Request.swift ================================================ // // Request.swift // WalletSegueHost // // Created by Jungho Bang on 6/21/22. // import Foundation public struct Request: Codable { public let actions: [Action] public let account: Account? public init(actions: [Action], account: Account? = nil) { self.actions = actions self.account = account } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Request/RequestMessage.swift ================================================ // // RequestMessage.swift // WalletSegue // // Created by Jungho Bang on 6/9/22. // import Foundation public enum RequestContent: UnencryptedContent { case handshake(appId: String, callback: URL, initialActions: [Action]?) case request(actions: [Action], account: Account? = nil) } public typealias RequestMessage = Message ================================================ FILE: ios/CoinbaseWalletSDK/Message/Request/Web3JSONRPC.swift ================================================ // // Web3JSONRPC.swift // WalletSegue // // Created by Jungho Bang on 6/28/22. // import Foundation public typealias EthAddress = String public typealias EthTxData = String public typealias BigInt = String public enum Web3JSONRPC: Codable { case eth_requestAccounts case personal_sign( address: EthAddress, message: String ) case eth_signTypedData_v3( address: EthAddress, typedDataJson: JSONString ) case eth_signTypedData_v4( address: EthAddress, typedDataJson: JSONString ) case eth_signTransaction( fromAddress: EthAddress, toAddress: EthAddress?, weiValue: BigInt, data: EthTxData, nonce: Int?, gasPriceInWei: BigInt?, maxFeePerGas: BigInt?, maxPriorityFeePerGas: BigInt?, gasLimit: BigInt?, chainId: BigInt ) case eth_sendTransaction( fromAddress: EthAddress, toAddress: EthAddress?, weiValue: BigInt, data: EthTxData, nonce: Int?, gasPriceInWei: BigInt?, maxFeePerGas: BigInt?, maxPriorityFeePerGas: BigInt?, gasLimit: BigInt?, chainId: BigInt, actionSource: ActionSource? ) case wallet_switchEthereumChain( chainId: BigInt ) case wallet_addEthereumChain( chainId: BigInt, blockExplorerUrls: [String]?, chainName: String?, iconUrls: [String]?, nativeCurrency: AddChainNativeCurrency?, rpcUrls: [String] ) case wallet_watchAsset( type: String, options: WatchAssetOptions ) var rawValues: (method: String, params: [String: Any]) { let json = try! JSONEncoder().encode(self) let dictionary = try! JSONSerialization.jsonObject(with: json) as! [String: [String: Any]] let method = dictionary.keys.first! let params = dictionary[method]! return (method, params) } } public struct AddChainNativeCurrency: Codable { let name: String let symbol: String let decimals: Int public init(name: String, symbol: String, decimals: Int) { self.name = name self.symbol = symbol self.decimals = decimals } } public struct WatchAssetOptions: Codable { let address: String let symbol: String? let decimals: Int? let image: String? public init(address: String, symbol: String?, decimals: Int?, image: String?) { self.address = address self.symbol = symbol self.decimals = decimals self.image = image } } public struct ActionSource: Codable { let url: String public init(url: String) { self.url = url } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Response/ActionResult.swift ================================================ // // ActionResult.swift // WalletSegue // // Created by Jungho Bang on 6/24/22. // import Foundation public typealias ActionResult = Result extension ActionResult: BaseContent {} public struct ActionError: Swift.Error { public let code: Int public let message: String } public typealias ResponseResult = Result, Error> extension ResponseContent.Value { var asActionResult: ActionResult { switch self { case let .result(value): return .success(value) case let .error(code, message): return .failure(.init(code: code, message: message)) } } } public typealias ResponseHandler = (ResponseResult) -> Void extension ResponseMessage { var result: ResponseResult { switch self.content { case .response(_, let values): let results: [ActionResult] = values.map { $0.asActionResult } return .success( BaseMessage<[ActionResult]>.copy(self, replaceContentWith: results) ) case .failure(_, let description): return .failure(CoinbaseWalletSDK.Error.walletReturnedError(description)) } } } ================================================ FILE: ios/CoinbaseWalletSDK/Message/Response/EncryptedResponseContent.swift ================================================ // // EncryptedResponseContent.swift // WalletSegue // // Created by Jungho Bang on 6/23/22. // import Foundation import CryptoKit public enum EncryptedResponseContent: EncryptedContent { case response(requestId: UUID, data: Data) case failure(requestId: UUID, description: String) public func decrypt(with symmetricKey: SymmetricKey?) throws -> ResponseContent { switch self { case let .response(requestId, encryptedResults): guard let symmetricKey = symmetricKey else { throw CoinbaseWalletSDK.Error.missingSymmetricKey } let values: [ResponseContent.Value] = try Cipher.decrypt(encryptedResults, with: symmetricKey) return .response(requestId: requestId, values: values) case let .failure(requestId, description): return .failure(requestId: requestId, description: description) } } var requestId: UUID { switch self { case .response(let requestId, _), .failure(let requestId, _): return requestId } } } extension ResponseContent { public func encrypt(with symmetricKey: SymmetricKey?) throws -> EncryptedResponseContent { switch self { case let .response(requestId, results): guard let symmetricKey = symmetricKey else { throw CoinbaseWalletSDK.Error.missingSymmetricKey } let encrypted = try Cipher.encrypt(results, with: symmetricKey) return .response(requestId: requestId, data: encrypted) case let .failure(requestId, description): return .failure(requestId: requestId, description: description) } } } typealias EncryptedResponseMessage = EncryptedMessage ================================================ FILE: ios/CoinbaseWalletSDK/Message/Response/ResponseMessage.swift ================================================ // // Response.swift // WalletSegue // // Created by Jungho Bang on 6/13/22. // import Foundation public enum ResponseContent: UnencryptedContent { case response(requestId: UUID, values: [Value]) case failure(requestId: UUID, description: String) public enum Value: Codable { case result(value: JSONString) case error(code: Int, message: String) } var requestId: UUID { switch self { case .response(let requestId, _), .failure(let requestId, _): return requestId } } } public typealias ResponseMessage = Message ================================================ FILE: ios/CoinbaseWalletSDK/Message/URL+extension.swift ================================================ // // URL+extension.swift // WalletSegue // // Created by Jungho Bang on 6/29/22. // import Foundation extension URL { var isHttp: Bool { return self.scheme?.lowercased().hasPrefix("http") ?? false } } ================================================ FILE: ios/CoinbaseWalletSDK/Resources/CoinbaseWalletSDK+version.swift ================================================ // // CoinbaseWalletSDK+version.swift // WalletSegue // // Created by Jungho Bang on 9/12/22. // import Foundation extension CoinbaseWalletSDK { // CFBundleShortVersionString doesn't exist if the SDK is built as a static library. static private(set) var version = "1.1.0" #if CROSS_PLATFORM static public func appendVersionTag(_ tag: String) { self.version += "/\(tag)" } #endif } ================================================ FILE: ios/CoinbaseWalletSDK/Task/Task.swift ================================================ // // Task.swift // WalletSegue // // Created by Jungho Bang on 6/14/22. // import Foundation struct Task { let request: RequestMessage let handler: ResponseHandler let timestamp: Date } ================================================ FILE: ios/CoinbaseWalletSDK/Task/TaskManager.swift ================================================ // // TaskManager.swift // WalletSegue // // Created by Jungho Bang on 6/9/22. // import Foundation class TaskManager { private var tasks = [UUID: Task]() func registerResponseHandler( for request: RequestMessage, _ handler: @escaping ResponseHandler ) { let uuid = request.uuid tasks[uuid] = Task( request: request, handler: handler, timestamp: Date() ) } @discardableResult func runResponseHandler(with response: ResponseMessage) -> Bool { let requestId = response.content.requestId guard let task = tasks[requestId] else { return false } task.handler(response.result) tasks.removeValue(forKey: requestId) return true } func findRequest(for response: EncryptedResponseMessage) -> RequestMessage? { guard let task = tasks[response.content.requestId] else { return nil } return task.request } func reset() { tasks.removeAll() } } ================================================ FILE: ios/CoinbaseWalletSDK/Test/ExampleTest.swift ================================================ // // ExampleTest.swift // CoinbaseWalletSDK-Unit-Test // // Created by Jungho Bang on 10/7/22. // import XCTest @testable import CoinbaseWalletSDK class ExampleTest: XCTestCase { func testCoinbaseWalletSDKConfigure() { XCTAssertFalse(CoinbaseWalletSDK.isConfigured) CoinbaseWalletSDK.configure(callback: URL(string: "myappxyz://mycallback")!) XCTAssertTrue(CoinbaseWalletSDK.isConfigured) } } ================================================ FILE: ios/README.md ================================================ # Coinbase Wallet Mobile SDK [![Cocoapods](https://img.shields.io/cocoapods/v/CoinbaseWalletSDK)](https://cocoapods.org/pods/CoinbaseWalletSDK) Coinbase Wallet Mobile SDK is an open source SDK which allows you to connect your native mobile applications to millions of Coinbase Wallet users. ## Install The Coinbase Wallet Mobile SDK is available on both [CocoaPods](https://cocoapods.org/) and [Swift Package Manager](https://swift.org/package-manager). ### Cocoapods Add Coinbase Wallet SDK to your `Podfile`. ```ruby use_frameworks! target 'YOUR_TARGET_NAME' do pod 'CoinbaseWalletSDK', '1.1.2' end ``` Replace `YOUR_TARGET_NAME`, and then in the `Podfile` directory run: ```bash pod install ``` ### Swift Package Manager Add Coinbase Wallet SDK to your `Package.swift` file. Under **File > Add packages…** enter the package url: [https://github.com/coinbase/wallet-mobile-sdk](https://github.com/coinbase/wallet-mobile-sdk) ```swift import PackageDescription let package = Package( name: "YOUR_PROJECT_NAME", dependencies: [ .package(url: "https://github.com/coinbase/wallet-mobile-sdk.git", from: "1.1.2"), ] ) ``` Replace `YOUR_PROJECT_NAME`, and then run: ```bash swift build ``` ## Usage ### Setup Coinbase Wallet Mobile SDK uses Universal Links to communicate between Coinbase Wallet and your application. Before the SDK can be used, it needs to be configured with a Universal Link to your application. This callback URL will be used by the Coinbase Wallet application to navigate back to your application. ```swift CoinbaseWalletSDK.configure( callback: URL(string: "https://myappxyz.com/mycallback")! ) ``` When your application receives a response from Coinbase Wallet via a Universal Link, this URL needs to be handed off to the SDK via the handleResponse function. ```swift func application(_ app: UIApplication, open url: URL ...) -> Bool { if (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true { return true } // handle other types of deep links return false } ``` It’s recommended to place this configuration logic in the AppDelegate as shown in this [example](https://github.com/coinbase/wallet-mobile-sdk/blob/master/ios/example/SampleClient/AppDelegate.swift#L19). ### Establishing a connection A connection to Coinbase Wallet can be initiated by calling the `initiateHandshake` function provided by the SDK. The function also takes in an optional `initialActions` parameter which apps can use to take certain actions along with the initial handshake request. ```swift private let cbwallet = CoinbaseWalletSDK.shared cbwallet.initiateHandshake( initialActions: [ Action(jsonRpc: .eth_requestAccounts) ] ) { result, account in switch result { case .success(let response): self.logObject(label: "Response:\n", response) guard let account = account else { return } self.logObject(label: "Account:\n", account) self.address = account.address case .failure(let error): self.log("\(error)") } self.updateSessionStatus() } ``` An example handshake request is provided in the sample [application](https://github.com/coinbase/wallet-mobile-sdk/blob/master/ios/example/SampleClient/ViewController.swift#L63). ### Making requests Requests to Coinbase Wallet can be made by calling the `makeRequest` function provided by the SDK. This function also accepts a list of `actions` that can be taken in as a single batch request. ```swift cbwallet.makeRequest( Request(actions: [ Action(jsonRpc: .eth_signTypedData_v3( address: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", message: Data())) ]) ) { result in self.log("\(result)") } ``` An example request is provided in the sample [application](https://github.com/coinbase/coinbase-wallet-sdk/blob/master/examples/native-sdk-ios-client/SampleApp/ViewController.swift#L29). For more information on the types of requests you can make, visit our [developer documentation](https://docs.cloud.coinbase.com/wallet-sdk/docs/mobile-sdk-overview). ## References - Coinbase Wallet [Developer Documentation](https://docs.cloud.coinbase.com/wallet-sdk/docs) - Questions? Visit our [Developer Forums](https://forums.coinbasecloud.dev/). - For bugs, please report an issue on Github. ## License ``` Copyright © 2022 Coinbase, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: ios/example/Podfile ================================================ workspace 'Example.xcworkspace' platform :ios, '13.0' use_frameworks! target 'SampleClient' do project 'SampleClient' pod 'CoinbaseWalletSDK', path: '../../', :testspecs => ['Test'] end target 'SampleWallet' do project 'SampleWallet' pod 'CoinbaseWalletSDK/Host', path: '../../' end ================================================ FILE: ios/example/README.md ================================================ ![demo](./demo.gif) ================================================ FILE: ios/example/SampleClient/AppDelegate.swift ================================================ // // AppDelegate.swift // SampleWeb3App // // Created by Jungho Bang on 6/27/22. // import UIKit import CoinbaseWalletSDK @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { UIApplication.swizzleOpenURL() #warning("Should use universal links in production") CoinbaseWalletSDK.configure( // host: URL(string: "samplewallet://wsegue")!, callback: URL(string: "myappxyz://mycallback")! ) return true } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { if (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true { return true } // handle other types of deep links return false } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { if let url = userActivity.webpageURL, (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true { return true } // handle other types of deep links return false } } extension UIApplication { static func swizzleOpenURL() { guard let original = class_getInstanceMethod(UIApplication.self, #selector(open(_:options:completionHandler:))), let swizzled = class_getInstanceMethod(UIApplication.self, #selector(swizzledOpen(_:options:completionHandler:))) else { return } method_exchangeImplementations(original, swizzled) } @objc func swizzledOpen(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) { logWalletSegueMessage(url: url) // it's not recursive. below is actually the original open(_:) method self.swizzledOpen(url, options: options, completionHandler: completion) } } func logWalletSegueMessage(url: URL, function: String = #function) { let keyWindow = UIApplication.shared.connectedScenes .filter({$0.activationState == .foregroundActive}) .compactMap({$0 as? UIWindowScene}) .first?.windows .filter({$0.isKeyWindow}).first if let vc = keyWindow?.rootViewController as? ViewController { vc.logURL(url, function: function) } } ================================================ FILE: ios/example/SampleClient/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/example/SampleClient/Info.plist ================================================ CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName xyz.myapp CFBundleURLSchemes myappxyz LSApplicationQueriesSchemes cbwallet ================================================ FILE: ios/example/SampleClient/ViewController.swift ================================================ // // ViewController.swift // SampleWeb3App // // Created by Jungho Bang on 6/27/22. // import UIKit import CoinbaseWalletSDK class ViewController: UITableViewController { @IBOutlet weak var isCBWalletInstalledLabel: UILabel! @IBOutlet weak var isConnectedLabel: UILabel! @IBOutlet weak var ownPubKeyLabel: UILabel! @IBOutlet weak var peerPubKeyLabel: UILabel! @IBOutlet weak var logTextView: UITextView! private lazy var cbwallet = { CoinbaseWalletSDK.shared }() private var address: String? private let typedData = [ "types": [ "EIP712Domain": [ ["name": "name", "type": "string"], ["name": "version", "type": "string"], ["name": "chainId", "type": "uint256"], ["name": "verifyingContract", "type": "address"], ["name": "salt", "type": "bytes32"], ], "Bid": [ ["name": "amount", "type": "uint256"], ["name": "bidder", "type": "Identity"], ], "Identity": [ ["name": "userId", "type": "uint256"], ["name": "wallet", "type": "address"], ], ], "domain": [ "name": "DApp Browser Test DApp", "version": "1", "chainId": 1, "verifyingContract": "0x1C56346CD2A2Bf3202F771f50d3D14a367B48070", "salt": "0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558", ], "primaryType": "Bid", "message": [ "amount": 100, "bidder": [ "userId": 323, "wallet": "0x3333333333333333333333333333333333333333" ], ], ] as [String: Any] override func viewDidLoad() { super.viewDidLoad() isCBWalletInstalledLabel.text = "\(CoinbaseWalletSDK.isCoinbaseWalletInstalled())" self.log("Available MWP Version: \(CoinbaseWalletSDK.getCoinbaseWalletMWPVersion() ?? "none")") updateSessionStatus() } @IBAction func initiateHandshake() { cbwallet.initiateHandshake( initialActions: [ Action(jsonRpc: .eth_requestAccounts) ] ) { result, account in switch result { case .success(let response): self.log("Response: \(response.content)") guard let account = account else { return } self.logObject(label: "Account:\n", account) self.address = account.address case .failure(let error): self.log("\(error)") } self.updateSessionStatus() } } @IBAction func resetConnection() { self.address = nil let result = cbwallet.resetSession() self.log("\(result)") updateSessionStatus() } @IBAction func makeRequest() { let address = self.address ?? "" if address.isEmpty { self.log("address hasn't been set.") } cbwallet.makeRequest( Request(actions: [ Action(jsonRpc: .personal_sign(address: address, message: "message")), Action(jsonRpc: .eth_signTypedData_v3( address: address, typedDataJson: JSONString(encode: typedData)! )) ]) ) { result in guard case .success(let response) = result else { self.log("error: \(result)") return } for returnValue in response.content { switch returnValue { case .success(let value): self.log("result (raw JSON): \(value)") if let decoded = value.decode() { self.log("result (decoded): \(decoded)") } case .failure(let error): self.log("error \(error.code): \(error.message)") } } } } // i should have chosen SwiftUI template... private func updateSessionStatus() { DispatchQueue.main.async { let isConnected = self.cbwallet.isConnected() self.isConnectedLabel.textColor = isConnected ? .green : .red self.isConnectedLabel.text = "\(isConnected)" self.ownPubKeyLabel.text = self.cbwallet.ownPublicKey.rawRepresentation.base64EncodedString() self.peerPubKeyLabel.text = self.cbwallet.peerPublicKey?.rawRepresentation.base64EncodedString() ?? "(nil)" } } private func logObject(label: String = "", _ object: T, function: String = #function) { do { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(object) let jsonString = String(data: data, encoding: .utf8)! self.log("\(label)\(jsonString)", function: function) } catch { self.log("\(error)") } } func logURL(_ url: URL?, function: String = #function) { guard let url = url else { return } self.log("URL: \(url)", function: function) } private func log(_ text: String, function: String = #function) { DispatchQueue.main.async { self.logTextView.text = "\(function): \(text)\n\n\(self.logTextView.text ?? "")" // self.logTextView.text += "\(function): \(text)\n\n" // self.logTextView.scrollRangeToVisible(NSMakeRange(self.logTextView.text.count - 1, 1)) } } } ================================================ FILE: ios/example/SampleClient.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 55; objects = { /* Begin PBXBuildFile section */ 5DA9B3CF286A755E0089D6DB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA9B3CE286A755E0089D6DB /* AppDelegate.swift */; }; 5DA9B3D3286A755E0089D6DB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA9B3D2286A755E0089D6DB /* ViewController.swift */; }; 5DA9B3D6286A755E0089D6DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DA9B3D4286A755E0089D6DB /* Main.storyboard */; }; D6752D43EA87EC720AB457B0 /* Pods_SampleClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D748670EFBABCFCAAF467051 /* Pods_SampleClient.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 5DA9B3CB286A755E0089D6DB /* SampleClient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleClient.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5DA9B3CE286A755E0089D6DB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 5DA9B3D2286A755E0089D6DB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 5DA9B3D5286A755E0089D6DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 5DA9B3DC286A755F0089D6DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6AD987FC34FA605B42114137 /* Pods-SampleClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleClient.release.xcconfig"; path = "Target Support Files/Pods-SampleClient/Pods-SampleClient.release.xcconfig"; sourceTree = ""; }; AD64CFD01D3AB9B726DF335B /* Pods-NativeWeb3App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NativeWeb3App.release.xcconfig"; path = "Target Support Files/Pods-NativeWeb3App/Pods-NativeWeb3App.release.xcconfig"; sourceTree = ""; }; BE1C572549E74BC10010E50C /* Pods-SampleClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleClient.debug.xcconfig"; path = "Target Support Files/Pods-SampleClient/Pods-SampleClient.debug.xcconfig"; sourceTree = ""; }; D748670EFBABCFCAAF467051 /* Pods_SampleClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SampleClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FC120F48CB37E5C1FAFE2CAE /* Pods-NativeWeb3App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NativeWeb3App.debug.xcconfig"; path = "Target Support Files/Pods-NativeWeb3App/Pods-NativeWeb3App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 5DA9B3C8286A755E0089D6DB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D6752D43EA87EC720AB457B0 /* Pods_SampleClient.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 3E18EF9C1E2A7D3BF660A5CD /* Frameworks */ = { isa = PBXGroup; children = ( D748670EFBABCFCAAF467051 /* Pods_SampleClient.framework */, ); name = Frameworks; sourceTree = ""; }; 51AC5D6DCCB61B979D9D716E /* Pods */ = { isa = PBXGroup; children = ( FC120F48CB37E5C1FAFE2CAE /* Pods-NativeWeb3App.debug.xcconfig */, AD64CFD01D3AB9B726DF335B /* Pods-NativeWeb3App.release.xcconfig */, BE1C572549E74BC10010E50C /* Pods-SampleClient.debug.xcconfig */, 6AD987FC34FA605B42114137 /* Pods-SampleClient.release.xcconfig */, ); path = Pods; sourceTree = ""; }; 5DA9B3C2286A755E0089D6DB = { isa = PBXGroup; children = ( 5DA9B3CD286A755E0089D6DB /* SampleClient */, 5DA9B3CC286A755E0089D6DB /* Products */, 51AC5D6DCCB61B979D9D716E /* Pods */, 3E18EF9C1E2A7D3BF660A5CD /* Frameworks */, ); sourceTree = ""; }; 5DA9B3CC286A755E0089D6DB /* Products */ = { isa = PBXGroup; children = ( 5DA9B3CB286A755E0089D6DB /* SampleClient.app */, ); name = Products; sourceTree = ""; }; 5DA9B3CD286A755E0089D6DB /* SampleClient */ = { isa = PBXGroup; children = ( 5DA9B3CE286A755E0089D6DB /* AppDelegate.swift */, 5DA9B3D2286A755E0089D6DB /* ViewController.swift */, 5DA9B3D4286A755E0089D6DB /* Main.storyboard */, 5DA9B3DC286A755F0089D6DB /* Info.plist */, ); path = SampleClient; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 5DA9B3CA286A755E0089D6DB /* SampleClient */ = { isa = PBXNativeTarget; buildConfigurationList = 5DA9B3DF286A755F0089D6DB /* Build configuration list for PBXNativeTarget "SampleClient" */; buildPhases = ( 29FDABA7E59E1339DA4167A7 /* [CP] Check Pods Manifest.lock */, 5DA9B3C7286A755E0089D6DB /* Sources */, 5DA9B3C8286A755E0089D6DB /* Frameworks */, 5DA9B3C9286A755E0089D6DB /* Resources */, 4A2E3B84F8F828F77205E15C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = SampleClient; productName = NativeWeb3App; productReference = 5DA9B3CB286A755E0089D6DB /* SampleClient.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 5DA9B3C3286A755E0089D6DB /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1330; LastUpgradeCheck = 1330; TargetAttributes = { 5DA9B3CA286A755E0089D6DB = { CreatedOnToolsVersion = 13.3.1; }; }; }; buildConfigurationList = 5DA9B3C6286A755E0089D6DB /* Build configuration list for PBXProject "SampleClient" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 5DA9B3C2286A755E0089D6DB; productRefGroup = 5DA9B3CC286A755E0089D6DB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 5DA9B3CA286A755E0089D6DB /* SampleClient */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 5DA9B3C9286A755E0089D6DB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 5DA9B3D6286A755E0089D6DB /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 29FDABA7E59E1339DA4167A7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-SampleClient-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 4A2E3B84F8F828F77205E15C /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleClient/Pods-SampleClient-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleClient/Pods-SampleClient-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleClient/Pods-SampleClient-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 5DA9B3C7286A755E0089D6DB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 5DA9B3D3286A755E0089D6DB /* ViewController.swift in Sources */, 5DA9B3CF286A755E0089D6DB /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 5DA9B3D4286A755E0089D6DB /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 5DA9B3D5286A755E0089D6DB /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 5DA9B3DD286A755F0089D6DB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 5DA9B3DE286A755F0089D6DB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; 5DA9B3E0286A755F0089D6DB /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = BE1C572549E74BC10010E50C /* Pods-SampleClient.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = B7Y3D73M65; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SampleClient/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = Main; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.coinbase.SampleClient; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 5DA9B3E1286A755F0089D6DB /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 6AD987FC34FA605B42114137 /* Pods-SampleClient.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = B7Y3D73M65; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SampleClient/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = Main; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.coinbase.SampleClient; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 5DA9B3C6286A755E0089D6DB /* Build configuration list for PBXProject "SampleClient" */ = { isa = XCConfigurationList; buildConfigurations = ( 5DA9B3DD286A755F0089D6DB /* Debug */, 5DA9B3DE286A755F0089D6DB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 5DA9B3DF286A755F0089D6DB /* Build configuration list for PBXNativeTarget "SampleClient" */ = { isa = XCConfigurationList; buildConfigurations = ( 5DA9B3E0286A755F0089D6DB /* Debug */, 5DA9B3E1286A755F0089D6DB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 5DA9B3C3286A755E0089D6DB /* Project object */; } ================================================ FILE: ios/example/SampleWallet/AppDelegate.swift ================================================ // // AppDelegate.swift // SampleWallet // // Created by Jungho Bang on 7/1/22. // import UIKit import CoinbaseWalletSDK import CryptoKit @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { handleWalletSegue(url: url) return true } private var ownPrivateKey: CoinbaseWalletSDK.PrivateKey? private var peerPublicKey: CoinbaseWalletSDK.PublicKey? private var requestMessage: RequestMessage? private var peerCallback: URL? } extension AppDelegate: UIAlertViewDelegate { typealias AlertView = UIAlertView private var symmetricKey: SymmetricKey? { guard let o = ownPrivateKey, let p = peerPublicKey else { return nil } return try? CoinbaseWalletHostSDK.deriveSymmetricKey(with: o, p) } func handleWalletSegue(url: URL) { guard let request: RequestMessage = try? CoinbaseWalletHostSDK.decode(url, with: symmetricKey) else { return } self.requestMessage = request if case .handshake(_, let callback, _) = request.content { self.peerCallback = callback } let alert = AlertView( title: "Request", message: "decrypted content: \(request.content)", delegate: self, cancelButtonTitle: "Deny", otherButtonTitles: "Confirm" ) alert.show() } func alertView(_ alertView: AlertView, clickedButtonAt buttonIndex: Int) { guard let requestMessage = requestMessage else { preconditionFailure() } let content: ResponseContent let sender: CoinbaseWalletSDK.PublicKey if buttonIndex == 0 { // cancel content = .failure( requestId: requestMessage.uuid, description: "Request denied" ) sender = self.peerPublicKey ?? requestMessage.sender } else { let returnValues: [ResponseContent.Value] switch requestMessage.content { case .handshake: self.peerPublicKey = requestMessage.sender self.ownPrivateKey = CoinbaseWalletSDK.PrivateKey() let account = Account(chain: "eth", networkId: 0, address: "0x571a6a108adb08f9ca54fe8605280F9EE0eD4AF6") returnValues = [ .result(value: JSONString(encode: account)!) ] case let .request(actions, _): let error: ResponseContent.Value = .error(code: 713, message: "New CBWallet app will be able to actually handle those requests") returnValues = actions.map({ _ in error }) } content = .response( requestId: requestMessage.uuid, values: returnValues ) sender = self.ownPrivateKey!.publicKey } let response = ResponseMessage( sender: sender, content: content ) let url = try! CoinbaseWalletHostSDK.encode( response, to: peerCallback!, with: symmetricKey ) UIApplication.shared.open(url) } } ================================================ FILE: ios/example/SampleWallet/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/example/SampleWallet/Info.plist ================================================ CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName xyz.samplewallet CFBundleURLSchemes samplewallet ================================================ FILE: ios/example/SampleWallet.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 55; objects = { /* Begin PBXBuildFile section */ 5D3DD791286FB22000994152 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D3DD790286FB22000994152 /* AppDelegate.swift */; }; 5D3DD798286FB22000994152 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5D3DD796286FB22000994152 /* Main.storyboard */; }; D6FF46E37F6C3795EE2EFC4E /* Pods_SampleWallet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FF03AF7C2BAC9156E0E106E /* Pods_SampleWallet.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 1372E29251AC61A1A0F6DD2E /* Pods-SampleWallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleWallet.release.xcconfig"; path = "Target Support Files/Pods-SampleWallet/Pods-SampleWallet.release.xcconfig"; sourceTree = ""; }; 4515DD36563B470F9D13F93B /* Pods-SampleWallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleWallet.debug.xcconfig"; path = "Target Support Files/Pods-SampleWallet/Pods-SampleWallet.debug.xcconfig"; sourceTree = ""; }; 5D3DD78D286FB22000994152 /* SampleWallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleWallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5D3DD790286FB22000994152 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 5D3DD797286FB22000994152 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 5D3DD79E286FB22100994152 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5FF03AF7C2BAC9156E0E106E /* Pods_SampleWallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SampleWallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 5D3DD78A286FB22000994152 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D6FF46E37F6C3795EE2EFC4E /* Pods_SampleWallet.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 17AE06C6CE18D005E138A0E7 /* Pods */ = { isa = PBXGroup; children = ( 4515DD36563B470F9D13F93B /* Pods-SampleWallet.debug.xcconfig */, 1372E29251AC61A1A0F6DD2E /* Pods-SampleWallet.release.xcconfig */, ); path = Pods; sourceTree = ""; }; 5D3DD784286FB22000994152 = { isa = PBXGroup; children = ( 5D3DD78F286FB22000994152 /* SampleWallet */, 5D3DD78E286FB22000994152 /* Products */, 17AE06C6CE18D005E138A0E7 /* Pods */, 97A965865FD65B9253862A39 /* Frameworks */, ); sourceTree = ""; }; 5D3DD78E286FB22000994152 /* Products */ = { isa = PBXGroup; children = ( 5D3DD78D286FB22000994152 /* SampleWallet.app */, ); name = Products; sourceTree = ""; }; 5D3DD78F286FB22000994152 /* SampleWallet */ = { isa = PBXGroup; children = ( 5D3DD790286FB22000994152 /* AppDelegate.swift */, 5D3DD796286FB22000994152 /* Main.storyboard */, 5D3DD79E286FB22100994152 /* Info.plist */, ); path = SampleWallet; sourceTree = ""; }; 97A965865FD65B9253862A39 /* Frameworks */ = { isa = PBXGroup; children = ( 5FF03AF7C2BAC9156E0E106E /* Pods_SampleWallet.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 5D3DD78C286FB22000994152 /* SampleWallet */ = { isa = PBXNativeTarget; buildConfigurationList = 5D3DD7A1286FB22100994152 /* Build configuration list for PBXNativeTarget "SampleWallet" */; buildPhases = ( B4EBEDD6894AC736F4478EC1 /* [CP] Check Pods Manifest.lock */, 5D3DD789286FB22000994152 /* Sources */, 5D3DD78A286FB22000994152 /* Frameworks */, 5D3DD78B286FB22000994152 /* Resources */, A632CA3DEFDD6419D3331B57 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = SampleWallet; productName = SampleWallet; productReference = 5D3DD78D286FB22000994152 /* SampleWallet.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 5D3DD785286FB22000994152 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1330; LastUpgradeCheck = 1330; TargetAttributes = { 5D3DD78C286FB22000994152 = { CreatedOnToolsVersion = 13.3.1; }; }; }; buildConfigurationList = 5D3DD788286FB22000994152 /* Build configuration list for PBXProject "SampleWallet" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 5D3DD784286FB22000994152; productRefGroup = 5D3DD78E286FB22000994152 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 5D3DD78C286FB22000994152 /* SampleWallet */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 5D3DD78B286FB22000994152 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 5D3DD798286FB22000994152 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ A632CA3DEFDD6419D3331B57 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleWallet/Pods-SampleWallet-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleWallet/Pods-SampleWallet-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleWallet/Pods-SampleWallet-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; B4EBEDD6894AC736F4478EC1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-SampleWallet-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 5D3DD789286FB22000994152 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 5D3DD791286FB22000994152 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 5D3DD796286FB22000994152 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 5D3DD797286FB22000994152 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 5D3DD79F286FB22100994152 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 5D3DD7A0286FB22100994152 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; 5D3DD7A2286FB22100994152 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4515DD36563B470F9D13F93B /* Pods-SampleWallet.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = B7Y3D73M65; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SampleWallet/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.coinbase.SampleWallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 5D3DD7A3286FB22100994152 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 1372E29251AC61A1A0F6DD2E /* Pods-SampleWallet.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = B7Y3D73M65; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SampleWallet/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.coinbase.SampleWallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 5D3DD788286FB22000994152 /* Build configuration list for PBXProject "SampleWallet" */ = { isa = XCConfigurationList; buildConfigurations = ( 5D3DD79F286FB22100994152 /* Debug */, 5D3DD7A0286FB22100994152 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 5D3DD7A1286FB22100994152 /* Build configuration list for PBXNativeTarget "SampleWallet" */ = { isa = XCConfigurationList; buildConfigurations = ( 5D3DD7A2286FB22100994152 /* Debug */, 5D3DD7A3286FB22100994152 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 5D3DD785286FB22000994152 /* Project object */; } ================================================ FILE: react-native/.eslintrc.js ================================================ // @generated by expo-module-scripts module.exports = require('expo-module-scripts/eslintrc.base.js'); ================================================ FILE: react-native/.npmignore ================================================ example ================================================ FILE: react-native/README.md ================================================ # @coinbase/wallet-mobile-sdk Coinbase Wallet Mobile SDK for React Native # Installation in bare React Native projects For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing. ### Add the package to your npm dependencies ``` npm install @coinbase/wallet-mobile-sdk ``` ### Configure for iOS Run `npx pod-install` after installing the npm package. ### Configure for Android No additional setup necessary. ================================================ FILE: react-native/android/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'maven-publish' group = 'expo.modules.coinbasewalletsdkexpo' version = '0.1.0' buildscript { def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") if (expoModulesCorePlugin.exists()) { apply from: expoModulesCorePlugin applyKotlinExpoModulesCorePlugin() } // Simple helper that allows the root project to override versions declared by this library. ext.safeExtGet = { prop, fallback -> rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } // Ensures backward compatibility ext.getKotlinVersion = { if (ext.has("kotlinVersion")) { ext.kotlinVersion() } else { ext.safeExtGet("kotlinVersion", "1.8.10") } } repositories { mavenCentral() } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}") } } afterEvaluate { publishing { publications { release(MavenPublication) { from components.release } } repositories { maven { url = mavenLocal().url } } } } android { compileSdkVersion safeExtGet("compileSdkVersion", 33) def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION if (agpVersion.tokenize('.')[0].toInteger() < 8) { compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = JavaVersion.VERSION_11.majorVersion } } namespace "expo.modules.coinbasewalletsdkexpo" defaultConfig { minSdkVersion safeExtGet("minSdkVersion", 23) targetSdkVersion safeExtGet("targetSdkVersion", 31) versionCode 1 versionName "0.1.0" consumerProguardFiles 'proguard-rules.pro' } lintOptions { abortOnError false } publishing { singleVariant("release") { withSourcesJar() } } } repositories { mavenCentral() } dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" implementation "com.coinbase:coinbase-wallet-sdk:1.1.1" implementation 'androidx.appcompat:appcompat:1.4.2' } ================================================ FILE: react-native/android/proguard-rules.pro ================================================ -keepclassmembers class expo.modules.coinbasewalletsdkexpo.* { ; (); ; } ================================================ FILE: react-native/android/src/main/AndroidManifest.xml ================================================ ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/ActivityLifecycleListener.kt ================================================ package expo.modules.coinbasewalletsdkexpo import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import expo.modules.core.interfaces.ReactActivityLifecycleListener object IntentLauncher { var launcher: ActivityResultLauncher? = null var onResult: ((Uri) -> Unit)? = null } class ActivityLifecycleListener : ReactActivityLifecycleListener { override fun onCreate(activity: Activity?, savedInstanceState: Bundle?) { super.onCreate(activity, savedInstanceState) val currentActivity = requireNotNull(activity as? AppCompatActivity) { "CoinbaseWalletSDK: activity must be AppCompatActivity" } IntentLauncher.launcher = currentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val uri = result.data?.data ?: return@registerForActivityResult IntentLauncher.onResult?.invoke(uri) } } } ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/CoinbaseWalletSDKModule.kt ================================================ package expo.modules.coinbasewalletsdkexpo import android.net.Uri import android.util.Log import com.coinbase.android.nativesdk.CoinbaseWalletSDK import com.coinbase.android.nativesdk.message.request.Account import com.coinbase.android.nativesdk.message.request.Action import com.coinbase.android.nativesdk.message.request.RequestContent import expo.modules.coinbasewalletsdkexpo.records.AccountRecord import expo.modules.coinbasewalletsdkexpo.records.ActionRecord import expo.modules.coinbasewalletsdkexpo.records.ActionResultRecord import expo.modules.coinbasewalletsdkexpo.records.ConfigParamsRecord import expo.modules.coinbasewalletsdkexpo.records.HandshakeParamsRecord import expo.modules.coinbasewalletsdkexpo.records.RequestParamsRecord import expo.modules.coinbasewalletsdkexpo.records.asAction import expo.modules.coinbasewalletsdkexpo.records.asRecord import expo.modules.kotlin.Promise import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record class CoinbaseWalletSDKModule : Module() { private var sdk: CoinbaseWalletSDK? = null override fun definition() = ModuleDefinition { Name("CoinbaseWalletSDK") Function("configure") { params: ConfigParamsRecord -> try { IntentLauncher.onResult = { uri -> sdk?.handleResponse(uri) } val context = requireNotNull(appContext.reactContext?.applicationContext) { "CoinbaseWalletSDK: Application context must not be null" } sdk = CoinbaseWalletSDK( domain = Uri.parse(params.callbackURL), appContext = context, hostPackageName = params.hostPackageName ?: "org.toshi", openIntent = { IntentLauncher.launcher?.launch(it) } ) sdk?.appendVersionTag("rn") } catch (e: Exception) { Log.e("CoinbaseWalletSDK", "Configuration error", e) } } AsyncFunction("initiateHandshake") { params: HandshakeParamsRecord, promise: Promise -> val sdk = sdk if (sdk == null) { promise.reject("configure-error", "configure must be called before handshake can be initiated", null) return@AsyncFunction } val handshakeActions = params.initialActions.map { it.asAction } sdk.initiateHandshake(handshakeActions) { result, account -> result .onSuccess { responses -> val results: List = responses.map { it.asRecord } val accountRecord = account?.asRecord promise.resolve(listOf(results, accountRecord)) } .onFailure { error -> Log.e("CoinbaseWalletSDK", "Handshake error", error) promise.reject("handshake-error", error.message, error) } } } AsyncFunction("makeRequest") { params: RequestParamsRecord, promise: Promise -> val sdk = sdk if (sdk == null) { promise.reject("configure-error", "configure must be called before request can be initiated", null) return@AsyncFunction } val requestActions = params.actions.map { it.asAction } val requestAccount = params.account?.let { record -> Account( chain = record.chain, networkId = record.networkId.toLong(), address = record.address ) } val request = RequestContent.Request(actions = requestActions, account = requestAccount) sdk.makeRequest(request) { result -> result .onSuccess { responses -> val results: List = responses.map { it.asRecord } promise.resolve(results) } .onFailure { error -> Log.e("CoinbaseWalletSDK", "Request error", error) promise.reject("request-error", error.message, error) } } } Function("handleResponse") { url: String -> val responseURL = Uri.parse(url) try { return@Function sdk?.handleResponse(responseURL) ?: false } catch (error: Exception) { return@Function false } } Function("isCoinbaseWalletInstalled") { return@Function sdk?.isCoinbaseWalletInstalled ?: false } Function("getCoinbaseWalletMWPVersion") { val context = requireNotNull(appContext.reactContext?.applicationContext) { "CoinbaseWalletSDK: Application context must not be null" } return@Function CoinbaseWalletSDK.getCoinbaseWalletMWPVersion(context) } Function("isConnected") { return@Function sdk?.isConnected ?: false } Function("resetSession") { sdk?.resetSession() } } } ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/CoinbaseWalletSDKPackage.kt ================================================ package expo.modules.coinbasewalletsdkexpo import android.content.Context import expo.modules.core.interfaces.Package import expo.modules.core.interfaces.ReactActivityLifecycleListener class CoinbaseWalletSDKPackage : Package { override fun createReactActivityLifecycleListeners(activityContext: Context?): List { return listOf(ActivityLifecycleListener()) } } ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/records/AccountRecord.kt ================================================ package expo.modules.coinbasewalletsdkexpo.records import com.coinbase.android.nativesdk.message.request.Account import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record class AccountRecord : Record { @Field var chain: String = "" @Field var networkId: Int = 1 @Field var address: String = "" } val Account.asRecord: AccountRecord get() { val record = AccountRecord() record.chain = chain record.networkId = networkId.toInt() record.address = address return record } ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/records/ActionRecord.kt ================================================ package expo.modules.coinbasewalletsdkexpo.records import com.coinbase.android.nativesdk.message.request.Action import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record class ActionRecord : Record { @Field var method: String = "" @Field var paramsJson: String = "{}" @Field var optional: Boolean = false } val ActionRecord.asAction: Action get() { return Action( method = this.method, paramsJson = this.paramsJson, optional = this.optional ) } ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/records/ActionResultRecord.kt ================================================ package expo.modules.coinbasewalletsdkexpo.records import com.coinbase.android.nativesdk.message.response.ActionResult import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record class ActionResultRecord : Record { @Field var result: String? = null @Field var errorMessage: String? = null @Field var errorCode: Int? = null } val ActionResult.asRecord: ActionResultRecord get() { val record = ActionResultRecord() when (this) { is ActionResult.Result -> record.result = value is ActionResult.Error -> { record.errorCode = code.toInt() record.errorMessage = message } } return record } ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/records/ConfigParamsRecord.kt ================================================ package expo.modules.coinbasewalletsdkexpo.records import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record class ConfigParamsRecord : Record { @Field var callbackURL: String = "" @Field var hostURL: String? = null @Field var hostPackageName: String? = null } ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/records/HandshakeParamsRecord.kt ================================================ package expo.modules.coinbasewalletsdkexpo.records import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record class HandshakeParamsRecord : Record { @Field var initialActions: List = listOf() } ================================================ FILE: react-native/android/src/main/java/expo/modules/coinbasewalletsdkexpo/records/RequestParamsRecord.kt ================================================ package expo.modules.coinbasewalletsdkexpo.records import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record class RequestParamsRecord : Record { @Field var actions: List = listOf() @Field var account: AccountRecord? = null } ================================================ FILE: react-native/example/.buckconfig ================================================ [android] target = Google Inc.:Google APIs:23 [maven_repositories] central = https://repo1.maven.org/maven2 ================================================ FILE: react-native/example/.bundle/config ================================================ BUNDLE_PATH: "vendor/bundle" BUNDLE_FORCE_RUBY_PLATFORM: 1 ================================================ FILE: react-native/example/.eslintrc.js ================================================ module.exports = { root: true, extends: '@react-native-community', }; ================================================ FILE: react-native/example/.flowconfig ================================================ [ignore] ; We fork some components by platform .*/*[.]android.js ; Ignore "BUCK" generated dirs /\.buckd/ ; Ignore polyfills node_modules/react-native/Libraries/polyfills/.* ; Flow doesn't support platforms .*/Libraries/Utilities/LoadingView.js .*/node_modules/resolve/test/resolver/malformed_package_json/package\.json$ [untyped] .*/node_modules/@react-native-community/cli/.*/.* [include] [libs] node_modules/react-native/interface.js node_modules/react-native/flow/ [options] emoji=true exact_by_default=true format.bracket_spacing=false module.file_ext=.js module.file_ext=.json module.file_ext=.ios.js munge_underscores=true module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn unnecessary-invariant=warn [strict] deprecated-type nonstrict-import sketchy-null unclear-type unsafe-getters-setters untyped-import untyped-type-import [version] ^0.182.0 ================================================ FILE: react-native/example/.gitignore ================================================ # OSX # .DS_Store # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate ios/.xcode.env.local # Android/IntelliJ # build/ .idea .gradle local.properties *.iml *.hprof .cxx/ # node.js # node_modules/ npm-debug.log yarn-error.log # BUCK buck-out/ \.buckd/ *.keystore !debug.keystore # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/ **/fastlane/report.xml **/fastlane/Preview.html **/fastlane/screenshots **/fastlane/test_output # Bundle artifact *.jsbundle # Ruby / CocoaPods /ios/Pods/ /vendor/bundle/ .expo/ # Don't check in native directories android/ ios/ ================================================ FILE: react-native/example/.prettierrc.js ================================================ module.exports = { arrowParens: 'avoid', bracketSameLine: true, bracketSpacing: false, singleQuote: true, trailingComma: 'all', }; ================================================ FILE: react-native/example/.watchmanconfig ================================================ {} ================================================ FILE: react-native/example/App.js ================================================ import React from 'react'; import {useState, useMemo, useEffect, useCallback} from 'react'; import { Button, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, View, } from 'react-native'; import {Colors} from 'react-native/Libraries/NewAppScreen'; import { configure, getCoinbaseWalletMWPVersion, handleResponse, isCoinbaseWalletInstalled, WalletMobileSDKEVMProvider, } from '@coinbase/wallet-mobile-sdk'; import {MMKV} from 'react-native-mmkv'; // Configure Mobile SDK configure({ hostURL: new URL('https://wallet.coinbase.com/wsegue'), callbackURL: new URL('example.rn.dapp://'), // Your app's Universal Link hostPackageName: 'org.toshi', }); const provider = new WalletMobileSDKEVMProvider(); const storage = new MMKV(); const App = function () { const [log, setLog] = useState(''); const cachedAddress = useMemo(() => storage.getString('address'), []); const [address, setAddress] = useState(cachedAddress); const isConnected = address !== undefined; useEffect(function setupDeeplinkHandling() { // Pass incoming deeplinks into Mobile SDK const subscription = Linking.addEventListener('url', ({url}) => { console.log('-- handleResponse --', url); handleResponse(url); }); return function cleanup() { subscription.remove(); }; }, []); const logMessage = useCallback(message => { setLog(prev => `${message}\n${prev}`); }, []); useEffect(() => { async function logVersion() { logMessage( `Coinbase Wallet Installed?: ${await isCoinbaseWalletInstalled()}`, ); logMessage( `Available MWP Version: ${await getCoinbaseWalletMWPVersion()}`, ); } logVersion(); }, [logMessage]); // Initiate connection to Wallet const connectWallet = useCallback(async () => { logMessage('--> eth_requestAccounts\n'); try { const accounts = await provider.request({ method: 'eth_requestAccounts', params: [], }); setAddress(accounts[0]); storage.set('address', accounts[0]); logMessage(`<-- ${accounts}`); } catch (e) { console.error(e.message); logMessage('<-- error connecting'); } }, [logMessage]); // Reset connection to Wallet const resetConnection = useCallback(() => { logMessage('--- Disconnect\n'); provider.disconnect(); setAddress(undefined); storage.delete('address'); }, [logMessage]); const personalSign = useCallback(async () => { logMessage('--> personal_sign\n'); try { const result = await provider.request({ method: 'personal_sign', params: ['0x48656c6c6f20776f726c64', address], }); logMessage(`<-- ${result}`); } catch (e) { logMessage(`<-- ${e.message}`); } }, [logMessage, address]); const switchToEthereumChain = useCallback(async () => { logMessage('--> wallet_switchEthereumChain: 0x1\n'); try { const result = await provider.request({ method: 'wallet_switchEthereumChain', params: [{chainId: '0x1'}], }); logMessage(`<-- ${result}`); } catch (e) { console.error(e.message); logMessage('<-- error'); } }, [logMessage]); const switchToPolygonChain = useCallback(async () => { logMessage('--> wallet_switchEthereumChain: 0x89\n'); try { const result = await provider.request({ method: 'wallet_switchEthereumChain', params: [{chainId: '0x89'}], }); logMessage(`<-- ${result}`); } catch (e) { console.error(e.message); logMessage('<-- error'); } }, [logMessage]); const addMumbaiTestnet = useCallback(async () => { logMessage('--> wallet_addEthereumChain: Mumbai Testnet\n'); try { const result = await provider.request({ method: 'wallet_addEthereumChain', params: [ { chainId: '0x13881', chainName: 'Matic(Polygon) Mumbai Testnet', nativeCurrency: { name: 'tMATIC', symbol: 'tMATIC', decimals: 18, }, rpcUrls: ['https://rpc-mumbai.maticvigil.com'], blockExplorerUrls: ['https://mumbai.polygonscan.com/'], }, ], }); logMessage(`<-- ${result}`); } catch (e) { console.error(e.message); logMessage('<-- error'); } }, [logMessage]); const backgroundStyle = { backgroundColor: Colors.lighter, }; return (
{!isConnected ? (
{log}
); }; const Section = function ({children, title}) { return ( {title} {children} ); }; const styles = StyleSheet.create({ sectionContainer: { marginTop: 24, paddingHorizontal: 24, }, sectionTitle: { fontSize: 24, fontWeight: '600', }, sectionDescription: { marginTop: 8, fontSize: 18, fontWeight: '400', }, highlight: { fontWeight: '700', }, scrollViewStyle: { height: '50%', }, }); export default App; ================================================ FILE: react-native/example/__tests__/App-test.js ================================================ /** * @format */ import 'react-native'; import React from 'react'; import App from '../App'; // Note: test renderer must be required after react-native. import renderer from 'react-test-renderer'; it('renders correctly', () => { renderer.create(); }); ================================================ FILE: react-native/example/_node-version ================================================ 16 ================================================ FILE: react-native/example/app.json ================================================ { "name": "example", "displayName": "example", "android": { "package": "com.anonymous.example" }, "ios": { "bundleIdentifier": "com.anonymous.example" }, "plugins": [ [ "expo-build-properties", { "android": { "minSdkVersion": 23 } } ] ] } ================================================ FILE: react-native/example/babel.config.js ================================================ const path = require('path'); const pak = require('../package.json'); const libraryIndex = path.join(__dirname, '..', 'src', 'CoinbaseWalletSDK.ts'); module.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], plugins: [ [ 'module-resolver', { extensions: ['.tsx', '.ts', '.js', '.json'], alias: { // For development, we want to alias the library to the source [pak.name]: libraryIndex, }, }, ], ], }; }; ================================================ FILE: react-native/example/index.js ================================================ import {registerRootComponent} from 'expo'; import App from './App'; // registerRootComponent calls AppRegistry.registerComponent('main', () => App); // It also ensures that whether you load the app in Expo Go or in a native build, // the environment is set up appropriately registerRootComponent(App); ================================================ FILE: react-native/example/metro.config.js ================================================ const {getDefaultConfig} = require('expo/metro-config'); const path = require('path'); const config = getDefaultConfig(__dirname); config.resolver.blockList = [ ...Array.from(config.resolver.blockList ?? []), new RegExp(path.resolve('..', 'node_modules', 'react-native')), ]; config.resolver.nodeModulesPaths = [ path.resolve(__dirname, './node_modules'), path.resolve(__dirname, '../node_modules'), ]; config.watchFolders = [path.resolve(__dirname, '..')]; config.transformer.getTransformOptions = async () => ({ transform: { experimentalImportSupport: false, inlineRequires: true, }, }); module.exports = config; ================================================ FILE: react-native/example/package.json ================================================ { "name": "example", "version": "0.0.1", "private": true, "scripts": { "start": "expo start --dev-client", "android": "expo run:android", "ios": "expo run:ios", "test": "jest", "lint": "eslint ." }, "dependencies": { "expo": "50.0.3", "expo-build-properties": "0.11.0", "expo-splash-screen": "0.26.4", "expo-status-bar": "~1.6.0", "react": "18.2.0", "react-native": "0.73.0", "react-native-mmkv": "^2.11.0" }, "devDependencies": { "@babel/core": "^7.12.9", "@babel/runtime": "^7.12.5", "@react-native-community/eslint-config": "^2.0.0", "babel-jest": "^26.6.3", "eslint": "^7.32.0", "jest": "^29.2.1", "metro-react-native-babel-preset": "^0.72.1", "react-test-renderer": "18.1.0" }, "jest": { "preset": "react-native" }, "resolutions": { "expo/**/json5": "^2.2.2" }, "expo": { "autolinking": { "nativeModulesDir": ".." } } } ================================================ FILE: react-native/expo-module.config.json ================================================ { "platforms": ["ios", "android"], "ios": { "modules": ["CoinbaseWalletSDKModule"] }, "android": { "modules": ["expo.modules.coinbasewalletsdkexpo.CoinbaseWalletSDKModule"] } } ================================================ FILE: react-native/ios/CoinbaseWalletSDKExpo.podspec ================================================ require 'json' package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json'))) Pod::Spec.new do |s| s.name = 'CoinbaseWalletSDKExpo' s.version = package['version'] s.summary = package['description'] s.description = package['description'] s.license = package['license'] s.author = package['author'] s.homepage = package['homepage'] s.platform = :ios, '13.0' s.swift_version = '5.4' s.source = { git: 'https://github.com/coinbase/wallet-mobile-sdk' } s.static_framework = true s.dependency 'ExpoModulesCore' s.dependency 'CoinbaseWalletSDK/CrossPlatform', '1.1.2' # Swift/Objective-C compatibility s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'SWIFT_COMPILATION_MODE' => 'wholemodule' } if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0') s.source_files = "**/*.h" s.vendored_frameworks = "#{s.name}.xcframework" else s.source_files = "**/*.{h,m,swift}" end end ================================================ FILE: react-native/ios/CoinbaseWalletSDKModule.swift ================================================ import ExpoModulesCore import CoinbaseWalletSDK import Foundation public class CoinbaseWalletSDKModule: Module { public func definition() -> ModuleDefinition { Name("CoinbaseWalletSDK") Function("configure") { (params: ConfigParamsRecord) in guard !CoinbaseWalletSDK.isConfigured else { return } let host: URL if let hostURLStr = params.hostURL { host = URL(string: hostURLStr)! } else { host = URL(string: "https://wallet.coinbase.com/wsegue")! } CoinbaseWalletSDK.configure( host: host, callback: URL(string: params.callbackURL)! ) CoinbaseWalletSDK.appendVersionTag("rn") } AsyncFunction("initiateHandshake") { (params: HandshakeParamsRecord, promise: Promise) in let actions: [Action] = params.initialActions.map { $0.asAction } DispatchQueue.main.async { CoinbaseWalletSDK.shared.initiateHandshake(initialActions: actions) { result, account in switch result { case .success(let response): let results: [ActionResultRecord.Dict] = response.content.map { $0.asRecord } let accountRecord = account?.asRecord promise.resolve([results, accountRecord]) case .failure(let error): promise.reject("handshake-error", "\(error)"); } } } } AsyncFunction("makeRequest") { (params: RequestParamsRecord, promise: Promise) in let requestActions: [Action] = params.actions.map { $0.asAction } let requestAccount: Account? if let account = params.account { requestAccount = Account( chain: account.chain, networkId: UInt(account.networkId), address: account.address ) } else { requestAccount = nil } DispatchQueue.main.async { CoinbaseWalletSDK.shared.makeRequest( Request(actions: requestActions, account: requestAccount) ) { result in switch result { case .success(let response): let results: [ActionResultRecord.Dict] = response.content.map { $0.asRecord } promise.resolve(results) case .failure(let error): promise.reject("request-error", "\(error)") } } } } Function("handleResponse") { (url: String) -> Bool in guard CoinbaseWalletSDK.isConfigured else { return false } let responseURL = URL(string: url)! if (try? CoinbaseWalletSDK.shared.handleResponse(responseURL)) == true { return true } return false } AsyncFunction("isCoinbaseWalletInstalled") { (promise: Promise) in DispatchQueue.main.async { promise.resolve(CoinbaseWalletSDK.isCoinbaseWalletInstalled()) } } AsyncFunction("getCoinbaseWalletMWPVersion") { (promise: Promise) in DispatchQueue.main.async { promise.resolve(CoinbaseWalletSDK.getCoinbaseWalletMWPVersion()) } } Function("isConnected") { () -> Bool in return CoinbaseWalletSDK.shared.isConnected() } Function("resetSession") { CoinbaseWalletSDK.shared.resetSession() } } } ================================================ FILE: react-native/ios/Records/AccountRecord.swift ================================================ import CoinbaseWalletSDK import ExpoModulesCore import Foundation struct AccountRecord : Record { @Field var chain: String @Field var networkId: Int @Field var address: String } extension Account { var asRecord: AccountRecord.Dict { let record = AccountRecord() record.chain = self.chain record.networkId = Int(self.networkId) record.address = self.address return record.toDictionary() } } ================================================ FILE: react-native/ios/Records/ActionRecord.swift ================================================ import ExpoModulesCore import CoinbaseWalletSDK import Foundation struct ActionRecord : Record { @Field var method: String @Field var paramsJson: String @Field var optional: Bool } extension ActionRecord { var asAction: Action { let paramsJson = self.paramsJson.data(using: .utf8)! let params = try! JSONSerialization.jsonObject(with: paramsJson) as! [String: Any] return Action(method: self.method, params: params) } } ================================================ FILE: react-native/ios/Records/ActionResultRecord.swift ================================================ import CoinbaseWalletSDK import ExpoModulesCore import Foundation struct ActionResultRecord : Record { @Field var result: String? @Field var errorMessage: String? @Field var errorCode: Int? } extension ActionResult { var asRecord: ActionResultRecord.Dict { let record = ActionResultRecord() switch self { case .success(let value): record.result = value.rawValue case .failure(let error): record.errorCode = error.code record.errorMessage = error.message } return record.toDictionary() } } ================================================ FILE: react-native/ios/Records/ConfigParamsRecord.swift ================================================ import ExpoModulesCore import Foundation struct ConfigParamsRecord : Record { @Field var callbackURL: String @Field var hostURL: String? @Field var hostPackageName: String? } ================================================ FILE: react-native/ios/Records/HandshakeParamsRecord.swift ================================================ import ExpoModulesCore import Foundation struct HandshakeParamsRecord : Record { @Field var initialActions: [ActionRecord] } ================================================ FILE: react-native/ios/Records/RequestParamsRecord.swift ================================================ import ExpoModulesCore import Foundation struct RequestParamsRecord : Record { @Field var actions: [ActionRecord] @Field var account: AccountRecord? } ================================================ FILE: react-native/package.json ================================================ { "name": "@coinbase/wallet-mobile-sdk", "version": "1.1.2", "description": "Coinbase Wallet Mobile SDK for React Native", "main": "build/CoinbaseWalletSDK.js", "types": "build/CoinbaseWalletSDK.d.ts", "scripts": { "build": "expo-module build", "clean": "expo-module clean", "lint": "expo-module lint", "test": "expo-module test", "prepare": "expo-module prepare", "prepublishOnly": "expo-module prepublishOnly", "expo-module": "expo-module" }, "keywords": [ "react-native", "expo", "coinbase-wallet-sdk-expo", "CoinbaseWalletSDK" ], "repository": "https://github.com/coinbase/wallet-mobile-sdk", "bugs": { "url": "https://github.com/coinbase/wallet-mobile-sdk/issues" }, "author": "Coinbase, Inc.", "license": "Apache-2.0", "homepage": "https://github.com/coinbase/wallet-mobile-sdk#readme", "dependencies": { "@metamask/safe-event-emitter": "2.0.0", "eth-rpc-errors": "4.0.3", "buffer": "6.0.3", "bn.js": "5.2.1", "events": "^3.0.0", "react-native-mmkv": "^2.11.0" }, "devDependencies": { "@types/react": "^18.0.25", "@types/react-native": "^0.70.6", "expo-module-scripts": "^3.0.4", "expo-modules-core": "^1.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" }, "resolutions": { "expo-module-scripts/**/json5": "^2.2.2", "expo-module-scripts/**/jsdom": "^16.5.0" } } ================================================ FILE: react-native/src/CoinbaseWalletSDK.ts ================================================ import CoinbaseWalletSDK from "./CoinbaseWalletSDKModule"; import { Action, Account, ConfigurationParams, Result, } from "./CoinbaseWalletSDK.types"; export { Action, Account, ConfigurationParams, Result, } from "./CoinbaseWalletSDK.types"; export { WalletMobileSDKEVMProvider } from "./WalletMobileSDKEVMProvider"; export function configure({ callbackURL, hostURL, hostPackageName, }: ConfigurationParams) { CoinbaseWalletSDK.configure({ callbackURL: callbackURL.toString(), hostURL: hostURL?.toString(), hostPackageName, }); } export async function initiateHandshake( initialActions?: Action[] ): Promise<[Result[], Account?]> { const actions = initialActions?.map((action) => { return { method: action.method, paramsJson: JSON.stringify(action.params), optional: action.optional ?? false, }; }) ?? []; return await CoinbaseWalletSDK.initiateHandshake({ initialActions: actions }); } export async function makeRequest( actions: Action[], account?: Account ): Promise { const requestActions = actions.map((action) => { return { method: action.method, paramsJson: JSON.stringify(action.params), optional: action.optional ?? false, }; }); return await CoinbaseWalletSDK.makeRequest({ actions: requestActions, account, }); } export function handleResponse(url: URL): boolean { return CoinbaseWalletSDK.handleResponse(url.toString()); } export async function isCoinbaseWalletInstalled(): Promise { return CoinbaseWalletSDK.isCoinbaseWalletInstalled(); } export async function getCoinbaseWalletMWPVersion(): Promise { return CoinbaseWalletSDK.getCoinbaseWalletMWPVersion(); } export function isConnected(): boolean { return CoinbaseWalletSDK.isConnected(); } export function resetSession() { CoinbaseWalletSDK.resetSession(); } ================================================ FILE: react-native/src/CoinbaseWalletSDK.types.ts ================================================ export type ConfigurationParams = { callbackURL: URL; hostURL?: URL; hostPackageName?: string; }; export type Action = { method: string; params: any; optional?: boolean; }; export type Account = { chain: string; networkId: number; address: string; }; export type Result = { result: string | null; errorCode: number | null; errorMessage: string | null; }; export type ActionSource = { url: string; } ================================================ FILE: react-native/src/CoinbaseWalletSDKModule.ts ================================================ import { requireNativeModule } from 'expo-modules-core'; // It loads the native module object from the JSI or falls back to // the bridge module (from NativeModulesProxy) if the remote debugger is on. export default requireNativeModule('CoinbaseWalletSDK'); ================================================ FILE: react-native/src/WalletMobileSDKEVMProvider.ts ================================================ import { RequestArguments, Web3Provider, } from "./types/provider/Web3Provider"; import { JSONRPCRequest, JSONRPCResponse, } from "./types/provider/JSONRPC"; import { AddressString, Callback, IntNumber, } from "./types/core/type"; import { ethErrors } from "eth-rpc-errors"; import { initiateHandshake, isConnected, makeRequest, resetSession, } from "./CoinbaseWalletSDK"; import { Account, Action, ActionSource, Result } from "./CoinbaseWalletSDK.types"; import { bigIntStringFromBN, ensureAddressString, ensureBN, ensureBuffer, ensureIntNumber, ensureParsedJSONObject, hexStringFromBuffer, hexStringFromIntNumber, prepend0x, } from "./types/core/util"; import BN from "bn.js"; import { MMKV, NativeMMKV } from "react-native-mmkv"; import SafeEventEmitter from "@metamask/safe-event-emitter"; global.Buffer = global.Buffer || require("buffer").Buffer; const CACHED_ADDRESSES_KEY = "mobile_sdk.addresses"; const CHAIN_ID_KEY = "mobile_sdk.chain_id"; export interface WalletMobileSDKProviderOptions { chainId?: number; storage?: KVStorage; jsonRpcUrl?: string; address?: string; } export interface KVStorage extends Pick {} interface AddEthereumChainParams { chainId: string; blockExplorerUrls?: string[]; chainName?: string; iconUrls?: string[]; rpcUrls?: string[]; nativeCurrency?: { name: string; symbol: string; decimals: number; }; } interface SwitchEthereumChainParams { chainId: string; } interface WatchAssetParams { type: string; options: { address: string; symbol?: string; decimals?: number; image?: string; }; } interface EthereumTransactionParams { fromAddress: AddressString; toAddress: AddressString | null; weiValue: BN; data: Buffer; nonce: IntNumber | null; gasPriceInWei: BN | null; maxFeePerGas: BN | null; // in wei maxPriorityFeePerGas: BN | null; // in wei gasLimit: BN | null; chainId: IntNumber; actionSource?: ActionSource; } export class WalletMobileSDKEVMProvider extends SafeEventEmitter implements Web3Provider { private _chainId?: number; private _jsonRpcUrl?: string; private _addresses: AddressString[] = []; private _storage: KVStorage; constructor(opts?: WalletMobileSDKProviderOptions) { super(); this.send = this.send.bind(this); this.sendAsync = this.sendAsync.bind(this); this.request = this.request.bind(this); this._updateChainId = this._updateChainId.bind(this); this._setAddresses = this._setAddresses.bind(this); this._getChainId = this._getChainId.bind(this); this._storage = opts?.storage ?? new MMKV({ id: "mobile_sdk.store" }); this._chainId = opts?.chainId; this._jsonRpcUrl = opts?.jsonRpcUrl; const chainId = this._chainId ?? this._getChainId(); const chainIdStr = prepend0x(chainId.toString(16)); this.emit("connect", { chainId: chainIdStr }); const cachedAddresses = opts?.address ?? this._storage.getString(CACHED_ADDRESSES_KEY); if (cachedAddresses) { const addresses = cachedAddresses.split(" ") as AddressString[]; if (addresses[0] && addresses[0] !== "") { this._setAddresses(addresses); } } } public get selectedAddress(): AddressString | undefined { return this._addresses[0] || undefined; } public get networkVersion(): string { return this._getChainId().toString(10); } public get host(): string { if (this._jsonRpcUrl) { return this._jsonRpcUrl; } else { throw new Error("No jsonRpcUrl provided"); } } public get connected(): boolean { return isConnected(); } public get chainId(): string { return prepend0x(this._getChainId().toString(16)); } public supportsSubscriptions(): boolean { return false; } public disconnect(): boolean { resetSession(); this._addresses = []; this._storage.delete(CACHED_ADDRESSES_KEY); this.emit("disconnect"); return true; } private _send = this.send.bind(this); private _sendAsync = this.sendAsync.bind(this); public send(request: JSONRPCRequest): JSONRPCResponse; public send(request: JSONRPCRequest[]): JSONRPCResponse[]; public send( request: JSONRPCRequest, callback: Callback ): void; public send( request: JSONRPCRequest[], callback: Callback ): void; public send(method: string, params?: any[] | any): Promise; public send( requestOrMethod: JSONRPCRequest | JSONRPCRequest[] | string, callbackOrParams?: | Callback | Callback | any[] | any ): JSONRPCResponse | JSONRPCResponse[] | void | Promise { // send(method, params): Promise if (typeof requestOrMethod === "string") { const method = requestOrMethod; const params = Array.isArray(callbackOrParams) ? callbackOrParams : callbackOrParams !== undefined ? [callbackOrParams] : []; const request: JSONRPCRequest = { jsonrpc: "2.0", id: 0, method, params, }; return this._sendRequestAsync(request).then((res) => res.result); } // send(JSONRPCRequest | JSONRPCRequest[], callback): void if (typeof callbackOrParams === "function") { const request = requestOrMethod as any; const callback = callbackOrParams; return this._sendAsync(request, callback); } // send(JSONRPCRequest[]): JSONRPCResponse[] if (Array.isArray(requestOrMethod)) { const requests = requestOrMethod; return requests.map((r) => this._sendRequest(r)); } // send(JSONRPCRequest): JSONRPCResponse const req: JSONRPCRequest = requestOrMethod; return this._sendRequest(req); } public sendAsync( request: JSONRPCRequest, callback: Callback ): void; public sendAsync( request: JSONRPCRequest[], callback: Callback ): void; public async sendAsync( request: JSONRPCRequest | JSONRPCRequest[], callback: Callback | Callback ): Promise { if (typeof callback !== "function") { throw new Error("callback is required"); } // send(JSONRPCRequest[], callback): void if (Array.isArray(request)) { const arrayCb = callback as Callback; this._sendMultipleRequestsAsync(request) .then((responses) => arrayCb(null, responses)) .catch((err) => arrayCb(err, null)); return; } // send(JSONRPCRequest, callback): void const cb = callback as Callback; return this._sendRequestAsync(request) .then((response) => cb(null, response)) .catch((err) => cb(err, null)); } // request public async request(args: RequestArguments): Promise { if (!args || typeof args !== "object" || Array.isArray(args)) { throw ethErrors.rpc.invalidRequest({ message: "Expected a single, non-array, object argument.", data: args, }); } const { method, params } = args; if (typeof method !== "string" || method.length === 0) { throw ethErrors.rpc.invalidRequest({ message: "'args.method' must be a non-empty string.", data: args, }); } if ( params !== undefined && !Array.isArray(params) && (typeof params !== "object" || params === null) ) { throw ethErrors.rpc.invalidRequest({ message: "'args.params' must be an object or array if provided.", data: args, }); } const newParams = params === undefined ? [] : params; const id = 0; const result = await this._sendRequestAsync({ method, params: newParams, jsonrpc: "2.0", id, }); return result.result as T; } private _sendRequest(request: JSONRPCRequest): JSONRPCResponse { const result = this._handleSynchronousMethods(request); if (result === undefined) { throw ethErrors.provider.unsupportedMethod( `Unsupported synchronous method: ${request.method}` ); } return { jsonrpc: "2.0", id: request.id, result, }; } private _sendMultipleRequestsAsync( requests: JSONRPCRequest[] ): Promise { return Promise.all(requests.map((r) => this._sendRequestAsync(r))); // TODO: Request batching } private _sendRequestAsync(request: JSONRPCRequest): Promise { return new Promise((resolve, reject) => { try { // Handle synchronous methods const syncResult = this._handleSynchronousMethods(request); if (syncResult !== undefined) { return resolve({ jsonrpc: "2.0", id: request.id, result: syncResult, }); } } catch (error) { return reject(error); } // Handle asynchronous methods this._handleAsynchronouseMethods(request) .then((res) => res && resolve({ ...res, id: request.id })) .catch((error) => reject(error)); }); } private _handleSynchronousMethods({ method }: JSONRPCRequest) { switch (method) { case "eth_accounts": return this._eth_accounts(); case "eth_coinbase": return this._eth_coinbase(); case "net_version": return this._net_version(); case "eth_chainId": return this._eth_chainId(); default: return undefined; } } private async _handleAsynchronouseMethods( request: JSONRPCRequest ): Promise { const method = request.method; const params = request.params || []; switch (method) { case "eth_requestAccounts": return this._eth_requestAccounts(); case "personal_sign": return this._personal_sign(params); case "eth_signTypedData_v3": return this._eth_signTypedData(params, "v3"); case "eth_signTypedData_v4": return this._eth_signTypedData(params, "v4"); case "eth_signTransaction": return this._eth_signTransaction(params, false); case "eth_sendTransaction": return this._eth_signTransaction(params, true); case "wallet_switchEthereumChain": return this._wallet_switchEthereumChain(params); case "wallet_addEthereumChain": return this._wallet_addEthereumChain(params); case "wallet_watchAsset": return this._wallet_watchAsset(params); default: if (this._jsonRpcUrl) { return this._makeEthereumJsonRpcRequest(request, this._jsonRpcUrl); } else { throw ethErrors.provider.unsupportedMethod({ message: `Unsupported method: ${method}`, }); } } } private _eth_accounts(): string[] { return [...this._addresses]; } private _eth_coinbase(): string | null { return this.selectedAddress ?? null; } private _net_version(): string { return this._getChainId().toString(10); } private _eth_chainId(): string { return hexStringFromIntNumber(this._getChainId()); } private async _eth_requestAccounts(): Promise { const action: Action = { method: "eth_requestAccounts", params: {}, }; const [, account] = await this._makeHandshakeRequest(action); this._setAddresses([account.address]); return { jsonrpc: "2.0", id: 0, result: [account.address], }; } private async _personal_sign(params: unknown[]): Promise { this._requireAuthorization(); const message = ensureBuffer(params[0]); const address = ensureAddressString(params[1]); const action: Action = { method: "personal_sign", params: { message, address, }, }; const res = await this._makeSDKRequest(action); return { jsonrpc: "2.0", id: 0, result: res, }; } private async _eth_signTypedData( params: unknown[], type: "v3" | "v4" ): Promise { this._requireAuthorization(); const address = ensureAddressString(params[0]); const typedDataJson = JSON.stringify(ensureParsedJSONObject(params[1])); const action: Action = { method: type === "v3" ? "eth_signTypedData_v3" : "eth_signTypedData_v4", params: { address, typedDataJson, }, }; const res = await this._makeSDKRequest(action); return { jsonrpc: "2.0", id: 0, result: res, }; } private async _eth_signTransaction( params: unknown[], shouldSubmit: boolean ): Promise { this._requireAuthorization(); const tx = this._prepareTransactionParams((params[0] as any) || {}); const action: Action = { method: shouldSubmit ? "eth_sendTransaction" : "eth_signTransaction", params: { fromAddress: tx.fromAddress, toAddress: tx.toAddress, weiValue: bigIntStringFromBN(tx.weiValue), data: hexStringFromBuffer(tx.data), nonce: tx.nonce, gasPriceInWei: tx.gasPriceInWei ? bigIntStringFromBN(tx.gasPriceInWei) : null, maxFeePerGas: tx.maxFeePerGas ? bigIntStringFromBN(tx.maxFeePerGas) : null, maxPriorityFeePerGas: tx.maxPriorityFeePerGas ? bigIntStringFromBN(tx.maxPriorityFeePerGas) : null, gasLimit: tx.gasLimit ? bigIntStringFromBN(tx.gasLimit) : null, chainId: tx.chainId.toString(), }, }; const res = await this._makeSDKRequest(action); return { jsonrpc: "2.0", id: 0, result: res, }; } private _prepareTransactionParams(tx: { from?: unknown; to?: unknown; gasPrice?: unknown; maxFeePerGas?: unknown; maxPriorityFeePerGas?: unknown; gas?: unknown; value?: unknown; data?: unknown; nonce?: unknown; chainId?: unknown; }): EthereumTransactionParams { const fromAddress = tx.from ? ensureAddressString(tx.from) : null; if (!fromAddress) { throw new Error("Ethereum address is unavailable"); } const toAddress = tx.to ? ensureAddressString(tx.to) : null; const weiValue = tx.value != null ? ensureBN(tx.value) : new BN(0); const data = tx.data ? ensureBuffer(tx.data) : Buffer.alloc(0); const nonce = tx.nonce != null ? ensureIntNumber(tx.nonce) : null; const gasPriceInWei = tx.gasPrice != null ? ensureBN(tx.gasPrice) : null; const maxFeePerGas = tx.maxFeePerGas != null ? ensureBN(tx.maxFeePerGas) : null; const maxPriorityFeePerGas = tx.maxPriorityFeePerGas != null ? ensureBN(tx.maxPriorityFeePerGas) : null; const gasLimit = tx.gas != null ? ensureBN(tx.gas) : null; const chainId = tx.chainId ? ensureIntNumber(tx.chainId) : this._getChainId(); return { fromAddress, toAddress, weiValue, data, nonce, gasPriceInWei, maxFeePerGas, maxPriorityFeePerGas, gasLimit, chainId, }; } private async _wallet_switchEthereumChain( params: unknown[] ): Promise { this._requireAuthorization(); const request = params[0] as SwitchEthereumChainParams; const chainId = parseInt(request.chainId, 16); const successResponse: JSONRPCResponse = { jsonrpc: "2.0", id: 0, result: null, }; if (ensureIntNumber(chainId) === this._getChainId()) { return successResponse; } const action: Action = { method: "wallet_switchEthereumChain", params: { chainId: chainId.toString(), }, }; const res = await this._makeSDKRequest(action); if (res === null) { this._updateChainId(chainId); } return { jsonrpc: "2.0", id: 0, result: res, }; } private async _wallet_addEthereumChain( params: unknown[] ): Promise { this._requireAuthorization(); const request = params[0] as AddEthereumChainParams; if (!request.rpcUrls || request.rpcUrls?.length === 0) { throw ethErrors.rpc.invalidParams({ message: "please pass in at least 1 rpcUrl", }); } if (!request.chainName || request.chainName.trim() === "") { throw ethErrors.rpc.invalidParams({ message: "chainName is a required field", }); } if (!request.nativeCurrency) { throw ethErrors.rpc.invalidParams({ message: "nativeCurrency is a required field", }); } const chainIdNumber = parseInt(request.chainId, 16); const action: Action = { method: "wallet_addEthereumChain", params: { chainId: chainIdNumber.toString(), blockExplorerUrls: request.blockExplorerUrls ?? null, chainName: request.chainName ?? null, iconUrls: request.iconUrls ?? null, nativeCurrency: request.nativeCurrency ?? null, rpcUrls: request.rpcUrls ?? [], }, }; const res = await this._makeSDKRequest(action); return { jsonrpc: "2.0", id: 0, result: res, }; } private async _wallet_watchAsset(params: unknown): Promise { this._requireAuthorization(); const request = ( Array.isArray(params) ? params[0] : params ) as WatchAssetParams; if (!request.type) { throw ethErrors.rpc.invalidParams({ message: "Type is required", }); } if (request?.type !== "ERC20") { throw ethErrors.rpc.invalidParams({ message: `Asset of type '${request.type}' is not supported`, }); } if (!request?.options) { throw ethErrors.rpc.invalidParams({ message: "Options are required", }); } if (!request?.options.address) { throw ethErrors.rpc.invalidParams({ message: "Address is required", }); } const { address, symbol, image, decimals } = request.options; const action: Action = { method: "wallet_watchAsset", params: { type: request.type, options: { address, symbol: symbol ?? null, decimals: decimals ?? null, image: image ?? null, }, }, }; const res = await this._makeSDKRequest(action); return { jsonrpc: "2.0", id: 0, result: res, }; } private async _makeEthereumJsonRpcRequest( request: JSONRPCRequest, jsonRpcUrl: string ): Promise { return fetch(jsonRpcUrl, { method: "POST", body: JSON.stringify(request), mode: "cors", headers: { "Content-Type": "application/json" }, }) .then((res) => res.json()) .then((json) => { if (!json) { throw ethErrors.rpc.parse(); } const response = json as JSONRPCResponse; if (response.error) { throw ethErrors.provider.custom(response.error); } return response; }); } private async _makeHandshakeRequest( action: Action ): Promise<[unknown, Account]> { try { const [[res], account] = await initiateHandshake([action]); if (!res.result || !account) { throw this._getProviderError(res); } return [JSON.parse(res.result), account]; } catch (error) { if (error.message.match(/(session not found|session expired)/i)) { this.disconnect(); throw ethErrors.provider.disconnected(error.message); } if (error.message.match(/(denied|rejected)/i)) { throw ethErrors.provider.userRejectedRequest(); } throw error; } } private async _makeSDKRequest(action: Action): Promise { try { const [res] = await makeRequest([action]); if (res.errorMessage || !res.result) { throw this._getProviderError(res); } return JSON.parse(res.result); } catch (error) { if (error.message.match(/(session not found|session expired)/i)) { this.disconnect(); throw ethErrors.provider.disconnected(error.message); } if (error.message.match(/(denied|rejected)/i)) { throw ethErrors.provider.userRejectedRequest(); } throw error; } } private _getProviderError(result: Result) { const errorMessage = result.errorMessage ?? ""; if (errorMessage.match(/(denied|rejected)/i)) { return ethErrors.provider.userRejectedRequest(); } else { return ethErrors.provider.custom({ code: result.errorCode ?? 1000, message: errorMessage, }); } } private _getChainId(): IntNumber { const chainIdStr = this._storage.getString(CHAIN_ID_KEY) || "1"; const chainId = parseInt(chainIdStr, 10); return ensureIntNumber(chainId); } private _updateChainId(chainId: number) { const originalChainId = this._getChainId(); this._storage.set(CHAIN_ID_KEY, chainId.toString(10)); const chainChanged = ensureIntNumber(chainId) !== originalChainId; if (chainChanged) { this.emit("chainChanged", prepend0x(this._getChainId().toString(16))); } } private _setAddresses(addresses: string[]) { const newAddresses = addresses.map((address) => ensureAddressString(address) ); if (JSON.stringify(this._addresses) === JSON.stringify(newAddresses)) { return; } this._addresses = newAddresses; this._storage.set(CACHED_ADDRESSES_KEY, newAddresses.join(" ")); this.emit("accountsChanged", this._addresses); } private _requireAuthorization() { if (!this.connected) { throw ethErrors.provider.unauthorized(); } } } ================================================ FILE: react-native/src/types/core/type.ts ================================================ // Copyright (c) 2018-2023 Coinbase, Inc. // Licensed under the Apache License, version 2.0 interface Tag { __tag__: T; __realType__: RealType; } export type OpaqueType = U & Tag; export function OpaqueType>() { return (value: T extends Tag ? U : never): T => value as T; } export type HexString = OpaqueType<'HexString', string>; export const HexString = OpaqueType(); export type AddressString = OpaqueType<'AddressString', string>; export const AddressString = OpaqueType(); export type BigIntString = OpaqueType<'BigIntString', string>; export const BigIntString = OpaqueType(); export type IntNumber = OpaqueType<'IntNumber', number>; export function IntNumber(num: number): IntNumber { return Math.floor(num) as IntNumber; } export type Callback = (err: Error | null, result: T | null) => void; ================================================ FILE: react-native/src/types/core/util.ts ================================================ // Copyright (c) 2018-2023 Coinbase, Inc. // Licensed under the Apache License, version 2.0 import BN from 'bn.js'; import { AddressString, BigIntString, HexString, IntNumber } from './type'; const INT_STRING_REGEX = /^[0-9]*$/; const HEXADECIMAL_STRING_REGEX = /^[a-f0-9]*$/; export function hexStringToUint8Array(hexString: string): Uint8Array { return new Uint8Array(hexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))); } export function hexStringFromBuffer(buf: Buffer, includePrefix = false): HexString { const hex = buf.toString('hex'); return HexString(includePrefix ? `0x${hex}` : hex); } export function bigIntStringFromBN(bn: BN): BigIntString { return BigIntString(bn.toString(10)); } export function intNumberFromHexString(hex: HexString): IntNumber { return IntNumber(new BN(ensureEvenLengthHexString(hex, false), 16).toNumber()); } export function hexStringFromIntNumber(num: IntNumber): HexString { return HexString(`0x${new BN(num).toString(16)}`); } export function has0xPrefix(str: string): boolean { return str.startsWith('0x') || str.startsWith('0X'); } export function strip0x(hex: string): string { if (has0xPrefix(hex)) { return hex.slice(2); } return hex; } export function prepend0x(hex: string): string { if (has0xPrefix(hex)) { return `0x${hex.slice(2)}`; } return `0x${hex}`; } export function isHexString(hex: unknown): hex is HexString { if (typeof hex !== 'string') { return false; } const s = strip0x(hex).toLowerCase(); return HEXADECIMAL_STRING_REGEX.test(s); } class InvalidParamsError extends Error { code = -32602; constructor(message: string) { super(message); } } export function ensureHexString(hex: unknown, includePrefix = false): HexString { if (typeof hex === 'string') { const s = strip0x(hex).toLowerCase(); if (HEXADECIMAL_STRING_REGEX.test(s)) { return HexString(includePrefix ? `0x${s}` : s); } } throw new InvalidParamsError(`"${String(hex)}" is not a hexadecimal string`); } export function ensureEvenLengthHexString(hex: unknown, includePrefix = false): HexString { let h = ensureHexString(hex, false); if (h.length % 2 === 1) { h = HexString(`0${h}`); } return includePrefix ? HexString(`0x${h}`) : h; } export function ensureAddressString(str: unknown): AddressString { if (typeof str === 'string') { const s = strip0x(str).toLowerCase(); if (isHexString(s) && s.length === 40) { return AddressString(prepend0x(s)); } } throw new InvalidParamsError(`Invalid Ethereum address: ${String(str)}`); } export function ensureBuffer(str: unknown): Buffer { if (Buffer.isBuffer(str)) { return str; } if (typeof str === 'string') { if (isHexString(str)) { const s = ensureEvenLengthHexString(str, false); return Buffer.from(s, 'hex'); } return Buffer.from(str, 'utf8'); } throw new InvalidParamsError(`Not binary data: ${String(str)}`); } export function ensureIntNumber(num: unknown): IntNumber { if (typeof num === 'number' && Number.isInteger(num)) { return IntNumber(num); } if (typeof num === 'string') { if (INT_STRING_REGEX.test(num)) { return IntNumber(Number(num)); } if (isHexString(num)) { return IntNumber(new BN(ensureEvenLengthHexString(num, false), 16).toNumber()); } } throw new InvalidParamsError(`Not an integer: ${String(num)}`); } export function ensureBN(val: unknown): BN { if (val !== null && (BN.isBN(val) || isBigNumber(val))) { return new BN((val as any).toString(10), 10); } if (typeof val === 'number') { return new BN(ensureIntNumber(val)); } if (typeof val === 'string') { if (INT_STRING_REGEX.test(val)) { return new BN(val, 10); } if (isHexString(val)) { return new BN(ensureEvenLengthHexString(val, false), 16); } } throw new InvalidParamsError(`Not an integer: ${String(val)}`); } export function ensureParsedJSONObject(val: unknown): T { if (typeof val === 'string') { return JSON.parse(val) as T; } if (typeof val === 'object') { return val as T; } throw new InvalidParamsError(`Not a JSON string or an object: ${String(val)}`); } export function isBigNumber(val: unknown): boolean { if (val == null || typeof (val as any).constructor !== 'function') { return false; } const { constructor } = val as any; return typeof constructor.config === 'function' && typeof constructor.EUCLID === 'number'; } ================================================ FILE: react-native/src/types/provider/JSONRPC.ts ================================================ // Copyright (c) 2018-2023 Coinbase, Inc. // Licensed under the Apache License, version 2.0 export type JSONRPCMethod = // synchronous or asynchronous | 'eth_accounts' | 'eth_coinbase' | 'net_version' | 'eth_chainId' | 'eth_uninstallFilter' // synchronous // asynchronous only | 'eth_requestAccounts' | 'eth_sign' | 'eth_ecRecover' | 'personal_sign' | 'personal_ecRecover' | 'eth_signTransaction' | 'eth_sendRawTransaction' | 'eth_sendTransaction' | 'eth_signTypedData_v1' | 'eth_signTypedData_v2' | 'eth_signTypedData_v3' | 'eth_signTypedData_v4' | 'eth_signTypedData' | 'walletlink_arbitrary' // compatibility | 'wallet_addEthereumChain' | 'wallet_switchEthereumChain' | 'wallet_watchAsset' // asynchronous pub/sub | 'eth_subscribe' | 'eth_unsubscribe' // asynchronous filter methods | 'eth_newFilter' | 'eth_newBlockFilter' | 'eth_newPendingTransactionFilter' | 'eth_getFilterChanges' | 'eth_getFilterLogs'; export interface JSONRPCRequest { jsonrpc: '2.0'; id: number; method: string; params: T; } export interface JSONRPCResponse { jsonrpc: '2.0'; id: number; result?: T; error?: { code: number; message: string; data?: U; } | null; } ================================================ FILE: react-native/src/types/provider/Web3Provider.ts ================================================ // Copyright (c) 2018-2023 Coinbase, Inc. // Licensed under the Apache License, version 2.0 import { Callback } from '../core/type'; import { JSONRPCRequest, JSONRPCResponse } from './JSONRPC'; export interface Web3Provider { send(request: JSONRPCRequest): JSONRPCResponse; send(request: JSONRPCRequest[]): JSONRPCResponse[]; send(request: JSONRPCRequest, callback: Callback): void; send(request: JSONRPCRequest[], callback: Callback): void; send(method: string, params?: unknown[] | unknown): Promise; sendAsync(request: JSONRPCRequest, callback: Callback): void; sendAsync(request: JSONRPCRequest[], callback: Callback): void; request(args: RequestArguments): Promise; host: string; connected: boolean; chainId: string; supportsSubscriptions(): boolean; disconnect(): boolean; } export interface RequestArguments { /** The RPC method to request. */ method: string; /** The params of the RPC method, if any. */ params?: unknown; } ================================================ FILE: react-native/tsconfig.json ================================================ // @generated by expo-module-scripts { "extends": "expo-module-scripts/tsconfig.base", "compilerOptions": { "outDir": "./build" }, "include": ["./src"], "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__stories__/*"] }