> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SignerAdapter

> Unified signing interface with implementations for passkeys, direct keys, and external providers.

> [!WARNING]
>
> `@chipi-stack/core` is in development (v0.1.0). APIs may change before stable release.

## Overview

`SignerAdapter` is an interface that abstracts over different key management strategies. Instead of hardwiring passkey logic into transaction flows, you program against the `SignerAdapter` interface and swap implementations as needed.

## The Interface

```typescript theme={null}
interface SignerAdapter {
  getPublicKey(): Promise<string>;
  sign(messageHash: string): Promise<SignResult>;
  getAccountAddress(): Promise<string>;
}

interface SignResult {
  r: string;
  s: string;
}
```

## Implementations

### DirectSigner

Signs with a local private key. For development and testing only.

```typescript theme={null}
import { DirectSigner } from "@chipi-stack/core";

const signer = new DirectSigner(
  "0x1234...privateKey",
  "0xabcd...accountAddress" // optional
);

const pubKey = await signer.getPublicKey();
const sig = await signer.sign(messageHash);
```

> [!CAUTION]
>
> Never use DirectSigner with real private keys in production. It exists for local development and testing.

#### Constructor

```typescript theme={null}
new DirectSigner(privateKey: string, accountAddress?: string)
```

* `privateKey` (string): Hex-encoded StarkNet private key (must start with `0x`)
* `accountAddress` (string, optional): Account address. Defaults to the derived public key.

#### Throws

* If `privateKey` is not a valid hex string starting with `0x`

### PasskeySigner

Wraps a WebAuthn assertion function that produces Starknet-compatible signatures `{r, s}` from a challenge. Users authenticate via biometrics (Face ID, fingerprint) and the result is used to sign transactions.

```typescript theme={null}
import { PasskeySigner } from "@chipi-stack/core";

// Your WebAuthn assertion function that returns a Starknet signature
async function passkeySign(challenge: string): Promise<{ r: string; s: string }> {
  // Call your WebAuthn authentication flow here
  // The challenge is the message hash to sign
  const assertion = await navigator.credentials.get({
    publicKey: { challenge: new TextEncoder().encode(challenge), /* ... */ },
  });
  // Extract r, s from the assertion response
  return { r: "0x...", s: "0x..." };
}

const signer = new PasskeySigner({
  authenticateFn: passkeySign,
  publicKey: userPasskeyPublicKey,
  accountAddress: userAccountAddress,
});

// Now use with TxBuilder via the Account
const sig = await signer.sign(messageHash);
```

#### Constructor

```typescript theme={null}
new PasskeySigner(opts: {
  authenticateFn: (challenge: string) => Promise<{ r: string; s: string }>;
  publicKey: string;
  accountAddress: string;
})
```

* `authenticateFn`: A function that takes a challenge hash and returns a Starknet-compatible signature `{r, s}` via WebAuthn assertion
* `publicKey`: The passkey public key obtained during registration
* `accountAddress`: The user's StarkNet account address

> [!NOTE]
>
> Most developers should use the `usePasskeyAuth` hook from `@chipi-stack/chipi-passkey/hooks` instead of `PasskeySigner` directly. `PasskeySigner` is for advanced use cases where you need low-level control over the signing flow.

### ExternalSigner

Generic adapter for a signer that can sign a **raw Starknet hash** with one STARK key and return `{ r, s }`: a key in a KMS or HSM, your own signing service, an MPC provider. You supply the three interface methods and ExternalSigner wraps them.

```typescript theme={null}
import { ExternalSigner } from "@chipi-stack/core";

// Example: a STARK key held by your signing service (KMS, HSM or MPC).
declare const signingService: {
  publicKey(): Promise<string>;
  signHash(hash: string): Promise<{ r: string; s: string }>;
};
declare const accountAddress: string;

const signer = new ExternalSigner({
  getPublicKey: () => signingService.publicKey(),
  sign: (hash) => signingService.signHash(hash),
  getAccountAddress: async () => accountAddress,
});
```

`createAccount` sends exactly `[r, s]` as the transaction signature, so the account must accept a plain two-felt signature from that key (an OpenZeppelin account, for example).

> [!NOTE]
>
> Browser wallets don't fit `ExternalSigner`. Bramble, Ready and Braavos never sign a raw hash: their only signing method, `wallet_signTypedData`, signs SNIP-12 typed data, whose hash includes the signing account's own address. The signature shape also depends on the account: `[r, s]` for Bramble's OpenZeppelin account and Braavos, `["1", "0", publicKey, r, s]` for Ready (Argent).
>
> To send from the user's own wallet, wrap it in starknet.js's `WalletAccount` and pass that to `TxBuilder` ([example](/sdk/core/tx-builder#with-bramble-browser-wallet)). To make Bramble a co-owner or guardian of a Chipi wallet, see [Trusted wallets](/sdk/guides/trusted-wallets).

Signing typed data with Bramble through `WalletAccount`:

```typescript theme={null}
import { RpcProvider, WalletAccount, type TypedData } from "starknet";

declare const typedData: TypedData; // its domain chainId must be Bramble's network ("SN_MAIN")

const bramble = (window as unknown as { starknet_bramble?: WalletAccount["walletProvider"] })
  .starknet_bramble;
if (!bramble) throw new Error("Bramble is not installed");

// From a click: sends wallet_requestAccounts with { silent_mode: false }.
const account = await WalletAccount.connect(
  new RpcProvider({ nodeUrl: "https://api.cartridge.gg/x/starknet/mainnet" }),
  bramble
);
const signature = await account.signMessage(typedData); // wallet_signTypedData, string[]
```

#### Constructor

```typescript theme={null}
new ExternalSigner(opts: {
  getPublicKey: () => Promise<string>;
  sign: (messageHash: string) => Promise<SignResult>;
  getAccountAddress: () => Promise<string>;
})
```

## Use Cases

### Backend Automation

Use `DirectSigner` with a key stored in a vault/HSM for programmatic transaction execution:

```typescript theme={null}
const signer = new DirectSigner(process.env.AUTOMATION_KEY!);
// Build and sign transactions programmatically
```

### Multi-Wallet Support

Support several key holders without branching transaction code:

```typescript theme={null}
import { ExternalSigner, PasskeySigner, type SignerAdapter } from "@chipi-stack/core";

declare const passkeyOptions: ConstructorParameters<typeof PasskeySigner>[0];
declare const kmsOptions: ConstructorParameters<typeof ExternalSigner>[0];

function getSignerForUser(user: { keyHolder: "passkey" | "kms" }): SignerAdapter {
  return user.keyHolder === "passkey"
    ? new PasskeySigner(passkeyOptions)
    : new ExternalSigner(kmsOptions);
}
```

Transaction code is the same regardless of signer: `createAccount({ signer, provider })` gives `TxBuilder` an `Account`. A browser wallet (Bramble, Braavos, Ready) skips this step: its `WalletAccount` already is an `Account`.

### Easy Testing

Mock signers for unit tests:

```typescript theme={null}
const mockSigner = new ExternalSigner({
  getPublicKey: async () => "0xtest_pubkey",
  sign: async (hash) => ({ r: "0x1", s: "0x2" }),
  getAccountAddress: async () => "0xtest_account",
});
```

### Auth Provider Migration

If you switch from Privy to another provider, only the SignerAdapter implementation changes. All TxBuilder, Erc20, and Amount code stays the same.

## Connecting to TxBuilder

`createAccount()` bridges any `SignerAdapter` into a starknet.js `Account` for use with `TxBuilder`.

### Gasless (recommended)

```typescript theme={null}
import { createAccount, DirectSigner, TxBuilder } from "@chipi-stack/core";
import { ChipiServerSDK } from "@chipi-stack/backend";

const sdk = new ChipiServerSDK({ apiPublicKey: "pk_...", apiSecretKey: "sk_..." });
const paymaster = sdk.createPaymasterAdapter();

const provider = { nodeUrl: "https://starknet-mainnet.infura.io/v3/YOUR_KEY" };
const accountAddress = "0xYOUR_ACCOUNT_ADDRESS";
const signer = new DirectSigner(process.env.PRIVATE_KEY!, accountAddress);
const account = await createAccount({ signer, provider });

const txHash = await new TxBuilder(account, { paymaster })
  .transfer(USDC, [{ to, amount }])
  .sendSponsored(); // gasless!
```

### Direct (user pays gas)

```typescript theme={null}
const provider = { nodeUrl: "https://starknet-mainnet.infura.io/v3/YOUR_KEY" };
const accountAddress = "0xYOUR_ACCOUNT_ADDRESS";
const signer = new DirectSigner(process.env.PRIVATE_KEY!, accountAddress);
const account = await createAccount({ signer, provider });

const result = await new TxBuilder(account)
  .transfer(USDC, [{ to, amount }])
  .send(); // user pays STRK for gas
```

## Related

* [TxBuilder](/sdk/core/tx-builder): Uses the Account (which uses the signer) to execute transactions
* [Introduction](/sdk/core/introduction): Overview of when to use core vs hooks


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.