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 Nip5ManagePage from "Pages/settings/ManageNip5";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
import DelegationPage from "./settings/Delegation";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -48,5 +49,9 @@ export const SettingsRoutes: RouteObject[] = [
|
|||||||
path: "nip5",
|
path: "nip5",
|
||||||
element: <Nip5ManagePage />,
|
element: <Nip5ManagePage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "delegation",
|
||||||
|
element: <DelegationPage />,
|
||||||
|
},
|
||||||
...WalletSettingsRoutes,
|
...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)" />
|
<FormattedMessage defaultMessage="Manage Nostr Adddress (NIP-05)" />
|
||||||
<Icon name="arrowFront" />
|
<Icon name="arrowFront" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="settings-row" onClick={() => navigate("delegation")}>
|
||||||
|
<Icon name="badge" />
|
||||||
|
<FormattedMessage defaultMessage="Delegation" />
|
||||||
|
<Icon name="arrowFront" />
|
||||||
|
</div>
|
||||||
<div className="settings-row" onClick={handleLogout}>
|
<div className="settings-row" onClick={handleLogout}>
|
||||||
<Icon name="logout" />
|
<Icon name="logout" />
|
||||||
<FormattedMessage {...messages.LogOut} />
|
<FormattedMessage {...messages.LogOut} />
|
||||||
|
@ -8,6 +8,7 @@ import type { AppDispatch, RootState } from "State/Store";
|
|||||||
import { ImgProxySettings } from "Hooks/useImgProxy";
|
import { ImgProxySettings } from "Hooks/useImgProxy";
|
||||||
import { sanitizeRelayUrl, unwrap } from "Util";
|
import { sanitizeRelayUrl, unwrap } from "Util";
|
||||||
import { DmCache } from "Cache";
|
import { DmCache } from "Cache";
|
||||||
|
import { DelegationData } from "Nip26";
|
||||||
|
|
||||||
const PrivateKeyItem = "secret";
|
const PrivateKeyItem = "secret";
|
||||||
const PublicKeyItem = "pubkey";
|
const PublicKeyItem = "pubkey";
|
||||||
@ -205,6 +206,11 @@ export interface LoginStore {
|
|||||||
* Users cusom preferences
|
* Users cusom preferences
|
||||||
*/
|
*/
|
||||||
preferences: UserPreferences;
|
preferences: UserPreferences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NIP-26 Delegation information
|
||||||
|
*/
|
||||||
|
delegation?: DelegationData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultImgProxy = {
|
export const DefaultImgProxy = {
|
||||||
@ -465,6 +471,9 @@ const LoginSlice = createSlice({
|
|||||||
state.preferences = action.payload;
|
state.preferences = action.payload;
|
||||||
window.localStorage.setItem(UserPreferencesKey, JSON.stringify(state.preferences));
|
window.localStorage.setItem(UserPreferencesKey, JSON.stringify(state.preferences));
|
||||||
},
|
},
|
||||||
|
setDelegation: (state, action: PayloadAction<DelegationData>) => {
|
||||||
|
state.delegation = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -487,6 +496,7 @@ export const {
|
|||||||
markNotificationsRead,
|
markNotificationsRead,
|
||||||
setLatestNotifications,
|
setLatestNotifications,
|
||||||
setPreferences,
|
setPreferences,
|
||||||
|
setDelegation,
|
||||||
} = LoginSlice.actions;
|
} = LoginSlice.actions;
|
||||||
|
|
||||||
export function sendNotification({
|
export function sendNotification({
|
||||||
|
@ -275,6 +275,7 @@ textarea {
|
|||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
input[type="number"],
|
input[type="number"],
|
||||||
|
input[type="datetime-local"],
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@ -293,7 +294,7 @@ textarea {
|
|||||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
option,
|
select:not([multiple]) > option,
|
||||||
optgroup {
|
optgroup {
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user