What it does
Latch owns wallet selection and the request for approval of an exact transaction. Use it to manage a device's test wallets, connect a compatible wallet provider or obtain a signed devnet payment after policy review.
Keep wallet management, wallet connection, transaction approval and submission separate in the UI. A selected address, unlocked wallet or ready status is not approval. The built-in devnet signing paths return signed bytes without submitting; an external provider can declare that it already submitted, which the coordinator must respect.
Use independently
Select uk.capp.sidecar.solana-latch. Start with device-wallet for explicitly persisted devnet test wallets, or demo-confirm for a simulation without keys. The owning API is window.capp.solana.latch; use the portable client only from your application's trusted owner context.
Public wallet lists/status do not include secrets. Import/export/recovery are different, sensitive operations: do not route their arguments or results through generic Agent logs, analytics, activity capture or documentation screenshots.
Provider dependencies
| Mode | Requirements | Persistence and authority |
|---|---|---|
demo-confirm | A confirmation-capable app window | Simulated approval only; no real wallet or signature. |
devnet-test-wallet | Secure-context cryptography; Conduit for signing checks | Temporary in-memory devnet key, lost on reload. |
device-wallet | App storage/cryptography; matching Conduit for signing | Saved wallet manager; built-in payment signing remains devnet-only. |
native-mwa | A compatible provider actually installed by the host | A declared mode is not an implemented/available native handoff. |
external-adapter | Qualified HTTPS authority provider | Must return the exact operation/attempt/wallet/message/policy binding. |
Wallet Standard discovery is part of the device-wallet manager. It requires a provider exposed in the owning browser/host with compatible connect/sign features and network/account support. A wallet app installed on Android is not automatically a Wallet Standard provider inside a WebView, and Wallet Standard discovery is not native MWA.
First successful wallet workflow
- Select
device-wallet, networkdevnet, then build/install the target app. Open the wallet workspace or callwalletList(). - Create or import a devnet wallet deliberately. Creation normally selects it; use
select: falseto leave an existing payer unchanged. - Choose a saved wallet explicitly with
walletSelect(). Read back the public address/network before preparing a payment. - Keep an independent private backup. An export result means a backup was requested, not that a usable copy was safely retained.
- Prepare/preflight through Edge, then ask for wallet approval of those exact bytes. Changing the active wallet after preparation is not permission to switch the payer.
connect() is not a passive query: device-wallet mode can create a devnet wallet when none is selected, and the temporary provider can generate its in-memory key. Use status()/walletList() for inspection instead. walletLock() clears all unlocked local sessions, not only a particular feature's session; do not call it as routine view cleanup.
Add a devnet wallet without changing the payer
Copy the client module beside this module. Invoke the helper from Create test wallet, not from render or automatic recovery. It does not request funds, export keys or sign a transaction.
import {createSolanaClient} from './solana-client.mjs';
const client = createSolanaClient(() => window.capp?.solana);
export async function addUnselectedDevnetWallet(label) {
const status = await client.call('latch', 'status');
if (!status.ready || status.providerMode !== 'device-wallet' || status.network !== 'devnet') {
throw new Error('Select a healthy device-wallet manager for devnet');
}
return client.call('latch', 'walletCreate', {network: 'devnet', label, select: false});
}
export async function selectSavedDevnetWallet(walletId) {
const listing = await client.call('latch', 'walletList');
const row = listing.wallets.find(wallet => wallet.walletId === walletId && wallet.network === 'devnet');
if (!row) throw new Error('Choose a saved devnet wallet from this app');
return client.call('latch', 'walletSelect', {walletId: row.walletId});
}Expect a public walletId, address, label, network and protection/selection fields. Do not assume the new wallet is selected or funded. Disable the creation control while its promise is pending: a second successful create is another wallet, not an idempotent replay. The manager supports up to 32 saved wallets.
Exact signing and token policy
Direct authorise() requires operation/attempt IDs, network, wallet public key, policy digest, message digest and the matching messageBytesBase64 for live work. Prefer Edge to carry these bindings correctly. Its returned signed transaction is not permission to change the message.
The device/temporary paths decode the supported transfer, check the current wallet, devnet/blockhash and local amount policy, and request confirmation. Original SPL-token transfers also need their own per-mint atomic limit; an empty token-limit list disables token signing. Match Sentinel's limits, asset identity, account/rent and exact-byte policy rather than relying only on a symbol or a declared allowed flag.
A generic Latch response filters provider fields and checks bindings; Edge additionally verifies the actual returned transaction signature/bytes. Standalone integrations need an equivalent exact signature-verification boundary. Treat submitted: true from a qualified provider as an already-performed effect to observe, not a reason to send again.
Scope and limitations
Devnet device keys are stored unencrypted. Mainnet management requires password encryption and a prominent loss warning; mainnet signing is disabled in this built-in recipe. The older temporary provider loses its key on reload. This alpha is not an audited wallet, hardware wallet or protection against hostile JavaScript running inside the owning app.
Mainnet create/import needs the build opt-in, explicit loss acknowledgement, user confirmation and a valid password. Encrypted local sessions are bounded and can lock when the app is hidden. A password-protected backup remains private; an unencrypted keypair export gives its holder spending authority.
External provider IDs are runtime discovery handles, not durable wallet identities. Refresh the list after reload. walletConnectExternal() can return chooseAccount: true with a public account list; make the user choose before treating a session as connected. Disconnecting drops local sessions and requests the provider's optional disconnect; it is not proof of universal revocation elsewhere.
Recover without erasing history
On damaged wallet storage, stop ordinary wallet writes. walletRecoveryStatus() reports the bounded damaged-store digest when recovery is possible. walletRecover() requires that digest, an independent supported backup, matching address and explicit acknowledgement/confirmation.
Recovery preserves the damaged bytes before replacing the wallet store and restores one wallet, not every wallet the store once held. Other wallets require their own backups. Payment and signing-attempt history is left intact. For a healthy store, use ordinary import instead of damaged-store recovery. A read/storage permission error is not evidence that the stored keys are corrupt.
Application API
The owning interface is window.capp.solana.latch. The portable client calls the same methods. This table describes main arguments, not a stricter typed guarantee than the current provider implements. Exact transaction/provider bindings must be preserved; unsupported state or arguments are rejected.
import {createSolanaClient} from './solana-client.mjs';
const client = createSolanaClient(() => window.capp?.solana);
const status = await client.call('latch', 'status');status
client.call('latch', 'status', input)
Main input: No arguments required.
Read wallet-provider readiness and public signing policy when supported. Does not return secret keys.
authorise
client.call('latch', 'authorise', input)
Main input: operationId, attemptId, network, walletPubkey, policyDigest, messageDigest, messageBytesBase64.
Ask the selected wallet provider for approval. The built-in device signing recipe is devnet-only. Prefer Edge to carry the reviewed binding.
connect
client.call('latch', 'connect', input)
Main input: No arguments for the built-in device/temporary connection; other provider input is provider-specific.
Connect the configured wallet provider. Native MWA is a host implementation boundary, not guaranteed by this method name.
walletList
client.call('latch', 'walletList', input)
Main input: No arguments required.
List public wallet metadata, selected wallets and compatible external-provider descriptors.
walletCreate
client.call('latch', 'walletCreate', input)
Main input: network; optional label, select, password, acknowledgeMainnetLoss.
Create a device wallet. select:false preserves the current payer. Mainnet management requires explicit permission, consent and password protection; it does not enable mainnet signing.
walletImport
client.call('latch', 'walletImport', input)
Main input: network; backup or privateKey; optional label, password and mainnet consent.
Import a supported backup, 64-byte Solana keypair array or base58 private key. Recovery phrases are not supported. Keep this material out of logs and public examples.
walletSelect
client.call('latch', 'walletSelect', input)
Main input: walletId.
Select a saved wallet for its network. This does not authorize a transaction.
walletRename
client.call('latch', 'walletRename', input)
Main input: walletId, label.
Change the local wallet label, not the public key or network.
walletRemove
client.call('latch', 'walletRemove', input)
Main input: walletId, confirmAddress matching the wallet public key.
Remove a local wallet only through its explicit confirmation and backup-warning path. Do not treat this as ordinary error recovery.
walletUnlock
client.call('latch', 'walletUnlock', input)
Main input: walletId, password.
Unlock a password-protected local wallet for the bounded session.
walletLock
client.call('latch', 'walletLock')
Main input: No arguments required.
Clear every unlocked local wallet session in the owning runtime; returns {locked: true}.
walletExport
client.call('latch', 'walletExport', input)
Main input: walletId; optional format: backup or keypair; password and mainnet consent when required.
Request explicit backup/export confirmation. The result may contain private material; a returned export does not prove a backup was retained.
walletConnectExternal
client.call('latch', 'walletConnectExternal', input)
Main input: network, providerId; optional walletPubkey and mainnet consent.
Connect a discovered Wallet Standard provider. May return chooseAccount and a public account list instead of a selected account.
walletDisconnectExternal
client.call('latch', 'walletDisconnectExternal', input)
Main input: network.
Drop local sessions for the selected provider and request its optional disconnect operation.
walletRecoveryStatus
client.call('latch', 'walletRecoveryStatus', input)
Main input: No arguments required.
Report healthy/damaged state and the damaged-store digest when bounded recovery is available.
walletRecover
client.call('latch', 'walletRecover', input)
Main input: network, backup, storeDigest, confirmAddress, acknowledgeRecovery:true; password where required.
Restore one independently backed-up wallet after explicit confirmation. Preserve damaged bytes before replacement; other wallets need separate backups.
Troubleshooting
| Symptom or error | Next step |
|---|---|
CAPP_SOLANA_MWA_UNAVAILABLE | This host does not expose the required native provider. Do not advertise a completed wallet handoff. |
| No Wallet Standard providers | Use a supported owning browser/host and network; an installed wallet app alone is insufficient. |
WALLET after review | Select the exact payer used to prepare the message; otherwise create a new reviewed operation. |
LIMIT / token policy rejection | Inspect exact mint/amount and configured ceilings without silently increasing them. |
EXPIRED | Reconcile first; only a provably unsent expired Edge review may be refreshed. |
WALLET_STORAGE / RECOVERY_* | Preserve app data and use the explicit recovery flow with an independent backup. |
DUPLICATE on import | Use the already-saved wallet; keep devnet and mainnet keys separate. |
Verify your integration
Test create-without-select, explicit selection, rejected confirmation, wrong active wallet, reload, absent external provider and storage failure. Ensure list/status never expose keys and export paths never enter activity logs. Qualify native/provider signing separately on the actual installed target; a simulation does not establish it.
Integration recipes
Composition · Devnet payment · Recovery · Typed client · Schemas
Related data contracts
These schemas describe payload shape, not evidence of approval or successful execution. Match the version in your payload; older versions remain listed for compatibility.
- capp.solana-latch.authority.v2.schema.json
- capp.solana-authority-request.v1.schema.json
- capp.solana-signing-policy.v1.schema.json
Configuration reference
These are the selected package's configuration fields. String credential values and installation-specific defaults are never published here. A field's presence does not bypass its runtime policy.
| Field | Type | Meaning and constraints |
|---|---|---|
providerMode | select | Select the implementation this build will use. Configuring an endpoint does not verify its connection or behavior.. Choices: demo-confirm, native-mwa, external-adapter, devnet-test-wallet, device-wallet |
network | select | Must match the live provider and requests. Arena and demo confirmation always use simulation.. Choices: devnet, testnet, mainnet-beta, demo |
externalAdapterUrl | text | Absolute HTTPS endpoint, or loopback HTTP for development. Keep private credentials on the backend; packaged configuration is visible to users. |
testMaxLamports | number | Devnet only. 1 SOL = 1,000,000,000 lamports; a wallet confirmation is always required.. Bounds: 1 to 1000000000 |
allowMainnetWallets | boolean | Enables create/import, receive, balance and backup for real-fund wallets after an explicit loss warning. Password encryption required. Does not enable mainnet signing or RPC writes. |
tokenLimits | textarea | Original Token Program only. Add each allowed mint and its maximum atomic amount as a string. Empty means token signing is disabled. Configure matching limits in Sentinel and Latch; token symbols do not identify an asset. |
Agent interfaces
These names are declared by the package for host/Agent integration, not JavaScript methods to invoke directly. The host must register a tool, grant its scope and enforce its approval policy. A manifest declaration alone does not prove availability in every Agent catalogue. Prefer the application API above when integrating your own app.
| Tool | Declared risk | Approval requirement |
|---|---|---|
solana.latch.status | low | mode-policy |