Skip to content

Commit

Permalink
progress
Browse files Browse the repository at this point in the history
  • Loading branch information
0age committed Dec 5, 2024
1 parent 3e92c7b commit 692e606
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 4 deletions.
105 changes: 105 additions & 0 deletions src/__tests__/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,5 +343,110 @@ describe('API Routes', () => {
expect(response.statusCode).toBe(404);
});
});

describe('GET /balance/:chainId/:lockId', () => {
it('should return balance information for valid lock', async () => {
const freshCompact = getFreshCompact();
const lockId = freshCompact.id.toString();

const response = await server.inject({
method: 'GET',
url: `/balance/1/${lockId}`,
headers: {
'x-session-id': sessionId,
},
});

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(result).toHaveProperty('allocatableBalance');
expect(result).toHaveProperty('allocatedBalance');
expect(result).toHaveProperty('balanceAvailableToAllocate');
expect(result).toHaveProperty('withdrawalStatus');

// Verify numeric string formats
expect(/^\d+$/.test(result.allocatableBalance)).toBe(true);
expect(/^\d+$/.test(result.allocatedBalance)).toBe(true);
expect(/^\d+$/.test(result.balanceAvailableToAllocate)).toBe(true);
expect(typeof result.withdrawalStatus).toBe('number');
});

it('should return 401 without session', async () => {
const freshCompact = getFreshCompact();
const lockId = freshCompact.id.toString();

const response = await server.inject({
method: 'GET',
url: `/balance/1/${lockId}`,
});

expect(response.statusCode).toBe(401);
});

it('should return 404 for non-existent lock', async () => {
const response = await server.inject({
method: 'GET',
url: '/balance/1/0x0000000000000000000000000000000000000000000000000000000000000000',
headers: {
'x-session-id': sessionId,
},
});

expect(response.statusCode).toBe(404);
});

it('should return zero balanceAvailableToAllocate when withdrawal enabled', async () => {
// Store original function
const originalRequest = graphqlClient.request;

// Mock GraphQL response with withdrawal status = 1
graphqlClient.request = async (): Promise<
AllocatorResponse & AccountDeltasResponse & AccountResponse
> => ({
allocator: {
supportedChains: {
items: [{ allocatorId: '1' }],
},
},
accountDeltas: {
items: [],
},
account: {
resourceLocks: {
items: [
{
withdrawalStatus: 1,
balance: '1000000000000000000000',
},
],
},
claims: {
items: [],
},
},
});

try {
const freshCompact = getFreshCompact();
const lockId = freshCompact.id.toString();

const response = await server.inject({
method: 'GET',
url: `/balance/1/${lockId}`,
headers: {
'x-session-id': sessionId,
},
});

expect(response.statusCode).toBe(200);
const result = JSON.parse(response.payload);
expect(result.balanceAvailableToAllocate).toBe('0');
expect(result.withdrawalStatus).toBe(1);
} finally {
// Restore original function
graphqlClient.request = originalRequest;
}
});
});
});
});
4 changes: 2 additions & 2 deletions src/__tests__/utils/test-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export async function createTestSession(

export const validCompact = {
// Set allocatorId to 1 in bits 160-251 (92 bits) and reset period index 7 in bits 252-254
id: (1n << 160n) | (7n << 252n), // Reset period index 7 = 2592000 seconds (30 days)
id: (BigInt(1) << BigInt(160)) | (BigInt(7) << BigInt(252)), // Reset period index 7 = 2592000 seconds (30 days)
arbiter: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
sponsor: validPayload.address,
// Create nonce where first 20 bytes match sponsor address
Expand All @@ -121,7 +121,7 @@ export const validCompact = {
};

// Helper to get fresh compact with current expiration
let compactCounter = 0n;
let compactCounter = BigInt(0);
export function getFreshCompact(): typeof validCompact {
const counter = compactCounter++;
// Create nonce as 32-byte hex where first 20 bytes are sponsor address
Expand Down
107 changes: 106 additions & 1 deletion src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ import {
getCompactByHash,
type CompactSubmission,
} from './compact';
import { getCompactDetails } from './graphql';
import { getAllocatedBalance } from './balance';

// Extend FastifyRequest to include session
// Declare db property on FastifyInstance
declare module 'fastify' {
interface FastifyInstance {
db: import('@electric-sql/pglite').PGlite;
}
interface FastifyRequest {
session?: {
id: string;
Expand Down Expand Up @@ -233,6 +238,106 @@ export async function setupRoutes(server: FastifyInstance): Promise<void> {
// Add authentication to all routes in this context
protectedRoutes.addHook('preHandler', authenticateRequest);

// Get available balance for a specific lock
protectedRoutes.get<{
Params: { chainId: string; lockId: string };
}>(
'/balance/:chainId/:lockId',
async (
request: FastifyRequest<{
Params: { chainId: string; lockId: string };
}>,
reply: FastifyReply
) => {
if (!request.session) {
reply.code(401);
return { error: 'Unauthorized' };
}

try {
const { chainId, lockId } = request.params;
const sponsor = request.session.address;

// Extract allocatorId from the lockId
const lockIdBigInt = BigInt(lockId);
const allocatorId =
(lockIdBigInt >> BigInt(160)) &
((BigInt(1) << BigInt(92)) - BigInt(1));

// Get details from GraphQL
const response = await getCompactDetails({
allocator: process.env.ALLOCATOR_ADDRESS!,
sponsor,
lockId,
chainId,
});

// Verify the resource lock exists
const resourceLock = response.account.resourceLocks.items[0];
if (!resourceLock) {
reply.code(404);
return { error: 'Resource lock not found' };
}

// Verify allocatorId matches
const graphqlAllocatorId =
response.allocator.supportedChains.items[0]?.allocatorId;
if (
!graphqlAllocatorId ||
BigInt(graphqlAllocatorId) !== allocatorId
) {
reply.code(400);
return { error: 'Invalid allocator ID' };
}

// Calculate pending balance
const pendingBalance = response.accountDeltas.items.reduce(
(sum, delta) => sum + BigInt(delta.delta),
BigInt(0)
);

// Calculate allocatable balance
const resourceLockBalance = BigInt(resourceLock.balance);
const allocatableBalance =
resourceLockBalance > pendingBalance
? resourceLockBalance - pendingBalance
: BigInt(0);

// Get allocated balance from database
const allocatedBalance = await getAllocatedBalance(
server.db,
sponsor,
chainId,
lockId,
response.account.claims.items.map((item) => item.claimHash)
);

// Calculate balance available to allocate
let balanceAvailableToAllocate = BigInt(0);
if (resourceLock.withdrawalStatus === 0) {
if (allocatedBalance < allocatableBalance) {
balanceAvailableToAllocate =
allocatableBalance - allocatedBalance;
}
}

return {
allocatableBalance: allocatableBalance.toString(),
allocatedBalance: allocatedBalance.toString(),
balanceAvailableToAllocate: balanceAvailableToAllocate.toString(),
withdrawalStatus: resourceLock.withdrawalStatus,
};
} catch (error) {
server.log.error('Failed to get balance:', error);
reply.code(500);
return {
error:
error instanceof Error ? error.message : 'Failed to get balance',
};
}
}
);

// Submit a new compact
protectedRoutes.post<{
Body: CompactSubmission;
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2022",
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
Expand Down

0 comments on commit 692e606

Please sign in to comment.