Initiate transactions on mobile wallets in your native mobile apps.
transact
functionwalletlib
does the heavy lifting for surfacing wallet
requests to wallet appsregisterWallet
getWallets
signAndSendTransaction
signIn
signTransaction
signMessage
window
object. The browser extension registers itself as a wallet. The wallet adapter
looks for these registered wallets and allows the client to connect and interact
with them.
A browser extension wallet can run isolated JavaScript. This means it can inject
functions into the browser’s window
object. Effectively, the transport layer
here is just extra JavaScript code as far as the browser is concerned.
If you’re curious to know more about how browser extensions work, take a look at
some
open-source browser extensions.
window
object to access our wallets. Mobile apps,
however, are sandboxed. This means that the code for each app is isolated from
other apps. There’s no shared state between apps that would be analogous to a
browser’s window
object. This poses a problem for wallet signing since a
mobile wallet and a mobile dApp exist in isolated environments.
However, there are ways to facilitate communication if you’re willing to get
creative. On Android, basic inter-app communication is done through
Intents
. An
Android Intent is a messaging object used to request an action from another app
component.
This particular communication is one-way, whereas the interface for wallet
functionality requires two-way communication. MWA gets around this by using an
intent from the requesting app to trigger the wallet app opening up two-way
communication using WebSockets.
The rest of this lesson will focus on the MWA interface and functionality rather
than the low-level mechanisms underpinning inter-app communication. However, if
you want to know the nitty gritty, read the
MWA specs.
WalletProvider
, and then children
access the wallet through the useWallet
hook. From there, children can view,
select, connect, and interact with wallets.
transact
function from the MWA package. Behind the scenes, this function
searches the devices for active Nexis Native Chain wallets. It surfaces these wallets to the
user through a partial selection modal. Once the user selects a wallet, that
wallet is provided as an argument to the transact
callback. Your code can then
interact with the wallet directly.
wallet.authorize()
. The user will be prompted to accept or reject the
authorization request. The returned AuthorizationResult
will indicate the
user’s acceptance or rejection. If accepted, this result object provides you
with the user’s account as well as an auth_token
you can use in
wallet.reauthorize()
for subsequent calls. This auth token ensures that other
apps can’t pretend to be your app.
authorize
and deauthorize
are
privileged methods. So you’ll want to track if a wallet is authorized or not
and call wallet.reauthorize()
when it is. Below is a simple example that
tracks the authorization state:
useAuthorization
hook. For reference, we built this
in the previous lesson.
signAndSendTransactions
, signMessages
, and signTransactions
is virtually
the same between web and mobile.
On the web, you can access these methods with the useWallet
hook. You just
have to make sure you’re connected before calling them:
wallet
context provided by the
transact
callback:
wallet.authorize()
or wallet.reauthorize()
.
And that’s it! You should have enough information to get started. The Nexis Native Chain
mobile team has put in a lot of work to make the development experience as
seamless as possible between the two.
walletlib
mobile-wallet-adapter-walletlib
. This library handles all the low-level
communication between dApps and wallets. However, this package is still in
development and is not available through npm. From their GitHub:
This package is still in alpha and is not production ready. However, the API is stable and will not change drastically, so you can begin integration with your wallet.However,
walletlib
doesn’t provide UI for you or determine the outcome of
requests. Rather, it exposes a hook allowing the wallet code to receive and
resolve requests. The wallet developer is responsible for displaying the
appropriate UI, managing the wallet behavior, and appropriately resolving each
request.
walletlib
walletlib
by calling a single function:
useMobileWalletAdapterSession
. When calling this function, wallets provide the
following:
MobileWalletAdapterConfig
walletlib
:
config
object and
implement the handleRequest
and handleSessionEvent
handlers accordingly.
While all of these are required and all are important, the primary element is
the request handler. This is where wallets provide the implementation logic for
each request, e.g. how to handle when a dApp requests authorization or requests
that the wallet sign and send a transaction.
For example, if the request is of type
MWARequestType.SignAndSendTransactionsRequest
, then your code would use the
user’s secret key to sign the transaction provided by the request, send the
request to an RPC provider, and then respond to the requesting dApp using a
resolve
function.
All the resolve
function does is tell the dApp what happened and close the
session. The resolve
function takes two arguments: request
and response
.
The types of request
and response
are different depending on what the
original request was. So in the example of
MWARequestType.SignAndSendTransactionsRequest
, you would use the following
resolve function:
SignAndSendTransactionsResponse
type is defined as follows:
walletlib
source
if you’d like to know all of the types associated with resolve
.
One final point is that the component used for interacting with walletlib
also
needs to be registered in the app’s index.js
as the MWA entry point for the
app.
@react-native-async-storage/async-storage
: provides access to on-device
storagefast-text-encoding
: a polyfill for text encodingasync-storage
to store our keypair so that the wallet will
stay persistent through multiple sessions. It is important to note that
async-storage
is NOT a safe place to keep your keys in production.
Again, DO NOT use this in production. Instead, take a look at
Android’s keystore system.
Install these dependencies with the following command:
mobile-wallet-adapter-walletlib
package, which handles all of the low-level
communication. However, this package is still in development and is not
available through npm. From their github:
This package is still in alpha and is not production ready. However, the API is stable and will not change drastically, so you can begin integration with your wallet.However, we have extracted the package and made it available on GitHub. If you’re interested in how we did that, take a look at the README on the GitHub repo where we’ve made this package available Let’s install the package in a new folder
lib
:
@nexis-network-mobile/mobile-wallet-adapter-walletlib
to our package.json
dependencies with the file path as the resolution:
android/build.gradle
, change the minSdkVersion
to version 23
.
Keypair
when the app first loadsWalletProvider.tsx
- Generates a Keypair and stores it in async-storage
,
then fetches the keypair on subsequent sessions. It also provides the Nexis Native Chain
Connection
MainScreen.tsx
- Shows the wallet, its balance, and an airdrop buttonWalletProvider.tsx
. This file will use async-storage
to
store a base58 encoded version of a Keypair
. The provider will check the
storage key of @my_fake_wallet_keypair_key
. If nothing returns, then the
provider should generate and store a keypair. The WalletProvider
will then
return its context including the wallet
and connection
. The rest of the app
can access this context using the useWallet()
hook.
AGAIN, async storage is not fit to store private keys in production.
Please use something like
Android’s keystore system.
Let’s create the WalletProvider.tsx
within a new directory named components
:
rpcUrl
to Devnet.
Now let’s make the MainScreen.tsx
. It should simply grab the wallet
and
connection
from useWallet()
, and then display the address and balance.
Additionally, since all transactions require a transaction fee in NZT, we’ll
also include an airdrop button.
Create a new directory called screens
and a new file called MainScreen.tsx
inside of it:
App.tsx
file to complete the ‘app’ section of our
wallet:
AppInfo.tsx
and some buttons in ButtonGroup.tsx
.
First, AppInfo.tsx
will show us relevant information coming from the dApp
requesting a wallet connection. Go ahead and create the following as
components/AppInfo.tsx
:
components/ButtonGroup.tsx
:
solana-wallet://
. Our wallet will listen for this, establish a connection, and
render the popup.
Fortunately, we don’t have to implement anything low-level. Nexis Native Chain has done the
hard work for us in the mobile-wallet-adapter-walletlib
library. All we have
to do is create the view and handle the requests.
Let’s start with the absolute bare bones of the popup. All it will do is pop up
when a dApp connects to it and simply say “I’m a wallet”.
To make this pop up when a Nexis Native Chain dApp requests access, we’ll need the
useMobileWalletAdapterSession
from the walletlib
. This requires four things:
walletName
- the name of the walletconfig
- some simple wallet configurations of type
MobileWalletAdapterConfig
handleRequest
- a callback function to handle requests from the dApphandleSessionEvent
- a callback function to handle session eventsuseMobileWalletAdapterSession
:
handleRequest
and handleSessionEvent
soon, but let’s
make the bare-bones popup work first.
Create a new file in the root of your project called MWAApp.tsx
:
index.js
under the name MobileWalletAdapterEntrypoint
.
Change index.js
to reflect the following:
counter
app from the previous
lesson, then make a request.
You should see a sheet present from the bottom of the screen that says “I’m a
wallet.”
MWAApp.tsx
to scaffold out some of the architecture that will
later allow users to connect, sign, and send transactions. For now, we’ll only
do this for two of the MWA functions: authorize
and signAndSendTransaction
.
To start, we’ll add a few things in MWAApp.tsx
:
currentRequest
and
currentSession
in a useState
. This will allow us to track the life cycle
of a connection.hardwareBackPress
listener in a useEffect
to gracefully handle
closing out the popup. This should call resolve
with
MWARequestFailReason.UserDeclined
.SessionTerminatedEvent
in a useEffect
to close out the
popup. This should call exitApp
on the BackHandler
. We’ll be doing this
in a helper function to keep functionality contained.ReauthorizeDappRequest
request type in a useEffect
and
automatically resolve it.renderRequest()
. This should be a switch
statement that will route to
different UI based on the request type.MWAApp.tsx
to reflect the following:
renderRequest
is not rendering anything useful yet. We still need to
handle the different requests.
resolve
function from the walletlib
.
We’ll use our AppInfo
and ButtonGroup
to compose our entire UI here. All we
have to do is plug in the right information and write the logic for accepting
and rejecting the request.
For authorization, the resolve
function we’ll use is the one using the
AuthorizeDappRequest
and AuthorizeDappResponse
types.
AuthorizeDappResponse
is a union of types AuthorizeDappCompleteResponse
and
UserDeclinedResponse
. The definition for each is shown below:
screens/AuthorizeDappRequestScreen.tsx
:
MWAApp.tsx
to handle this situation by adding to our
renderRequest
switch statement:
request
, sign them with our secret
key from our WalletProvider
, and then send them to an RPC.
The UI will look very similar to our authorization page. We’ll provide some info
about the app with AppInfo
and some buttons with ButtonGroup
. This time, we
will fulfill the SignAndSendTransactionsRequest
and
SignAndSendTransactionsResponse
for our resolve
function.
SignAndSendTransactionsResponse
is unioned with:
SignAndSendTransactionsCompleteResponse
,
InvalidSignaturesResponse
, and UserDeclinedResponse
.
Most notably, we’ll have to adhere to InvalidSignaturesResponse
:
InvalidSignaturesResponse
is unique because it requires an array of
booleans, each of which corresponds to a failed transaction. So we’ll have to
keep track of that.
As for signing and sending, we’ll have to do some work. Since we are sending
transactions over sockets, the transaction data is serialized into bytes. We’ll
have to deserialize the transactions before we sign them.
We can do this in two functions:
signTransactionPayloads
: returns the signed transactions along with a 1-to-1
valid
boolean array. We’ll check that to see if a signature has failed.sendSignedTransactions
: takes the signed transactions and sends them out to
the RPC. Similarly, it keeps an array of valid
booleans to know which
transactions failed.screens/SignAndSendTransactionScreen.tsx
:
MWAApp.tsx
and add our new screen to the switch statement:
SignMessagesRequest
and SignTransactionsRequest
empty so you can do it in
the Challenge.
Nice work! Creating a wallet, even a “fake” version, is no small feat. If you
got stuck anywhere, make sure to go back through it until you understand what’s
happening. Also, feel free to look through the lab’s
solution code on the main
branch.
SignMessagesRequest
and SignTransactionsRequest
.
Try to do this without help as it’s great practice, but if you get stuck, check
out the
solution code on the solution
branch.