Account Abstraction for RESTful API with Backend Session
In this guide, we will walk through building a RESTful API app with session management using the Abstraxion Core library, demonstrating how to create a web2 user experience without any web3 interactive behavior in frontend. It also includes the Abstraxion account and gasless transaction experience for users, but all the blockchain related implementation will be handled in the backend.
Install the required dependencies for the backend session management:
3. Project Structure
You can adjust your project structure like this:
Environment Configuration
1. Environment Variables
Create a .env.local file in your project root:
2. Generate Encryption Key
Create a script to generate a secure encryption key:
Run the script:
Database Setup
1. Prisma Configuration
Create a prisma/schema.prisma file:
2. Initialize Database
AbstraxionBackend Library Implementation
We have implemented a version of the AbstraxionBackend library that you can use in your project.
You can directly copy the backend folder from the xion.js repository (Folder Here). In future, we may move this library to a separate package.
In the following sections, we will use @/lib/xion/backend to refer to the AbstraxionBackend library.
AbstraxionBackend Integration
1. Database Adapter
Create the database adapter that implements the BaseDatabaseAdapter interface from the AbstraxionBackend library:
2. AbstraxionBackend Configuration
Create the main configuration file:
Authentication Configuration
NextAuth Setup
Set up NextAuth for user authentication:
Request Validation
The demo uses Zod for request validation. For detailed validation schemas and implementation, refer to the validation.ts file in the demo repository.
RESTful API Endpoints
API Utilities
Before implementing the API endpoints, it's important to understand the utility functions used throughout the implementation:
createApiWrapper - A wrapper function that encapsulates Next.js API routes with common functionality including request validation, rate limiting, error handling, and response formatting. For detailed implementation, refer to the api-wrapper.ts file in the demo repository.
requireAuth - A NextAuth.js middleware function that handles user authentication and session validation for API routes. It ensures that only authenticated users can access protected endpoints. For detailed implementation, refer to the auth-middleware.ts file in the demo repository.
Note: For this example, we are using the MsgSend message to send XION tokens. So please make sure in your treasury permission, you have granted the Send Token permission. Learn more about how to grant permissions.
Testing Your API
Basic API Testing
You can test your API endpoints using curl or any HTTP client:
Deployment
Production Environment Variables
Create production environment variables:
Build and Deploy
API Documentation
Endpoints Summary
Method
Endpoint
Description
Authentication
POST
/api/wallet/connect
Initiate wallet connection
Required
GET
/api/wallet/status
Check wallet status
Required
DELETE
/api/wallet/disconnect
Disconnect wallet
Required
POST
/api/wallet/transaction/send
Send transaction
Required
GET
/api/callback/grant_session
OAuth callback handler
None
Response Format
All API responses follow this format:
Security Considerations
Encryption
All sensitive data (private keys) is encrypted using AES-256-CBC
Encryption keys are generated securely and stored in environment variables
Each encryption operation uses a unique IV for security
Session Management
Automatic key rotation before expiry
Configurable refresh threshold
Background monitoring service for expired sessions
Troubleshooting
Common Issues
Database Connection Errors
Ensure DATABASE_URL is correctly set
Check database server is running
Verify database permissions
Encryption Key Issues
Ensure ENCRYPTION_KEY is base64 encoded
Key must be exactly 32 bytes (256 bits)
Use the provided script to generate keys
XION Network Issues
Verify XION_NETWORK is correct
Check network connectivity
Ensure treasury address is valid
Session Key Problems
Check SESSION_KEY_EXPIRY_MS configuration
Verify REFRESH_THRESHOLD_MS settings
Next Steps
Now that you have a fully functional RESTful API with account abstraction and backend session management, you can:
Extend the API with additional endpoints for specific use cases
Add a frontend to interact with your API
Implement additional security features like 2FA or biometric authentication
Scale the application with load balancers and multiple instances
This implementation provides a solid foundation for building Web3 applications with Web2 user experience, leveraging XION's account abstraction capabilities while maintaining security and scalability.
For more detailed implementation examples, comprehensive error handling, advanced features, and complete source code, please refer to the backend-session demo repository.
// scripts/generate-key.ts
import crypto from 'crypto';
const key = crypto.randomBytes(32).toString('base64');
console.log('Generated encryption key:', key);
console.log('Add this to your .env.local file as ENCRYPTION_KEY');
npx tsx scripts/generate-key.ts
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
username String @unique
email String? @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
sessionKeys SessionKey[]
accounts Account[]
sessions Session[]
@@index([username])
@@index([email])
@@map("users")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model SessionKey {
id String @id @default(cuid())
userId String
sessionKeyAddress String @unique
sessionKeyMaterial String
sessionKeyExpiry DateTime
sessionPermissions String @default("{}")
sessionState String @default("PENDING")
metaAccountAddress String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, sessionKeyAddress])
@@index([userId, sessionState])
@@index([userId, sessionKeyExpiry])
@@index([userId, metaAccountAddress])
@@index([metaAccountAddress])
@@map("session_keys")
}
# Generate Prisma client
npx prisma generate
# Push schema to database
npx prisma db push
// src/lib/xion/abstraxion-backend.ts
import { AbstraxionBackend } from "@/lib/xion/backend";
import { PrismaDatabaseAdapter, prisma } from "./database";
const globalForAbstraxion = globalThis as unknown as {
abstraxionBackend: AbstraxionBackend | undefined;
};
export function getAbstraxionBackend(): AbstraxionBackend {
if (globalForAbstraxion.abstraxionBackend) {
return globalForAbstraxion.abstraxionBackend;
}
// Ensure all environment variables are set
if (!process.env.XION_NETWORK) {
process.env.XION_NETWORK = "testnet";
}
if (!process.env.XION_REDIRECT_URL) {
throw new Error("XION_REDIRECT_URL is not set");
}
if (!process.env.XION_TREASURY) {
throw new Error("XION_TREASURY is not set");
}
if (!process.env.ENCRYPTION_KEY) {
throw new Error("ENCRYPTION_KEY is not set");
}
const databaseAdapter = new PrismaDatabaseAdapter(prisma);
const config = {
network: process.env.XION_NETWORK as "testnet" | "mainnet",
redirectUrl: process.env.XION_REDIRECT_URL,
treasury: process.env.XION_TREASURY,
encryptionKey: process.env.ENCRYPTION_KEY,
databaseAdapter,
sessionKeyExpiryMs: parseInt(
process.env.SESSION_KEY_EXPIRY_MS || "86400000",
),
refreshThresholdMs: parseInt(process.env.REFRESH_THRESHOLD_MS || "3600000"),
};
globalForAbstraxion.abstraxionBackend = new AbstraxionBackend(config);
return globalForAbstraxion.abstraxionBackend;
}
// src/lib/auth.ts
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { z } from "zod";
import { prisma } from "@/lib/xion/database";
const loginSchema = z.object({
username: z.string().min(1, "Username is required"),
password: z.string().min(6, "Password must be at least 6 characters"),
});
const registerSchema = z.object({
username: z.string().min(3, "Username must be at least 3 characters"),
email: z.string().email("Invalid email address").optional(),
password: z.string().min(6, "Password must be at least 6 characters"),
});
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.username || !credentials.password) {
return null;
}
try {
// Validate input
const { username, password } = loginSchema.parse(credentials);
// Find user by username
const user = await prisma.user.findUnique({
where: { username },
});
if (!user) {
return null;
}
// Verify password
const isValidPassword = await bcrypt.compare(
password,
(user as any).password,
);
if (!isValidPassword) {
return null;
}
// Return user object (without password)
return {
id: user.id,
username: user.username,
email: user.email,
};
} catch (error) {
console.error("Auth error:", error);
return null;
}
},
}),
],
session: {
strategy: "jwt",
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.username = user.username;
}
return token;
},
async session({ session, token }) {
if (token) {
session.user.id = token.id;
session.user.username = token.username;
}
return session;
},
},
pages: {
signIn: "/auth/signin",
},
secret: process.env.NEXTAUTH_SECRET,
};
// Helper function to hash passwords
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12);
}
// Export the schemas for use in API routes
export { loginSchema, registerSchema };
// Helper function to validate registration data
export function validateRegistration(data: unknown) {
return registerSchema.parse(data);
}
// Helper function to validate login data
export function validateLogin(data: unknown) {
return loginSchema.parse(data);
}
// src/app/api/wallet/connect/route.ts
import { getAbstraxionBackend } from "@/lib/xion/abstraxion-backend";
import { connectWalletSchema } from "@/lib/validation";
import { createApiWrapper } from "@/lib/api-wrapper";
import { requireAuth } from "@/lib/auth-middleware";
export const dynamic = "force-dynamic";
export const POST = createApiWrapper(
async (context) => {
const { validatedData } = context;
const { permissions, grantedRedirectUrl } = validatedData;
// Get authenticated user from session
const authContext = await requireAuth(context.request);
const { user } = authContext;
// Get AbstraxionBackend instance
const abstraxionBackend = getAbstraxionBackend();
// Initiate wallet connection
const result = await abstraxionBackend.connectInit(
user.id,
permissions,
grantedRedirectUrl,
);
return result;
},
{
schema: connectWalletSchema,
schemaType: "body",
rateLimit: "strict",
allowedMethods: ["POST"],
},
);
// src/app/api/wallet/status/route.ts
import { getAbstraxionBackend } from "@/lib/xion/abstraxion-backend";
import { createApiWrapper } from "@/lib/api-wrapper";
import { requireAuth } from "@/lib/auth-middleware";
export const dynamic = "force-dynamic";
export const GET = createApiWrapper(
async (context) => {
// Get authenticated user from session
const authContext = await requireAuth(context.request);
const { user } = authContext;
// Get AbstraxionBackend instance
const abstraxionBackend = getAbstraxionBackend();
// Check status
const result = await abstraxionBackend.checkStatus(user.id);
return result;
},
{
rateLimit: "normal",
allowedMethods: ["GET"],
},
);
// src/app/api/wallet/disconnect/route.ts
import { getAbstraxionBackend } from "@/lib/xion/abstraxion-backend";
import { createApiWrapper } from "@/lib/api-wrapper";
import { requireAuth } from "@/lib/auth-middleware";
import { ApiException } from "@/lib/api-response";
export const dynamic = "force-dynamic";
export const DELETE = createApiWrapper(
async (context) => {
// Get authenticated user from session
const authContext = await requireAuth(context.request);
const { user } = authContext;
// Get AbstraxionBackend instance
const abstraxionBackend = getAbstraxionBackend();
// Disconnect wallet
const result = await abstraxionBackend.disconnect(user.id);
if (!result.success) {
throw new ApiException(
result.error || "Disconnect failed",
400,
"DISCONNECT_FAILED",
);
}
return result;
},
{
rateLimit: "normal",
allowedMethods: ["DELETE"],
},
);
// src/app/api/callback/grant_session/route.ts
import { getAbstraxionBackend } from "@/lib/xion/abstraxion-backend";
import { grantSessionCallbackSchema } from "@/lib/validation";
import { createApiWrapper, handleRedirectResponse } from "@/lib/api-wrapper";
import { ApiException } from "@/lib/api-response";
export const dynamic = "force-dynamic";
export const GET = createApiWrapper(
async (context) => {
const { validatedData } = context;
const { granted, granter, state } = validatedData;
// Get AbstraxionBackend instance
const abstraxionBackend = getAbstraxionBackend();
// Handle callback
const result = await abstraxionBackend.handleCallback({
granted,
granter,
state,
});
if (!result.success) {
throw new ApiException(
result.error || "Callback failed",
400,
"CALLBACK_FAILED",
);
}
// Use the generic redirect handler
return handleRedirectResponse(result, result.grantedRedirectUrl);
},
{
schema: grantSessionCallbackSchema,
schemaType: "query",
rateLimit: "strict",
allowedMethods: ["GET"],
},
);
// src/app/api/wallet/transaction/send/route.ts
import { getAbstraxionBackend } from "@/lib/xion/abstraxion-backend";
import { createApiWrapper } from "@/lib/api-wrapper";
import { requireAuth } from "@/lib/auth-middleware";
import {
sendTransactionSchema,
SendTransactionRequest,
} from "@/lib/validation";
import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx";
export const dynamic = "force-dynamic";
export const POST = createApiWrapper(
async (context) => {
const { validatedData } = context;
const { to, amount, denom } = validatedData as SendTransactionRequest;
// Get authenticated user from session
const authContext = await requireAuth(context.request);
const { user } = authContext;
// Get AbstraxionBackend instance
const abstraxionBackend = getAbstraxionBackend();
// Check if user has an active wallet connection
const status = await abstraxionBackend.checkStatus(user.id);
if (!status.connected || !status.metaAccountAddress) {
throw new Error("No active wallet connection found");
}
try {
// Start AbstraxionAuth to get the signing client
const abstraxionAuth = await abstraxionBackend.startAbstraxionBackendAuth(
user.id,
context.request as any,
);
const signer = await abstraxionAuth.getSigner(
abstraxionBackend.gasPriceDefault,
);
// Convert amount to micro units
const amountNum = parseFloat(amount);
const microAmount = Math.floor(amountNum * 1_000_000).toString();
const denomMicro = denom === "XION" ? "uxion" : "uusdc";
// Create the bank send message
const msgSend: MsgSend = {
fromAddress: status.metaAccountAddress,
toAddress: to,
amount: [
{
denom: denomMicro,
amount: microAmount,
},
],
};
// Sign and broadcast the transaction
const result = await signer.signAndBroadcast(
status.metaAccountAddress,
[
{
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: msgSend,
},
],
"auto",
);
return {
transactionHash: result.transactionHash,
fromAddress: status.metaAccountAddress,
toAddress: to,
amount: amount,
denom: denom,
gasUsed: result.gasUsed?.toString(),
gasWanted: result.gasWanted?.toString(),
};
} catch (error) {
console.error("Error sending transaction:", error);
throw new Error(
error instanceof Error ? error.message : "Failed to send transaction",
);
}
},
{
schema: sendTransactionSchema,
schemaType: "body",
rateLimit: "normal",
allowedMethods: ["POST"],
},
);
# Test wallet connection
curl -X POST http://localhost:3000/api/wallet/connect \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"permissions": {
"contracts": ["contract1"],
"bank": [{"denom": "XION", "amount": "1000"}]
}
}'
# Test wallet status
curl -X GET http://localhost:3000/api/wallet/status \
-H "Authorization: Bearer <token>"
# Test transaction
curl -X POST http://localhost:3000/api/wallet/transaction/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"to": "xion1...",
"amount": "100",
"denom": "XION"
}'