Compare commits
8 Commits
more-polle
...
frontend-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b228d9b57 | ||
|
|
08bc3e1ea2 | ||
|
|
a091d39c08 | ||
|
|
35699a41bd | ||
|
|
2e96982d3a | ||
|
|
e48de2d6b6 | ||
|
|
9414af8108 | ||
|
|
068ca2f834 |
@@ -16,14 +16,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
rpcTimeoutMS = 30000
|
||||
rpcMaxRetries = 3
|
||||
rpcRetryBaseMS = 2000
|
||||
rpcTimeoutMS = 30000
|
||||
rpcMaxRetries = 3
|
||||
rpcRetryBaseMS = 2000
|
||||
rpcMinIntervalMS = 1000
|
||||
)
|
||||
|
||||
type JsonRpcEthereum struct {
|
||||
rpcURL string
|
||||
client *http.Client
|
||||
rpcURL string
|
||||
client *http.Client
|
||||
lastCallAt time.Time
|
||||
}
|
||||
|
||||
func NewJsonRpcEthereum(rpcURL string) (*JsonRpcEthereum, error) {
|
||||
@@ -58,6 +60,8 @@ type rpcError struct {
|
||||
}
|
||||
|
||||
func (j *JsonRpcEthereum) callRPC(ctx context.Context, method string, params ...interface{}) (json.RawMessage, error) {
|
||||
j.throttle(ctx)
|
||||
|
||||
if params == nil {
|
||||
params = []interface{}{}
|
||||
}
|
||||
@@ -72,7 +76,25 @@ func (j *JsonRpcEthereum) callRPC(ctx context.Context, method string, params ...
|
||||
return nil, fmt.Errorf("marshal RPC request: %w", err)
|
||||
}
|
||||
|
||||
return j.callWithRetry(ctx, method, body)
|
||||
result, callErr := j.callWithRetry(ctx, method, body)
|
||||
j.lastCallAt = time.Now()
|
||||
|
||||
return result, callErr
|
||||
}
|
||||
|
||||
func (j *JsonRpcEthereum) throttle(ctx context.Context) {
|
||||
if j.lastCallAt.IsZero() {
|
||||
return
|
||||
}
|
||||
minInterval := time.Duration(rpcMinIntervalMS) * time.Millisecond
|
||||
elapsed := time.Since(j.lastCallAt)
|
||||
if elapsed >= minInterval {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(minInterval - elapsed):
|
||||
}
|
||||
}
|
||||
|
||||
// callWithRetry executes a JSON-RPC POST with exponential backoff on transient errors.
|
||||
|
||||
BIN
backend/poller
Executable file
BIN
backend/poller
Executable file
Binary file not shown.
BIN
frontend/public/koin_spin.mp4
Normal file
BIN
frontend/public/koin_spin.mp4
Normal file
Binary file not shown.
@@ -1,19 +1,38 @@
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background-image: url(/ping.png);
|
||||
background-size: 67%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.login-bg-video {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
filter: grayscale(85%);
|
||||
-webkit-filter: grayscale(85%);
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 400px;
|
||||
margin: 20px auto;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
padding-top: 8rem;
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-xl);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.login-heading {
|
||||
@@ -32,7 +51,6 @@
|
||||
padding: 0.75rem;
|
||||
font-size: 1.2rem;
|
||||
margin-top: 16px;
|
||||
;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
@@ -55,10 +73,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
.login-form-hidden {
|
||||
.login-card-fadein {
|
||||
opacity: 0;
|
||||
animation: fadeIn 2s ease-in-out forwards;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.login-form-visible {
|
||||
.login-tagline {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 630px;
|
||||
margin: 5rem auto 0;
|
||||
padding-left: 2rem;
|
||||
font-style: italic;
|
||||
color: #FFFFFF;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.login-interactive-fadein {
|
||||
opacity: 0;
|
||||
animation: fadeIn 2s ease-in-out forwards;
|
||||
animation-delay: 6s;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import Input from "../../components/Input";
|
||||
import "./Login.css";
|
||||
|
||||
export default function Login() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
@@ -36,12 +35,22 @@ export default function Login() {
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-card" onMouseEnter={() => setIsVisible(true)}>
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="login-bg-video"
|
||||
>
|
||||
<source src="/koin_spin.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
<div className="login-card login-card-fadein">
|
||||
<h1 className="login-heading">
|
||||
<span className="login-brand">Koin Ping</span> - Login
|
||||
</h1>
|
||||
|
||||
<div className={isVisible ? "login-form-visible" : "login-form-hidden"}>
|
||||
<div className="login-interactive-fadein">
|
||||
{error && <div className="alert alert--error">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -78,6 +87,11 @@ export default function Login() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="login-tagline login-card-fadein">
|
||||
A lightweight, on-chain monitoring system giving users real-time
|
||||
situational awareness over blockchain addresses.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user