@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
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.
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);
Never use DirectSigner with real private keys in production. It exists for local development and testing.
Constructor
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.
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
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
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 any external signing provider. You supply the three interface methods and ExternalSigner wraps them.
import { ExternalSigner } from "@chipi-stack/core";
// Example: Argent Web Wallet integration
const signer = new ExternalSigner({
getPublicKey: async () => argentWallet.getPublicKey(),
sign: async (hash) => {
const sig = await argentWallet.signMessage(hash);
return { r: sig.r, s: sig.s };
},
getAccountAddress: async () => argentWallet.address,
});
Constructor
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:
const signer = new DirectSigner(process.env.AUTOMATION_KEY!);
// Build and sign transactions programmatically
Multi-Wallet Support
Support multiple wallet providers without branching transaction code:
function getSignerForUser(user: User): SignerAdapter {
switch (user.walletType) {
case "passkey":
return new PasskeySigner({ ... });
case "argent":
return new ExternalSigner({ ... });
case "braavos":
return new ExternalSigner({ ... });
}
}
// Transaction code is the same regardless of signer
const signer = getSignerForUser(currentUser);
Easy Testing
Mock signers for unit tests:
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)
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)
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: Uses the Account (which uses the signer) to execute transactions
- Introduction: Overview of when to use core vs hooks