solana-mobile-wallet

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Solana wallets on mobile

移动端Solana钱包

Wallet connection and transaction signing through Mobile Wallet Adapter (MWA), wrapped by
@wallet-ui/react-native-kit
(or
@wallet-ui/react-native-web3js
on the legacy stack).
MWA requires a development build on Android. Expo Go will not work. If the project has no development build yet, or does not exist, start with the
solana-mobile
skill.
通过Mobile Wallet Adapter(MWA)实现钱包连接与交易签署,由
@wallet-ui/react-native-kit
封装(旧技术栈使用
@wallet-ui/react-native-web3js
)。
MWA在Android上需要开发构建版本,Expo Go无法运行。 如果项目还没有开发构建版本或尚未创建,请从
solana-mobile
技能开始。

Step 1: pick the stack — do this before writing any code

步骤1:选择技术栈——编写代码前完成此步骤

Write kit code.
@solana/kit
with
@wallet-ui/react-native-kit
is the stack to reach for, and everything in this file describes it.
There is exactly one reason to write
@solana/web3.js
instead: the project already runs on it. Check
package.json
first.
package.json
says
Do this
@wallet-ui/react-native-kit
, or no Solana client yet
Kit. This file, plus references/kit.md
@wallet-ui/react-native-web3js
is the app's Solana client
references/web3js.md
The user explicitly asked for web3.jsreferences/web3js.md, and say why kit would be better
Do not introduce web3.js into a kit project, or mix the two in one app. Their provider props, hook return values, and transaction construction all differ, so code from one silently fails on the other. If a project has no Solana client at all, that is a new build — use kit.
Adding a new wallet feature to an existing web3.js app is not a reason to migrate mid-task. Match what is there, and mention migration as a follow-up if it seems worth it.
编写Kit代码。
@solana/kit
搭配
@wallet-ui/react-native-kit
是首选技术栈,本文档所有内容均围绕该栈展开。
只有一种情况需要使用
@solana/web3.js
:项目已基于该库运行。请先查看
package.json
package.json
内容
操作建议
包含
@wallet-ui/react-native-kit
,或无Solana客户端
使用Kit。参考本文档及references/kit.md
应用的Solana客户端为
@wallet-ui/react-native-web3js
参考references/web3js.md
用户明确要求使用web3.js参考references/web3js.md,并说明使用Kit的优势
不要在Kit项目中引入web3.js,也不要在同一应用中混合使用两者。它们的提供者属性、Hook返回值和交易构造逻辑均不同,因此基于其中一个栈编写的代码在另一个栈中会静默失败。如果项目完全没有Solana客户端,属于全新构建——请使用Kit。
在现有web3.js应用中添加新钱包功能,不是中途迁移的理由。匹配现有技术栈,若值得迁移可在后续提及。

Step 2: confirm the provider is mounted

步骤2:确认提供者已挂载

useMobileWallet
returns empty state without
MobileWalletProvider
above it. Look for it in the root layout or an app-providers module.
Build the cluster with the
createSolana*
helpers rather than by hand — a
SolanaCluster
also needs a
label
, which the helpers fill in:
tsx
import {
  type AppIdentity,
  createSolanaDevnet,
  MobileWalletProvider,
  type SolanaCluster,
} from '@wallet-ui/react-native-kit'

const identity: AppIdentity = { name: 'My App' }
const cluster: SolanaCluster = createSolanaDevnet({ url: 'https://api.devnet.solana.com' })

<MobileWalletProvider cluster={cluster} identity={identity}>
  {children}
</MobileWalletProvider>
createSolanaDevnet
,
createSolanaTestnet
, and
createSolanaLocalnet
take optional props;
createSolanaMainnet
requires a
url
, since there is no sensible public default for mainnet.
The provider props are
cluster
,
identity
, and optional
cache
,
createClient
,
children
. There is no
chain
prop and no
endpoint
prop
— passing those does nothing.
Every
AppIdentity
field is optional.
name
alone is enough to get started; add
uri
as a real deep link for anything shipping, since wallets display it during authorization and a placeholder can read as a phishing attempt.
Put
QueryClientProvider
from
@tanstack/react-query
above the wallet provider — the hook patterns below are queries and mutations.
若没有在上方挂载
MobileWalletProvider
useMobileWallet
会返回空状态。请在根布局或应用提供者模块中查找它。
使用
createSolana*
辅助函数构建集群,而非手动创建——
SolanaCluster
还需要一个
label
,辅助函数会自动填充该字段:
tsx
import {
  type AppIdentity,
  createSolanaDevnet,
  MobileWalletProvider,
  type SolanaCluster,
} from '@wallet-ui/react-native-kit'

const identity: AppIdentity = { name: 'My App' }
const cluster: SolanaCluster = createSolanaDevnet({ url: 'https://api.devnet.solana.com' })

<MobileWalletProvider cluster={cluster} identity={identity}>
  {children}
</MobileWalletProvider>
createSolanaDevnet
createSolanaTestnet
createSolanaLocalnet
接受可选参数;
createSolanaMainnet
需要传入
url
,因为主网没有合理的公共默认值。
提供者的属性包括
cluster
identity
,以及可选的
cache
createClient
children
没有
chain
属性和
endpoint
属性
——传入这些属性不会产生任何效果。
AppIdentity
的所有字段都是可选的。仅
name
即可启动开发;对于即将发布的应用,请添加真实的深层链接
uri
,因为钱包在授权过程中会显示该链接,占位符可能会被视为钓鱼行为。
@tanstack/react-query
QueryClientProvider
放在钱包提供者上方——下面的Hook模式属于查询和突变操作。

Step 3: use the hook

步骤3:使用Hook

tsx
import { useMobileWallet } from '@wallet-ui/react-native-kit'

const { account, connect, disconnect, client } = useMobileWallet()
What the hook actually returns on the kit stack:
ValueTypeNotes
account
Account | undefined
undefined
when disconnected, not
null
accounts
Account[] | null
All authorized accounts
connect
() => Promise<Account>
Opens the wallet picker
disconnect
() => Promise<void>
client
Client
Kit client — use
client.rpc
for RPC calls
chain
SolanaClusterId
The active cluster. Put it in query keys
sendTransactions
(instructions: Instruction[]) => Promise<string>
Simplest send path
signAndSendTransaction
(tx, minContextSlot) => Promise<SignatureBytes>
Note the second argument
signTransaction
(tx) => Promise<Transaction>
Sign without broadcasting
signMessages
(msg: Uint8Array) => Promise<Uint8Array>
signIn
(payload) => Promise<SignInOutput>
Sign-in with Solana; can also connect
identity
,
store
Config and authorization store
Singular aliases exist for several of these —
sendTransaction
,
signMessage
,
signTransactions
— with the same signatures. The templates use the plural forms; either works, so follow whatever the project already uses.
Four things that trip people up:
  1. There is no
    connected
    boolean.
    Derive it:
    const connected = !!account
    .
  2. signAndSendTransaction
    takes
    minContextSlot
    as a second argument.
    Calling it with only a transaction fails. Get the slot from
    getLatestBlockhash
    , or use
    sendTransactions(instructions)
    , which handles this for you.
  3. The kit hook exposes
    client
    , not
    connection
    .
    connection
    only exists on the web3.js stack.
  4. Include
    chain
    in every React Query key that holds chain data.
    Otherwise switching cluster serves the previous network's cached balances, which looks like a wallet bug.
account.address
is a kit
Address
(a branded string), so it interpolates into text directly.
account.label
is the wallet-supplied name and may be undefined.
tsx
import { useMobileWallet } from '@wallet-ui/react-native-kit'

const { account, connect, disconnect, client } = useMobileWallet()
在Kit栈中,该Hook实际返回的内容:
返回值类型说明
account
Account | undefined
断开连接时为
undefined
,而非
null
accounts
Account[] | null
所有已授权账户
connect
() => Promise<Account>
打开钱包选择器
disconnect
() => Promise<void>
断开连接
client
Client
Kit客户端——使用
client.rpc
进行RPC调用
chain
SolanaClusterId
当前活跃集群。请将其加入查询键
sendTransactions
(instructions: Instruction[]) => Promise<string>
最简单的交易发送方式
signAndSendTransaction
(tx, minContextSlot) => Promise<SignatureBytes>
注意第二个参数
signTransaction
(tx) => Promise<Transaction>
签署但不广播交易
signMessages
(msg: Uint8Array) => Promise<Uint8Array>
签署消息
signIn
(payload) => Promise<SignInOutput>
Solana登录;也可用于连接钱包
identity
,
store
配置和授权存储
其中部分返回值存在单数别名——
sendTransaction
signMessage
signTransactions
——签名相同。模板使用复数形式;两种形式均可,请遵循项目现有规范。
容易出错的四点:
  1. 没有
    connected
    布尔值
    。需自行推导:
    const connected = !!account
  2. signAndSendTransaction
    的第二个参数是
    minContextSlot
    。仅传入交易调用会失败。从
    getLatestBlockhash
    获取插槽,或使用
    sendTransactions(instructions)
    ,该方法会自动处理此问题。
  3. Kit Hook暴露的是
    client
    ,而非
    connection
    connection
    仅存在于web3.js栈中。
  4. 所有包含链数据的React Query键中都要包含
    chain
    。否则切换集群时会显示之前网络的缓存余额,看起来像是钱包BUG。
account.address
是Kit的
Address
类型(带标记的字符串),可直接插入文本。
account.label
是钱包提供的名称,可能为undefined。

Connect and disconnect

连接与断开连接

tsx
import { Pressable, Text } from 'react-native'
import { useMobileWallet } from '@wallet-ui/react-native-kit'

export function ConnectButton() {
  const { account, connect, disconnect } = useMobileWallet()

  async function onPress() {
    try {
      if (account) await disconnect()
      else await connect()
    } catch (error) {
      // The user dismissing the wallet picker lands here. Do not treat it as a crash.
      console.error(error)
    }
  }

  return (
    <Pressable onPress={onPress}>
      <Text>{account ? 'Disconnect' : 'Connect Wallet'}</Text>
    </Pressable>
  )
}
Always wrap
connect()
in try/catch — cancelling the wallet picker rejects the promise. Cancellation is a normal outcome, not an error state worth alarming the user about; see references/kit.md for how to tell cancellation apart from real failures.
Authorization is cached, so the app reconnects on restart without a new prompt.
tsx
import { Pressable, Text } from 'react-native'
import { useMobileWallet } from '@wallet-ui/react-native-kit'

export function ConnectButton() {
  const { account, connect, disconnect } = useMobileWallet()

  async function onPress() {
    try {
      if (account) await disconnect()
      else await connect()
    } catch (error) {
      // 用户关闭钱包选择器会进入此处。不要将其视为崩溃。
      console.error(error)
    }
  }

  return (
    <Pressable onPress={onPress}>
      <Text>{account ? 'Disconnect' : 'Connect Wallet'}</Text>
    </Pressable>
  )
}
始终将
connect()
包裹在try/catch中——取消钱包选择器会拒绝Promise。取消是正常结果,无需提醒用户;如需区分取消与真实失败,请参考references/kit.md
授权信息会被缓存,因此应用重启时无需重新提示即可重新连接。

Read chain data

读取链上数据

Use
client
from the hook. There is no need to build a client of your own:
tsx
import type { Address } from '@solana/kit'
import { useQuery } from '@tanstack/react-query'
import { useMobileWallet } from '@wallet-ui/react-native-kit'

export function useGetBalance({ address }: { address: Address }) {
  const { chain, client } = useMobileWallet()

  return useQuery({
    queryFn: () => client.rpc.getBalance(address).send(),
    queryKey: ['get-balance', chain, address],
  })
}
Kit RPC calls are lazy:
client.rpc.someMethod(...)
builds a request and
.send()
runs it. Forget
.send()
and nothing errors — the data simply never arrives.
Balances come back as
bigint
lamports. Convert deliberately, and never with
parseFloat
:
ts
export function lamportsToSol(lamports: bigint) {
  return Number(lamports) / 1e9
}
使用Hook返回的
client
,无需自行构建客户端:
tsx
import type { Address } from '@solana/kit'
import { useQuery } from '@tanstack/react-query'
import { useMobileWallet } from '@wallet-ui/react-native-kit'

export function useGetBalance({ address }: { address: Address }) {
  const { chain, client } = useMobileWallet()

  return useQuery({
    queryFn: () => client.rpc.getBalance(address).send(),
    queryKey: ['get-balance', chain, address],
  })
}
Kit的RPC调用是惰性的:
client.rpc.someMethod(...)
构建请求,
.send()
执行请求。忘记
.send()
不会报错——只是永远不会返回数据。
余额以
bigint
类型的lamports返回。请谨慎转换,切勿使用
parseFloat
ts
export function lamportsToSol(lamports: bigint) {
  return Number(lamports) / 1e9
}

Send a transaction

发送交易

Build instructions and hand them over. This covers most cases:
tsx
import { getAddMemoInstruction } from '@solana-program/memo'
import type { Instruction } from '@solana/kit'

const { sendTransactions } = useMobileWallet()

const instructions: Instruction[] = [getAddMemoInstruction({ memo: 'gm' })]
const signature = await sendTransactions(instructions)
sendTransactions
handles blockhash,
minContextSlot
, fee payer, and signature decoding.
Reach for the explicit
pipe
form only when you need fee-payer control, a specific blockhash lifetime, or a fee pre-check. Full worked example, with the balance-versus-fee assertion and signature decoding: references/kit.md.
构建指令并传入即可,这适用于大多数场景:
tsx
import { getAddMemoInstruction } from '@solana-program/memo'
import type { Instruction } from '@solana/kit'

const { sendTransactions } = useMobileWallet()

const instructions: Instruction[] = [getAddMemoInstruction({ memo: 'gm' })]
const signature = await sendTransactions(instructions)
sendTransactions
会处理区块哈希、
minContextSlot
、付费方和签名解码。
仅当需要控制付费方、特定区块哈希有效期或预检查手续费时,才使用显式的
pipe
形式。完整示例(包含余额与手续费断言及签名解码):references/kit.md

Reference material

参考资料

  • references/kit.md — kit stack: clusters and config, reading chain data, transactions, sign-in with Solana, message signing, error handling
  • references/web3js.md — legacy
    @solana/web3.js
    stack, and a migration sketch
  • references/troubleshooting.md — connection and signing failures with known causes
When something here is ambiguous, read the template. The patterns in this skill follow
expo-kit-minimal
, which is a complete working app and stays current in a way prose does not:
bash
npx solana-mobile@latest create /tmp/reference-app --template expo-kit-minimal --skip-install
  • references/kit.md — Kit栈:集群与配置、读取链上数据、交易、Solana登录、消息签署、错误处理
  • references/web3js.md — 旧版
    @solana/web3.js
    栈,及迁移概述
  • references/troubleshooting.md — 已知原因导致的连接与签署失败问题
若本文档内容存在歧义,请参考模板。本技能中的模式遵循
expo-kit-minimal
,这是一个完整的可运行应用,且会持续更新,比文字文档更及时:
bash
npx solana-mobile@latest create /tmp/reference-app --template expo-kit-minimal --skip-install

Related skills

相关技能

  • solana-mobile
    — project setup, templates, emulators, development builds
  • integration-privy
    — add Privy accounts and sessions on top of this wallet connection
  • seeker-genesis-token
    — verify Seeker device ownership after connecting
  • seeker-domains
    — display
    .skr
    names instead of raw addresses
  • solana-mobile
    — 项目搭建、模板、模拟器、开发构建版本
  • integration-privy
    — 在此钱包连接基础上添加Privy账户与会话
  • seeker-genesis-token
    — 连接后验证Seeker设备所有权
  • seeker-domains
    — 显示
    .skr
    域名而非原始地址

Links

链接