Skip to content

Commit 201e263

Browse files
Merge pull request #81 from mijinummi/feat/lease-badge-minting
On-Chain Lease Badge Minting Service (#40)
2 parents 141d61a + cde581a commit 201e263

File tree

5 files changed

+136
-0
lines changed

5 files changed

+136
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const badgeService = require('../services/badge.service');
2+
3+
class BadgeController {
4+
async mintBadge(req, res) {
5+
try {
6+
const { leaseId } = req.body;
7+
const userId = req.user.id;
8+
const badge = await badgeService.mintBadge(userId, leaseId);
9+
res.status(201).json(badge);
10+
} catch (err) {
11+
res.status(400).json({ error: err.message });
12+
}
13+
}
14+
15+
async listBadges(req, res) {
16+
try {
17+
const userId = req.user.id;
18+
const badges = await badgeService.listBadges(userId);
19+
res.json(badges);
20+
} catch (err) {
21+
res.status(500).json({ error: err.message });
22+
}
23+
}
24+
}
25+
26+
module.exports = new BadgeController();

src/jobs/badge-mint.job.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const badgeService = require('../services/badge.service');
2+
const db = require('../db');
3+
4+
async function runBadgeMintJob() {
5+
const leases = await db('leases')
6+
.where({ status: 'closed', durationMonths: 12 })
7+
.andWhereNotExists(db('tenant_badges').whereRaw('tenant_badges.leaseId = leases.id'));
8+
9+
for (const lease of leases) {
10+
try {
11+
await badgeService.mintBadge(lease.userId, lease.id);
12+
console.log(`Minted badge for lease ${lease.id}`);
13+
} catch (err) {
14+
console.error(`Failed to mint badge for lease ${lease.id}: ${err.message}`);
15+
}
16+
}
17+
}
18+
19+
module.exports = { runBadgeMintJob };

src/routes/badge.routes.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const badgeController = require('../controllers/badge.controller');
4+
const { authMiddleware } = require('../services/auth.service');
5+
6+
router.post('/badges/mint', authMiddleware, (req, res) => badgeController.mintBadge(req, res));
7+
router.get('/badges', authMiddleware, (req, res) => badgeController.listBadges(req, res));
8+
9+
module.exports = router;

src/services/badge.service.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const stellar = require('stellar-sdk');
2+
const db = require('../db');
3+
4+
class BadgeService {
5+
constructor() {
6+
this.server = new stellar.Server('https://horizon.stellar.org');
7+
this.issuerSecret = process.env.BADGE_ISSUER_SECRET;
8+
this.issuerKeypair = stellar.Keypair.fromSecret(this.issuerSecret);
9+
}
10+
11+
async mintBadge(userId, leaseId) {
12+
// Verify lease completion
13+
const lease = await db('leases').where({ id: leaseId, userId }).first();
14+
if (!lease || lease.status !== 'closed' || lease.durationMonths < 12) {
15+
throw new Error('Lease not eligible for badge');
16+
}
17+
18+
const tenant = await db('tenants').where({ id: userId }).first();
19+
if (!tenant) throw new Error('Tenant not found');
20+
21+
// Build NFT asset
22+
const assetCode = `LEASEBADGE-${leaseId}`;
23+
const asset = new stellar.Asset(assetCode, this.issuerKeypair.publicKey());
24+
25+
// Create trustline and payment (simplified)
26+
const account = await this.server.loadAccount(tenant.stellarAddress);
27+
const tx = new stellar.TransactionBuilder(account, {
28+
fee: await this.server.fetchBaseFee(),
29+
networkPassphrase: stellar.Networks.PUBLIC,
30+
})
31+
.addOperation(stellar.Operation.changeTrust({ asset }))
32+
.addOperation(stellar.Operation.payment({
33+
destination: tenant.stellarAddress,
34+
asset,
35+
amount: '1',
36+
}))
37+
.setTimeout(30)
38+
.build();
39+
40+
tx.sign(this.issuerKeypair);
41+
await this.server.submitTransaction(tx);
42+
43+
// Archive badge record
44+
const [badge] = await db('tenant_badges')
45+
.insert({
46+
userId,
47+
leaseId,
48+
assetCode,
49+
mintedAt: new Date(),
50+
})
51+
.returning('*');
52+
53+
return badge;
54+
}
55+
56+
async listBadges(userId) {
57+
return db('tenant_badges').where({ userId });
58+
}
59+
}
60+
61+
module.exports = new BadgeService();

tests/badge.e2e-spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const request = require('supertest');
2+
const app = require('../index');
3+
4+
describe('Lease Badge Minting API', () => {
5+
it('POST /badges/mint mints badge for eligible lease', async () => {
6+
const res = await request(app)
7+
.post('/badges/mint')
8+
.set('Authorization', 'Bearer tenantToken')
9+
.send({ leaseId: 'lease-123' });
10+
expect(res.status).toBe(201);
11+
expect(res.body).toHaveProperty('assetCode');
12+
});
13+
14+
it('GET /badges lists tenant badges', async () => {
15+
const res = await request(app)
16+
.get('/badges')
17+
.set('Authorization', 'Bearer tenantToken');
18+
expect(res.status).toBe(200);
19+
expect(Array.isArray(res.body)).toBe(true);
20+
});
21+
});

0 commit comments

Comments
 (0)