This commit is contained in:
Kieran 2023-04-10 10:45:08 +01:00
parent 74cc772ede
commit db5738ccf5
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
6 changed files with 245 additions and 1 deletions

View 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));
}

View File

@ -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,
];

View 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>
</>
);
}

View File

@ -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} />

View File

@ -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({

View File

@ -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);
}