Framework-agnostic HTTP request handler builder for Anvil Vault. This package provides a flexible system for creating vault API endpoints that work with any web framework through adapters.
All functions return Result types from the trynot library for consistent error handling.
See the Express Quick Start for a complete example.
See the Hono Quick Start for a complete example.
Creates a framework-specific request handler for vault operations.
Warning
The handler does not provide any form of authentication
Input:
type CreateVaultHandlerConfig<
TParams extends AnyParams,
TContext,
TResponse
> = {
vault: IVault; // Vault instance implementing the IVault interface
adapter: HandlerAdapter<TParams, TContext, TResponse>; // Framework adapter
};Returns: Framework-specific handler function
Example:
import { createVaultHandler } from "@ada-anvil/vault";
import { expressAdapter } from "@ada-anvil/vault/express";
app.use(
createVaultHandler({
vault,
adapter: expressAdapter,
})
);Low-level function for processing vault requests. Used internally by createVaultHandler.
function handleVaultRequest<TParams extends AnyParams, TContext, TResponse>(
context: TContext,
vault: IVault,
adapter: HandlerAdapter<TParams, TContext, TResponse>
): Promise<Result<{ response: unknown }, VaultError>>;Parameters:
context: TContext- Framework-specific context objectvault: IVault- Vault instanceadapter: HandlerAdapter<TParams, TContext, TResponse>- Framework adapter
Returns: Promise<Result<{ response: unknown }, VaultError>>
Example:
import { handleVaultRequest } from "@ada-anvil/vault";
import { expressAdapter } from "@ada-anvil/vault/express";
const context = await expressAdapter.getContext(req, res);
const result = await handleVaultRequest(context, vault, expressAdapter);Interface for creating framework-specific adapters.
Type:
type HandlerAdapter<TParams extends AnyParams, TContext, TResponse> = {
getContext: (...args: TParams) => MaybePromise<TContext>;
getBody: (context: TContext) => MaybePromise<Record<string, unknown>>;
getMethod: (context: TContext) => MaybePromise<string>;
getPath: (context: TContext) => MaybePromise<string>;
getQuery: (context: TContext) => MaybePromise<Record<string, unknown>>;
sendResponse: (
context: TContext,
result: Result<{ response: unknown }, VaultError>
) => MaybePromise<TResponse>;
};Helper function for creating type-safe adapters.
Parameters: adapter: HandlerAdapter<TParams, TContext, TResponse> - Adapter implementation
Returns: HandlerAdapter<TParams, TContext, TResponse>
Example:
import { createHandlerAdapter } from "@ada-anvil/vault";
export const customAdapter = createHandlerAdapter({
getContext: (req, res) => ({ req, res }),
getBody: async (ctx) => ctx.req.body,
getMethod: (ctx) => ctx.req.method,
getPath: (ctx) => ctx.req.path,
getQuery: (ctx) => ctx.req.query,
sendResponse: (ctx, result) => {
if (isErr(result)) {
return ctx.res.status(result.statusCode).json(errorToJson(result));
}
return ctx.res.json(result.response);
},
});Resolves a derivation strategy into a concrete derivation path.
Parameters:
type GetDerivationInput<TContext = undefined> = {
userId: string; // User identifier (UUID)
derivation: Derivation<TContext>; // Derivation strategy
};
type Params = {
input: GetDerivationInput;
context?: TContext; // Optional context for custom derivations
};Returns: Promise<Result<number | number[]>>
Example:
import { getDerivation } from "@ada-anvil/vault";
import { isOk } from "trynot";
const result = await getDerivation({
userId: "550e8400-e29b-41d4-a716-446655440000",
derivation: { type: "unique" },
});
if (isOk(result)) {
console.log(result); // [85, 14, 132, 0, ...] (16 bytes from UUID)
}Defines how to derive keys for a user.
Type:
type Derivation<TContext = undefined> =
| {
type: "unique";
scrambler?: (
derivationPath: number[],
input: { userId: string },
context: TContext
) => MaybePromise<Result<number[]>>;
}
| {
type: "pool";
size: number;
}
| {
type: "constant";
value: number | number[];
}
| {
type: "custom";
provider: (
input: { userId: string },
context: TContext
) => MaybePromise<Result<number | number[] | Derivation<TContext>>>;
};Strategies:
-
unique: Derives a unique path from the user's UUID (16 bytes)- Optional
scramblerfunction to transform the derivation path - Recommended for payment keys to prevent address correlation
- Optional
-
pool: Distributes users across a fixed pool of keyssize: Number of keys in the pool- Deterministic assignment based on userId
- Useful for stake keys to consolidate rewards
-
constant: Uses the same derivation for all usersvalue: Single index or array of indices- Simple but provides no privacy
-
custom: Fully custom derivation logicprovider: Function that returns derivation path or another Derivation- Maximum flexibility for complex requirements
Example:
import type { Derivation } from "@anvil-vault/handler";
// Unique with scrambling
const paymentDerivation: Derivation = {
type: "unique",
scrambler: (path) => path.reverse(),
};
// Pool of 10 keys
const stakeDerivation: Derivation = {
type: "pool",
size: 10,
};Interface that vault implementations must satisfy.
Type:
type IVault = {
getWallet: (input: { userId: string }) => MaybePromise<
Result<{
addresses: {
base: { bech32: string; hex: string };
enterprise: { bech32: string; hex: string };
reward: { bech32: string; hex: string };
};
}>
>;
signData: (input: {
userId: string;
payload: string;
externalAad?: string;
}) => MaybePromise<
Result<{
signature: string;
key: string;
}>
>;
signTransaction: (input: {
userId: string;
transaction: string;
}) => MaybePromise<
Result<{
signedTransaction: string;
witnessSet: string;
}>
>;
};Configuration for vault instances.
Type:
type VaultConfig = RequiredVaultConfig & {
accountDerivation?: Derivation<RequiredVaultConfig>;
paymentDerivation?: Derivation<RequiredVaultConfig>;
stakeDerivation?: Derivation<RequiredVaultConfig>;
customWalletDerivation?: (
input: { userId: string },
config: RequiredVaultConfig
) => MaybePromise<Result<DeriveWalletOutput>>;
additionalWalletDerivation?: (
keys: DeriveWalletOutput,
input: { userId: string },
config: RequiredVaultConfig
) => MaybePromise<Result<DeriveWalletOutput>>;
ignoreDefaultPaymentDerivationWarning?: boolean;
};
type RequiredVaultConfig = {
rootKey: () => MaybePromise<Bip32PrivateKey | string>;
network: Network | NetworkId;
};The handler automatically creates the following REST endpoints:
Get wallet addresses for a user.
Path Parameters:
userId: string- User identifier (UUID)
Response:
{
"addresses": {
"base": {
"bech32": "addr1qx...",
"hex": "00a1b2c3..."
},
"enterprise": {
"bech32": "addr1vx...",
"hex": "60a1b2c3..."
},
"reward": {
"bech32": "stake1ux...",
"hex": "e0a1b2c3..."
}
}
}Example:
curl http://localhost:3000/users/550e8400-e29b-41d4-a716-446655440000/walletSign arbitrary data with the user's payment key (CIP-8/CIP-30 compliant).
Path Parameters:
userId: string- User identifier (UUID)
Body:
{
"payload": "48656c6c6f2c2043617264616e6f21",
"externalAad": "optional-hex-string"
}Response:
{
"signature": "845846a201276761646472657373...",
"key": "a401022001215820..."
}Example:
curl -X POST http://localhost:3000/users/550e8400-e29b-41d4-a716-446655440000/sign-data \
-H "Content-Type: application/json" \
-d '{"payload":"48656c6c6f2c2043617264616e6f21"}'Sign a Cardano transaction with the user's payment and stake keys.
Path Parameters:
userId: string- User identifier (UUID)
Body:
{
"transaction": "84a500d90102818258203b1663796602c0d84b03c0f201c4ed3a76667e1e05698c2aee7168ab327eb6de0001818258390048dc188cd7a3fa245498144a5469c34ea11c54975587529269430016a2b990e0c40026e9e9381abdb18ba9f4bf80bd65f7c19263357f6497821b0000000403b4a354a4581c698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9da14574445249501823581cb784ba558baab378e670b8285f8c079ef002b5a0eb26fd6a533a5611a14d4d79436f6f6c4173736574233101581cc82a4452eaebccb82aced501b3c94d3662cf6cd2915ad7148b459aeca14341584f1a000d66b7581cf0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9aa14d000de140646f64696c616e6e6501021a0002f43d031a05e2d418081a05e2b7f8a0f5f6"
}Response:
{
"signedTransaction": "84a500d90102818258203b1663796602c0d84b03c0f201c4ed3a76667e1e05698c2aee7168ab327eb6de0001818258390048dc188cd7a3fa245498144a5469c34ea11c54975587529269430016a2b990e0c40026e9e9381abdb18ba9f4bf80bd65f7c19263357f6497821b0000000403b4a354a4581c698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9da14574445249501823581cb784ba558baab378e670b8285f8c079ef002b5a0eb26fd6a533a5611a14d4d79436f6f6c4173736574233101581cc82a4452eaebccb82aced501b3c94d3662cf6cd2915ad7148b459aeca14341584f1a000d66b7581cf0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9aa14d000de140646f64696c616e6e6501021a0002f43d031a05e2d418081a05e2b7f8a0f5f6",
"witnessSet": "a100d9010281825820a841677b40416b65c59be000e4fa10c7a48f96da053bb1b75722e3a8249b7b355840b2e42cb89b8c56b9615c71461bb1e545a0084699be22aa22085a86ba28162dfe268d4407f82310d2cca6721b616a4af96aa6c6cff4a9124ef8acd2dbe80b2f00"
}Example:
curl -X POST http://localhost:3000/users/550e8400-e29b-41d4-a716-446655440000/sign-transaction \
-H "Content-Type: application/json" \
-d '{"transaction":"84a500d90102818258203b1663796602c0d84b03c0f201c4ed3a76667e1e05698c2aee7168ab327eb6de0001818258390048dc188cd7a3fa245498144a5469c34ea11c54975587529269430016a2b990e0c40026e9e9381abdb18ba9f4bf80bd65f7c19263357f6497821b0000000403b4a354a4581c698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9da14574445249501823581cb784ba558baab378e670b8285f8c079ef002b5a0eb26fd6a533a5611a14d4d79436f6f6c4173736574233101581cc82a4452eaebccb82aced501b3c94d3662cf6cd2915ad7148b459aeca14341584f1a000d66b7581cf0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9aa14d000de140646f64696c616e6e6501021a0002f43d031a05e2d418081a05e2b7f8a0f5f6"}'All endpoints return consistent JSON error responses:
{
"statusCode": 400,
"error": "Bad request"
}Status Codes:
400- Invalid input (validation failed)404- Invalid path or operation405- Wrong HTTP method500- Vault operation failed
For general error handling patterns, see Error Handling.
- @ada-anvil/vault/vault: Vault implementation
- @ada-anvil/vault/express: Express.js adapter
- @ada-anvil/vault/hono: Hono adapter
- @ada-anvil/vault/utils: Shared utilities