Skip to content

Commit e72100b

Browse files
Merge branch 'main' into Calendar
2 parents 819f132 + bda391a commit e72100b

File tree

6 files changed

+427
-2
lines changed

6 files changed

+427
-2
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Algorand Network Configuration
2+
ALGOD_TOKEN=
3+
ALGOD_SERVER=https://testnet-api.algonode.cloud
4+
ALGOD_PORT=443
5+
6+
# Owner Account (for executing reclaim transactions)
7+
OWNER_MNEMONIC=

index.js

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

56
const app = express();
67
const port = 3000;
@@ -96,6 +97,25 @@ if (require.main === module) {
9697
});
9798
}).catch(error => {
9899
console.error('Failed to initialize Availability Service:', error);
100+
app.get('/status', (req, res) => {
101+
res.json({
102+
auto_reclaim_worker: 'Active',
103+
schedule: 'Every 10 minutes',
104+
last_check: new Date().toISOString()
105+
});
106+
});
107+
108+
if (require.main === module) {
109+
const autoReclaimWorker = new AutoReclaimWorker();
110+
111+
autoReclaimWorker.initialize().then(() => {
112+
autoReclaimWorker.start();
113+
app.listen(port, () => {
114+
console.log(`LeaseFlow Backend listening at http://localhost:${port}`);
115+
console.log('Auto-Reclaim Worker started');
116+
});
117+
}).catch(error => {
118+
console.error('Failed to initialize Auto-Reclaim Worker:', error);
99119
process.exit(1);
100120
});
101121
}

package-lock.json

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"algosdk": "^2.0.0",
1212
"cors": "^2.8.6",
1313
"dotenv": "^17.3.1",
14-
"express": "^5.2.1"
14+
"express": "^5.2.1",
15+
"node-cron": "^3.0.3"
1516
},
1617
"devDependencies": {
1718
"jest": "^30.3.0",

services/autoReclaimWorker.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
const cron = require('node-cron');
2+
const algosdk = require('algosdk');
3+
4+
class AutoReclaimWorker {
5+
constructor() {
6+
this.contractId = 'CAEGD57WVTVQSYWYB23AISBW334QO7WNA5XQ56S45GH6BP3D2AVHKUG4';
7+
this.algodClient = null;
8+
this.isRunning = false;
9+
}
10+
11+
async initialize() {
12+
require('dotenv').config();
13+
14+
const algodToken = process.env.ALGOD_TOKEN || '';
15+
const algodServer = process.env.ALGOD_SERVER || 'https://testnet-api.algonode.cloud';
16+
const algodPort = parseInt(process.env.ALGOD_PORT) || 443;
17+
18+
this.algodClient = new algosdk.Algodv2(algodToken, algodServer, algodPort);
19+
console.log('AutoReclaimWorker initialized');
20+
}
21+
22+
async checkExpiredLeases() {
23+
if (!this.algodClient) {
24+
throw new Error('Worker not initialized');
25+
}
26+
27+
try {
28+
const appInfo = await this.algodClient.getApplicationByID(parseInt(this.contractId)).do();
29+
30+
const globalState = appInfo.params['global-state'] || [];
31+
const leases = this.extractLeasesFromGlobalState(globalState);
32+
33+
const expiredLeases = leases.filter(lease =>
34+
lease.renter_balance && lease.renter_balance <= 0
35+
);
36+
37+
console.log(`Found ${expiredLeases.length} expired leases out of ${leases.length} total leases`);
38+
39+
return expiredLeases;
40+
} catch (error) {
41+
console.error('Error checking expired leases:', error);
42+
throw error;
43+
}
44+
}
45+
46+
extractLeasesFromGlobalState(globalState) {
47+
const leases = [];
48+
49+
globalState.forEach(state => {
50+
const key = Buffer.from(state.key, 'base64').toString('utf8');
51+
52+
if (key.startsWith('lease_')) {
53+
const leaseData = this.parseLeaseData(state.value);
54+
leases.push({
55+
id: key.replace('lease_', ''),
56+
...leaseData
57+
});
58+
}
59+
});
60+
61+
return leases.filter(lease => lease.renter_balance !== undefined);
62+
}
63+
64+
parseLeaseData(value) {
65+
if (value.type === 1) {
66+
const intValue = parseInt(value.uint);
67+
return { renter_balance: intValue };
68+
}
69+
70+
if (value.type === 2) {
71+
const byteValue = Buffer.from(value.bytes, 'base64').toString('utf8');
72+
try {
73+
return JSON.parse(byteValue);
74+
} catch {
75+
return { renter_balance: 0 };
76+
}
77+
}
78+
79+
return { renter_balance: 0 };
80+
}
81+
82+
async executeReclaim(leaseId) {
83+
try {
84+
const senderAccount = this.getOwnerAccount();
85+
86+
const suggestedParams = await this.algodClient.getTransactionParams().do();
87+
88+
const appCallTxn = algosdk.makeApplicationCallTxnFromObject({
89+
from: senderAccount.addr,
90+
appIndex: parseInt(this.contractId),
91+
appArgs: [new Uint8Array(Buffer.from('reclaim'))],
92+
foreignAssets: [],
93+
accounts: [],
94+
appForeignApps: [],
95+
suggestedParams,
96+
onComplete: algosdk.OnApplicationComplete.NoOpOC
97+
});
98+
99+
const signedTxn = appCallTxn.signTxn(senderAccount.sk);
100+
const txId = appCallTxn.txID().toString();
101+
102+
await this.algodClient.sendRawTransaction(signedTxn).do();
103+
104+
const confirmedTxn = await algosdk.waitForConfirmation(this.algodClient, txId, 4);
105+
106+
console.log(`Successfully reclaimed lease ${leaseId}. Transaction ID: ${txId}`);
107+
return confirmedTxn;
108+
109+
} catch (error) {
110+
console.error(`Failed to reclaim lease ${leaseId}:`, error);
111+
throw error;
112+
}
113+
}
114+
115+
getOwnerAccount() {
116+
const ownerMnemonic = process.env.OWNER_MNEMONIC;
117+
if (!ownerMnemonic) {
118+
throw new Error('OWNER_MNEMONIC environment variable not set');
119+
}
120+
121+
return algosdk.mnemonicToSecretKey(ownerMnemonic);
122+
}
123+
124+
async runReclaimCycle() {
125+
if (this.isRunning) {
126+
console.log('Reclaim cycle already running, skipping...');
127+
return;
128+
}
129+
130+
this.isRunning = true;
131+
132+
try {
133+
console.log('Starting auto-reclaim cycle...');
134+
135+
const expiredLeases = await this.checkExpiredLeases();
136+
137+
for (const lease of expiredLeases) {
138+
console.log(`Reclaiming lease ${lease.id} with balance ${lease.renter_balance}`);
139+
await this.executeReclaim(lease.id);
140+
}
141+
142+
console.log('Auto-reclaim cycle completed');
143+
144+
} catch (error) {
145+
console.error('Error in reclaim cycle:', error);
146+
} finally {
147+
this.isRunning = false;
148+
}
149+
}
150+
151+
start() {
152+
console.log('Starting Auto-Reclaim Worker (every 10 minutes)...');
153+
154+
cron.schedule('*/10 * * * *', async () => {
155+
await this.runReclaimCycle();
156+
});
157+
158+
setTimeout(() => this.runReclaimCycle(), 5000);
159+
}
160+
}
161+
162+
module.exports = AutoReclaimWorker;

0 commit comments

Comments
 (0)