@degenlab/stacks-wallet-kit-mobile
v1.1.0
Published
A React Native/Expo SDK for building Stacks blockchain applications with Google authentication and wallet management.
Readme
stacks-wallet-kit/mobile 
A React Native/Expo SDK for building Stacks blockchain applications with Google authentication and wallet management.
Purpose
This SDK enables seamless Web2 authentication for mobile apps and provides easy wallet backup to a safe place without requiring users to store or remember their mnemonic phrase. It simplifies blockchain operations by handling the complexity of parsing arguments and data, allowing developers to focus on building their applications rather than managing low-level blockchain interactions.
The mobile SDK imports functionality from the @degenlab/stacks-wallet-kit-core package, which is platform-agnostic and shared between this mobile SDK and the web extension SDK, ensuring consistency across platforms.
Installation
npm install @degenlab/stacks-wallet-kit-mobile
# or
yarn add @degenlab/stacks-wallet-kit-mobile
# or
pnpm add @degenlab/stacks-wallet-kit-mobileNote: This package works with npm, yarn, and pnpm. Choose the package manager that fits your project.
Required Polyfills and Setup
The SDK requires Node.js polyfills to work in React Native/Expo environments. These polyfills must be imported before any SDK code runs.
Required Polyfill Packages
Install the following packages:
npm install react-native-get-random-values buffer
# For Expo projects:
npm install expo-standard-web-cryptoRequired packages:
react-native-get-random-values- Providescrypto.getRandomValues()for cryptographic operationsbuffer- Provides Node.jsBufferAPIexpo-standard-web-crypto(Expo only) - Provides Web Crypto API polyfill
Polyfill Setup
For Expo Router Projects
If you're using Expo Router, add the polyfills at the very top of your root layout file (app/_layout.tsx or app/_layout.js):
// app/_layout.tsx
// ⚠️ IMPORTANT: Polyfills MUST be imported FIRST, before any other imports
import 'react-native-get-random-values'
import 'expo-standard-web-crypto'
import { Buffer } from 'buffer'
// Make Buffer available globally for React Native
if (typeof global.Buffer === 'undefined') {
global.Buffer = Buffer
}
// Now import your other dependencies
import { Stack } from 'expo-router'
// ... rest of your importsFor React Native (Non-Expo) Projects
If you're using React Native without Expo, create an index.js file in your project root:
// index.js
// ⚠️ IMPORTANT: Polyfills MUST be imported FIRST
import 'react-native-get-random-values'
import { Buffer } from 'buffer'
// Make Buffer available globally
if (typeof global.Buffer === 'undefined') {
global.Buffer = Buffer
}
// Now import your app entry point
import { AppRegistry } from 'react-native'
import App from './App'
import { name as appName } from './app.json'
AppRegistry.registerComponent(appName, () => App)Then update your package.json:
{
"main": "index.js"
}For Expo (Non-Router) Projects
If you're using Expo without Router, add polyfills to your App.js or App.tsx:
// App.tsx
// ⚠️ IMPORTANT: Polyfills MUST be imported FIRST
import 'react-native-get-random-values'
import 'expo-standard-web-crypto'
import { Buffer } from 'buffer'
// Make Buffer available globally
if (typeof global.Buffer === 'undefined') {
global.Buffer = Buffer
}
// Now import your app components
import { View, Text } from 'react-native'
// ... rest of your appPlatform-Specific Configuration
Android Emulator
When testing on Android emulator, use 10.0.2.2 instead of localhost for devnet URLs:
import { Platform } from 'react-native'
const getDevnetUrl = (): string => {
if (Platform.OS === 'android') {
// Android emulator - use 10.0.2.2 (maps to host's localhost)
return 'http://10.0.2.2:3999/'
} else if (Platform.OS === 'web') {
// Web platform - use localhost
return 'http://localhost:3999/'
} else {
// iOS - use localhost (iOS simulator maps localhost correctly)
return 'http://localhost:3999/'
}
}
const client = new MobileClient(
'web-client-id',
'ios-client-id',
NetworkType.Devnet,
{
devnetUrl: getDevnetUrl(),
}
)Web Platform
When running on web (Expo web), the polyfills work the same way, but make sure to use localhost for devnet URLs instead of 10.0.2.2.
Common Errors and Solutions
Error: crypto.getRandomValues must be defined
- Solution: Make sure
react-native-get-random-valuesis imported at the very top of your entry file, before any other imports.
Error: Property 'Buffer' doesn't exist
- Solution: Import
bufferand setglobal.Buffer = Bufferbefore any SDK code runs.
Error: ReferenceError: Buffer is not defined
- Solution: Ensure the Buffer polyfill is set up correctly (see setup instructions above).
Troubleshooting Steps:
- Verify polyfills are imported first in your entry file
- Clear Metro bundler cache:
npx expo start --clearornpx react-native start --reset-cache - Delete
node_modulesand reinstall:rm -rf node_modules && npm install - Restart your development server completely
Quick Start
import { MobileClient, NetworkType } from '@degenlab/stacks-wallet-kit-mobile'
const client = new MobileClient(
'your-web-client-id',
'your-ios-client-id',
NetworkType.Testnet,
{
scopes: ['email', 'profile'],
devnetUrl: 'http://10.0.2.2:3999',
}
)
client.setNetwork(NetworkType.Devnet)
const wallet = await client.createWallet() // Optional passphrase
const accounts = await client.getWalletAccounts()
const account = accounts[0]
const balance = await client.getBalance(account)MobileClient Configuration
Constructor Parameters
new MobileClient(
webClientId: string, // Required: Google OAuth web client ID
iosClientId: string, // Required: Google OAuth iOS client ID
network: NetworkType, // Required: Initial network (Mainnet, Testnet, or Devnet)
configOptions?: { // Optional: Configuration options
scopes?: string[] // Optional: Additional OAuth scopes
storageManager?: IStorageManager // Optional: Custom storage manager
mainnetUrl?: string // Optional: Custom mainnet API URL
testnetUrl?: string // Optional: Custom testnet API URL
devnetUrl?: string // Optional: Custom devnet API URL
}
)Default Configuration
Storage Manager:
- If not provided,
MobileClientautomatically selects a storage implementation:- Expo: Uses
SecureStore(Expo SecureStore) - React Native: Uses
KeyChainStorage(react-native-keychain)
- Expo: Uses
OAuth Scopes:
- Default scope:
https://www.googleapis.com/auth/drive.appdata(required for Google Drive backup) - Additional scopes can be provided via the
scopesparameter and will be merged with the default scope
Stacks API URLs:
- Mainnet:
https://api.hiro.so/ - Testnet:
https://api.testnet.hiro.so/ - Devnet:
http://10.0.2.2:3999/(Android emulator compatible)
Google Sign-In Configuration:
offlineAccess: true- Enables refresh token supportforceCodeForRefreshToken: true- Forces code exchange for refresh tokens
Internal Components
MobileClient automatically initializes the following components:
- Authentication:
GoogleAuthwith provided client IDs and scopes - Backup Manager:
BackupManagerwithGoogleBackupClient - Wallet Manager:
WalletManagerfor wallet operations - Encryption Manager:
EncryptionManagerfor encryption/decryption - Stacks Client:
StacksClientwith configured network and API URLs - Stacking Client:
StackingClientwith configured network
API Reference
Authentication
loginWithGoogle()
Sign in with Google and check if a wallet backup exists.
import { User } from '@degenlab/stacks-wallet-kit-core'
const result: {
accessToken: string
hasBackup: boolean
userData: User | undefined
} = await client.loginWithGoogle()
// Access the returned values
console.log('Access Token:', result.accessToken)
console.log('Has Backup:', result.hasBackup)
console.log('User Data:', result.userData) // Contains Google user informationsignOut()
Sign out from Google authentication.
await client.signOut()Wallet
createWallet(passphrase?: string)
Create a new wallet with a generated mnemonic.
const wallet: Wallet = await client.createWallet() // Optional passphrasestoreExistingWallet(mnemonic: string, passphrase?: string)
Store an existing wallet from a mnemonic phrase.
const mnemonic: string = 'abandon abandon abandon ...'
const wallet: Wallet = await client.storeExistingWallet(mnemonic) // Optional passphrasegetWalletAccounts()
Get all accounts in the wallet.
const accounts: WalletAccount[] = await client.getWalletAccounts()createAccount()
Create a new account in the wallet.
const newAccount: WalletAccount = await client.createAccount()Backup
backupWallet(password: string)
Backup the wallet to Google Drive.
await client.backupWallet('wallet-password')retrieveWallet(password: string)
Retrieve a wallet from Google Drive backup.
const { wallet, mnemonic }: { wallet: Wallet; mnemonic: string } =
await client.retrieveWallet('wallet-password')deleteBackup(password: string)
Delete the wallet backup from Google Drive.
await client.deleteBackup('wallet-password')deleteBackupWithoutPassword()
Delete the wallet backup without requiring a password.
await client.deleteBackupWithoutPassword()Stacks
getBalance(account)
Get the STX balance for an account.
const accounts: WalletAccount[] = await client.getWalletAccounts()
const account: WalletAccount = accounts[0]
const balance: number = await client.getBalance(account) // Returns: numbersendStx(accountIndex, to, amount, network, memo?)
Send STX tokens to another address.
const txid: string = await client.sendStx(
0, // account index
'ST1AWHANXSGZ3SY8XQC2J7S18E22KJJW9B3JT2T54', // recipient
0.002, // amount in STX
NetworkType.Devnet, // network
'Test memo' // optional memo
) // Returns: stringtransferNFT(accountIndex, contractId, tokenId, to, network)
Transfer an NFT to another address. Designed for SIP-9 NFT trait compliant contracts.
const txid: string = await client.transferNFT(
0, // account index
'STJK0PY5JPPNR4PZWR7HVS7T92B1MKHQKBT5Q6MX.nft-test', // contract ID
'2', // token ID
'ST10WDE40SQ62CSWRE99NA0KWBZ74NNK4FNS9T19Q', // recipient
NetworkType.Devnet // network
) // Returns: stringtransferFT(accountIndex, contractId, amount, to, network)
Transfer fungible tokens to another address. Designed for SIP-10 FT trait compliant contracts.
const txid: string = await client.transferFT(
0, // account index
'STJK0PY5JPPNR4PZWR7HVS7T92B1MKHQKBT5Q6MX.ft-test', // contract ID
100 * 1000000, // amount in micro-units (100 tokens with 6 decimals)
'ST10WDE40SQ62CSWRE99NA0KWBZ74NNK4FNS9T19Q', // recipient
NetworkType.Devnet // network
) // Returns: stringmakeContractCall(contractAddress, functionName, functionArgs, postConditionMode?)
Make a contract call to any Stacks smart contract.
import {
uintCV,
standardPrincipalCV,
PostConditionMode,
ClarityValue,
} from '@stacks/transactions'
const contractAddress: string =
'STJK0PY5JPPNR4PZWR7HVS7T92B1MKHQKBT5Q6MX.nft-test'
const functionName: string = 'transfer'
const functionArgs: ClarityValue[] = [
uintCV('2'), // token-id
standardPrincipalCV(senderAddress), // sender
standardPrincipalCV(recipientAddress), // recipient
]
// With default post condition mode (PostConditionMode.Deny)
const txid: string = await client.makeContractCall(
contractAddress,
functionName,
functionArgs
) // Returns: string
// With custom post condition mode
const txid2: string = await client.makeContractCall(
contractAddress,
functionName,
functionArgs,
PostConditionMode.Allow // Optional: defaults to PostConditionMode.Deny
) // Returns: stringStacking
Solo Stacking
stackSTX(account, amount, lockPeriod, maxAmount, options?)
Stack STX to earn Bitcoin rewards.
Note: If options is not provided, the signature will be automatically generated by a backend service for mainnet and testnet networks.
const accounts: WalletAccount[] = await client.getWalletAccounts()
const account: WalletAccount = accounts[0]
// Basic usage (signature generated by backend)
const txid: string = await client.stackSTX(
account,
1000, // amount to stack in STX
1, // lock period in cycles
1000 // max amount in STX
) // Returns: string
// With custom signer options (skip backend signature generation)
const txid2: string = await client.stackSTX(account, 1000, 1, 1000, {
signerSignature: '0x...', // Optional: custom signer signature
signerKey: '0x...', // Optional: custom signer key
authId: '123', // Optional: custom auth ID
}) // Returns: stringstackExtend(account, extendCount, maxAmount, options?)
Extend the stacking lock period.
Note: If options is not provided, the signature will be automatically generated by a backend service for mainnet and testnet networks.
const accounts: WalletAccount[] = await client.getWalletAccounts()
const account: WalletAccount = accounts[0]
// Basic usage (signature generated by backend)
const txid: string = await client.stackExtend(
account,
2, // number of cycles to extend
1000 // max amount in STX
) // Returns: string
// With custom signer options (skip backend signature generation)
const txid2: string = await client.stackExtend(account, 2, 1000, {
signerSignature: '0x...', // Optional: custom signer signature
signerKey: '0x...', // Optional: custom signer key
authId: '123', // Optional: custom auth ID
}) // Returns: stringstackIncrease(account, increaseBy, maxAmount, currentLockPeriod, options?)
Increase the amount being stacked.
Note: If options is not provided, the signature will be automatically generated by a backend service for mainnet and testnet networks.
const accounts: WalletAccount[] = await client.getWalletAccounts()
const account: WalletAccount = accounts[0]
// Basic usage (signature generated by backend)
const txid: string = await client.stackIncrease(
account,
500, // amount to increase by in STX
1500, // max amount in STX
1 // current lock period
) // Returns: string
// With custom signer options (skip backend signature generation)
const txid2: string = await client.stackIncrease(account, 500, 1500, 1, {
signerSignature: '0x...', // Optional: custom signer signature
signerKey: '0x...', // Optional: custom signer key
authId: '123', // Optional: custom auth ID
}) // Returns: stringStacking with a Pool
delegateSTX(account, amount, delegateTo, untilBurnHeight?)
Delegate STX to a stacking pool.
import { StackingPool } from '@degenlab/stacks-wallet-kit-core'
const pool: StackingPool = {
name: 'Pool Name',
address: 'SP...',
}
const accounts: WalletAccount[] = await client.getWalletAccounts()
const account: WalletAccount = accounts[0]
const txid: string = await client.delegateSTX(
account,
1000, // amount to delegate in STX
pool, // stacking pool
100000 // optional: until burn height
) // Returns: stringrevokeDelegation(account)
Revoke STX delegation from a stacking pool.
const accounts: WalletAccount[] = await client.getWalletAccounts()
const account: WalletAccount = accounts[0]
const txid: string = await client.revokeDelegation(account) // Returns: stringNetwork
setNetwork(network)
Set the network for all operations.
import { NetworkType } from '@degenlab/stacks-wallet-kit-mobile'
client.setNetwork(NetworkType.Mainnet)
client.setNetwork(NetworkType.Testnet)
client.setNetwork(NetworkType.Devnet)Configuration
Platform-Specific Devnet URLs
See the Platform-Specific Configuration section above for details on setting up devnet URLs for different platforms (Android emulator, iOS simulator, Web).
Custom Storage Manager
You can provide a custom storage manager by implementing the IStorageManager interface from the @degenlab/stacks-wallet-kit-core package. Make sure to install the core package:
npm install @degenlab/stacks-wallet-kit-core
# or
yarn add @degenlab/stacks-wallet-kit-core
# or
pnpm add @degenlab/stacks-wallet-kit-coreimport { IStorageManager } from '@degenlab/stacks-wallet-kit-core'
class CustomStorage implements IStorageManager {
async setItem<T>(key: string, value: T): Promise<void> {
// Your implementation
}
async getItem<T>(key: string): Promise<T | null> {
// Your implementation
}
async removeItem(key: string): Promise<void> {
// Your implementation
}
async clear(): Promise<void> {
// Your implementation
}
}
const client = new MobileClient(
'web-client-id',
'ios-client-id',
NetworkType.Testnet,
{
storageManager: new CustomStorage(),
}
)Types
NetworkType
enum NetworkType {
Mainnet = 'mainnet',
Testnet = 'testnet',
Devnet = 'devnet',
}WalletAccount
interface WalletAccount {
index: number
publicKey: string
addresses: {
mainnet: string
testnet: string
}
}StackingPool
interface StackingPool {
name: string
address: string
}Examples
Complete Wallet Flow
import { MobileClient, NetworkType } from '@degenlab/stacks-wallet-kit-mobile'
import { Wallet, WalletAccount, User } from '@degenlab/stacks-wallet-kit-core'
async function walletFlow(): Promise<void> {
const client: MobileClient = new MobileClient(
'web-client-id',
'ios-client-id',
NetworkType.Devnet,
{ devnetUrl: 'http://10.0.2.2:3999' }
)
const {
hasBackup,
userData,
}: {
accessToken: string
hasBackup: boolean
userData: User | undefined
} = await client.loginWithGoogle()
let wallet: Wallet
if (hasBackup) {
const result: { wallet: Wallet; mnemonic: string } =
await client.retrieveWallet('password')
wallet = result.wallet
} else {
wallet = await client.createWallet() // Optional passphrase
await client.backupWallet('password')
}
const accounts: WalletAccount[] = await client.getWalletAccounts()
const account: WalletAccount = accounts[0]
const balance: number = await client.getBalance(account)
if (balance > 0.01) {
const txid: string = await client.sendStx(
0,
'ST1AWHANXSGZ3SY8XQC2J7S18E22KJJW9B3JT2T54',
0.01,
NetworkType.Devnet,
'Test payment'
)
}
}NFT Transfer Example
import { MobileClient, NetworkType } from '@degenlab/stacks-wallet-kit-mobile'
import { WalletAccount } from '@degenlab/stacks-wallet-kit-core'
async function transferNFT(): Promise<void> {
const client: MobileClient = new MobileClient(
'web-client-id',
'ios-client-id',
NetworkType.Devnet
)
const accounts: WalletAccount[] = await client.getWalletAccounts()
const account: WalletAccount = accounts[0]
const txid: string = await client.transferNFT(
account.index,
'SP1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.my-nft',
'1',
'ST1AWHANXSGZ3SY8XQC2J7S18E22KJJW9B3JT2T54',
NetworkType.Devnet
)
}Custom Contract Call Example
import { MobileClient, NetworkType } from '@degenlab/stacks-wallet-kit-mobile'
import {
uintCV,
standardPrincipalCV,
ClarityValue,
PostConditionMode,
} from '@stacks/transactions'
async function customContractCall(): Promise<void> {
const client: MobileClient = new MobileClient(
'web-client-id',
'ios-client-id',
NetworkType.Devnet
)
const contractAddress: string =
'SP1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.my-contract'
const functionName: string = 'my-function'
const functionArgs: ClarityValue[] = [
uintCV(100),
standardPrincipalCV('ST1AWHANXSGZ3SY8XQC2J7S18E22KJJW9B3JT2T54'),
]
// With optional post condition mode
const txid: string = await client.makeContractCall(
contractAddress,
functionName,
functionArgs,
PostConditionMode.Allow
)
}