Skip to content

Commit 59ea005

Browse files
Merge pull request #52 from Tebrihk/Calendar
calendar
2 parents bda391a + e72100b commit 59ea005

File tree

4 files changed

+787
-0
lines changed

4 files changed

+787
-0
lines changed

index.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const express = require('express');
22
const cors = require('cors');
3+
const AvailabilityService = require('./services/availabilityService');
34
const AutoReclaimWorker = require('./services/autoReclaimWorker');
45

56
const app = express();
@@ -16,6 +17,86 @@ app.get('/', (req, res) => {
1617
});
1718
});
1819

20+
app.get('/api/asset/:id/availability', async (req, res) => {
21+
try {
22+
const { id } = req.params;
23+
24+
if (!id || isNaN(id)) {
25+
return res.status(400).json({
26+
error: 'Invalid asset ID. Must be a number.',
27+
code: 'INVALID_ASSET_ID'
28+
});
29+
}
30+
31+
const availability = await availabilityService.getAssetAvailability(id);
32+
33+
res.json({
34+
success: true,
35+
data: availability
36+
});
37+
38+
} catch (error) {
39+
console.error(`Error fetching availability for asset ${req.params.id}:`, error);
40+
41+
res.status(500).json({
42+
error: 'Failed to fetch asset availability',
43+
code: 'FETCH_ERROR',
44+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
45+
});
46+
}
47+
});
48+
49+
app.get('/api/assets/availability', async (req, res) => {
50+
try {
51+
const { ids } = req.query;
52+
53+
if (ids) {
54+
const assetIds = ids.split(',').map(id => id.trim()).filter(id => id && !isNaN(id));
55+
56+
if (assetIds.length === 0) {
57+
return res.status(400).json({
58+
error: 'No valid asset IDs provided',
59+
code: 'INVALID_ASSET_IDS'
60+
});
61+
}
62+
63+
const availability = await availabilityService.getMultipleAssetAvailability(assetIds);
64+
65+
res.json({
66+
success: true,
67+
data: availability
68+
});
69+
} else {
70+
const availability = await availabilityService.getAllAssetsAvailability();
71+
72+
res.json({
73+
success: true,
74+
data: availability
75+
});
76+
}
77+
78+
} catch (error) {
79+
console.error('Error fetching assets availability:', error);
80+
81+
res.status(500).json({
82+
error: 'Failed to fetch assets availability',
83+
code: 'FETCH_ERROR',
84+
details: process.env.NODE_ENV === 'development' ? error.message : undefined
85+
});
86+
}
87+
});
88+
89+
if (require.main === module) {
90+
const availabilityService = new AvailabilityService();
91+
92+
availabilityService.initialize().then(() => {
93+
app.locals.availabilityService = availabilityService;
94+
app.listen(port, () => {
95+
console.log(`LeaseFlow Backend listening at http://localhost:${port}`);
96+
console.log('Availability Service started');
97+
});
98+
}).catch(error => {
99+
console.error('Failed to initialize Availability Service:', error);
19100
app.get('/status', (req, res) => {
20101
res.json({
21102
auto_reclaim_worker: 'Active',
@@ -39,4 +120,5 @@ if (require.main === module) {
39120
});
40121
}
41122

123+
const availabilityService = new AvailabilityService();
42124
module.exports = app;

services/availabilityService.js

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
const algosdk = require('algosdk');
2+
3+
class AvailabilityService {
4+
constructor() {
5+
this.contractId = 'CAEGD57WVTVQSYWYB23AISBW334QO7WNA5XQ56S45GH6BP3D2AVHKUG4';
6+
this.algodClient = null;
7+
this.SECONDS_PER_BLOCK = 4.5; // Algorand average block time
8+
}
9+
10+
async initialize() {
11+
require('dotenv').config();
12+
13+
const algodToken = process.env.ALGOD_TOKEN || '';
14+
const algodServer = process.env.ALGOD_SERVER || 'https://testnet-api.algonode.cloud';
15+
const algodPort = parseInt(process.env.ALGOD_PORT) || 443;
16+
17+
this.algodClient = new algosdk.Algodv2(algodToken, algodServer, algodPort);
18+
console.log('AvailabilityService initialized');
19+
}
20+
21+
async getAssetAvailability(assetId) {
22+
if (!this.algodClient) {
23+
throw new Error('Service not initialized');
24+
}
25+
26+
try {
27+
const appInfo = await this.algodClient.getApplicationByID(parseInt(this.contractId)).do();
28+
const globalState = appInfo.params['global-state'] || [];
29+
30+
const leaseData = this.extractLeaseDataForAsset(globalState, assetId);
31+
32+
if (!leaseData) {
33+
return {
34+
assetId,
35+
status: 'available',
36+
currentLease: null,
37+
expiryDate: null,
38+
nextAvailableDate: null
39+
};
40+
}
41+
42+
const expiryDate = this.calculateExpiryDate(leaseData);
43+
const isExpired = this.isLeaseExpired(leaseData, expiryDate);
44+
45+
return {
46+
assetId,
47+
status: isExpired ? 'available' : 'leased',
48+
currentLease: {
49+
tenant: leaseData.tenant || 'unknown',
50+
startDate: new Date(leaseData.start_timestamp * 1000).toISOString(),
51+
renterBalance: leaseData.renter_balance || 0
52+
},
53+
expiryDate: expiryDate ? expiryDate.toISOString() : null,
54+
nextAvailableDate: expiryDate && !isExpired ? expiryDate.toISOString() : new Date().toISOString()
55+
};
56+
57+
} catch (error) {
58+
console.error(`Error fetching availability for asset ${assetId}:`, error);
59+
throw error;
60+
}
61+
}
62+
63+
extractLeaseDataForAsset(globalState, assetId) {
64+
const leaseKey = `lease_${assetId}`;
65+
66+
for (const state of globalState) {
67+
const key = Buffer.from(state.key, 'base64').toString('utf8');
68+
69+
if (key === leaseKey) {
70+
return this.parseLeaseData(state.value);
71+
}
72+
}
73+
74+
return null;
75+
}
76+
77+
parseLeaseData(value) {
78+
if (value.type === 1) {
79+
const intValue = parseInt(value.uint);
80+
return { renter_balance: intValue };
81+
}
82+
83+
if (value.type === 2) {
84+
const byteValue = Buffer.from(value.bytes, 'base64').toString('utf8');
85+
try {
86+
return JSON.parse(byteValue);
87+
} catch {
88+
return { renter_balance: 0 };
89+
}
90+
}
91+
92+
return { renter_balance: 0 };
93+
}
94+
95+
calculateExpiryDate(leaseData) {
96+
if (!leaseData.start_timestamp || !leaseData.duration_blocks) {
97+
return null;
98+
}
99+
100+
const startTimestamp = leaseData.start_timestamp;
101+
const durationBlocks = leaseData.duration_blocks;
102+
const durationSeconds = durationBlocks * this.SECONDS_PER_BLOCK;
103+
const expiryTimestamp = startTimestamp + durationSeconds;
104+
105+
return new Date(expiryTimestamp * 1000);
106+
}
107+
108+
isLeaseExpired(leaseData, expiryDate) {
109+
if (!expiryDate) {
110+
return leaseData.renter_balance <= 0;
111+
}
112+
113+
const now = new Date();
114+
const balanceExpired = leaseData.renter_balance <= 0;
115+
const timeExpired = now > expiryDate;
116+
117+
return balanceExpired || timeExpired;
118+
}
119+
120+
async getMultipleAssetAvailability(assetIds) {
121+
const availabilityPromises = assetIds.map(id =>
122+
this.getAssetAvailability(id).catch(error => ({
123+
assetId: id,
124+
status: 'error',
125+
error: error.message
126+
}))
127+
);
128+
129+
return Promise.all(availabilityPromises);
130+
}
131+
132+
async getAllAssetsAvailability() {
133+
if (!this.algodClient) {
134+
throw new Error('Service not initialized');
135+
}
136+
137+
try {
138+
const appInfo = await this.algodClient.getApplicationByID(parseInt(this.contractId)).do();
139+
const globalState = appInfo.params['global-state'] || [];
140+
141+
const assetIds = this.extractAllAssetIds(globalState);
142+
143+
if (assetIds.length === 0) {
144+
return [];
145+
}
146+
147+
return await this.getMultipleAssetAvailability(assetIds);
148+
149+
} catch (error) {
150+
console.error('Error fetching all assets availability:', error);
151+
throw error;
152+
}
153+
}
154+
155+
extractAllAssetIds(globalState) {
156+
const assetIds = new Set();
157+
158+
globalState.forEach(state => {
159+
const key = Buffer.from(state.key, 'base64').toString('utf8');
160+
161+
if (key.startsWith('lease_')) {
162+
const assetId = key.replace('lease_', '');
163+
assetIds.add(assetId);
164+
}
165+
});
166+
167+
return Array.from(assetIds);
168+
}
169+
}
170+
171+
module.exports = AvailabilityService;

0 commit comments

Comments
 (0)