unionlabs/union · error · Error
Invalid Sui signer: expected Ed25519Keypair
Error message
Invalid Sui signer: expected Ed25519Keypair
What it means
ts-sdk-sui's walletClientLayer duck-types opts.signer: it must be an object exposing a callable getPublicKey, i.e. an @mysten/sui Ed25519Keypair. Anything else — a raw private key string/bytes, an Ed25519PublicKey, or undefined/null — triggers this throw, which the catch then wraps into a typed Sui.CreateWalletClientError.
Source
Thrown at ts-sdk-sui/src/internal/sui.ts:38
new Sui.CreatePublicClientError({
cause: Utils.extractErrorDetails(err as Sui.CreatePublicClientError),
}),
}),
Effect.map((client) => ({ client })),
),
)
/** @internal */
export const walletClientLayer = <Id>(
tag: Context.Tag<Id, Sui.Sui.WalletClient>,
) =>
(opts: { url: string; signer: Ed25519Keypair }): Layer.Layer<Id, Sui.CreateWalletClientError> =>
Layer.effect(
tag,
Effect.try({
try: () => {
if (!opts?.signer || typeof opts.signer.getPublicKey !== "function") {
throw new Error("Invalid Sui signer: expected Ed25519Keypair")
}
const client = new SuiClient({ url: opts.url } satisfies SuiClientOptions)
return { client, signer: opts.signer, rpc: opts.url }
},
catch: (err) =>
new Sui.CreateWalletClientError({
cause: Utils.extractErrorDetails(err as Error),
}),
}),
)
View on GitHub (pinned to 031785bb6d)
Solutions
- Construct the keypair first: Ed25519Keypair.fromSecretKey(fromB64(privateKey)) or Ed25519Keypair.deriveKeypair(mnemonic), then pass that object as signer
- Pass an Ed25519Keypair, never the string key, public key, or raw bytes
- Fail fast with a clear message when opts.signer is missing before building the layer
Example fix
// before
const layer = walletClientLayer(Tag)({
url: rpcUrl,
signer: process.env.SUI_PRIVATE_KEY as unknown as Ed25519Keypair, // string!
})
// after
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"
import { fromB64 } from "@mysten/sui/utils"
const signer = Ed25519Keypair.fromSecretKey(fromB64(process.env.SUI_PRIVATE_KEY!))
const layer = walletClientLayer(Tag)({ url: rpcUrl, signer }) Defensive patterns
Strategy: type-guard
Validate before calling
if (!opts?.signer || typeof opts.signer.getPublicKey !== "function") {
throw new Error("Pass an Ed25519Keypair — build it with Ed25519Keypair.fromSecretKey()")
} Type guard
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"
const isEd25519Keypair = (s: unknown): s is Ed25519Keypair =>
!!s && typeof (s as Ed25519Keypair).getPublicKey === "function" Try / catch
try {
const layer = walletClientLayer(Tag)({ url, signer })
} catch (e) {
if (e instanceof Sui.CreateWalletClientError && /Invalid Sui signer/.test(String(e.cause))) {
throw new Error("signer must be an Ed25519Keypair (fromSecretKey/deriveKeypair)")
}
throw e
} Prevention
- Always construct the keypair object before building the layer
- Type the signer field as Ed25519Keypair at the boundary so strings fail at compile time
- Never cast env-loaded strings to keypair types
When it happens
Trigger: Passing a hex/base64 private key string where the keypair object is expected; passing Ed25519PublicKey instead of the keypair; omitting signer in the layer options so opts?.signer is undefined.
Common situations: Loading keys from environment variables and forgetting to construct the keypair; mixing up @mysten/sui/keypairs/ed25519 exports when integrating the SDK.
Related errors
- no `signer` passed
- Wallet does not support standard:connect
- No Sui account returned by wallet
- Not a MoveObject
- No return value from channel_balance
AI-assisted analysis of unionlabs/union@031785bb6d (2026-08-16).
Data as JSON: /api/errors/01deb8ee6cc0faf0.
Report an issue: GitHub.