diff --git a/.env b/.env
index 30ed0af..351d2d6 100644
--- a/.env
+++ b/.env
@@ -2,5 +2,5 @@
# change if you're using a different noauthd server
REACT_APP_WEB_PUSH_PUBKEY=BNW_39YcKbV4KunFxFhvMW5JUs8AljfFnGUeZpaerO-gwCoWyQat5ol0xOGB8MLaqqCbz0iptd2Qv3SToSGynMk
#REACT_APP_NOAUTHD_URL=http://localhost:8000
-REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org
+REACT_APP_NOAUTHD_URL=https://noauthd.nsec.app
REACT_APP_DOMAIN=nsec.app
\ No newline at end of file
diff --git a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx
index 216353d..2bfc572 100644
--- a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx
+++ b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx
@@ -28,7 +28,8 @@ export const ModalImportKeys = () => {
e.preventDefault()
try {
if (!enteredNsec.trim().length) return
- const k: any = await swicCall('importKey', enteredNsec)
+ const enteredName = '' // FIXME get from input
+ const k: any = await swicCall('importKey', enteredName, enteredNsec)
notify('Key imported!', 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {
diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx
index bc9b838..0dcc091 100644
--- a/src/components/Modal/ModalLogin/ModalLogin.tsx
+++ b/src/components/Modal/ModalLogin/ModalLogin.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { swicCall } from '@/modules/swic'
@@ -6,7 +6,6 @@ import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { IconButton, Stack, Typography } from '@mui/material'
import { StyledAppLogo } from './styled'
-import { nip19 } from 'nostr-tools'
import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button'
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
@@ -15,6 +14,8 @@ import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { FormInputType, schema } from './const'
import { yupResolver } from '@hookform/resolvers/yup'
+import { DOMAIN } from '@/utils/consts'
+import { fetchNip05 } from '@/utils/helpers/helpers'
export const ModalLogin = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
@@ -51,18 +52,15 @@ export const ModalLogin = () => {
const submitHandler = async (values: FormInputType) => {
try {
- const [username, domain] = values.username.split('@')
- const response = await fetch(
- `https://${domain}/.well-known/nostr.json?name=${username}`,
- )
- const getNpub: {
- names: {
- [name: string]: string
- }
- } = await response.json()
-
- const pubkey = getNpub.names[username]
- const npub = nip19.npubEncode(pubkey)
+ let npub = values.username
+ if (!npub.startsWith('npub1') && !npub.includes('@')) {
+ npub += '@' + DOMAIN
+ }
+ if (npub.includes('@')) {
+ const npubNip05 = await fetchNip05(npub)
+ if (!npubNip05) throw new Error(`Username ${npub} not found`)
+ npub = npubNip05
+ }
const passphrase = values.password
console.log('fetch', npub, passphrase)
@@ -103,9 +101,9 @@ export const ModalLogin = () => {
@@ -130,7 +128,7 @@ export const ModalLogin = () => {
error={!!errors.password}
/>
diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx
index 07cfe0b..4920b23 100644
--- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx
+++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx
@@ -10,6 +10,8 @@ import { Button } from '@/shared/Button/Button'
import { CheckmarkIcon } from '@/assets'
import { swicCall } from '@/modules/swic'
import { useNavigate } from 'react-router-dom'
+import { DOMAIN, NOAUTHD_URL } from '@/utils/consts'
+import { fetchNip05 } from '@/utils/helpers/helpers'
export const ModalSignUp = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
@@ -21,27 +23,41 @@ export const ModalSignUp = () => {
const navigate = useNavigate()
const [enteredValue, setEnteredValue] = useState('')
+ const [isAvailable, setIsAvailable] = useState(false)
- const handleInputChange = (e: ChangeEvent) => {
+ const handleInputChange = async (e: ChangeEvent) => {
setEnteredValue(e.target.value)
+ const name = e.target.value.trim()
+ if (name) {
+ const npubNip05 = await fetchNip05(`${name}@${DOMAIN}`)
+ setIsAvailable(!npubNip05)
+ } else {
+ setIsAvailable(false)
+ }
}
- const isAvailable = enteredValue.trim().length > 2
-
- const inputHelperText = isAvailable ? (
- <>
- Available
- >
+ const inputHelperText = enteredValue
+ ? (
+ isAvailable ? (
+ <>
+ Available
+ >
+ ) : (
+ <>
+ Already taken
+ >
+ )
) : (
"Don't worry, username can be changed later."
- )
+ );
const handleSubmit = async (e: React.FormEvent) => {
- if (!enteredValue.trim().length) return
+ const name = enteredValue.trim()
+ if (!name.length) return
e.preventDefault()
try {
- const k: any = await swicCall('generateKey')
- notify(`New key ${k.npub}`, 'success')
+ const k: any = await swicCall('generateKey', name)
+ notify(`Account created for "${name}"`, 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {
notify(error.message, 'error')
@@ -73,22 +89,26 @@ export const ModalSignUp = () => {
placeholder='Username'
helperText={inputHelperText}
endAdornment={
- @nsec.app
+ @{DOMAIN}
}
onChange={handleInputChange}
value={enteredValue}
helperTextProps={{
sx: {
'&.helper_text': {
- color: isAvailable
+ color: enteredValue && isAvailable
? theme.palette.success.main
- : theme.palette.textSecondaryDecorate.main,
+ : (enteredValue && !isAvailable
+ ? theme.palette.error.main
+ : theme.palette.textSecondaryDecorate.main
+ )
+ ,
},
},
}}
/>
diff --git a/src/modules/backend.ts b/src/modules/backend.ts
index e50ebed..ea1ea65 100644
--- a/src/modules/backend.ts
+++ b/src/modules/backend.ts
@@ -8,9 +8,10 @@ import NDK, {
NDKPrivateKeySigner,
NDKSigner,
} from '@nostr-dev-kit/ndk'
-import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from '../utils/consts'
+import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW } from '../utils/consts'
import { Nip04 } from './nip04'
import { getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
+import { NostrPowEvent, minePow } from './pow'
//import { PrivateKeySigner } from './signer'
//const PERF_TEST = false
@@ -286,7 +287,8 @@ export class NoauthBackend {
})
if (r.status !== 200 && r.status !== 201) {
console.log('Fetch error', url, method, r.status)
- throw new Error('Failed to fetch ' + url)
+ const body = await r.json()
+ throw new Error('Failed to fetch ' + url, { cause: body })
}
return await r.json()
@@ -297,11 +299,13 @@ export class NoauthBackend {
url,
method = 'GET',
body = '',
+ pow = 0
}: {
npub: string
url: string
method: string
body: string
+ pow?: number
}) {
const { data: pubkey } = nip19.decode(npub)
@@ -320,6 +324,15 @@ export class NoauthBackend {
})
if (body) authEvent.tags.push(['payload', await this.sha256(body)])
+ // generate pow on auth evevnt
+ if (pow) {
+ const start = Date.now()
+ const powEvent: NostrPowEvent = authEvent.rawEvent()
+ const minedEvent = minePow(powEvent, pow)
+ console.log("mined pow of", pow, "in", Date.now() - start, "ms", minedEvent)
+ authEvent.tags = minedEvent.tags
+ }
+
authEvent.sig = await authEvent.sign(key.signer)
const auth = this.swg.btoa(JSON.stringify(authEvent.rawEvent()))
@@ -354,6 +367,7 @@ export class NoauthBackend {
body,
})
}
+
private async sendKeyToServer(npub: string, enckey: string, pwh: string) {
const body = JSON.stringify({
npub,
@@ -389,6 +403,38 @@ export class NoauthBackend {
})
}
+ private async sendNameToServer(npub: string, name: string) {
+ const body = JSON.stringify({
+ npub,
+ name
+ })
+
+ const method = 'POST'
+ const url = `${NOAUTHD_URL}/name`
+
+ // mas pow should be 21 or something like that
+ let pow = MIN_POW;
+ while(pow <= MAX_POW) {
+ console.log("Try name", name, "pow", pow);
+ try {
+ return await this.sendPostAuthd({
+ npub,
+ url,
+ method,
+ body,
+ pow
+ })
+ } catch (e: any) {
+ console.log("error", e.cause);
+ if (e.cause && e.cause.minPow > pow)
+ pow = e.cause.minPow
+ else
+ throw e;
+ }
+ }
+ throw new Error("Too many requests, retry later")
+ }
+
private notify() {
// FIXME collect info from accessBuffer and confirmBuffer
// and update the notifications
@@ -475,7 +521,11 @@ export class NoauthBackend {
return generatePrivateKey()
}
- public async addKey(nsec?: string): Promise {
+ public async addKey(name: string, nsec?: string): Promise {
+
+ // lowercase
+ name = name.trim().toLocaleLowerCase()
+
let sk = ''
if (nsec) {
const { type, data } = nip19.decode(nsec)
@@ -486,14 +536,22 @@ export class NoauthBackend {
}
const pubkey = getPublicKey(sk)
const npub = nip19.npubEncode(pubkey)
+
const localKey = await this.keysModule.generateLocalKey()
const enckey = await this.keysModule.encryptKeyLocal(sk, localKey)
// @ts-ignore
- const dbKey: DbKey = { npub, enckey, localKey }
+ const dbKey: DbKey = { npub, name, enckey, localKey }
await dbi.addKey(dbKey)
this.enckeys.push(dbKey)
await this.startKey({ npub, sk })
+ // assign nip05 before adding the key
+ // FIXME set name to db and if this call to 'send' fails
+ // then retry later
+ console.log("adding key", npub, name)
+ if (name)
+ await this.sendNameToServer(npub, name)
+
const sub = await this.swg.registration.pushManager.getSubscription()
if (sub) await this.sendSubscriptionToServer(npub, sub)
@@ -766,14 +824,14 @@ export class NoauthBackend {
await this.startKey({ npub, sk })
}
- private async generateKey() {
- const k = await this.addKey()
+ private async generateKey(name: string) {
+ const k = await this.addKey(name)
this.updateUI()
return k
}
- private async importKey(nsec: string) {
- const k = await this.addKey(nsec)
+ private async importKey(name: string, nsec: string) {
+ const k = await this.addKey(name, nsec)
this.updateUI()
return k
}
@@ -879,9 +937,9 @@ export class NoauthBackend {
//console.log("UI message", id, method, args)
let result = undefined
if (method === 'generateKey') {
- result = await this.generateKey()
+ result = await this.generateKey(args[0])
} else if (method === 'importKey') {
- result = await this.importKey(args[0])
+ result = await this.importKey(args[0], args[1])
} else if (method === 'saveKey') {
result = await this.saveKey(args[0], args[1])
} else if (method === 'fetchKey') {
@@ -902,6 +960,7 @@ export class NoauthBackend {
result,
})
} catch (e: any) {
+ console.log("backend error", e)
event.source.postMessage({
id,
error: e.toString(),
diff --git a/src/modules/pow.ts b/src/modules/pow.ts
new file mode 100644
index 0000000..33f03a7
--- /dev/null
+++ b/src/modules/pow.ts
@@ -0,0 +1,51 @@
+// based on https://git.v0l.io/Kieran/snort/src/branch/main/packages/system/src/pow-util.ts
+
+import { sha256 } from "@noble/hashes/sha256";
+import { bytesToHex } from "@noble/hashes/utils";
+
+export interface NostrPowEvent {
+ id?: string;
+ pubkey: string;
+ created_at: number;
+ kind?: number;
+ tags: Array>;
+ content: string;
+ sig?: string;
+}
+
+export function minePow(e: NostrPowEvent, target: number) {
+ let ctr = 0;
+
+ let nonceTagIdx = e.tags.findIndex(a => a[0] === "nonce");
+ if (nonceTagIdx === -1) {
+ nonceTagIdx = e.tags.length;
+ e.tags.push(["nonce", ctr.toString(), target.toString()]);
+ }
+ do {
+ e.tags[nonceTagIdx][1] = (++ctr).toString();
+ e.id = createId(e);
+ } while (countLeadingZeros(e.id) < target);
+
+ return e;
+}
+
+function createId(e: NostrPowEvent) {
+ const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content];
+ return bytesToHex(sha256(JSON.stringify(payload)));
+}
+
+export function countLeadingZeros(hex: string) {
+ let count = 0;
+
+ for (let i = 0; i < hex.length; i++) {
+ const nibble = parseInt(hex[i], 16);
+ if (nibble === 0) {
+ count += 4;
+ } else {
+ count += Math.clz32(nibble) - 28;
+ break;
+ }
+ }
+
+ return count;
+}
\ No newline at end of file
diff --git a/src/pages/HomePage/Home.Page.tsx b/src/pages/HomePage/Home.Page.tsx
index 964cf3b..98f9f4c 100644
--- a/src/pages/HomePage/Home.Page.tsx
+++ b/src/pages/HomePage/Home.Page.tsx
@@ -24,7 +24,7 @@ const HomePage = () => {
return (
- {isNoKeys ? 'Welcome' : 'Keys:'}
+ {isNoKeys ? 'Welcome' : 'Accounts:'}
{isNoKeys && (
diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx
index 82992ff..cb85195 100644
--- a/src/pages/KeyPage/Key.Page.tsx
+++ b/src/pages/KeyPage/Key.Page.tsx
@@ -18,18 +18,22 @@ import UserValueSection from './components/UserValueSection'
import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
import { useLiveQuery } from 'dexie-react-hooks'
import { checkNpubSyncQuerier } from './utils'
+import { DOMAIN } from '@/utils/consts'
const KeyPage = () => {
const { npub = '' } = useParams<{ npub: string }>()
- const { apps, pending, perms } = useAppSelector((state) => state.content)
+ const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
const { handleOpen } = useModalSearchParams()
- const { userNameWithPrefix } = useProfile(npub)
+ // const { userNameWithPrefix } = useProfile(npub)
const { handleEnableBackground, showWarning, isEnabling } =
useBackgroundSigning()
+ const key = keys.find(k => k.npub === npub)
+ const username = key?.name ? `${key?.name}@${DOMAIN}` : ''
+
const filteredApps = apps.filter((a) => a.npub === npub)
const { prepareEventPendings } = useTriggerConfirmModal(
npub,
@@ -53,8 +57,8 @@ const KeyPage = () => {
)}
void;
+ debounceTimeout: number;
+};
+
+export const DebounceInput = (props: InputProps & DebounceProps) => {
+ const { handleDebounce, debounceTimeout, ...rest } = props;
+
+ const timerRef = useRef();
+
+ const handleChange = (event: React.ChangeEvent) => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+
+ timerRef.current = window.setTimeout(() => {
+ handleDebounce(event.target.value);
+ }, debounceTimeout);
+ };
+
+ // @ts-ignore
+ return ;
+}
+
diff --git a/src/utils/consts.ts b/src/utils/consts.ts
index ac08807..1a53d68 100644
--- a/src/utils/consts.ts
+++ b/src/utils/consts.ts
@@ -3,6 +3,9 @@ export const NOAUTHD_URL = process.env.REACT_APP_NOAUTHD_URL
export const WEB_PUSH_PUBKEY = process.env.REACT_APP_WEB_PUSH_PUBKEY
export const DOMAIN = process.env.REACT_APP_DOMAIN
+export const MIN_POW = 14
+export const MAX_POW = 19
+
export enum ACTION_TYPE {
BASIC = 'basic',
ADVANCED = 'advanced',
diff --git a/src/utils/helpers/helpers.ts b/src/utils/helpers/helpers.ts
index 41bd196..bc8d343 100644
--- a/src/utils/helpers/helpers.ts
+++ b/src/utils/helpers/helpers.ts
@@ -1,82 +1,101 @@
-import { nip19 } from 'nostr-tools'
-import { ACTION_TYPE, NIP46_RELAYS } from '../consts'
-import { DbPending } from '@/modules/db'
-import { MetaEvent } from '@/types/meta-event'
+import { nip19 } from "nostr-tools";
+import { ACTION_TYPE, NIP46_RELAYS } from "../consts";
+import { DbPending } from "@/modules/db";
+import { MetaEvent } from "@/types/meta-event";
export async function call(cb: () => any) {
- try {
- return await cb()
- } catch (e) {
- console.log(`Error: ${e}`)
- }
+ try {
+ return await cb();
+ } catch (e) {
+ console.log(`Error: ${e}`);
+ }
}
-export const getShortenNpub = (npub = '') => {
- return npub.substring(0, 10) + '...' + npub.slice(-4)
-}
+export const getShortenNpub = (npub = "") => {
+ return npub.substring(0, 10) + "..." + npub.slice(-4);
+};
export const getProfileUsername = (profile: MetaEvent | null, npub: string) => {
- return (
- profile?.info?.name ||
- profile?.info?.display_name ||
- getShortenNpub(npub)
- )
-}
+ return (
+ profile?.info?.name || profile?.info?.display_name || getShortenNpub(npub)
+ );
+};
-export const getBunkerLink = (npub = '') => {
- if (!npub) return ''
- const { data: pubkey } = nip19.decode(npub)
- return `bunker://${pubkey}?relay=${NIP46_RELAYS[0]}`
-}
+export const getBunkerLink = (npub = "") => {
+ if (!npub) return "";
+ const { data: pubkey } = nip19.decode(npub);
+ return `bunker://${pubkey}?relay=${NIP46_RELAYS[0]}`;
+};
export async function askNotificationPermission() {
- return new Promise((ok, rej) => {
- // Let's check if the browser supports notifications
- if (!('Notification' in window)) {
- rej('This browser does not support notifications.')
- } else {
- Notification.requestPermission().then(() => {
- if (Notification.permission === 'granted') ok()
- else rej()
- })
- }
- })
+ return new Promise((ok, rej) => {
+ // Let's check if the browser supports notifications
+ if (!("Notification" in window)) {
+ rej("This browser does not support notifications.");
+ } else {
+ Notification.requestPermission().then(() => {
+ if (Notification.permission === "granted") ok();
+ else rej();
+ });
+ }
+ });
}
export function getSignReqKind(req: DbPending): number | undefined {
- try {
- const data = JSON.parse(JSON.parse(req.params)[0])
- return data.kind
- } catch {}
- return undefined
+ try {
+ const data = JSON.parse(JSON.parse(req.params)[0]);
+ return data.kind;
+ } catch {}
+ return undefined;
}
export function getReqPerm(req: DbPending): string {
- if (req.method === 'sign_event') {
- const kind = getSignReqKind(req)
- if (kind !== undefined) return `${req.method}:${kind}`
- }
- return req.method
+ if (req.method === "sign_event") {
+ const kind = getSignReqKind(req);
+ if (kind !== undefined) return `${req.method}:${kind}`;
+ }
+ return req.method;
}
export function isPackagePerm(perm: string, reqPerm: string) {
- if (perm === ACTION_TYPE.BASIC) {
- switch (reqPerm) {
- case 'connect':
- case 'get_public_key':
- case 'nip04_decrypt':
- case 'nip04_encrypt':
- case 'sign_event:0':
- case 'sign_event:1':
- case 'sign_event:3':
- case 'sign_event:6':
- case 'sign_event:7':
- case 'sign_event:9734':
- case 'sign_event:10002':
- case 'sign_event:30023':
- case 'sign_event:10000':
- return true
- }
- }
- return false
+ if (perm === ACTION_TYPE.BASIC) {
+ switch (reqPerm) {
+ case "connect":
+ case "get_public_key":
+ case "nip04_decrypt":
+ case "nip04_encrypt":
+ case "sign_event:0":
+ case "sign_event:1":
+ case "sign_event:3":
+ case "sign_event:6":
+ case "sign_event:7":
+ case "sign_event:9734":
+ case "sign_event:10002":
+ case "sign_event:30023":
+ case "sign_event:10000":
+ return true;
+ }
+ }
+ return false;
+}
+
+export async function fetchNip05(value: string, origin?: string) {
+ try {
+ const [username, domain] = value.split("@");
+ if (!origin) origin = `https://${domain}`
+ const response = await fetch(
+ `${origin}/.well-known/nostr.json?name=${username}`
+ );
+ const getNpub: {
+ names: {
+ [name: string]: string;
+ };
+ } = await response.json();
+
+ const pubkey = getNpub.names[username];
+ return nip19.npubEncode(pubkey);
+ } catch (e) {
+ console.log("Failed to fetch nip05", value, "error: " + e);
+ return ''
+ }
}