tmp
This commit is contained in:
parent
74cc772ede
commit
db5738ccf5
87
packages/app/src/Nip26/index.ts
Normal file
87
packages/app/src/Nip26/index.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import * as secp from "@noble/secp256k1";
|
||||
|
||||
import { EventKind, HexKey } from "@snort/nostr";
|
||||
import { sha256, unwrap } from "Util";
|
||||
|
||||
export interface DelegationData {
|
||||
delegatee: HexKey;
|
||||
delegator?: HexKey;
|
||||
sig?: string;
|
||||
kinds?: Array<EventKind>;
|
||||
createdBefore?: number;
|
||||
createdAfter?: number;
|
||||
}
|
||||
|
||||
export function delegationToQuery(d: DelegationData) {
|
||||
const opt: Array<string> = [];
|
||||
if (d.kinds && d.kinds.length > 0) {
|
||||
opt.push(...d.kinds.map(v => `kind=${v}`));
|
||||
}
|
||||
if (d.createdAfter) {
|
||||
opt.push(`created_at>${d.createdAfter}`);
|
||||
}
|
||||
if (d.createdBefore) {
|
||||
opt.push(`created_at<${d.createdBefore}`);
|
||||
}
|
||||
|
||||
if (opt.length === 0) {
|
||||
throw new Error("Invalid delegation data, no query");
|
||||
}
|
||||
return opt.join("&");
|
||||
}
|
||||
|
||||
export function delegationToToken(d: DelegationData) {
|
||||
return `nostr:delegation:${d.delegatee}:${delegationToQuery(d)}`;
|
||||
}
|
||||
|
||||
export function parseDelegationToken(s: string) {
|
||||
const sp = s.split(":");
|
||||
if (sp.length !== 4 || sp[0] !== "nostr" || sp[1] !== "delegation") {
|
||||
throw new Error("Invalid delegation token");
|
||||
}
|
||||
|
||||
const delegatee = sp[2];
|
||||
const params = sp[3];
|
||||
if (delegatee.length !== 64) {
|
||||
throw new Error("Invalid delegation token, delegatee pubkey must be 64 char hex string");
|
||||
}
|
||||
|
||||
const paramsSplit = params.split("&");
|
||||
|
||||
const ret = {
|
||||
delegatee,
|
||||
} as DelegationData;
|
||||
|
||||
for (const x of paramsSplit) {
|
||||
const xS = x.split(/[<>=]/);
|
||||
switch (xS[0]) {
|
||||
case "kind": {
|
||||
ret.kinds ??= [];
|
||||
ret.kinds.push(Number(xS[1]));
|
||||
break;
|
||||
}
|
||||
case "created_at": {
|
||||
const op = x.substring(10, 11);
|
||||
if (op === ">") {
|
||||
ret.createdAfter = Number(xS[1]);
|
||||
} else if (op === "<") {
|
||||
ret.createdBefore = Number(xS[1]);
|
||||
} else {
|
||||
throw new Error("Invalid delegation token, created_at operator must be '<' or '>'");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid delegation token, unknown param ${xS[0]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function verifyDelegation(d: DelegationData) {
|
||||
const token = sha256(delegationToToken(d));
|
||||
console.debug(token);
|
||||
return secp.schnorr.verify(unwrap(d.sig), token, unwrap(d.delegator));
|
||||
}
|
@ -9,6 +9,7 @@ import { WalletSettingsRoutes } from "Pages/settings/WalletSettings";
|
||||
import Nip5ManagePage from "Pages/settings/ManageNip5";
|
||||
|
||||
import messages from "./messages";
|
||||
import DelegationPage from "./settings/Delegation";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
@ -48,5 +49,9 @@ export const SettingsRoutes: RouteObject[] = [
|
||||
path: "nip5",
|
||||
element: <Nip5ManagePage />,
|
||||
},
|
||||
{
|
||||
path: "delegation",
|
||||
element: <DelegationPage />,
|
||||
},
|
||||
...WalletSettingsRoutes,
|
||||
];
|
||||
|
136
packages/app/src/Pages/settings/Delegation.tsx
Normal file
136
packages/app/src/Pages/settings/Delegation.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { delegationToToken, parseDelegationToken, verifyDelegation } from "Nip26";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setDelegation } from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
export default function DelegationPage() {
|
||||
const dispatch = useDispatch();
|
||||
const [from, setFrom] = useState(new Date().toISOString().split("Z")[0]);
|
||||
const [to, setTo] = useState(new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 30).toISOString().split("Z")[0]);
|
||||
|
||||
const delegation = useSelector((s: RootState) => s.login.delegation);
|
||||
const [newToken, setNewToken] = useState("");
|
||||
const [newDelegator, setNewDelegator] = useState("");
|
||||
const [newSig, setNewSig] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (delegation) {
|
||||
setNewToken(delegationToToken(delegation));
|
||||
setNewDelegator(delegation.delegator ?? "");
|
||||
setNewSig(delegation.sig ?? "");
|
||||
}
|
||||
}, [delegation]);
|
||||
|
||||
async function trySetDelegation() {
|
||||
try {
|
||||
setError("");
|
||||
const parsedToken = parseDelegationToken(newToken);
|
||||
parsedToken.sig = newSig;
|
||||
parsedToken.delegator = newDelegator;
|
||||
console.debug(parsedToken);
|
||||
if (!(await verifyDelegation(parsedToken))) {
|
||||
throw new Error("Invalid delegation, invalid sig");
|
||||
}
|
||||
|
||||
dispatch(setDelegation(parsedToken));
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Delegation" />
|
||||
</h3>
|
||||
<div className="form-group card">
|
||||
<div>
|
||||
<FormattedMessage defaultMessage="Delegation Token" />
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="nostr:delegation:"
|
||||
value={newToken}
|
||||
onChange={e => setNewToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group card">
|
||||
<div>
|
||||
<FormattedMessage defaultMessage="Delegator Pubkey" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" value={newDelegator} onChange={e => setNewDelegator(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group card">
|
||||
<div>
|
||||
<FormattedMessage defaultMessage="Delegator Signature" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" value={newSig} onChange={e => setNewSig(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<button className="button mb10" onClick={() => trySetDelegation()}>
|
||||
<FormattedMessage defaultMessage="Save" />
|
||||
</button>
|
||||
{error && <b className="error m10">{error}</b>}
|
||||
<div className="flex mb10">
|
||||
<FormattedMessage defaultMessage="OR" />
|
||||
<div className="divider w-max"></div>
|
||||
</div>
|
||||
<div className="form-group card">
|
||||
<div>
|
||||
<FormattedMessage defaultMessage="Kinds" />
|
||||
</div>
|
||||
<div>
|
||||
<select multiple={true}>
|
||||
<option value={0}>[0] SetMetadata</option>
|
||||
<option value={1} selected={true}>
|
||||
[1] TextNote
|
||||
</option>
|
||||
<option value={3}>[3] Contacts</option>
|
||||
<option value={4}>[4] DM's</option>
|
||||
<option value={5}>[5] Delete</option>
|
||||
<option value={6}>[6] Repost</option>
|
||||
<option value={7} selected={true}>
|
||||
[7] Reaction
|
||||
</option>
|
||||
<option value={9734} selected={true}>
|
||||
[9734] Zap
|
||||
</option>
|
||||
<option value={10_002}>[10002] Relay List Metadata</option>
|
||||
<option value={27_235} selected={true}>
|
||||
[27235] Snort Nostr Address Managment
|
||||
</option>
|
||||
<option value={30_000}>[30000] Pubkey lists (Blocked/Muted)</option>
|
||||
<option value={30_001}>[30001] Event lists (Pinned/Bookmarked)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group card">
|
||||
<div>
|
||||
<FormattedMessage defaultMessage="Valid From" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="datetime-local" value={from} onChange={e => setFrom(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group card">
|
||||
<div>
|
||||
<FormattedMessage defaultMessage="Valid Until" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="datetime-local" value={to} onChange={e => setTo(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<button className="button">
|
||||
<FormattedMessage defaultMessage="Show QR" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
@ -52,6 +52,11 @@ const SettingsIndex = () => {
|
||||
<FormattedMessage defaultMessage="Manage Nostr Adddress (NIP-05)" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("delegation")}>
|
||||
<Icon name="badge" />
|
||||
<FormattedMessage defaultMessage="Delegation" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
<div className="settings-row" onClick={handleLogout}>
|
||||
<Icon name="logout" />
|
||||
<FormattedMessage {...messages.LogOut} />
|
||||
|
@ -8,6 +8,7 @@ import type { AppDispatch, RootState } from "State/Store";
|
||||
import { ImgProxySettings } from "Hooks/useImgProxy";
|
||||
import { sanitizeRelayUrl, unwrap } from "Util";
|
||||
import { DmCache } from "Cache";
|
||||
import { DelegationData } from "Nip26";
|
||||
|
||||
const PrivateKeyItem = "secret";
|
||||
const PublicKeyItem = "pubkey";
|
||||
@ -205,6 +206,11 @@ export interface LoginStore {
|
||||
* Users cusom preferences
|
||||
*/
|
||||
preferences: UserPreferences;
|
||||
|
||||
/**
|
||||
* NIP-26 Delegation information
|
||||
*/
|
||||
delegation?: DelegationData;
|
||||
}
|
||||
|
||||
export const DefaultImgProxy = {
|
||||
@ -465,6 +471,9 @@ const LoginSlice = createSlice({
|
||||
state.preferences = action.payload;
|
||||
window.localStorage.setItem(UserPreferencesKey, JSON.stringify(state.preferences));
|
||||
},
|
||||
setDelegation: (state, action: PayloadAction<DelegationData>) => {
|
||||
state.delegation = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -487,6 +496,7 @@ export const {
|
||||
markNotificationsRead,
|
||||
setLatestNotifications,
|
||||
setPreferences,
|
||||
setDelegation,
|
||||
} = LoginSlice.actions;
|
||||
|
||||
export function sendNotification({
|
||||
|
@ -275,6 +275,7 @@ textarea {
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
input[type="datetime-local"],
|
||||
select,
|
||||
textarea {
|
||||
padding: 12px;
|
||||
@ -293,7 +294,7 @@ textarea {
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
option,
|
||||
select:not([multiple]) > option,
|
||||
optgroup {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user