Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a636023
feat: wire up GridPlus SafeCard wallet UID validation
gomesalexandre Nov 26, 2025
0cd71ce
fix: properly destructure wallet from pairConnectedDevice result
gomesalexandre Nov 26, 2025
9c9b3e7
fix: capture wallet UID for all GridPlus SafeCard flows
gomesalexandre Nov 26, 2025
334cd26
feat: add UI enhancements for GridPlus SafeCards
gomesalexandre Nov 26, 2025
ba339c9
feat(gridplus): add SafeCard validation for improved UX
gomesalexandre Nov 26, 2025
0f6dd54
fix: update web to use new hdwallet adapter API
gomesalexandre Nov 26, 2025
91b4a59
refactor: rename walletUid to activeWalletId for consistency
gomesalexandre Nov 26, 2025
2225d12
chore: update hdwallet packages to .13
gomesalexandre Nov 26, 2025
5c107c0
fix: remove duplicate connectDevice() call in pairConnectedDevice
gomesalexandre Nov 26, 2025
c454ac6
chore: update hdwallet packages to .14
gomesalexandre Nov 26, 2025
fcc7785
fix: restore physicalDeviceId storage for Add New and Connect buttons
gomesalexandre Nov 26, 2025
a3302da
feat: display specific error message for SafeCard mismatch
gomesalexandre Nov 26, 2025
969a2e2
refactor: update SafeCard tag display to use consistent color
gomesalexandre Nov 26, 2025
864fcce
feat: add context-aware error messages for SafeCard validation
gomesalexandre Nov 26, 2025
bdfea04
feat: wire up expectedType parameter through reconnection flow
gomesalexandre Nov 26, 2025
9238b52
chore: cleanup - remove comments and console.errors
gomesalexandre Nov 26, 2025
a385cfb
[skip ci] refactor: remove password argument from connectDevice call
gomesalexandre Nov 26, 2025
c3400c1
[skip ci] chore: move hardcoded text to translations and cleanup
gomesalexandre Nov 26, 2025
40aea87
[skip ci] fix: store physicalDeviceId before wallet check
gomesalexandre Nov 27, 2025
2c7216a
[skip ci] chore: simplify undefined checks to boolean
gomesalexandre Nov 27, 2025
f027dfc
[skip ci] chore: remove unnecessary method existence check
gomesalexandre Nov 27, 2025
2eea5a4
Merge remote-tracking branch 'origin/develop' into feat_gridplus_vali…
gomesalexandre Nov 27, 2025
11675f3
[skip ci] chore: merge develop
gomesalexandre Nov 27, 2025
f3af080
[skip ci] chore: remove unnecessary braces from single-line return
gomesalexandre Nov 27, 2025
566e685
Merge branch 'develop' into feat_gridplus_validation
gomesalexandre Nov 28, 2025
7c92ff4
feat: bump
gomesalexandre Nov 28, 2025
07bd2c8
chore: trigger CI
gomesalexandre Nov 28, 2025
ba5dca5
fix: lint
gomesalexandre Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .yarnrc.yml
Comment thread
gomesalexandre marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
nodeLinker: node-modules

npmRegistryServer: "https://registry.npmjs.org"
npmAuthToken: "${NPM_AUTH-fallback}"

npmRegistryServer: "http://127.0.0.1:4873"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
Expand All @@ -15,4 +17,3 @@ unsafeHttpWhitelist:
- 127.0.0.1

yarnPath: .yarn/releases/yarn-3.5.0.cjs
npmAuthToken: ${NPM_AUTH-fallback}
60 changes: 60 additions & 0 deletions gridplus-safecard-validation.md
Comment thread
gomesalexandre marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# GridPlus SafeCard Validation

## Problem

Previously, ShapeShift operated on a "trust me bro" basis when handling GridPlus SafeCards. The app would:
- Store SafeCards using app-generated UUIDs only
- Never validate which physical SafeCard was actually inserted
- Allow users to connect the wrong SafeCard without detection
- Proceed with signing transactions without verifying the correct card was present

This could lead to confusing UX where users might accidentally use the wrong wallet or end up in weird app states.

## Solution

Implement hardware-based wallet UID validation at three critical points:

### 1. Initial Pairing
When a user first pairs a SafeCard, we now:
- Fetch the hardware wallet UID from the device (32-byte identifier from firmware)
- Store this UID alongside the user's SafeCard in Redux state
- Use this UID as the source of truth for that SafeCard

### 2. Reconnection
When the app loads and finds a GridPlus wallet from a previous session, we now:
- Validate that the inserted SafeCard's UID matches the expected stored UID
- Reject the connection if the wrong SafeCard is inserted
- Provide clear error messages to guide users to insert the correct card

### 3. Just-In-Time (JIT) Signing Validation
Before every signing operation (transaction, message, typed data), we now:
- Re-validate that the correct SafeCard is still inserted
- Prevent signing if the SafeCard was swapped after connection
- Support all chains: Ethereum, Bitcoin, Solana, Cosmos, Thorchain, Mayachain

## Implementation

### hdwallet Package Changes (shapeshift/hdwallet)
Added to `GridPlusHDWallet` class:
- `setExpectedWalletUid(walletUid: string)`: Configure expected UID for validation
- `validateActiveWallet(expectedUid?: string)`: Fetch current wallet UID and validate
- Private `validateBeforeSigning()`: Called before all signing methods

### Web Changes (shapeshift/web)
- **WalletProvider reconnection**: Validates SafeCard on app load
- **finalizeWalletSetup**: Sets expected UID after connection for JIT validation
- **GridPlus utils**: Wire up validation through connection flow
- **Redux state**: Store `walletUid` and `isExternal` flag per SafeCard

## User Experience

Users will now see clear feedback when:
- They try to reconnect with the wrong SafeCard inserted
- They swap SafeCards after connecting but before signing
- Legacy SafeCards without stored UIDs (requires cache clear to use)

This ensures the app state always matches the physical hardware, preventing confusion and improving overall UX.

## Migration

No data migration provided. Users with existing SafeCards will need to clear their cache and re-pair their devices. This is a one-time operation that establishes the UID mapping for future sessions.
36 changes: 18 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,24 +100,24 @@
"@shapeshiftoss/chain-adapters": "workspace:^",
"@shapeshiftoss/contracts": "workspace:^",
"@shapeshiftoss/errors": "workspace:^",
"@shapeshiftoss/hdwallet-coinbase": "1.62.10",
"@shapeshiftoss/hdwallet-core": "1.62.10",
"@shapeshiftoss/hdwallet-gridplus": "1.62.10",
"@shapeshiftoss/hdwallet-keepkey": "1.62.10",
"@shapeshiftoss/hdwallet-keepkey-webusb": "1.62.10",
"@shapeshiftoss/hdwallet-keplr": "1.62.10",
"@shapeshiftoss/hdwallet-ledger": "1.62.10",
"@shapeshiftoss/hdwallet-ledger-webhid": "1.62.10",
"@shapeshiftoss/hdwallet-ledger-webusb": "1.62.10",
"@shapeshiftoss/hdwallet-metamask-multichain": "1.62.10",
"@shapeshiftoss/hdwallet-native": "1.62.10",
"@shapeshiftoss/hdwallet-native-vault": "1.62.10",
"@shapeshiftoss/hdwallet-phantom": "1.62.10",
"@shapeshiftoss/hdwallet-trezor": "1.62.10",
"@shapeshiftoss/hdwallet-trezor-connect": "1.62.10",
"@shapeshiftoss/hdwallet-vultisig": "1.62.10",
"@shapeshiftoss/hdwallet-walletconnect": "1.62.10",
"@shapeshiftoss/hdwallet-walletconnectv2": "1.62.10",
"@shapeshiftoss/hdwallet-coinbase": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-core": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-gridplus": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-keepkey": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-keepkey-webusb": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-keplr": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-ledger": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-ledger-webhid": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-ledger-webusb": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-metamask-multichain": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-native": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-native-vault": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-phantom": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-trezor": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-trezor-connect": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-vultisig": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-walletconnect": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/hdwallet-walletconnectv2": "1.62.13-gridplus-validation.16",
"@shapeshiftoss/swapper": "workspace:^",
"@shapeshiftoss/types": "workspace:^",
"@shapeshiftoss/unchained-client": "workspace:^",
Expand Down
18 changes: 18 additions & 0 deletions packages/chain-adapters/src/error/ErrorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ export const ErrorHandler = async (err: unknown, metadata?: ErrorMetadata): Prom
})
}

if (
err instanceof Error &&
err.message.includes('Remove inserted SafeCard to access internal GridPlus wallet')
) {
throw new ChainAdapterError(err, {
translation: 'chainAdapters.errors.gridplus.removeSafeCard',
})
}

if (
err instanceof Error &&
err.message.includes("Active SafeCard doesn't match expected SafeCard")
) {
throw new ChainAdapterError(err, {
translation: 'chainAdapters.errors.gridplus.wrongSafeCard',
})
}

if ((err as AxiosError).isAxiosError) {
const response = JSON.stringify((err as AxiosError).response?.data)
if (metadata) throw new ChainAdapterError(response, metadata)
Expand Down
9 changes: 8 additions & 1 deletion src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,9 @@
"deviceNotConnected": "Device not connected. Call connectDevice first.",
"connectFailure": "Unable to connect GridPlus wallet",
"unknown": "An unexpected error occurred communicating with GridPlus wallet"
},
"menu": {
"switchSafeCard": "Switch SafeCard"
}
},
"ledger": {
Expand Down Expand Up @@ -2518,7 +2521,11 @@
"getFeeData": "Failed to estimate network fees.",
"getPublicKey": "Failed to get public key from wallet.",
"getUtxos": "Failed to get utxos.",
"getValidator": "Failed to get validator."
"getValidator": "Failed to get validator.",
"gridplus": {
"wrongSafeCard": "Wrong SafeCard inserted. Please insert the correct SafeCard and try again.",
"removeSafeCard": "Remove inserted SafeCard to access internal GridPlus wallet."
}
}
},
"actionCenter": {
Expand Down
38 changes: 38 additions & 0 deletions src/context/WalletProvider/GridPlus/components/GridPlusMenu.tsx
Comment thread
gomesalexandre marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RepeatIcon } from '@chakra-ui/icons'
import { MenuItem } from '@chakra-ui/react'
import { useCallback } from 'react'
import { useTranslate } from 'react-polyglot'

import { ManageAccountsMenuItem } from '@/components/Layout/Header/NavBar/ManageAccountsMenuItem'
import { WalletActions } from '@/context/WalletProvider/actions'
import { useWallet } from '@/hooks/useWallet/useWallet'

const switchIcon = <RepeatIcon />

type GridPlusMenuProps = {
onClose?: () => void
}

export const GridPlusMenu = ({ onClose }: GridPlusMenuProps) => {
const translate = useTranslate()
const { dispatch } = useWallet()

const handleSwitchSafeCard = useCallback(() => {
dispatch({ type: WalletActions.RESET_STATE })
dispatch({
type: WalletActions.SET_INITIAL_ROUTE,
payload: '/gridplus/connect',
})
dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true })
onClose?.()
}, [dispatch, onClose])

return (
<>
<MenuItem icon={switchIcon} onClick={handleSwitchSafeCard}>
{translate('walletProvider.gridplus.menu.switchSafeCard')}
</MenuItem>
<ManageAccountsMenuItem onClose={onClose} />
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,21 @@ export const GridPlusPair = () => {
throw new Error(translate('walletProvider.gridplus.errors.adapterNotAvailable'))
}

const wallet = await pairConnectedDevice({
const { wallet, activeWalletId, type } = await pairConnectedDevice({
adapter,
deviceId,
pairingCode,
dispatch: appDispatch,
})

appDispatch(gridplusSlice.actions.setActiveSafeCard(safeCardUuid))

navigate('/gridplus/setup', {
state: { safeCardUuid, wallet, safeCardWalletId: `gridplus:${safeCardUuid}` },
state: {
safeCardUuid,
wallet,
safeCardWalletId: `gridplus:${safeCardUuid}`,
activeWalletId,
type,
},
})
} catch (err) {
setError((err as Error).message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type LocationState = {
defaultName?: string
needsPairing?: boolean
deviceId?: string
activeWalletId?: string
type?: 'external' | 'internal'
}

export const GridPlusSetup = () => {
Expand All @@ -32,7 +34,6 @@ export const GridPlusSetup = () => {
const appDispatch = useAppDispatch()

const physicalDeviceId = useAppSelector(gridplusSlice.selectors.selectPhysicalDeviceId)
const sessionId = useAppSelector(gridplusSlice.selectors.selectSessionId)

const state = location.state as LocationState | undefined
const safeCardUuid = state?.safeCardUuid
Expand Down Expand Up @@ -65,6 +66,9 @@ export const GridPlusSetup = () => {

try {
let finalWallet = wallet
// Use activeWalletId and type from state if available (from pairing flow)
let activeWalletId: string | undefined = state?.activeWalletId
let type: 'external' | 'internal' | undefined = state?.type

if (!finalWallet) {
const adapter = await getAdapter(KeyManager.GridPlus)
Expand All @@ -77,12 +81,13 @@ export const GridPlusSetup = () => {
if (!deviceId) {
throw new Error(translate('walletProvider.gridplus.errors.deviceIdRequired'))
}
finalWallet = await pairConnectedDevice({
const result = await pairConnectedDevice({
adapter,
deviceId,
pairingCode,
dispatch: appDispatch,
})
finalWallet = result.wallet
activeWalletId = result.activeWalletId
type = result.type
} else {
const connectionDeviceId = physicalDeviceId || deviceId || state?.deviceId
if (!connectionDeviceId) {
Expand All @@ -92,7 +97,6 @@ export const GridPlusSetup = () => {
const result = await connectAndPairDevice({
adapter,
deviceId: connectionDeviceId,
sessionId: sessionId ?? undefined,
dispatch: appDispatch,
})
finalWallet = result ?? undefined
Expand All @@ -107,13 +111,23 @@ export const GridPlusSetup = () => {
throw new Error(translate('walletProvider.gridplus.errors.walletNotAvailable'))
}

// If we don't have activeWalletId yet (e.g., when adding new SafeCard with existing wallet),
// we need to fetch it from the wallet
if (activeWalletId === undefined || type === undefined) {
const validation = await finalWallet.validateActiveWallet()
activeWalletId = validation.activeWalletId
type = validation.type
}

const safeCardWalletId = state?.safeCardWalletId || `gridplus:${safeCardUuid}`
const finalSafeCardName = safeCardName.trim() || defaultName

appDispatch(
gridplusSlice.actions.addSafeCard({
id: safeCardUuid,
name: finalSafeCardName,
activeWalletId,
type,
}),
)

Expand Down Expand Up @@ -163,8 +177,9 @@ export const GridPlusSetup = () => {
defaultName,
state?.safeCardWalletId,
state?.deviceId,
state?.activeWalletId,
state?.type,
physicalDeviceId,
sessionId,
translate,
getAdapter,
walletDispatch,
Expand Down
21 changes: 19 additions & 2 deletions src/context/WalletProvider/GridPlus/components/SafeCardRow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Box, Button, ButtonGroup, HStack, IconButton, Input, Text, VStack } from '@chakra-ui/react'
import {
Box,
Button,
ButtonGroup,
HStack,
IconButton,
Input,
Tag,
Text,
VStack,
} from '@chakra-ui/react'
import { memo } from 'react'
import { FaWallet } from 'react-icons/fa'
import { IoMdCreate, IoMdTrash } from 'react-icons/io'
Expand Down Expand Up @@ -68,7 +78,14 @@ export const SafeCardRow = memo(
/>
) : (
<VStack align='start' flex={1} spacing={0}>
<Text fontWeight='medium'>{safeCard.name}</Text>
<HStack spacing={2}>
<Text fontWeight='medium'>{safeCard.name}</Text>
{safeCard.type && (
Comment thread
gomesalexandre marked this conversation as resolved.
<Tag size='sm' colorScheme='blue' variant='subtle'>
{safeCard.type === 'internal' ? 'Internal SafeCard' : 'SafeCard'}
</Tag>
)}
</HStack>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{safeCard.lastConnectedAt && (
<Text fontSize='xs' color='text.subtle'>
{translate('walletProvider.gridplus.list.lastConnected', {
Expand Down
Loading
Loading