diff --git a/backend/requests/wallet.http b/backend/requests/wallet.http new file mode 100644 index 0000000..d489bcd --- /dev/null +++ b/backend/requests/wallet.http @@ -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"); + }); + }); +%} diff --git a/backend/src/StealthBackend/src/main/java/org/backend/stealth/controller/WalletResource.java b/backend/src/StealthBackend/src/main/java/org/backend/stealth/controller/WalletResource.java new file mode 100644 index 0000000..108f9b6 --- /dev/null +++ b/backend/src/StealthBackend/src/main/java/org/backend/stealth/controller/WalletResource.java @@ -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 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 vulnerabilities + ) {} + + public record SummaryData(int total, int clean, int vulnerable) {} + + public record ReportResponse(String descriptor, SummaryData summary, List 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(); + } +} diff --git a/backend/src/StealthBackend/src/main/java/org/backend/stealth/mocks/WalletMockData.java b/backend/src/StealthBackend/src/main/java/org/backend/stealth/mocks/WalletMockData.java new file mode 100644 index 0000000..00162af --- /dev/null +++ b/backend/src/StealthBackend/src/main/java/org/backend/stealth/mocks/WalletMockData.java @@ -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 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); + } +} diff --git a/frontend/src/services/walletService.js b/frontend/src/services/walletService.js index e83b36d..7fae2d3 100644 --- a/frontend/src/services/walletService.js +++ b/frontend/src/services/walletService.js @@ -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() } diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 9ffcc67..caec31c 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -3,4 +3,9 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], + server: { + proxy: { + '/api': 'http://localhost:8080' + } + } })