Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@
"message": "chore(release): publish %v"
}
},
"ignoreChanges": ["**/__fixtures__/**", "**/__tests__/**", "**/*.md", "**/.env"]
"ignoreChanges": [
"**/__fixtures__/**",
"**/__tests__/**",
"**/*.md",
"**/.env"
]
}
23 changes: 23 additions & 0 deletions packages/hdwallet-seeker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@shapeshiftoss/hdwallet-seeker",
"version": "1.62.41",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "dist/index.js",
"source": "src/index.ts",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc --build",
"clean": "rm -rf dist tsconfig.tsbuildinfo",
"prepublishOnly": "yarn clean && yarn build"
},
"dependencies": {
"@shapeshiftoss/hdwallet-core": "^1.62.41",
"@solana/web3.js": "^1.95.8"
}
}
210 changes: 210 additions & 0 deletions packages/hdwallet-seeker/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import type {
BIP32Path,
DescribePath,
GetPublicKey,
HDWallet,
PathDescription,
Ping,
Pong,
PublicKey,
SolanaAccountPath,
SolanaGetAccountPaths,
SolanaGetAddress,
SolanaSignedTx,
SolanaSignTx,
SolanaTxSignature,
} from '@shapeshiftoss/hdwallet-core'
import { solanaBuildTransaction } from '@shapeshiftoss/hdwallet-core'

import type { SeekerMessageHandler } from './types'

export class SeekerHDWallet implements HDWallet {
private deviceId: string
private pubkey: string
private messageHandler: SeekerMessageHandler

readonly _supportsSolana = true
readonly _supportsSolanaInfo = true

constructor(deviceId: string, pubkey: string, messageHandler: SeekerMessageHandler) {
this.deviceId = deviceId
this.pubkey = pubkey
this.messageHandler = messageHandler
}

getVendor(): string {
return 'Seeker'
}

hasOnDevicePinEntry(): boolean {
return false
}

hasOnDevicePassphrase(): boolean {
return false
}

hasOnDeviceDisplay(): boolean {
return true
}

hasOnDeviceRecovery(): boolean {
return true
}

hasNativeShapeShift(): boolean {
return false
}

supportsBip44Accounts(): boolean {
return false
}

supportsOfflineSigning(): boolean {
return false
}

supportsBroadcast(): boolean {
return true
}

describePath(_msg: DescribePath): PathDescription {
return {
isKnown: false,
verbose: 'Seeker Solana',
coin: 'Solana',
}
}

getDeviceID(): Promise<string> {
return Promise.resolve(this.deviceId)
}

getFeatures(): Promise<Record<string, unknown>> {
return Promise.resolve({
vendor: 'Seeker',
model: 'Seeker',
label: 'Seeker Wallet',
})
}

getFirmwareVersion(): Promise<string> {
return Promise.resolve('1.0.0')
}

getModel(): Promise<string> {
return Promise.resolve('Seeker')
}

getLabel(): Promise<string> {
return Promise.resolve('Seeker Wallet')
}

getPublicKeys(_msg: GetPublicKey[]): Promise<(PublicKey | null)[] | null> {
return Promise.resolve([{ xpub: this.pubkey }])
}
Comment on lines +103 to +105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align getPublicKeys output length with the request.

Line 103 returns a single element regardless of input size, which can break callers expecting one result per request. Either validate single-key usage or map to the input length.

✅ Suggested fix (mirror input length)
-  getPublicKeys(_msg: GetPublicKey[]): Promise<(PublicKey | null)[] | null> {
-    return Promise.resolve([{ xpub: this.pubkey }])
-  }
+  getPublicKeys(msg: GetPublicKey[]): Promise<(PublicKey | null)[] | null> {
+    return Promise.resolve(msg.map(() => ({ xpub: this.pubkey })))
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getPublicKeys(_msg: GetPublicKey[]): Promise<(PublicKey | null)[] | null> {
return Promise.resolve([{ xpub: this.pubkey }])
}
getPublicKeys(msg: GetPublicKey[]): Promise<(PublicKey | null)[] | null> {
return Promise.resolve(msg.map(() => ({ xpub: this.pubkey })))
}
🧰 Tools
🪛 ESLint

[error] 103-103: '_msg' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 104-104: Insert ;

(prettier/prettier)

🤖 Prompt for AI Agents
In `@packages/hdwallet-seeker/src/adapter.ts` around lines 103 - 105,
getPublicKeys currently always returns a single PublicKey regardless of the
number of GetPublicKey requests, which breaks callers expecting one result per
request; update getPublicKeys to return an array matching _msg.length (e.g., map
over _msg and return { xpub: this.pubkey } for each entry) or, if only
single-key use is supported, validate and throw a clear error when _msg.length
!== 1; refer to getPublicKeys, GetPublicKey, PublicKey and this.pubkey when
making the change.


isInitialized(): Promise<boolean> {
return Promise.resolve(true)
}

isLocked(): Promise<boolean> {
return Promise.resolve(false)
}

clearSession(): Promise<void> {
return Promise.resolve()
}

initialize(): Promise<void> {
return Promise.resolve()
}

ping(_msg: Ping): Promise<Pong> {
return Promise.resolve({ msg: 'pong' })
}

sendPin(_pin: string): Promise<void> {
return Promise.resolve()
}

sendPassphrase(_passphrase: string): Promise<void> {
return Promise.resolve()
}

sendCharacter(_character: string): Promise<void> {
return Promise.resolve()
}

sendWord(_word: string): Promise<void> {
return Promise.resolve()
}

cancel(): Promise<void> {
return Promise.resolve()
}

wipe(): Promise<void> {
return Promise.resolve()
}

reset(): Promise<void> {
return Promise.resolve()
}

recover(): Promise<void> {
return Promise.resolve()
}

loadDevice(): Promise<void> {
return Promise.resolve()
}

disconnect(): Promise<void> {
return Promise.resolve()
}

getAddress(): string {
return this.pubkey
}

solanaGetAddress(_msg: SolanaGetAddress): Promise<string | null> {
return Promise.resolve(this.pubkey)
}

solanaGetAccountPaths(_msg: SolanaGetAccountPaths): SolanaAccountPath[] {
const SOLANA_BIP44_PATH: BIP32Path = [0x80000000 + 44, 0x80000000 + 501, 0x80000000 + 0]
return [{ addressNList: SOLANA_BIP44_PATH }]
}

solanaNextAccountPath(_msg: SolanaAccountPath): SolanaAccountPath | undefined {
return undefined
}

async solanaSignTx(msg: SolanaSignTx): Promise<SolanaSignedTx | null> {
const transaction = solanaBuildTransaction(msg, this.pubkey)
const serializedTx = Buffer.from(transaction.serialize()).toString('base64')

const result = await this.messageHandler.signTransaction(serializedTx)
if (!result.success || !result.signedTransaction) {
throw new Error(result.error ?? 'Failed to sign transaction')
}

return {
serialized: result.signedTransaction,
signatures: [result.signedTransaction],
}
Comment on lines +184 to +196
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find SolanaSignedTx definition and examine its structure
rg -n "interface SolanaSignedTx|type SolanaSignedTx|class SolanaSignedTx" packages -g '*.ts' -A 5

Repository: shapeshift/hdwallet

Length of output: 45


🏁 Script executed:

# Find all solanaSignTx implementations
rg -n "solanaSignTx" packages -g '*.ts' -B 2 -A 15

Repository: shapeshift/hdwallet

Length of output: 45


🏁 Script executed:

# Check SolanaSignTx and related types in hdwallet-core
fd -e ts packages/hdwallet-core | head -20

Repository: shapeshift/hdwallet

Length of output: 456


🏁 Script executed:

# List available packages
ls -la packages/ | head -20

Repository: shapeshift/hdwallet

Length of output: 1368


🏁 Script executed:

# Search for SolanaSignedTx definition
rg "SolanaSignedTx" packages --type ts -B 2 -A 5

Repository: shapeshift/hdwallet

Length of output: 45


🏁 Script executed:

# Search for solanaSignTx method implementations
rg "solanaSignTx\s*\(" packages --type ts -B 1 -A 15

Repository: shapeshift/hdwallet

Length of output: 45


🏁 Script executed:

# Continue listing packages
ls -la packages/ | tail -10

Repository: shapeshift/hdwallet

Length of output: 745


🏁 Script executed:

# Search for SolanaSignedTx without context flags
rg "SolanaSignedTx" packages

Repository: shapeshift/hdwallet

Length of output: 45


🏁 Script executed:

# Look for seeker package
ls -la packages/ | grep seeker

Repository: shapeshift/hdwallet

Length of output: 127


🏁 Script executed:

# Search for Solana-related types more broadly
rg "Solana" packages --type ts | grep -i "interface\|type\|class" | head -20

Repository: shapeshift/hdwallet

Length of output: 45


🏁 Script executed:

# Look at the seeker adapter file mentioned in the review
cat packages/hdwallet-seeker/src/adapter.ts | head -220 | tail -50

Repository: shapeshift/hdwallet

Length of output: 1659


🏁 Script executed:

# Check hdwallet-core for Solana types
find packages/hdwallet-core -name "*.ts" | xargs grep -l "Solana" | head -5

Repository: shapeshift/hdwallet

Length of output: 172


🏁 Script executed:

# Check SolanaSignedTx type definition in hdwallet-core
cat packages/hdwallet-core/src/solana.ts | head -150

Repository: shapeshift/hdwallet

Length of output: 4945


🏁 Script executed:

# Look for other solanaSignTx implementations
find packages -name "*.ts" -exec grep -l "solanaSignTx" {} \;

Repository: shapeshift/hdwallet

Length of output: 583


🏁 Script executed:

# Check if there are any other Solana adapter implementations
ls -la packages/hdwallet-*/src/ | grep -i solana

Repository: shapeshift/hdwallet

Length of output: 482


🏁 Script executed:

# Check phantom solanaSignTx implementation
cat packages/hdwallet-phantom/src/solana.ts

Repository: shapeshift/hdwallet

Length of output: 1096


🏁 Script executed:

# Check ledger solanaSignTx implementation
cat packages/hdwallet-ledger/src/solana.ts

Repository: shapeshift/hdwallet

Length of output: 1876


🏁 Script executed:

# Check native solanaSignTx implementation
cat packages/hdwallet-native/src/solana.ts

Repository: shapeshift/hdwallet

Length of output: 2589


🏁 Script executed:

# Check gridplus solanaSignTx implementation
cat packages/hdwallet-gridplus/src/solana.ts

Repository: shapeshift/hdwallet

Length of output: 1977


🏁 Script executed:

# Check trezor solanaSignTx implementation
cat packages/hdwallet-trezor/src/solana.ts

Repository: shapeshift/hdwallet

Length of output: 2918


signatures array should contain individual signature strings, not the full serialized transaction.

The current code returns [result.signedTransaction], but downstream Solana consumers expect signatures to contain the actual signature bytes (base64-encoded). This mirrors all other adapter implementations (Phantom, Ledger, Native, GridPlus, Trezor), which extract signatures from the signed transaction and convert to base64.

Deserialize the signed transaction and extract its signatures:

Suggested fix
+import { Transaction } from '@solana/web3.js'

 async solanaSignTx(msg: SolanaSignTx): Promise<SolanaSignedTx | null> {
   const transaction = solanaBuildTransaction(msg, this.pubkey)
   const serializedTx = Buffer.from(transaction.serialize()).toString('base64')

   const result = await this.messageHandler.signTransaction(serializedTx)
   if (!result.success || !result.signedTransaction) {
     throw new Error(result.error ?? 'Failed to sign transaction')
   }

+  const signedTx = Transaction.from(Buffer.from(result.signedTransaction, 'base64'))
+  const signatures = signedTx.signatures
+    .map(s => (s.signature ? Buffer.from(s.signature).toString('base64') : null))
+    .filter((s): s is string => Boolean(s))
+
   return {
     serialized: result.signedTransaction,
-    signatures: [result.signedTransaction],
+    signatures,
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async solanaSignTx(msg: SolanaSignTx): Promise<SolanaSignedTx | null> {
const transaction = solanaBuildTransaction(msg, this.pubkey)
const serializedTx = Buffer.from(transaction.serialize()).toString('base64')
const result = await this.messageHandler.signTransaction(serializedTx)
if (!result.success || !result.signedTransaction) {
throw new Error(result.error ?? 'Failed to sign transaction')
}
return {
serialized: result.signedTransaction,
signatures: [result.signedTransaction],
}
import { Transaction } from '@solana/web3.js'
async solanaSignTx(msg: SolanaSignTx): Promise<SolanaSignedTx | null> {
const transaction = solanaBuildTransaction(msg, this.pubkey)
const serializedTx = Buffer.from(transaction.serialize()).toString("base64");
const result = await this.messageHandler.signTransaction(serializedTx)
if (!result.success || !result.signedTransaction) {
throw new Error(result.error ?? "Failed to sign transaction");
}
const signedTx = Transaction.from(Buffer.from(result.signedTransaction, "base64"));
const signatures = signedTx.signatures
.map(s => (s.signature ? Buffer.from(s.signature).toString("base64") : null))
.filter((s): s is string => Boolean(s))
return {
serialized: result.signedTransaction,
signatures,
};
}
🧰 Tools
🪛 ESLint

[error] 185-185: Insert ;

(prettier/prettier)


[error] 186-186: Replace 'base64') with "base64");

(prettier/prettier)


[error] 188-188: Insert ;

(prettier/prettier)


[error] 190-190: Replace 'Failed·to·sign·transaction') with "Failed·to·sign·transaction");

(prettier/prettier)


[error] 196-196: Insert ;

(prettier/prettier)

🤖 Prompt for AI Agents
In `@packages/hdwallet-seeker/src/adapter.ts` around lines 184 - 196, In
solanaSignTx, instead of returning the full serialized transaction in the
signatures array, deserialize result.signedTransaction (the base64 signed
transaction) back into a Transaction, extract each signature
(Transaction.signatures or equivalent) and convert each signature's .signature
bytes to base64 strings, then return signatures as an array of those
base64-encoded signature strings; update the return to use the serialized
signedTransaction for serialized and the extracted base64 signatures for
signatures, referencing solanaSignTx, solanaBuildTransaction,
messageHandler.signTransaction, and result.signedTransaction to locate the
change.

}

async solanaSendTx(msg: SolanaSignTx): Promise<SolanaTxSignature | null> {
const transaction = solanaBuildTransaction(msg, this.pubkey)
const serializedTx = Buffer.from(transaction.serialize()).toString('base64')

const result = await this.messageHandler.signAndSendTransaction(serializedTx)
if (!result.success || !result.signature) {
throw new Error(result.error ?? 'Failed to sign and send transaction')
}

return { signature: result.signature }
}
}
12 changes: 12 additions & 0 deletions packages/hdwallet-seeker/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export { SeekerHDWallet } from './adapter'
export type {
SeekerMessageHandler,
SeekerAvailabilityResult,
SeekerAuthResult,
SeekerAddressResult,
SeekerStatusResult,
SeekerSignResult,
SeekerSendResult,
SeekerAppIdentity,
SeekerConfig,
} from './types'
53 changes: 53 additions & 0 deletions packages/hdwallet-seeker/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export type SeekerAvailabilityResult = {
available: boolean
}

export type SeekerAuthResult = {
success: boolean
address?: string
label?: string
error?: string
}

export type SeekerAddressResult = {
address: string | null
}

export type SeekerStatusResult = {
available: boolean
isAuthorized: boolean
address: string | null
}

export type SeekerSignResult = {
success: boolean
signedTransaction?: string
error?: string
}

export type SeekerSendResult = {
success: boolean
signature?: string
error?: string
}

export interface SeekerMessageHandler {
checkAvailability(): Promise<SeekerAvailabilityResult>
authorize(cluster?: 'mainnet-beta' | 'devnet' | 'testnet'): Promise<SeekerAuthResult>
deauthorize(): Promise<{ success: boolean; error?: string }>
getAddress(): Promise<SeekerAddressResult>
getStatus(): Promise<SeekerStatusResult>
signTransaction(transaction: string): Promise<SeekerSignResult>
signAndSendTransaction(transaction: string): Promise<SeekerSendResult>
}

export interface SeekerAppIdentity {
name: string
uri: string
icon: string
}

export interface SeekerConfig {
appIdentity: SeekerAppIdentity
cluster?: 'mainnet-beta' | 'devnet' | 'testnet'
}
10 changes: 10 additions & 0 deletions packages/hdwallet-seeker/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"],
"references": [{ "path": "../hdwallet-core" }]
}
Loading