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
@wallet-ui/react-native-web3js
是项目的Solana客户端
参考references/web3js.md
用户明确要求使用web3.js参考references/web3js.md,并说明使用Kit技术栈的优势
不要在Kit项目中引入web3.js,也不要在同一应用中混合使用两种技术栈。它们的提供者属性、钩子返回值和交易构造逻辑均不同,因此一种技术栈的代码在另一种中会静默失败。如果项目完全没有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
放在钱包提供者的上级——以下的钩子模式均基于查询和突变实现。

Step 3: use the hook

步骤3:使用钩子

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技术栈中,该钩子实际返回的内容如下:
返回值类型说明
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钩子暴露的是
    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
}
使用钩子返回的
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

链接