iOS native SDK and example app (#589)

* first commit

* ios example app scaffolding

* README

* Update README.md

* pod

* integrate pod

* Create .codeflow.yml

* remove example on pod

* cleanup

* cleanup

* handshake

* handshake request encoder / response handler

* ios proof of concept

* Update README.md

* reorg

* host example app

* build settings

* cleanup

* request message renderer

* url rendering

* isCoinbaseWalletInstalled

* refactor

* key manager

* url codable / task manager

pod

* make request

* reintroduce message renderer

* dedup Message with custom coding

* response handler

* key storage

* hmmmmm

* key storage / no error throwing during init

* add domain to key storage

* response handling / error handling

* distinguish request/response types

* access control

* initial request

* host example app

* host

* handle encrypted response

* simplify symkey generation flow

* rename

Update README.md

Update README.md

* update path

* ios 12 support

* make RawRepresentableKey public

* clean up cryptography

* RawRepresentableKey

* make initialzier public

* ugh

* Update README.md

* Update README.md

* Update README.md

* sample host app

* reorg

* Merge remote-tracking branch 'native/master'

# Conflicts:
#	.gitignore

# Conflicts:
#	native/ios/CoinbaseWalletSDK.swift
#	native/ios/Message/Message.swift
#	native/ios/Message/Request/EncryptedRequestContent.swift
#	native/ios/Message/Request/Request.swift
#	native/ios/Message/Request/RequestMessage.swift
#	native/ios/Message/Response/EncryptedResponseContent.swift
#	native/ios/Message/Response/ResponseMessage.swift
#	native/ios/Task/TaskManager.swift

* Merge branch 'master' of github.com:coinbase/coinbase-wallet-sdk

# Conflicts:
#	.github/workflows/codeql-analysis.yml
#	.github/workflows/main.yml
#	.github/workflows/versioning.yml
#	.gitignore
#	README.md
#	host/ios/LICENSE
#	packages/wallet-sdk/.eslintignore
#	packages/wallet-sdk/.eslintrc.js
#	packages/wallet-sdk/.prettierignore
#	packages/wallet-sdk/.prettierrc.yml
#	packages/wallet-sdk/LICENSE
#	packages/wallet-sdk/__tests__/addressStorage.test.js
#	packages/wallet-sdk/__tests__/encryptDecrypt.test.js
#	packages/wallet-sdk/assets/coinbase_wallet_logo_kit.zip
#	packages/wallet-sdk/babel.config.js
#	packages/wallet-sdk/compile-assets.js
#	packages/wallet-sdk/jest.config.ts
#	packages/wallet-sdk/jest.setup.ts
#	packages/wallet-sdk/karma.conf.js
#	packages/wallet-sdk/package.json
#	packages/wallet-sdk/scripts/release.sh
#	packages/wallet-sdk/src/CoinbaseWalletSDK.test.ts
#	packages/wallet-sdk/src/CoinbaseWalletSDK.ts
#	packages/wallet-sdk/src/__mocks__/provider.ts
#	packages/wallet-sdk/src/__mocks__/relay.ts
#	packages/wallet-sdk/src/assets/wallet-logo.ts
#	packages/wallet-sdk/src/components/LinkFlow.test.tsx
#	packages/wallet-sdk/src/components/LinkFlow.tsx
#	packages/wallet-sdk/src/components/QRCode.tsx
#	packages/wallet-sdk/src/components/Snackbar.scss
#	packages/wallet-sdk/src/components/Snackbar.test.tsx
#	packages/wallet-sdk/src/components/Snackbar.tsx
#	packages/wallet-sdk/src/components/SnackbarContainer.test.tsx
#	packages/wallet-sdk/src/components/Spinner.scss
#	packages/wallet-sdk/src/components/Spinner.test.tsx
#	packages/wallet-sdk/src/components/Spinner.tsx
#	packages/wallet-sdk/src/components/TryExtensionLinkDialog.scss
#	packages/wallet-sdk/src/components/TryExtensionLinkDialog.test.tsx
#	packages/wallet-sdk/src/components/TryExtensionLinkDialog.tsx
#	packages/wallet-sdk/src/components/icons/QRLogo.ts
#	packages/wallet-sdk/src/components/icons/globe-icon.svg
#	packages/wallet-sdk/src/components/icons/link-icon.svg
#	packages/wallet-sdk/src/components/icons/lock-icon.svg
#	packages/wallet-sdk/src/connection/ClientMessage.ts
#	packages/wallet-sdk/src/connection/DiagnosticLogger.test.ts
#	packages/wallet-sdk/src/connection/DiagnosticLogger.ts
#	packages/wallet-sdk/src/connection/EventListener.ts
#	packages/wallet-sdk/src/connection/RxWebSocket.test.ts
#	packages/wallet-sdk/src/connection/RxWebSocket.ts
#	packages/wallet-sdk/src/connection/ServerMessage.ts
#	packages/wallet-sdk/src/connection/SessionConfig.ts
#	packages/wallet-sdk/src/connection/WalletSDKConnection.ts
#	packages/wallet-sdk/src/fixtures/provider.ts
#	packages/wallet-sdk/src/index.ts
#	packages/wallet-sdk/src/lib/ScopedLocalStorage.test.ts
#	packages/wallet-sdk/src/lib/ScopedLocalStorage.ts
#	packages/wallet-sdk/src/lib/cssReset.scss
#	packages/wallet-sdk/src/lib/cssReset.ts
#	packages/wallet-sdk/src/provider/CoinbaseWalletProvider.test.ts
#	packages/wallet-sdk/src/provider/CoinbaseWalletProvider.ts
#	packages/wallet-sdk/src/provider/FilterPolyfill.ts
#	packages/wallet-sdk/src/provider/JSONRPC.ts
#	packages/wallet-sdk/src/provider/SubscriptionManager.ts
#	packages/wallet-sdk/src/provider/WalletSDKUI.test.ts
#	packages/wallet-sdk/src/provider/WalletSDKUI.ts
#	packages/wallet-sdk/src/provider/WalletUI.ts
#	packages/wallet-sdk/src/provider/WalletUIError.ts
#	packages/wallet-sdk/src/provider/Web3Provider.ts
#	packages/wallet-sdk/src/relay/EthereumTransactionParams.ts
#	packages/wallet-sdk/src/relay/RelayMessage.ts
#	packages/wallet-sdk/src/relay/Session.ts
#	packages/wallet-sdk/src/relay/WalletSDKRelay.ts
#	packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts
#	packages/wallet-sdk/src/relay/WalletSDKRelayEventManager.test.ts
#	packages/wallet-sdk/src/relay/WalletSDKRelayEventManager.ts
#	packages/wallet-sdk/src/relay/Web3Method.ts
#	packages/wallet-sdk/src/relay/Web3Request.ts
#	packages/wallet-sdk/src/relay/Web3RequestCanceledMessage.ts
#	packages/wallet-sdk/src/relay/Web3RequestMessage.ts
#	packages/wallet-sdk/src/relay/Web3Response.ts
#	packages/wallet-sdk/src/relay/Web3ResponseMessage.ts
#	packages/wallet-sdk/src/relay/aes256gcm.test.ts
#	packages/wallet-sdk/src/relay/aes256gcm.ts
#	packages/wallet-sdk/src/types.ts
#	packages/wallet-sdk/src/util.test.ts
#	packages/wallet-sdk/src/util.ts
#	packages/wallet-sdk/src/vendor-js/eth-eip712-util/LICENSE
#	packages/wallet-sdk/src/vendor-js/eth-eip712-util/abi.js
#	packages/wallet-sdk/src/vendor-js/eth-eip712-util/index.d.ts
#	packages/wallet-sdk/src/vendor-js/eth-eip712-util/index.js
#	packages/wallet-sdk/src/vendor-js/eth-eip712-util/util.js
#	packages/wallet-sdk/src/vendor-js/qrcode-svg/LICENSE
#	packages/wallet-sdk/src/vendor-js/qrcode-svg/index.d.ts
#	packages/wallet-sdk/src/vendor-js/qrcode-svg/index.js
#	packages/wallet-sdk/src/version.ts
#	packages/wallet-sdk/tsconfig.build.json
#	packages/wallet-sdk/tsconfig.json
#	packages/wallet-sdk/webpack.config.js
#	packages/wallet-sdk/yarn.lock
#	src/vendor-js/qrcode-svg/LICENSE

* move under packages

* readme

* sample ios client app

* pod settings

* add `optional` filed to `Action`

* update example app

* Merge branch 'walletsegue' of github.com:coinbase/coinbase-wallet-sdk into walletsegue

* define ReturnType

* Update README.md

Update README.md

* introduce Web3JSONRPC helper

* nested params

* access control

* # This is a combination of 2 commits.
# This is the 1st commit message:

access control

# This is the commit message #2:

Revert "access control"

This reverts commit c8ea3dac36749ce9632b990708e960e96e230c6e.

* remove public initializers

access control

# This is the commit message #2:

Revert "access control"

This reverts commit c8ea3dac36749ce9632b990708e960e96e230c6e.

# This is the commit message #3:

remove public initializers

* use JSONSerialization to simplify Web3JSONRPC encoding

* singleton

* clean up error type

* host example

* ios example app

* example app

* add timestamp to message

* allow custom scheme / keystorage error message

* keystorageerror

* sampel ios app

* response handler

* swizzle

* update request on sample app

* cleanup keymanager implementation

* remove codeflow config

* remove old files

* gitignore

* cleanup

* rename

* missing changes

* sample app name

* update podspec

* author
This commit is contained in:
Jungho Bang
2022-06-30 15:26:44 -07:00
committed by GitHub
parent cf9254d461
commit 9f31f4ca43
39 changed files with 2444 additions and 0 deletions

125
.gitignore vendored
View File

@@ -1,2 +1,127 @@
# 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

19
CoinbaseWalletSDK.podspec Normal file
View File

@@ -0,0 +1,19 @@
Pod::Spec.new do |s|
s.name = 'CoinbaseWalletSDK'
s.version = '0.1.0'
s.summary = 'Swift SDK to interact with Coinbase Wallet iOS app'
s.description = <<-DESC
Swift implementation of WalletSegue protocol for iOS web3 apps to interact with web3 wallets.
Inter-app messaging contained within user's device, not requiring a bridge server.
Secure communication channel between client app and host wallet.
Decentralized verification of participating apps' authenticity without centralized registry.
DESC
s.homepage = 'https://www.coinbase.com/wallet/developers'
s.license = { :type => 'Apache', :file => 'LICENSE' }
s.author = { 'Coinbase Wallet' => 'wallet-build-squad@coinbase.com' }
s.source = { :git => 'https://github.com/coinbase/coinbase-wallet-sdk.git', :tag => "ios-v#{s.version}" }
s.social_media_url = 'https://twitter.com/CoinbaseWallet'
s.ios.deployment_target = '12.0'
s.swift_version = '5.0'
s.source_files = 'packages/wallet-native-sdk/ios/**/*.swift'
end

13
LICENSE Normal file
View File

@@ -0,0 +1,13 @@
Copyright (c) 2018-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.

View File

@@ -0,0 +1,6 @@
platform :ios, '13.0'
use_frameworks!
target 'NativeWeb3App' do
pod 'CoinbaseWalletSDK', path: '../../'
end

View File

@@ -0,0 +1,16 @@
PODS:
- CoinbaseWalletSDK (0.1.0)
DEPENDENCIES:
- CoinbaseWalletSDK (from `../../`)
EXTERNAL SOURCES:
CoinbaseWalletSDK:
:path: "../../"
SPEC CHECKSUMS:
CoinbaseWalletSDK: cae36a0585ac804b9a421659092d64476e6e9b85
PODFILE CHECKSUM: 258b9c0ec0f0d17a701912e5f713afb0de91d709
COCOAPODS: 1.11.3

View File

@@ -0,0 +1,433 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
5DA9B3CF286A755E0089D6DB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA9B3CE286A755E0089D6DB /* AppDelegate.swift */; };
5DA9B3D1286A755E0089D6DB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA9B3D0286A755E0089D6DB /* SceneDelegate.swift */; };
5DA9B3D3286A755E0089D6DB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA9B3D2286A755E0089D6DB /* ViewController.swift */; };
5DA9B3D6286A755E0089D6DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DA9B3D4286A755E0089D6DB /* Main.storyboard */; };
5DA9B3D8286A755F0089D6DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5DA9B3D7286A755F0089D6DB /* Assets.xcassets */; };
5DA9B3DB286A755F0089D6DB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DA9B3D9286A755F0089D6DB /* LaunchScreen.storyboard */; };
9A156AE0D18D300163D57907 /* Pods_NativeWeb3App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 333C157DDC56839FC68B8E8F /* Pods_NativeWeb3App.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
333C157DDC56839FC68B8E8F /* Pods_NativeWeb3App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NativeWeb3App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5DA9B3CB286A755E0089D6DB /* NativeWeb3App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NativeWeb3App.app; sourceTree = BUILT_PRODUCTS_DIR; };
5DA9B3CE286A755E0089D6DB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5DA9B3D0286A755E0089D6DB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
5DA9B3D2286A755E0089D6DB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
5DA9B3D5286A755E0089D6DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
5DA9B3D7286A755F0089D6DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
5DA9B3DA286A755F0089D6DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
5DA9B3DC286A755F0089D6DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
5DA9B3C8286A755E0089D6DB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9A156AE0D18D300163D57907 /* Pods_NativeWeb3App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
3E18EF9C1E2A7D3BF660A5CD /* Frameworks */ = {
isa = PBXGroup;
children = (
333C157DDC56839FC68B8E8F /* Pods_NativeWeb3App.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
51AC5D6DCCB61B979D9D716E /* Pods */ = {
isa = PBXGroup;
children = (
FC120F48CB37E5C1FAFE2CAE /* Pods-NativeWeb3App.debug.xcconfig */,
AD64CFD01D3AB9B726DF335B /* Pods-NativeWeb3App.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
5DA9B3C2286A755E0089D6DB = {
isa = PBXGroup;
children = (
5DA9B3CD286A755E0089D6DB /* SampleApp */,
5DA9B3CC286A755E0089D6DB /* Products */,
51AC5D6DCCB61B979D9D716E /* Pods */,
3E18EF9C1E2A7D3BF660A5CD /* Frameworks */,
);
sourceTree = "<group>";
};
5DA9B3CC286A755E0089D6DB /* Products */ = {
isa = PBXGroup;
children = (
5DA9B3CB286A755E0089D6DB /* NativeWeb3App.app */,
);
name = Products;
sourceTree = "<group>";
};
5DA9B3CD286A755E0089D6DB /* SampleApp */ = {
isa = PBXGroup;
children = (
5DA9B3CE286A755E0089D6DB /* AppDelegate.swift */,
5DA9B3D0286A755E0089D6DB /* SceneDelegate.swift */,
5DA9B3D2286A755E0089D6DB /* ViewController.swift */,
5DA9B3D4286A755E0089D6DB /* Main.storyboard */,
5DA9B3D7286A755F0089D6DB /* Assets.xcassets */,
5DA9B3D9286A755F0089D6DB /* LaunchScreen.storyboard */,
5DA9B3DC286A755F0089D6DB /* Info.plist */,
);
path = SampleApp;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
5DA9B3CA286A755E0089D6DB /* NativeWeb3App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 5DA9B3DF286A755F0089D6DB /* Build configuration list for PBXNativeTarget "NativeWeb3App" */;
buildPhases = (
29FDABA7E59E1339DA4167A7 /* [CP] Check Pods Manifest.lock */,
5DA9B3C7286A755E0089D6DB /* Sources */,
5DA9B3C8286A755E0089D6DB /* Frameworks */,
5DA9B3C9286A755E0089D6DB /* Resources */,
4A2E3B84F8F828F77205E15C /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = NativeWeb3App;
productName = NativeWeb3App;
productReference = 5DA9B3CB286A755E0089D6DB /* NativeWeb3App.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 "SampleApp" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 5DA9B3C2286A755E0089D6DB;
productRefGroup = 5DA9B3CC286A755E0089D6DB /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
5DA9B3CA286A755E0089D6DB /* NativeWeb3App */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
5DA9B3C9286A755E0089D6DB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5DA9B3DB286A755F0089D6DB /* LaunchScreen.storyboard in Resources */,
5DA9B3D8286A755F0089D6DB /* Assets.xcassets in Resources */,
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-NativeWeb3App-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-NativeWeb3App/Pods-NativeWeb3App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-NativeWeb3App/Pods-NativeWeb3App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NativeWeb3App/Pods-NativeWeb3App-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 */,
5DA9B3D1286A755E0089D6DB /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
5DA9B3D4286A755E0089D6DB /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
5DA9B3D5286A755E0089D6DB /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
5DA9B3D9286A755F0089D6DB /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
5DA9B3DA286A755F0089D6DB /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 = FC120F48CB37E5C1FAFE2CAE /* Pods-NativeWeb3App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = B7Y3D73M65;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SampleApp/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
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.NativeWeb3App;
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 = AD64CFD01D3AB9B726DF335B /* Pods-NativeWeb3App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = B7Y3D73M65;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SampleApp/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
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.NativeWeb3App;
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 "SampleApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
5DA9B3DD286A755F0089D6DB /* Debug */,
5DA9B3DE286A755F0089D6DB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
5DA9B3DF286A755F0089D6DB /* Build configuration list for PBXNativeTarget "NativeWeb3App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
5DA9B3E0286A755F0089D6DB /* Debug */,
5DA9B3E1286A755F0089D6DB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 5DA9B3C3286A755E0089D6DB /* Project object */;
}

View File

@@ -0,0 +1,68 @@
//
// AppDelegate.swift
// SampleWeb3App
//
// Created by Jungho Bang on 6/27/22.
//
import UIKit
import CoinbaseWalletSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIApplication.swizzleOpenURL()
CoinbaseWalletSDK.configure(
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)
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="N0H-g8-v1I">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="zug-Ox-8lY">
<objects>
<tableViewController id="N0H-g8-v1I" customClass="ViewController" customModule="NativeWeb3App" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" id="fK4-3E-Da2">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<sections>
<tableViewSection id="1cD-BP-bqt">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" textLabel="XMs-AC-dDw" detailTextLabel="crr-DB-0oO" style="IBUITableViewCellStyleValue2" id="TZr-pJ-2rz">
<rect key="frame" x="0.0" y="18" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="TZr-pJ-2rz" id="gRR-Nr-iUX">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="isCBWInstalled" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="XMs-AC-dDw">
<rect key="frame" x="20" y="15" width="91" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="bool" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="crr-DB-0oO">
<rect key="frame" x="117" y="15" width="25" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" textLabel="bIA-Fa-7iI" detailTextLabel="aQU-OY-KUj" style="IBUITableViewCellStyleValue2" id="oiO-9C-Bhr">
<rect key="frame" x="0.0" y="61.5" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oiO-9C-Bhr" id="vlQ-II-7Nj">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="isConnected" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bIA-Fa-7iI">
<rect key="frame" x="20" y="15" width="91" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="bool" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="aQU-OY-KUj">
<rect key="frame" x="117" y="15" width="25" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" textLabel="Pah-Wo-mhl" detailTextLabel="kmY-1i-sqZ" style="IBUITableViewCellStyleValue2" id="JIf-L3-HZ0">
<rect key="frame" x="0.0" y="105" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JIf-L3-HZ0" id="Yy5-ga-LhL">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="ownPubKey" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Pah-Wo-mhl">
<rect key="frame" x="20" y="15" width="91" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="string" textAlignment="natural" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="kmY-1i-sqZ">
<rect key="frame" x="117" y="15" width="32.5" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" textLabel="Zi0-rV-vVI" detailTextLabel="Ax0-s5-kcP" style="IBUITableViewCellStyleValue2" id="yFs-wz-EUW">
<rect key="frame" x="0.0" y="148.5" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="yFs-wz-EUW" id="e9K-23-pZe">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="peerPubKey" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Zi0-rV-vVI">
<rect key="frame" x="20" y="15" width="91" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="string" textAlignment="natural" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Ax0-s5-kcP">
<rect key="frame" x="117" y="15" width="32.5" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="MDv-N0-vVu">
<rect key="frame" x="0.0" y="192" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MDv-N0-vVu" id="I60-ow-F4F">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="f0g-3n-F8G">
<rect key="frame" x="6" y="6" width="402" height="31.5"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Initiate handshake"/>
<connections>
<action selector="initiateHandshake" destination="N0H-g8-v1I" eventType="touchUpInside" id="3SX-uc-nCS"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="f0g-3n-F8G" firstAttribute="centerX" secondItem="I60-ow-F4F" secondAttribute="centerX" id="129-dN-ifQ"/>
<constraint firstItem="f0g-3n-F8G" firstAttribute="top" secondItem="I60-ow-F4F" secondAttribute="topMargin" constant="-5" id="YYh-Qh-kb4"/>
<constraint firstItem="f0g-3n-F8G" firstAttribute="centerY" secondItem="I60-ow-F4F" secondAttribute="centerY" id="o19-K7-jVy"/>
<constraint firstItem="f0g-3n-F8G" firstAttribute="leading" secondItem="I60-ow-F4F" secondAttribute="leadingMargin" constant="-14" id="ug1-r0-wzn"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="9Xf-Eq-qch">
<rect key="frame" x="0.0" y="235.5" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="9Xf-Eq-qch" id="Nvt-rd-Kzh">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UFh-Jt-LEP">
<rect key="frame" x="6" y="6" width="402" height="31.5"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Reset connection"/>
<connections>
<action selector="resetConnection" destination="N0H-g8-v1I" eventType="touchUpInside" id="MrE-Td-fVn"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="UFh-Jt-LEP" firstAttribute="centerY" secondItem="Nvt-rd-Kzh" secondAttribute="centerY" id="7dL-fu-KmW"/>
<constraint firstItem="UFh-Jt-LEP" firstAttribute="leading" secondItem="Nvt-rd-Kzh" secondAttribute="leadingMargin" constant="-14" id="Mbb-QD-ih0"/>
<constraint firstItem="UFh-Jt-LEP" firstAttribute="centerX" secondItem="Nvt-rd-Kzh" secondAttribute="centerX" id="NOQ-Wx-Y2k"/>
<constraint firstItem="UFh-Jt-LEP" firstAttribute="top" secondItem="Nvt-rd-Kzh" secondAttribute="topMargin" constant="-5" id="pJ8-md-cTD"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="TTq-yp-kei">
<rect key="frame" x="0.0" y="279" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="TTq-yp-kei" id="3Hl-hx-44M">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dpB-W6-PM0">
<rect key="frame" x="6" y="6" width="402" height="31.5"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Make request"/>
<connections>
<action selector="makeRequest" destination="N0H-g8-v1I" eventType="touchUpInside" id="I8M-jS-DMV"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="dpB-W6-PM0" firstAttribute="centerX" secondItem="3Hl-hx-44M" secondAttribute="centerX" id="I2F-d0-Dk9"/>
<constraint firstItem="dpB-W6-PM0" firstAttribute="top" secondItem="3Hl-hx-44M" secondAttribute="topMargin" constant="-5" id="MZ3-rN-ZWf"/>
<constraint firstItem="dpB-W6-PM0" firstAttribute="centerY" secondItem="3Hl-hx-44M" secondAttribute="centerY" id="UXD-Mk-QxF"/>
<constraint firstItem="dpB-W6-PM0" firstAttribute="leading" secondItem="3Hl-hx-44M" secondAttribute="leadingMargin" constant="-14" id="sVN-if-X5P"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="log" id="hdE-4l-6RU">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="400" id="7Ix-hF-4f6">
<rect key="frame" x="0.0" y="372.5" width="414" height="400"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7Ix-hF-4f6" id="hR3-G0-o2o">
<rect key="frame" x="0.0" y="0.0" width="414" height="400"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="PlN-Ph-shZ">
<rect key="frame" x="0.0" y="0.0" width="414" height="400"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<constraints>
<constraint firstItem="PlN-Ph-shZ" firstAttribute="top" secondItem="hR3-G0-o2o" secondAttribute="top" id="FYk-FQ-Jq1"/>
<constraint firstAttribute="bottom" secondItem="PlN-Ph-shZ" secondAttribute="bottom" id="QKc-h9-7Yn"/>
<constraint firstAttribute="trailing" secondItem="PlN-Ph-shZ" secondAttribute="trailing" id="Qxs-hq-uaU"/>
<constraint firstItem="PlN-Ph-shZ" firstAttribute="leading" secondItem="hR3-G0-o2o" secondAttribute="leading" id="c6s-Ww-A8k"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="N0H-g8-v1I" id="4Ky-tr-Rnr"/>
<outlet property="delegate" destination="N0H-g8-v1I" id="TYV-RP-LAU"/>
</connections>
</tableView>
<connections>
<outlet property="isCBWalletInstalledLabel" destination="crr-DB-0oO" id="OT3-0e-g8G"/>
<outlet property="isConnectedLabel" destination="aQU-OY-KUj" id="iPl-4X-Fjj"/>
<outlet property="logTextView" destination="PlN-Ph-shZ" id="BW2-a7-hgy"/>
<outlet property="ownPubKeyLabel" destination="kmY-1i-sqZ" id="R7K-YM-JdI"/>
<outlet property="peerPubKeyLabel" destination="Ax0-s5-kcP" id="Mlr-FA-CDT"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="wdV-MQ-XPI" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="142.02898550724638" y="132.58928571428572"/>
</scene>
</scenes>
<resources>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>xyz.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myappxyz</string>
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>cbwallet</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,29 @@
//
// SceneDelegate.swift
// SampleWeb3App
//
// Created by Jungho Bang on 6/27/22.
//
import UIKit
import CoinbaseWalletSDK
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
logWalletSegueMessage(url: url)
do {
if try CoinbaseWalletSDK.shared.handleResponse(url) {
return
}
} catch {
print(error)
}
}
}

View File

@@ -0,0 +1,106 @@
//
// 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 let cbwallet = CoinbaseWalletSDK.shared
override func viewDidLoad() {
super.viewDidLoad()
isCBWalletInstalledLabel.text = "\(CoinbaseWalletSDK.isCoinbaseWalletInstalled())"
updateSessionStatus()
}
@IBAction func initiateHandshake() {
cbwallet.initiateHandshake(
initialActions: [
Action(jsonRpc: .eth_requestAccounts)
]
) { result in
switch result {
case .success(let response):
self.logObject(label: "Response:\n", response)
case .failure(let error):
self.log("\(error)")
}
self.updateSessionStatus()
}
}
@IBAction func resetConnection() {
let result = cbwallet.resetSession()
self.log("\(result)")
updateSessionStatus()
}
@IBAction func makeRequest() {
cbwallet.makeRequest(
Request(actions: [
Action(jsonRpc: .personal_sign(address: "", message: "message".data(using: .utf8)!)),
Action(jsonRpc: .eth_signTypedData_v3(address: "", message: Data()))
])
) { result in
self.log("\(result)")
}
}
// 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.keyManager.ownPublicKey.rawRepresentation.base64EncodedString()
self.peerPubKeyLabel.text = self.cbwallet.keyManager.peerPublicKey?.rawRepresentation.base64EncodedString() ?? "(nil)"
}
}
private func logObject<T: Encodable>(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)
guard
!cbwallet.isConnected(),
let message: RequestMessage = try? MessageConverter.decode(url, with: nil)
else { return }
self.logObject(message, 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))
}
}
}

View File

@@ -0,0 +1,152 @@
# Coinbase Wallet SDK for native mobile apps
## Example data
### handshake
- URL
`https://go.cb-w.com/wsegue?p=eyJ2ZXJzaW9uIjoiMS4yLjMiLCJzZW5kZXIiOiI0UG9QTEFDUjNCb0VxWTJBdSthamRISGRSZStvU0ZaUVwvSkpDU091bHpTYz0iLCJjb250ZW50Ijp7ImhhbmRzaGFrZSI6eyJhcHBJZCI6ImNvbS5teWFwcC5wYWNrYWdlLmlkIiwiY2FsbGJhY2siOiJodHRwczpcL1wvbXlhcHAueHl6XC9uYXRpdmUtc2RrIiwiaW5pdGlhbEFjdGlvbnMiOlt7Im1ldGhvZCI6ImV0aF9zb21lTWV0aG9kIiwicGFyYW1zIjpbInBhcmFtMSIsInBhcmFtMiJdfV19fSwidXVpZCI6IjExMUYzRTI2LTE2N0YtNEJGNS1BQzM4LTZEOTNBNjU2RTMyQiJ9`
- JSON (decoded the URL above. handshake messages are not encrypted)
```json
{
"version" : "0.1.0",
"sender" : "bwf9U+VbjmvfBr3p3aoJyOEKS6mq7sSrg56V6FDYMBs=",
"content" : {
"handshake" : {
"appId" : "com.coinbase.NativeWeb3App",
"callback" : "https:\/\/myapp.xyz\/mycallback",
"initialActions" : [
{
"paramsJson" : "{}",
"method" : "eth_requestAccounts",
"optional" : false
},
{
"paramsJson" : "{\"fromAddress\":\"\",\"data\":\"bWVzc2FnZQ==\"}",
"method" : "personal_sign",
"optional" : false
}
]
}
},
"uuid" : "634A5C15-0316-4FD1-86FB-4818DBD6C12D"
}
```
### 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`

View File

@@ -0,0 +1,189 @@
//
// CoinbaseWalletSDK.swift
// WalletSegue
//
// Created by Jungho Bang on 5/20/22.
//
import Foundation
import CryptoKit
@available(iOS 13.0, *)
public final class CoinbaseWalletSDK {
public static func isCoinbaseWalletInstalled() -> Bool {
return UIApplication.shared.canOpenURL(URL(string: "cbwallet://")!)
}
// MARK: - Constructor
static private var host: URL?
static private var callback: URL?
static public func configure(
host: URL = URL(string: "https://go.cb-w.com/wsegue")!,
callback: URL
) {
guard self.host == nil && self.callback == nil 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 let version: String
public 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!
let sdkBundle = Bundle(for: Self.self)
self.version = sdkBundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0"
}
// MARK: - Send message
public func initiateHandshake(initialActions: [Action]? = nil, onResponse: @escaping ResponseHandler) {
try? keyManager.resetOwnPrivateKey()
let message = RequestMessage(
uuid: UUID(),
sender: keyManager.ownPublicKey,
content: .handshake(
appId: appId,
callback: callback,
initialActions: initialActions
),
version: version,
timestamp: Date()
)
self.send(message, onResponse)
}
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: version,
timestamp: Date()
)
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,
options: [.universalLinksOnly: url.isHttp]
) { 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
}
@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 sessionPublicKey: PublicKey {
return keyManager.ownPublicKey
}
@discardableResult
public func resetSession() -> Result<Void, Swift.Error> {
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)
}
}

View File

@@ -0,0 +1,19 @@
//
// Error.swift
// WalletSegue
//
// Created by Jungho Bang on 6/16/22.
//
import Foundation
@available(iOS 13.0, *)
extension CoinbaseWalletSDK {
enum Error: Swift.Error {
case encodingFailed
case decodingFailed
case missingSymmetricKey
case openUrlFailed
case walletReturnedError(String)
}
}

View File

@@ -0,0 +1,31 @@
//
// Key+RawRepresentable.swift
// WalletSegue
//
// Created by Jungho Bang on 6/13/22.
//
import Foundation
public protocol RawRepresentableKey: Codable {
init<D>(rawRepresentation data: D) throws where D: ContiguousBytes
var rawRepresentation: Data { get }
}
@available(iOS 13.0, *)
extension CoinbaseWalletSDK.PrivateKey: RawRepresentableKey {}
@available(iOS 13.0, *)
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)
}
}

View File

@@ -0,0 +1,66 @@
//
// KeyManager.swift
// WalletSegue
//
// Created by Jungho Bang on 6/9/22.
//
import Foundation
import CryptoKit
@available(iOS 13.0, *)
extension CoinbaseWalletSDK {
public typealias PrivateKey = Curve25519.KeyAgreement.PrivateKey
public typealias PublicKey = Curve25519.KeyAgreement.PublicKey
}
@available(iOS 13.0, *)
public final class KeyManager {
private(set) var ownPrivateKey: CoinbaseWalletSDK.PrivateKey
public var ownPublicKey: CoinbaseWalletSDK.PublicKey {
return ownPrivateKey.publicKey
}
public 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)
}
}

View File

@@ -0,0 +1,87 @@
//
// KeyStorage.swift
// WalletSegue
//
// Created by Jungho Bang on 6/17/22.
//
import Foundation
@available(iOS 13.0, *)
final class KeyStorage {
private let service: String
init(host: URL) {
service = "wsegue.keystorage.\((host.isHttp ? host.host : host.scheme) ?? host.absoluteString)"
}
func store<K>(_ data: K, at item: KeyStorageItem<K>) throws {
try? self.delete(item)
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: item.name,
kSecAttrService: service,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecUseDataProtectionKeychain: true,
kSecValueData: data.rawRepresentation
] as [String: Any]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeyStorage.Error.storeFailed(status.message)
}
}
func read<K>(_ item: KeyStorageItem<K>) throws -> K? {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: item.name,
kSecAttrService: service,
kSecUseDataProtectionKeychain: true,
kSecReturnData: true
] as [String: Any]
var item: CFTypeRef?
switch SecItemCopyMatching(query as CFDictionary, &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<K>(_ item: KeyStorageItem<K>) throws {
let query = [
kSecClass: kSecClassGenericPassword,
kSecUseDataProtectionKeychain: true,
kSecAttrAccount: item.name,
kSecAttrService: service
] as [String: Any]
switch SecItemDelete(query as CFDictionary) {
case errSecItemNotFound, errSecSuccess:
break // Okay to ignore
case let status:
throw KeyStorage.Error.deleteFailed(status.message)
}
}
}
@available(iOS 13.0, *)
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)
}
}

View File

@@ -0,0 +1,25 @@
//
// KeyStorageItem.swift
// WalletSegue
//
// Created by Jungho Bang on 6/17/22.
//
import Foundation
@available(iOS 13.0, *)
struct KeyStorageItem<K: RawRepresentableKey> {
let name: String
init(_ name: String) {
self.name = name
}
static var ownPrivateKey: KeyStorageItem<CoinbaseWalletSDK.PrivateKey> {
return KeyStorageItem<CoinbaseWalletSDK.PrivateKey>("wsegue.ownPrivateKey")
}
static var peerPublicKey: KeyStorageItem<CoinbaseWalletSDK.PublicKey> {
return KeyStorageItem<CoinbaseWalletSDK.PublicKey>("wsegue.peerPublicKey")
}
}

View File

@@ -0,0 +1,45 @@
//
// Cryptography.swift
// WalletSegue
//
// Created by Jungho Bang on 6/17/22.
//
import Foundation
import CryptoKit
@available(iOS 13.0, *)
public final class Cipher {
static func encrypt<C: Encodable>(
_ 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<C: Decodable>(
_ 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)
}
public 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
)
}
}

View File

@@ -0,0 +1,39 @@
//
// EncryptedMessage.swift
// WalletSegue
//
// Created by Jungho Bang on 6/23/22.
//
import Foundation
import CryptoKit
@available(iOS 13.0, *)
public protocol EncryptedContent: BaseContent {
associatedtype Unencrypted: UnencryptedContent where Unencrypted.Encrypted == Self
func decrypt(with symmetricKey: SymmetricKey?) throws -> Unencrypted
}
@available(iOS 13.0, *)
typealias EncryptedMessage<C> = BaseMessage<C> where C: EncryptedContent
@available(iOS 13.0, *)
extension EncryptedMessage {
func decrypt(with symmetricKey: SymmetricKey?) throws -> Message<C.Unencrypted> {
return Message<C.Unencrypted>.copy(
self,
replaceContentWith: try self.content.decrypt(with: symmetricKey)
)
}
}
@available(iOS 13.0, *)
extension Message {
func encrypt(with symmetricKey: SymmetricKey?) throws -> EncryptedMessage<C.Encrypted> {
return EncryptedMessage<C.Encrypted>.copy(
self,
replaceContentWith: try self.content.encrypt(with: symmetricKey)
)
}
}

View File

@@ -0,0 +1,43 @@
//
// Message.swift
// WalletSegue
//
// Created by Jungho Bang on 6/13/22.
//
import Foundation
import CryptoKit
@available(iOS 13.0, *)
public typealias Message<C> = BaseMessage<C> where C: UnencryptedContent
@available(iOS 13.0, *)
public protocol UnencryptedContent: BaseContent {
associatedtype Encrypted: EncryptedContent where Encrypted.Unencrypted == Self
func encrypt(with symmetricKey: SymmetricKey?) throws -> Encrypted
}
// MARK: - base types
public protocol BaseContent: Codable {}
extension Array : BaseContent where Element : BaseContent {}
@available(iOS 13.0, *)
public struct BaseMessage<C: BaseContent>: Codable {
public let uuid: UUID
public let sender: CoinbaseWalletSDK.PublicKey
public let content: C
public let version: String
public let timestamp: Date
static func copy<T>(_ orig: BaseMessage<T>, replaceContentWith content: C) -> BaseMessage<C> {
return BaseMessage<C>.init(
uuid: orig.uuid,
sender: orig.sender,
content: content,
version: orig.version,
timestamp: orig.timestamp
)
}
}

View File

@@ -0,0 +1,63 @@
//
// MessageRenderer.swift
// WalletSegue
//
// Created by Jungho Bang on 6/14/22.
//
import Foundation
import CryptoKit
@available(iOS 13.0, *)
public final class MessageConverter {
public static func encode<C>(
_ message: Message<C>,
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
}
public static func decode<C>(
_ url: URL,
with symmetricKey: SymmetricKey?
) throws -> Message<C> {
let encrypted: EncryptedMessage<C.Encrypted> = try self.decodeWithoutDecryption(url)
return try encrypted.decrypt(with: symmetricKey)
}
static func decodeWithoutDecryption<C>(
_ url: URL
) throws -> EncryptedMessage<C> {
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<C>.self, from: data)
}
}

View File

@@ -0,0 +1,20 @@
//
// 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
}
}

View File

@@ -0,0 +1,31 @@
//
// 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
)
}
}

View File

@@ -0,0 +1,45 @@
//
// EncryptedRequestContent.swift
// WalletSegue
//
// Created by Jungho Bang on 6/23/22.
//
import Foundation
import CryptoKit
@available(iOS 13.0, *)
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)
}
}
}
@available(iOS 13.0, *)
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))
}
}
}

View File

@@ -0,0 +1,18 @@
//
// 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
}
}

View File

@@ -0,0 +1,17 @@
//
// RequestMessage.swift
// WalletSegue
//
// Created by Jungho Bang on 6/9/22.
//
import Foundation
@available(iOS 13.0, *)
public enum RequestContent: UnencryptedContent {
case handshake(appId: String, callback: URL, initialActions: [Action]?)
case request(actions: [Action], account: Account? = nil)
}
@available(iOS 13.0, *)
public typealias RequestMessage = Message<RequestContent>

View File

@@ -0,0 +1,106 @@
//
// Web3JSONRPC.swift
// WalletSegue
//
// Created by Jungho Bang on 6/28/22.
//
import Foundation
public typealias BigInt = String
public enum Web3JSONRPC: Codable {
case eth_requestAccounts
case personal_sign(
address: String,
message: Data
)
case eth_signTypedData_v3(
address: String,
message: Data
)
case eth_signTypedData_v4(
address: String,
message: Data
)
case eth_signTransaction(
fromAddress: String,
toAddress: String?,
weiValue: BigInt,
data: Data,
nonce: Int?,
gasPriceInWei: BigInt?,
maxFeePerGas: BigInt?,
maxPriorityFeePerGas: BigInt?,
gasLimit: BigInt?,
chainId: Int
)
case eth_sendTransaction(
fromAddress: String,
toAddress: String?,
weiValue: BigInt,
data: Data,
nonce: Int?,
gasPriceInWei: BigInt?,
maxFeePerGas: BigInt?,
maxPriorityFeePerGas: BigInt?,
gasLimit: BigInt?,
chainId: Int
)
case wallet_switchEthereumChain(chainId: Int)
case wallet_addEthereumChain(
chainId: Int,
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
}
}

View File

@@ -0,0 +1,55 @@
//
// EncryptedResponseContent.swift
// WalletSegue
//
// Created by Jungho Bang on 6/23/22.
//
import Foundation
import CryptoKit
@available(iOS 13.0, *)
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: [ReturnValue] = 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
}
}
}
@available(iOS 13.0, *)
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)
}
}
}
@available(iOS 13.0, *)
typealias EncryptedResponseMessage = EncryptedMessage<EncryptedResponseContent>

View File

@@ -0,0 +1,25 @@
//
// Response.swift
// WalletSegue
//
// Created by Jungho Bang on 6/13/22.
//
import Foundation
@available(iOS 13.0, *)
public enum ResponseContent: UnencryptedContent {
case response(requestId: UUID, values: [ReturnValue])
case failure(requestId: UUID, description: String)
var requestId: UUID {
switch self {
case .response(let requestId, _),
.failure(let requestId, _):
return requestId
}
}
}
@available(iOS 13.0, *)
public typealias ResponseMessage = Message<ResponseContent>

View File

@@ -0,0 +1,33 @@
//
// ReturnValue.swift
// WalletSegue
//
// Created by Jungho Bang on 6/24/22.
//
import Foundation
public enum ReturnValue: BaseContent {
case result(value: String)
case error(code: Int, message: String)
}
@available(iOS 13.0, *)
public typealias ResponseResult = Result<BaseMessage<[ReturnValue]>, Error>
@available(iOS 13.0, *)
public typealias ResponseHandler = (ResponseResult) -> Void
@available(iOS 13.0, *)
extension ResponseMessage {
var result: ResponseResult {
switch self.content {
case .response(_, let results):
return .success(
BaseMessage<[ReturnValue]>.copy(self, replaceContentWith: results)
)
case .failure(_, let description):
return .failure(CoinbaseWalletSDK.Error.walletReturnedError(description))
}
}
}

View File

@@ -0,0 +1,14 @@
//
// 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
}
}

View File

@@ -0,0 +1,14 @@
# CoinbaseWalletSDK
## Installation
CoinbaseWalletSDK is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'CoinbaseWalletSDK'
```
## Author
jungho.bang@coinbase.com

View File

@@ -0,0 +1,15 @@
//
// Task.swift
// WalletSegue
//
// Created by Jungho Bang on 6/14/22.
//
import Foundation
@available(iOS 13.0, *)
struct Task {
let request: RequestMessage
let handler: ResponseHandler
let timestamp: Date
}

View File

@@ -0,0 +1,50 @@
//
// TaskManager.swift
// WalletSegue
//
// Created by Jungho Bang on 6/9/22.
//
import Foundation
@available(iOS 13.0, *)
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()
}
}