Advanced wallet operations
These operations change access or disclose key material. Start each one from a fresh user action, keep progress visible, and retain the returned identity or receipt for your audit trail.
Prerequisites
Complete wallet setup and authentication and obtain an active wallet session. Device linking also needs a camera or a way to deliver the QR payload between devices.
Link another device
Device 2 starts a short-lived session and displays its QR code. Device 1 scans that code and approves the request with fresh authentication.
tsx
import { useEffect, useState } from 'react';
import { QRScanMode, useDeviceLinking, useSeams } from '@seams/sdk/react';
import type { LinkDeviceFlowEvent, QrLinkedDeviceSessionPayloadV4 } from '@seams/sdk';
function logLinkEvent(event: LinkDeviceFlowEvent): void {
console.log(event.phase, event.status, event.message);
}
export function NewDeviceLinkCode() {
const { startDevice2LinkingFlow, cancelDeviceLinking } = useSeams();
const [qrCodeDataURL, setQrCodeDataURL] = useState<string | null>(null);
const onStart = async (): Promise<void> => {
const link = await startDevice2LinkingFlow({ ui: 'inline' });
setQrCodeDataURL(link.qrCodeDataURL);
};
useEffect(() => {
return () => {
void cancelDeviceLinking();
};
}, [cancelDeviceLinking]);
return (
<>
<button onClick={() => void onStart()}>Show link code</button>
{qrCodeDataURL ? <img src={qrCodeDataURL} alt="Device link QR code" /> : null}
</>
);
}
export function ApproveLinkedDevice(props: { qrData: QrLinkedDeviceSessionPayloadV4 }) {
const { linkDevice } = useDeviceLinking({
onEvent: logLinkEvent,
onError: (error) => console.error('Device link failed', error),
});
return (
<button onClick={() => void linkDevice(props.qrData, QRScanMode.CAMERA)}>Approve device</button>
);
}NewDeviceLinkCode cancels the linking session when it unmounts. Keep that cleanup, expire abandoned QR sessions, and show the device name after success so it can be recognized and revoked later.
Recover a wallet account
Call recovery synchronization with the wallet id from the account record.
ts
import type { SeamsWeb } from '@seams/sdk';
type SyncAccountResult = Awaited<ReturnType<SeamsWeb['recovery']['syncAccount']>>;
export async function recoverWalletAccount(
seams: SeamsWeb,
walletId: string,
): Promise<SyncAccountResult> {
const result = await seams.recovery.syncAccount({ walletId });
if (!result.success) {
throw new Error(result.error);
}
console.log('wallet account restored', result.walletId, result.nearAccountId);
return result;
}The successful result exposes the restored wallet id and NEAR account id. Use the returned values to refresh app state before rendering signing controls.
Export an Ed25519 or ECDSA key
Resolve the exact export lane first, then open the wallet-origin export viewer.
ts
import type { KeyExportFlowEvent, SeamsWeb } from '@seams/sdk';
import {
nearAccountRefFromAccountId,
thresholdEcdsaChainTargetFromConfig,
walletSessionRefFromSession,
} from '@seams/sdk/advanced';
function logExportEvent(event: KeyExportFlowEvent): void {
console.log(event.phase, event.status, event.message);
}
export async function exportNearKey(
seams: SeamsWeb,
walletId: string,
nearAccountId: string,
): Promise<void> {
const walletSession = walletSessionRefFromSession({
walletId,
walletSessionUserId: walletId,
});
const nearAccount = nearAccountRefFromAccountId(nearAccountId);
const lane = await seams.keys.resolveExactKeyExportLane({
kind: 'ed25519',
walletSession,
nearAccount,
});
if (lane.kind !== 'ed25519') {
throw new Error(`Expected an Ed25519 export lane, received ${lane.kind}`);
}
await seams.keys.exportKeypairWithUI({
kind: 'ed25519',
walletSession,
nearAccount,
laneIdentity: lane.laneIdentity,
materialActivation: lane.materialActivation,
options: { onEvent: logExportEvent },
});
}
export async function exportEvmKey(seams: SeamsWeb, walletId: string): Promise<void> {
const walletSession = walletSessionRefFromSession({
walletId,
walletSessionUserId: walletId,
});
const chainTarget = thresholdEcdsaChainTargetFromConfig({
network: 'tempo-testnet',
rpcUrl: 'https://rpc.moderato.tempo.xyz',
explorerUrl: 'https://explore.testnet.tempo.xyz',
chainId: 42431,
});
const lane = await seams.keys.resolveExactKeyExportLane({
kind: 'ecdsa',
walletSession,
chainTarget,
});
if (lane.kind !== 'ecdsa') {
throw new Error(`Expected an ECDSA export lane, received ${lane.kind}`);
}
await seams.keys.exportKeypairWithUI({
kind: 'ecdsa',
walletSession,
chainTarget,
laneIdentity: lane.laneIdentity,
options: { onEvent: logExportEvent },
});
}exportNearKey resolves an Ed25519 lane with a NEAR account and material activation. exportEvmKey resolves an ECDSA lane for the configured chain target. Both flows receive progress events through KeyExportFlowEvent.
Expected result
- Device linking creates a separate device credential and lane.
- Recovery synchronization returns the restored wallet and NEAR identity.
- Export opens a protected viewer after the exact lane is authorized.
Recoverable failures
- A cancelled or expired QR session must be started again. Do not approve an old QR payload.
- Recovery can return a failure result. Keep the existing account state until synchronization succeeds.
- Lane resolution can return a different key kind than the requested one. The export helpers stop instead of opening the wrong viewer.
- Export authorization and viewer errors should end the current disclosure attempt. Ask for fresh authentication before another export.
Read next: linked devices, recovery, export, and rotation, or results and errors.