Feat: Wire frontend to backend, add wallet API endpoints

- Replace frontend mock with real fetch calls to POST /api/wallet/analyze and GET /api/wallet/{id}/utxos
- Add Vite dev proxy for /api to avoid CORS in development
- Implement WalletResource.java with the two endpoints
- Add WalletMockData.java with the 5-UTXO dataset
- Configure CORS and port in application.properties
- Add backend/requests/wallet.http with kulala tests (29 assertions, all passing)
This commit is contained in:
LORDBABUINO
2026-02-26 23:14:19 -03:00
parent e6a8e77134
commit 1f7ecf321c
5 changed files with 207 additions and 4 deletions
+49
View File
@@ -0,0 +1,49 @@
@baseUrl = http://localhost:8080
@descriptor = wpkh([a1b2c3d4/84h/0h/0h]xpub6CatWdiZynkCminahu8Gmr7FAVnQXBTSMaBxn6qmBNkdm9tDkFzWmjmDrLBCQSTa7BHgpEjCXzMTCyDsQLSmcGYJHBB7cTwpqLNRKGP47uw/0/*)#qwer1234
### Analyze wallet
# @name analyze
POST {{baseUrl}}/api/wallet/analyze
Content-Type: application/json
{
"descriptor": "{{descriptor}}"
}
> {%
client.test("status is 200", function() {
client.assert(response.status === 200, "expected 200");
});
client.test("response has analysisId", function() {
client.assert(typeof response.body.analysisId === "string", "expected analysisId string");
client.assert(response.body.analysisId.length > 0, "expected non-empty analysisId");
});
%}
### Get UTXOs
GET {{baseUrl}}/api/wallet/{{analyze.response.body.$.analysisId}}/utxos
> {%
client.test("status is 200", function() {
client.assert(response.status === 200, "expected 200");
});
client.test("response has descriptor", function() {
client.assert(typeof response.body.descriptor === "string", "expected descriptor string");
});
client.test("summary totals are correct", function() {
client.assert(response.body.summary.total === 5, "expected 5 utxos");
client.assert(response.body.summary.clean === 1, "expected 1 clean");
client.assert(response.body.summary.vulnerable === 4, "expected 4 vulnerable");
});
client.test("utxos array has 5 items", function() {
client.assert(response.body.utxos.length === 5, "expected 5 utxos in array");
});
client.test("each utxo has required fields", function() {
response.body.utxos.forEach(function(utxo) {
client.assert(typeof utxo.txid === "string", "expected txid");
client.assert(typeof utxo.address === "string", "expected address");
client.assert(typeof utxo.amountBtc === "number", "expected amountBtc");
client.assert(Array.isArray(utxo.vulnerabilities), "expected vulnerabilities array");
});
});
%}
@@ -0,0 +1,67 @@
package org.backend.stealth.controller;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.backend.stealth.mocks.WalletMockData;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Path("/api/wallet")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class WalletResource {
private static final Map<String, String> sessions = new ConcurrentHashMap<>();
// DTOs
public record AnalyzeRequest(String descriptor) {}
public record AnalyzeResponse(String analysisId) {}
public record VulnerabilityData(String type, String severity, String description) {}
public record UtxoData(
String txid,
int vout,
String address,
double amountBtc,
int confirmations,
List<VulnerabilityData> vulnerabilities
) {}
public record SummaryData(int total, int clean, int vulnerable) {}
public record ReportResponse(String descriptor, SummaryData summary, List<UtxoData> utxos) {}
// Endpoints
@POST
@Path("/analyze")
public Response analyze(AnalyzeRequest req) {
if (req == null || req.descriptor() == null || req.descriptor().isBlank()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "descriptor is required"))
.build();
}
String analysisId = UUID.randomUUID().toString();
sessions.put(analysisId, req.descriptor());
return Response.ok(new AnalyzeResponse(analysisId)).build();
}
@GET
@Path("/{analysisId}/utxos")
public Response getUtxos(@PathParam("analysisId") String analysisId) {
String descriptor = sessions.get(analysisId);
if (descriptor == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "analysisId not found"))
.build();
}
return Response.ok(WalletMockData.buildReport(descriptor)).build();
}
}
@@ -0,0 +1,75 @@
package org.backend.stealth.mocks;
import org.backend.stealth.controller.WalletResource.ReportResponse;
import org.backend.stealth.controller.WalletResource.SummaryData;
import org.backend.stealth.controller.WalletResource.UtxoData;
import org.backend.stealth.controller.WalletResource.VulnerabilityData;
import java.util.List;
public class WalletMockData {
public static ReportResponse buildReport(String descriptor) {
List<UtxoData> utxos = List.of(
new UtxoData(
"3a7f2b8c1d4e9f0a6b5c2d7e8f3a1b4c9d2e5f0a7b8c1d4e9f2a5b6c3d7e8f1",
0,
"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
0.05234891,
1842,
List.of()
),
new UtxoData(
"b4c8e2f6a1d5b9c3e7f1a5d9b3c7e1f5a9d3b7c1e5f9a3d7b1c5e9f3a7d1b5",
1,
"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
0.00023000,
312,
List.of(
new VulnerabilityData("DUST_SPEND", "medium",
"This UTXO is near the dust threshold. Spending it may cost more in fees than its value, and dust outputs are often used as tracking vectors by chain surveillance companies."),
new VulnerabilityData("ADDRESS_REUSE", "high",
"This address has received funds in 3 separate transactions. Address reuse breaks the one-time-address privacy model and allows observers to link all deposits to the same wallet.")
)
),
new UtxoData(
"f9e3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9f3d7c1b5a9",
0,
"bc1q9h7garjcdkl4h5khfz2yxkhsmhep5j7g4cjtch",
0.12000000,
4521,
List.of(
new VulnerabilityData("CONSOLIDATION", "medium",
"This UTXO was created by consolidating 7 inputs in a single transaction. Consolidation reveals that all input addresses belong to the same wallet, reducing privacy significantly.")
)
),
new UtxoData(
"2c6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d6e0a4f8b2d",
2,
"bc1qm34mqf4vn8f5vhf0q3djg2zuzfm9aap6e3n4j",
0.87654321,
98,
List.of(
new VulnerabilityData("CIOH", "high",
"Common Input Ownership Heuristic (CIOH): this UTXO was spent alongside UTXOs from different derivation paths in the same transaction, strongly suggesting to analysts that all inputs share a common owner."),
new VulnerabilityData("ADDRESS_REUSE", "high",
"This address appears in 5 transactions as both sender and receiver, a pattern that severely compromises wallet privacy and makes cluster analysis trivial.")
)
),
new UtxoData(
"7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d1b5e9f3a7d",
0,
"bc1qcr8te4kr609gcawutmrza0j4xv80jy8zeqchgx",
0.00500000,
2103,
List.of(
new VulnerabilityData("DUST_SPEND", "low",
"A small dust amount was received at this address in a prior transaction. While the dust has not been spent, its presence could be used to track this UTXO if included in a future transaction.")
)
)
);
SummaryData summary = new SummaryData(5, 1, 4);
return new ReportResponse(descriptor, summary, utxos);
}
}
+11 -4
View File
@@ -1,6 +1,13 @@
import { mockReport } from '../mocks/mockData'
export const analyzeWallet = async (descriptor) => {
await new Promise((r) => setTimeout(r, 4000))
return mockReport
const res1 = await fetch('/api/wallet/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ descriptor }),
})
if (!res1.ok) throw new Error('Analysis request failed')
const { analysisId } = await res1.json()
const res2 = await fetch(`/api/wallet/${analysisId}/utxos`)
if (!res2.ok) throw new Error('Failed to fetch report')
return res2.json()
}
+5
View File
@@ -3,4 +3,9 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:8080'
}
}
})