# WhaleWatch - אפליקציה מלאה (HTML, CSS, Java)
---
# README.md
---
# פרויקט Demo של WhaleWatch
This project is a simple demo implementation of WhaleWatch:
- Frontend: index.html + styles.css + script.js (served as static resources)
- Backend: Spring Boot REST API that מספק נתוני "לווייתנים" מדומיינים ומספק SSE (Server-Sent Events) לנתונים בזמן-אמת
## דרישות
- Java 17+ (או 11 עם התאמות)
- Maven
## להרצה
1. התקנת Maven ו-Java
2. הרץ: `mvn spring-boot:run` בתיקיית הפרויקט
3. פתח בדפדפן: http://localhost:8080
---
# pom.xml
---
4.0.0
com.whalewatch
whalewatch
0.0.1-SNAPSHOT
jar
WhaleWatch Demo
17
3.1.4
org.springframework.boot
spring-boot-starter-web
com.fasterxml.jackson.core
jackson-databind
org.springframework.boot
spring-boot-maven-plugin
---
# src/main/java/com/whalewatch/WhalewatchApplication.java
---
package com.whalewatch;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WhalewatchApplication {
public static void main(String[] args) {
SpringApplication.run(WhalewatchApplication.class, args);
}
}
---
# src/main/java/com/whalewatch/model/WhaleTx.java
---
package com.whalewatch.model;
import java.time.Instant;
public class WhaleTx {
private String txId;
private String fromAddress;
private String toAddress;
private double amount; // amount in coin units
private String coin; // e.g., BTC, ETH
private Instant timestamp;
private String type; // BUY or SELL (inferred or tagged)
public WhaleTx() {}
public WhaleTx(String txId, String fromAddress, String toAddress, double amount, String coin, Instant timestamp, String type) {
this.txId = txId;
this.fromAddress = fromAddress;
this.toAddress = toAddress;
this.amount = amount;
this.coin = coin;
this.timestamp = timestamp;
this.type = type;
}
// getters and setters
public String getTxId() { return txId; }
public void setTxId(String txId) { this.txId = txId; }
public String getFromAddress() { return fromAddress; }
public void setFromAddress(String fromAddress) { this.fromAddress = fromAddress; }
public String getToAddress() { return toAddress; }
public void setToAddress(String toAddress) { this.toAddress = toAddress; }
public double getAmount() { return amount; }
public void setAmount(double amount) { this.amount = amount; }
public String getCoin() { return coin; }
public void setCoin(String coin) { this.coin = coin; }
public Instant getTimestamp() { return timestamp; }
public void setTimestamp(Instant timestamp) { this.timestamp = timestamp; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
}
---
# src/main/java/com/whalewatch/service/WhaleService.java
---
package com.whalewatch.service;
import com.whalewatch.model.WhaleTx;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
@Service
public class WhaleService {
private final List
recent = new CopyOnWriteArrayList<>();
private final Random rnd = new Random();
private final String[] coins = new String[]{"BTC","ETH","USDT","BNB"};
public WhaleService() {
// Seed some fake transactions
for (int i=0;i<8;i++) recent.add(generateRandomTx());
}
public WhaleTx generateRandomTx() {
String txId = UUID.randomUUID().toString();
String from = "addr_" + rnd.nextInt(99999);
String to = "addr_" + rnd.nextInt(99999);
String coin = coins[rnd.nextInt(coins.length)];
double amount = Math.round((rnd.nextDouble() * (coin.equals("BTC")?50:5000) + 10) * 100.0) / 100.0;
String type = rnd.nextBoolean()?"BUY":"SELL";
WhaleTx tx = new WhaleTx(txId, from, to, amount, coin, Instant.now(), type);
recent.add(0, tx);
if (recent.size() > 200) recent.remove(recent.size()-1);
return tx;
}
public List listRecent(int limit) {
return recent.subList(0, Math.min(limit, recent.size()));
}
// A tiny "AI-ish" predictor: looks at last N transactions and returns a naive signal
public String predictTrend() {
// naive implementation: count buys vs sells in recent
int buys=0,sells=0;
for (WhaleTx t : recent.subList(0, Math.min(recent.size(), 50))){
if ("BUY".equals(t.getType())) buys++; else sells++;
}
if (buys > sells + 5) return "BULLISH";
if (sells > buys + 5) return "BEARISH";
return "NEUTRAL";
}
}
---
# src/main/java/com/whalewatch/controller/WhaleController.java
---
package com.whalewatch.controller;
import com.whalewatch.model.WhaleTx;
import com.whalewatch.service.WhaleService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api")
public class WhaleController {
private final WhaleService whaleService;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public WhaleController(WhaleService whaleService) {
this.whaleService = whaleService;
}
@GetMapping("/whales")
public java.util.List getRecent(@RequestParam(defaultValue = "20") int limit) {
return whaleService.listRecent(limit);
}
// Simple SSE endpoint to push new whale txs every few seconds (demo)
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() {
SseEmitter emitter = new SseEmitter(0L); // no timeout
Runnable pushTask = () -> {
try {
WhaleTx tx = whaleService.generateRandomTx();
emitter.send(tx);
} catch (IOException e) {
emitter.completeWithError(e);
}
};
scheduler.scheduleAtFixedRate(pushTask, 1, 5, TimeUnit.SECONDS);
// complete emitter after a long timeout to avoid resource leak in demo (demo: 1 hour)
scheduler.schedule(() -> emitter.complete(), 1, TimeUnit.HOURS);
return emitter;
}
@GetMapping("/predict")
public java.util.Map predict() {
return java.util.Collections.singletonMap("trend", whaleService.predictTrend());
}
}
---
# src/main/resources/static/index.html
---
WhaleWatch - Demo
---
# src/main/resources/static/styles.css
---
* { box-sizing: border-box; font-family: Arial, Helvetica, sans-serif }
body { direction: rtl; margin: 0; background: #0f1724; color: #e6eef8 }
.top { padding: 18px; background: linear-gradient(90deg,#081226,#0b2233); display:flex; align-items:center; justify-content:space-between }
.top h1 { margin:0 }
.trend { padding:8px 12px; border-radius:8px; background: rgba(255,255,255,0.06) }
main { display:flex; gap:18px; padding:18px }
.panel { background: rgba(255,255,255,0.03); padding:12px; border-radius:10px; flex:1 }
.tx-list { list-style:none; padding:0; margin:0 }
.tx-item { padding:8px; border-bottom:1px solid rgba(255,255,255,0.03); display:flex; justify-content:space-between }
.tx-meta { font-size:12px; opacity:0.8 }
---
# src/main/resources/static/script.js
---
(async function(){
const txList = document.getElementById('txList');
const trendEl = document.getElementById('trend');
async function fetchRecent(){
const res = await fetch('/api/whales?limit=30');
const data = await res.json();
renderList(data);
}
function renderList(items){
txList.innerHTML = '';
items.forEach(it => {
const li = document.createElement('li');
li.className = 'tx-item';
const left = document.createElement('div');
left.innerHTML = `${it.coin} ${it.amount}`;
const right = document.createElement('div');
const time = new Date(it.timestamp);
right.innerHTML = `${it.type} • ${time.toLocaleTimeString()}
${it.fromAddress} → ${it.toAddress}
`;
li.appendChild(left);
li.appendChild(right);
txList.appendChild(li);
})
}
// SSE connection for real-time updates
const evtSource = new EventSource('/api/stream');
evtSource.onmessage = function(e) {
try {
const obj = JSON.parse(e.data);
// prepend
const li = document.createElement('li');
li.className = 'tx-item';
li.innerHTML = `${obj.coin} ${obj.amount}
${obj.type} • ${new Date(obj.timestamp).toLocaleTimeString()}
${obj.fromAddress} → ${obj.toAddress}
`;
txList.insertBefore(li, txList.firstChild);
// keep at most 50 items
while (txList.children.length > 50) txList.removeChild(txList.lastChild);
updateChartWith(obj);
// show desktop notification (if allowed)
if (Notification.permission === 'granted') {
new Notification('WhaleWatch', { body: `${obj.type} ${obj.amount} ${obj.coin}` });
}
} catch(err){ console.error(err); }
};
// request permission for notifications
if (Notification && Notification.permission !== 'granted') Notification.requestPermission();
// chart: simple canvas-based line of flows (sum of amounts by time)
const canvas = document.getElementById('flowChart');
const ctx = canvas.getContext('2d');
const points = [];
function updateChartWith(tx){
const now = Date.now();
points.push({t: now, value: tx.amount});
// keep last 30 points
while (points.length>30) points.shift();
drawChart();
}
function drawChart(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if (!points.length) return;
const max = Math.max(...points.map(p=>p.value));
ctx.beginPath();
for (let i=0;i
Go to Link