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

  1. Construct the keypair first: Ed25519Keypair.fromSecretKey(fromB64(privateKey)) or Ed25519Keypair.deriveKeypair(mnemonic), then pass that object as signer
  2. Pass an Ed25519Keypair, never the string key, public key, or raw bytes
  3. 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

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


AI-assisted analysis of unionlabs/union@031785bb6d (2026-08-16). Data as JSON: /api/errors/01deb8ee6cc0faf0. Report an issue: GitHub.