Compare commits
11 Commits
V0.01-ifra
...
V2.1-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da1d1b0d53 | ||
|
|
0ab66479d9 | ||
|
|
1129fb9341 | ||
|
|
4afe3f6d3e | ||
|
|
21bbd0fab1 | ||
|
|
bb3b5604a1 | ||
|
|
9faf7b5cef | ||
|
|
c984601352 | ||
|
|
8cdca7bb8c | ||
|
|
cbf2172fe4 | ||
|
|
a22040b705 |
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Production builds
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Vite and tooling caches
|
||||||
|
.vite/
|
||||||
|
.esbuild/
|
||||||
|
.rollup.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.env.production.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# TypeScript build info
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS metadata
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Misc secrets/keys (if present)
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
|
||||||
|
# Package tarballs
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
|
||||||
22
README.md
22
README.md
@@ -1,12 +1,14 @@
|
|||||||
# Noderunners Relay
|
# Noderunners Relay
|
||||||
|
|
||||||
A high-performance Nostr relay built by Bitcoiners, for Bitcoiners. This project provides a web interface for managing access to the Noderunners relay service.
|
A high-performance Nostr relay built by Bitcoiners, for Bitcoiners. This project provides a web interface for managing access to the Noderunners relay service.
|
||||||
|
```
|
||||||
|
wss://relay.noderunners.network
|
||||||
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🚀 Lightning-fast relay performance with strfry v1.0.3
|
|
||||||
- ⚡ Lightning Network integration for payments
|
- ⚡ Lightning Network integration for payments
|
||||||
- 🔒 Secure authentication with Nostr
|
- 🔒 Secure authentication with Nostr
|
||||||
- 💻 Modern, responsive web interface
|
- 💻 Modern, responsive web interface
|
||||||
@@ -116,20 +118,6 @@ You can combine iframe mode with URL-based authentication:
|
|||||||
<iframe src="https://your-relay-domain.com?iframe=1&npub=npub1..." width="100%" height="600px"></iframe>
|
<iframe src="https://your-relay-domain.com?iframe=1&npub=npub1..." width="100%" height="600px"></iframe>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supported NIPs
|
|
||||||
|
|
||||||
The relay supports the following Nostr Implementation Possibilities (NIPs):
|
|
||||||
- NIP-01: Basic protocol flow description
|
|
||||||
- NIP-02: Contact List and Petnames
|
|
||||||
- NIP-04: Encrypted Direct Messages
|
|
||||||
- NIP-09: Event Deletion
|
|
||||||
- NIP-11: Relay Information Document
|
|
||||||
- NIP-22: Event `created_at` Limits
|
|
||||||
- NIP-28: Public Chat
|
|
||||||
- NIP-40: Expiration Timestamp
|
|
||||||
- NIP-70: Relay Payment Info
|
|
||||||
- NIP-77: Lightning Network Relay Payment
|
|
||||||
|
|
||||||
## API Services
|
## API Services
|
||||||
|
|
||||||
### LNbits Integration
|
### LNbits Integration
|
||||||
@@ -156,10 +144,6 @@ The relay supports the following Nostr Implementation Possibilities (NIPs):
|
|||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For support, join our [Telegram group](https://t.me/noderunners) or visit [our website](https://noderunners.network).
|
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
- Built by the Noderunners community
|
- Built by the Noderunners community
|
||||||
|
|||||||
@@ -46,3 +46,4 @@ We reserve the right to terminate or suspend access to our service immediately,
|
|||||||
## 8. Changes to Terms
|
## 8. Changes to Terms
|
||||||
|
|
||||||
We reserve the right to modify these terms at any time. We will notify users of any changes by updating the date at the top of this page.
|
We reserve the right to modify these terms at any time. We will notify users of any changes by updating the date at the top of this page.
|
||||||
|
|
||||||
47
src/App.tsx
47
src/App.tsx
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
import { Home } from './pages/Home';
|
import { Home } from './pages/Home';
|
||||||
import { Login } from './pages/Login';
|
import { Login } from './pages/Login';
|
||||||
import { Dashboard } from './pages/Dashboard';
|
import { Dashboard } from './pages/Dashboard';
|
||||||
@@ -7,20 +7,43 @@ import { Payment } from './pages/Payment';
|
|||||||
import { ThankYou } from './pages/ThankYou';
|
import { ThankYou } from './pages/ThankYou';
|
||||||
import { Terms } from './pages/Terms';
|
import { Terms } from './pages/Terms';
|
||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
|
import { useStore } from './store/useStore';
|
||||||
|
|
||||||
|
// Wrapper component to handle auto-login logic
|
||||||
|
function AutoLoginHandler({ children }: { children: React.ReactNode }) {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { user, setUser } = useStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlPubkey = searchParams.get('npub') || searchParams.get('pubkey');
|
||||||
|
const isIframe = searchParams.get('iframe') === '1';
|
||||||
|
|
||||||
|
// Auto-login if pubkey/npub is in URL and user isn't already logged in
|
||||||
|
if (urlPubkey && !user) {
|
||||||
|
setUser({ pubkey: urlPubkey, isWhitelisted: false });
|
||||||
|
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||||
|
}
|
||||||
|
}, [searchParams, user, setUser, navigate]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<AutoLoginHandler>
|
||||||
<Route path="/" element={<Layout />}>
|
<Routes>
|
||||||
<Route index element={<Home />} />
|
<Route path="/" element={<Layout />}>
|
||||||
<Route path="login" element={<Login />} />
|
<Route index element={<Home />} />
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="payment" element={<Payment />} />
|
<Route path="dashboard" element={<Dashboard />} />
|
||||||
<Route path="thank-you" element={<ThankYou />} />
|
<Route path="payment" element={<Payment />} />
|
||||||
<Route path="terms" element={<Terms />} />
|
<Route path="thank-you" element={<ThankYou />} />
|
||||||
</Route>
|
<Route path="terms" element={<Terms />} />
|
||||||
</Routes>
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</AutoLoginHandler>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,21 @@ export function Login() {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [pubkeyInput, setPubkeyInput] = useState('');
|
const [pubkeyInput, setPubkeyInput] = useState('');
|
||||||
const isIframe = searchParams.get('iframe') === '1';
|
const isIframe = searchParams.get('iframe') === '1';
|
||||||
|
const urlPubkey = searchParams.get('npub') || searchParams.get('pubkey');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Handle URL-based login
|
||||||
|
if (urlPubkey && !user) {
|
||||||
|
setUser({ pubkey: urlPubkey, isWhitelisted: false });
|
||||||
|
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular user redirect
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
navigate(isIframe ? '/dashboard?iframe=1' : '/dashboard');
|
||||||
}
|
}
|
||||||
}, [user, navigate, isIframe]);
|
}, [user, navigate, isIframe, urlPubkey, setUser]);
|
||||||
|
|
||||||
const handleExtensionLogin = async () => {
|
const handleExtensionLogin = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -73,6 +82,15 @@ export function Login() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If we're processing URL-based login, show loading state
|
||||||
|
if (urlPubkey && !user) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-orange-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md mx-auto bg-gray-800 rounded-lg p-8">
|
<div className="max-w-md mx-auto bg-gray-800 rounded-lg p-8">
|
||||||
<h1 className="text-2xl font-bold mb-6 text-center">Connect with Nostr</h1>
|
<h1 className="text-2xl font-bold mb-6 text-center">Connect with Nostr</h1>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const apiService = {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
identifier: pubkey,
|
npub: pubkey,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export const apiService = {
|
|||||||
'X-Api-Key': apiKey,
|
'X-Api-Key': apiKey,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
identifier: pubkey,
|
npub: pubkey,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const api = axios.create({
|
|||||||
headers: {
|
headers: {
|
||||||
'X-Api-Key': API_KEY,
|
'X-Api-Key': API_KEY,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,16 +63,26 @@ export const lnbitsService = {
|
|||||||
extra = {},
|
extra = {},
|
||||||
}: CreateInvoiceParams): Promise<Invoice> {
|
}: CreateInvoiceParams): Promise<Invoice> {
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/api/v1/payments', {
|
// Build a V1-safe payload: only include known/supported fields when defined
|
||||||
|
const payload: Record<string, any> = {
|
||||||
out: false,
|
out: false,
|
||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
unit,
|
};
|
||||||
webhook,
|
if (unit) payload.unit = unit;
|
||||||
internal,
|
if (webhook) payload.webhook = webhook;
|
||||||
extra,
|
if (typeof internal === 'boolean') payload.internal = internal;
|
||||||
});
|
if (extra && Object.keys(extra).length > 0) payload.extra = extra;
|
||||||
return response.data;
|
|
||||||
|
const response = await api.post('/api/v1/payments', payload);
|
||||||
|
const data = response.data ?? {};
|
||||||
|
// Normalize response to ensure payment_request is always populated for the UI
|
||||||
|
const normalized: Invoice = {
|
||||||
|
...data,
|
||||||
|
payment_request: data.payment_request || data.bolt11,
|
||||||
|
bolt11: data.bolt11 || data.payment_request,
|
||||||
|
};
|
||||||
|
return normalized;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating invoice:', error);
|
console.error('Error creating invoice:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -82,10 +93,13 @@ export const lnbitsService = {
|
|||||||
async checkPayment(paymentHash: string): Promise<PaymentStatus> {
|
async checkPayment(paymentHash: string): Promise<PaymentStatus> {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/api/v1/payments/${paymentHash}`);
|
const response = await api.get(`/api/v1/payments/${paymentHash}`);
|
||||||
|
const data = response.data || {};
|
||||||
|
// Normalize potential V1 shapes
|
||||||
|
const paid: boolean = (data.paid === true) || (data.status === 'paid') || (data.settled === true);
|
||||||
return {
|
return {
|
||||||
paid: response.data.paid,
|
paid,
|
||||||
preimage: response.data.preimage,
|
preimage: data.preimage,
|
||||||
details: response.data.details,
|
details: data.details,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking payment:', error);
|
console.error('Error checking payment:', error);
|
||||||
@@ -150,17 +164,24 @@ export const lnbitsService = {
|
|||||||
|
|
||||||
// Long poll payment status
|
// Long poll payment status
|
||||||
async longPollPayment(paymentHash: string, timeout = 60000): Promise<boolean> {
|
async longPollPayment(paymentHash: string, timeout = 60000): Promise<boolean> {
|
||||||
try {
|
// Fallback to authenticated polling against V1 status endpoint to ensure compatibility
|
||||||
const response = await api.get(`/public/v1/payment/${paymentHash}`, {
|
const start = Date.now();
|
||||||
timeout,
|
const pollIntervalMs = 2000;
|
||||||
});
|
while (Date.now() - start < timeout) {
|
||||||
return response.data.paid;
|
try {
|
||||||
} catch (error) {
|
const status = await this.checkPayment(paymentHash);
|
||||||
if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') {
|
if (status.paid) return true;
|
||||||
return false; // Timeout reached
|
} catch (error) {
|
||||||
|
// If transient error, keep polling until timeout
|
||||||
|
if (axios.isAxiosError(error) && (error.response?.status ?? 0) >= 500) {
|
||||||
|
// continue
|
||||||
|
} else {
|
||||||
|
console.error('Error polling payment:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.error('Error polling payment:', error);
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user