- Mobile (Celo Wallet)
This is a wallet application for the Celo platform. It's a self-soverign wallet that enables anyone to onboard onto the Celo network, manage their currencies, and send payments.
The app uses React Native and a geth light node.
You must have the celo-monorepo successfully set up and built before setting up and running the mobile wallet.
To do this, follow the setup instructions.
Next, install watchman.
# On a mac
brew install watchman
In order to successfully set up your iOS development environment you will need to enroll in the Apple Developer Program. It is recommended that you enroll from an iOS device by downloading the Apple Developer App in the App Store. Using the app will result in the fastest processing of your enrollment.
If you are a cLabs employee, please ask to be added to the cLabs iOS development team.
Xcode is needed to build and deploy the mobile wallet to your iOS device. If you do not have an iOS device, Xcode can be used to emulate one.
Install Xcode 12.2 (an Apple Developer Account is needed to access this link).
We do not recommend installing Xcode through the App Store as it can auto update and become incompatible with our projects.
Note that using the method above, you can have multiple versions of Xcode installed in parallel if you'd like. Simply use different names for the different version of Xcode in your computer's Applications
folder (e.g., Xcode10.3.app
and Xcode11.app
).
Make sure you are in the ios
directory of the mobile
package before running the following:
# install cocopods and bundler if you don't already have it
gem install cocoapods
gem install bundler
# download the project dependencies
bundle install
# run inside mobile/ios
bundle exec pod install
If your machine does not recognize the gem
command, you may need to download Ruby first.
We need Java to be able to build and deploy the mobile app to Android devices. Android currently only builds correctly with Java 8. (Using OpenJDK because of Oracle being Oracle).
Install by running the following:
brew install cask
brew tap homebrew/cask-versions
brew cask install homebrew/cask-versions/adoptopenjdk8
Alternatively, install Jenv to manage multiple Java versions:
brew install jenv
eval "$(jenv init -)"
jenv add /Library/Java/JavaVirtualMachines/<java8 version here>/Contents/Home
Install by running the following:
sudo apt install openjdk-8-jdk
Install the Android SDK and platform tools:
brew cask install android-sdk
brew cask install android-platform-tools
Next install Android Studio and add the Android NDK.
Execute the following (and make sure the lines are in your ~/.bash_profile
).
Note that these paths may differ on your machine. You can find the path to the SDK and NDK via the Android Studio menu.
export ANDROID_HOME=/usr/local/share/android-sdk
export ANDROID_NDK=/usr/local/share/android-ndk
export ANDROID_SDK_ROOT=/usr/local/share/android-sdk
# this is an optional gradle configuration that should make builds faster
export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"'
Then install the Android 29 platform:
sdkmanager 'platforms;android-29'
You can download the complete Android Studio and SDK from the Android Developer download site.
The steps are:
-
Unpack the .zip file you downloaded to an appropriate location for your applications, such as within
/usr/local/
for your user profile, or/opt/
for shared users.If you're using a 64-bit version of Linux, make sure you first install the required libraries for 64-bit machines.
-
To launch Android Studio, open a terminal, navigate to the
android-studio/bin/
directory, and executestudio.sh
. -
Select whether you want to import previous Android Studio settings or not, then click OK.
-
The Android Studio Setup Wizard guides you through the rest of the setup, which includes downloading Android SDK components that are required for development.
You can find the complete instructions about how to install the tools in Linux environments in the Documentation page.
Install the Android 29 system image and create an Android Virtual Device:
sdkmanager "system-images;android-29;default;x86_64"
avdmanager create avd --force --name Pixel_API_29_AOSP_x86_64 --device pixel -k "system-images;android-29;default;x86_64"
Execute the following and add it to your ~/.bash_profile
:
export PATH=$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$PATH
Run the emulator with:
emulator -avd Pixel_API_29_AOSP_x86_64
Another Android emulator option is Genymotion.
brew cask install genymotion
Under OSX High Sierra and later, you'll get a message that you need to approve it in System Preferences > Security & Privacy > General.
Do that, and then repeat the line above.
Then make sure the ADB path is set correctly in Genymotion — set
Preferences > ADB > Use custom Android SDK tools
to
/usr/local/share/android-sdk
(same as $ANDROID_HOME
)
You can download the Linux version of Genymotion from the fun zone! (you need to sign in first).
After having the binary you only need to run the installer:
sudo ./genymotion-3.0.2-linux_x64.bin
The below steps should help you successfully run the mobile wallet on either a USB connected or emulated device. For additional information and troublshooting see the React Native docs.
Note: We've seen some issues running the metro bundler from iTerm
-
If you haven't already, run
yarn
from the monorepo root to install dependencies. -
Attach your device or start an emulated one.
-
Launch Xcode and use it to open the directory
celo.xcworkspace
. Confirm your iOS device has been detected by Xcode. -
Build the project by pressing the play button in the top left corner or selecting
Product > Build
from the Xcode menu bar. -
From the
mobile
directory runyarn run dev:ios
.
-
Follow these instructions to enable Developer Options on your Android device.
-
Unplug and replug your device. You'll be prompted to accept the connection and shown a public key (corresponding to the
abd_key.pub
file in~/.android
) -
To confirm your device is properly connected, running
adb devices
from the terminal should reflect your connected device. If it lists a device as "unauthorized", make sure you've accepted the prompt or troubleshoot here. -
From the
mobile
directory runyarn run dev:android
.
By default, the mobile wallet app runs geth in lightest sync mode where all the epoch headers are fetched. The default sync mode is defined in packages/mobile/.env file.
To run the wallet in forno (Data Saver) mode, using a trusted node rather than the local geth node as a provider, turn it on from the Data Saver page in settings or update the FORNO_ENABLED_INITIALLY
parameter in the .env file linked above. When forno mode is turned back off, the wallet will switch to the default sync mode as specified in the .env file. By default, the trusted node is https://{TESTNET}-forno.celo-testnet.org
, however any trusted node can be used by updating DEFAULT_FORNO_URL
. In forno mode, the wallet signs transactions locally in web3 then sends them to the trusted node.
To debug network requests in forno mode, we use Charles, a proxy for monitoring network traffic to see Celo JSON RPC calls and responses. Follow instructions here to configure Charles to proxy a test device.
To avoid debugging errors, ensure your device and laptop are connected to the same WiFi network even if they are connected via USB.
-
Either shake the device or run
yarn run dev:show-menu
(only for Android) to open up the developer menu. -
Select
Debug
(iOS) orStart Remote JS Debugging
(Android). This should open a new tab in your browser with React Native logger in the console. In order to get a full picture, the console's filter should be set toAll levels
. -
For the fastest development experience, you likely want to open the developer menu again and ensure
Fast Reloading
(iOS) orLive Reloading
andHot Reloading
(Android) is enabled.
The React Native Debugger bundles together the Redux and Chrome dev tools nicely and provides a clean debugging environment.
Run yarn run react-devtools
. It should automatically connect to the running app, and includes a profiler (second tab). Start recording with the profiler, use the app, and then stop recording.
The flame graph provides a view of each component and sub-component. The width is proportional to how long it took to load. If it is grey, it was not re-rendered at that 'commit' or DOM change. Details on the react native profiler are here. The biggest thing to look for are large number of renders when no state has changed. Reducing renders can be done via pure components in React or overloading the should component update method example here.
To execute the suite of tests, run yarn test
.
We use Jest snapshot testing to assert that no intentional changes to the
component tree have been made without explicit developer intention. See an
example at [src/send/SendAmount.test.tsx
]. If your snapshot is expected
to deviate, you can update the snapshot with the -u
or --updateSnapshot
flag when running the test.
We use react-native-testing-library to unit test
react components. It allows for deep rendering and interaction with the rendered
tree to assert proper reactions to user interaction and input. See an example at
[src/send/SendAmount.test.tsx
] or read more about the docs
We use redux-saga-test-plan to test complex sagas.
See [src/identity/verification.test.ts
] for an example.
We use Detox for E2E testing. In order to run the tests locally, you must have the proper emulator set up. Follow the instructions in e2e/README.md.
Once setup is done, you can run the tests with yarn test:e2e:android
or yarn test:e2e:ios
.
You can create your own custom build of the app via the command line or in Android Studio. For an exact set of commands, refer to the lanes in fastlane/FastFile
. For convinience, the basic are described below:
If you have not yet created a keystore, one will be required to generate a release APKs / bundles:
cd android/app
keytool -genkey -v -keystore celo-release-key.keystore -alias celo-key-alias -storepass celoFakeReleaseStorePass -keypass celoFakeReleaseKeyPass -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"
export CELO_RELEASE_STORE_PASSWORD=celoFakeReleaseStorePass
export CELO_RELEASE_KEY_PASSWORD=celoFakeReleaseKeyPass
# With fastlane:
bundle install
bundle exec fastlane android build_apk env:YOUR_BUILDING_VARIANT
# Or, manually
cd android/
./gradlew clean
./gradlew bundle{YOUR_BUILDING_VARIANT}JsAndAssets
# For an APK:
./gradlew assemble{YOUR_BUILDING_VARIANT} -x bundle{YOUR_BUILDING_VARIANT}JsAndAssets
# Or for a bundle:
./gradlew bundle{YOUR_BUILDING_VARIANT} -x bundle{YOUR_BUILDING_VARIANT}JsAndAssets
Where YOUR_BUILD_VARIANT
can be any of the app's build variants, such as debug or release.
On Android, the wallet app uses the SMS Retriever API to automatically input codes during phone number verification. When creating a new app build type this needs to be properly configured.
The service that route SMS messages to the app needs to be configured to append this app signature to the message. The hash depends on both the bundle id and the signing certificate. Since we use Google Play signing, we need to download the certificate.
- Go to the play console for the relevant app, Release management > App signing, and download the App signing certificate.
- Use this script to generate the hash code: https://github.com/michalbrz/sms-retriever-hash-generator
We're using GraphQL Code Generator to properly type GraphQL queries. If you make a change to a query, run yarn build:gen-graphql-types
to update the typings in the typings
directory.
Our Celo app has three types of codes.
- Javascript code - generated from Typescript, this runs in Javascript interpreter.
- Java bytecode - this runs on Dalvik/Art Virtual Machine.
- Native code - Geth code is written in Golang which compiles to native code, this runs directly on the CPU, no virtual machines involved.
One should note that, on iOS, there is no byte code and therefore, there are only two layers, one is the Javascript code, and the other is the Native code. Till now, we have been blind towards native crashes except Google Playstore logs.
Sentry, the crash logging mechanism we use, can catch both Javascript Errors as well as unhandled Java exceptions. It, however, does not catch Native crashes. There are quite a few tools to catch native crashes like Bugsnag and Crashlytics. They would have worked for us under normal circumstances. However, the Geth code produced by the Gomobile library and Go compiler logs a major chunk of information about the crash at Error level and not at the Fatal level. We hypothesize that this leads to incomplete stack traces showing up in Google Play store health checks. This issue is publicly known but has not been fixed.
We cannot use libraries like Bugsnag since they do not allow us to extract logcat logs immediately after the crash. Therefore, We use jndcrash, which uses ndcrash and enable us to log the logcat logs immediately after a native crash. We capture the results into a file and on next restart Sentry reads it. We need to do this two-step setup because once a native crash happens, running code to upload the data would be fragile. An error in sentry looks like this
Relevant code references:
- NDKCrashService
- Initialization of the NDKCrashService
- Sentry code to read NDK crash logs on restart
There are two major differences in Forno mode:
- Geth won't run at all. Instead, web3 connects to -forno.celo-testnet.org using an https provider, for example, https://integration-forno.celo-testnet.org.
- Transactions will be signed locally by contractkit.
Websockets (ws
) would have been a better choice but we cannot use unencrypted ws
provider since it would be bad to send plain-text data from a privacy perspective. Geth does not support wss
by default. And Kubernetes does not support it either. This forced us to use https provider.
- Start geth's HTTP RPC server by setting the config variable
GETH_START_HTTP_RPC_SERVER
to true. This is meant for development purposes only and can be a serious vulnerability if used in production. - Forward traffic from your computer's port 8545 to the android device's:
adb forward tcp:8545 tcp:8545
- Using a geth binary on your computer, run
geth attach http://localhost:8545
We need the IP address of the iOS device. If it is being run in a simulator, the IP address is 127.0.0.1
. If not running in a simulator:
- Ensure the iOS device is on the same network as your computer.
- Find the device's local IP address by going to the Settings app, Wi-Fi, and tapping the 'i' next to the network.
To attach:
- Start geth's HTTP RPC server by setting the config variable
GETH_START_HTTP_RPC_SERVER
to true. This is meant for development purposes only and can be a serious vulnerability if used in production. - Using a geth binary on your computer, run
geth attach http://<DEVICE_IP_ADDRESS>:8545
From time to time the app refuses to start showing this error:
557 actionable tasks: 525 executed, 32 up-to-date
info Running /usr/local/share/android-sdk/platform-tools/adb -s PL2GARH861213542 reverse tcp:8081 tcp:8081
info Starting the app on PL2GARH861213542 (/usr/local/share/android-sdk/platform-tools/adb -s PL2GARH861213542 shell am start -n org.celo.mobile.staging/org.celo.mobile.MainActivity)...
Starting: Intent { cmp=org.celo.mobile.staging/org.celo.mobile.MainActivity }
Error type 3
Error: Activity class {org.celo.mobile.staging/org.celo.mobile.MainActivity} does not exist.
Solution:
$ adb kill-server && adb start-server
* daemon not running; starting now at tcp:5037
* daemon started successfully