mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Add PIN encryption/decryption of the private key
This commit is contained in:
parent
823f94df04
commit
feff1269c4
@ -1,24 +1,15 @@
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { Component, createEffect, createSignal, For, Match, Show, Switch } from 'solid-js';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { useSettingsContext } from '../../contexts/SettingsContext';
|
||||
import { zapNote } from '../../lib/zap';
|
||||
import { userName } from '../../stores/profile';
|
||||
import { toastZapFail, zapCustomOption } from '../../translations';
|
||||
import { PrimalNote } from '../../types/primal';
|
||||
import { debounce } from '../../utils';
|
||||
import { Component, createEffect, createSignal } from 'solid-js';
|
||||
import Modal from '../Modal/Modal';
|
||||
import { useToastContext } from '../Toaster/Toaster';
|
||||
|
||||
import { login as tLogin, pin as tPin, actions as tActions } from '../../translations';
|
||||
|
||||
import styles from './CreatePinModal.module.scss';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import ButtonLink from '../Buttons/ButtonLink';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import TextInput from '../TextInput/TextInput';
|
||||
import ButtonSecondary from '../Buttons/ButtonSecondary';
|
||||
import { encryptWithPin, setCurrentPin } from '../../lib/PrimalNostr';
|
||||
|
||||
const CreatePinModal: Component<{
|
||||
id?: string,
|
||||
@ -29,23 +20,29 @@ const CreatePinModal: Component<{
|
||||
}> = (props) => {
|
||||
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
let pinInput: HTMLInputElement | undefined;
|
||||
|
||||
const [pin, setPin] = createSignal('');
|
||||
const [rePin, setRePin] = createSignal('');
|
||||
|
||||
const encriptWithPin = () => {
|
||||
const val = props.valueToEncrypt || '<NA>';
|
||||
return `${val}_${pin()}`;
|
||||
const encWithPin = async () => {
|
||||
const val = props.valueToEncrypt || '';
|
||||
const enc = await encryptWithPin(pin(), val);
|
||||
return enc;
|
||||
};
|
||||
|
||||
const onSetPin = () => {
|
||||
// Verify pin
|
||||
const onSetPin = async() => {
|
||||
if (!isValidPin || !isValidRePin()) return;
|
||||
|
||||
// Encrypt private key
|
||||
const enc = await encWithPin();
|
||||
|
||||
// Save PIN for the session
|
||||
setCurrentPin(pin());
|
||||
|
||||
// Execute callback
|
||||
props.onPinApplied && props.onPinApplied(encriptWithPin());
|
||||
props.onPinApplied && props.onPinApplied(enc);
|
||||
};
|
||||
|
||||
const onOptout = () => {
|
||||
|
94
src/components/EnterPinModal/EnterPinModal.module.scss
Normal file
94
src/components/EnterPinModal/EnterPinModal.module.scss
Normal file
@ -0,0 +1,94 @@
|
||||
.modal {
|
||||
position: fixed;
|
||||
width: 452px;
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--background-site);
|
||||
background: linear-gradient(var(--background-site),
|
||||
var(--background-site)) padding-box,
|
||||
var(--brand-gradient) border-box;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 22px;
|
||||
|
||||
.xClose {
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: fit-content;
|
||||
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
|
||||
.iconClose {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: inline-block;
|
||||
margin: 0px 0px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/close.svg) no-repeat center;
|
||||
mask: url(../../assets/icons/close.svg) no-repeat center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.iconClose {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 4px;
|
||||
> div {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
min-width: 192px;
|
||||
margin: 0px;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.alternative {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
}
|
121
src/components/EnterPinModal/EnterPinModal.tsx
Normal file
121
src/components/EnterPinModal/EnterPinModal.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { Component, createEffect, createSignal, For, Match, Show, Switch } from 'solid-js';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { useSettingsContext } from '../../contexts/SettingsContext';
|
||||
import { zapNote } from '../../lib/zap';
|
||||
import { userName } from '../../stores/profile';
|
||||
import { toastZapFail, zapCustomOption } from '../../translations';
|
||||
import { PrimalNote } from '../../types/primal';
|
||||
import { debounce } from '../../utils';
|
||||
import Modal from '../Modal/Modal';
|
||||
import { useToastContext } from '../Toaster/Toaster';
|
||||
import { base64 } from '@scure/base';
|
||||
|
||||
import { nip19, utils } from 'nostr-tools';
|
||||
|
||||
|
||||
import { login as tLogin, pin as tPin, actions as tActions } from '../../translations';
|
||||
|
||||
import styles from './EnterPinModal.module.scss';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import ButtonLink from '../Buttons/ButtonLink';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import TextInput from '../TextInput/TextInput';
|
||||
import ButtonSecondary from '../Buttons/ButtonSecondary';
|
||||
import { decryptWithPin, encryptWithPin, setCurrentPin } from '../../lib/PrimalNostr';
|
||||
|
||||
const EnterPinModal: Component<{
|
||||
id?: string,
|
||||
open?: boolean,
|
||||
valueToDecrypt?: string,
|
||||
onSuccess?: (decryptedValue: string) => void,
|
||||
onAbort?: () => void,
|
||||
}> = (props) => {
|
||||
|
||||
const intl = useIntl();
|
||||
const toast = useToastContext();
|
||||
|
||||
let pinInput: HTMLInputElement | undefined;
|
||||
|
||||
const [pin, setPin] = createSignal('');
|
||||
|
||||
const decWithPin = async () => {
|
||||
const val = props.valueToDecrypt || '';
|
||||
const dec = await decryptWithPin(pin(), val);
|
||||
// console.log('ENCODED: ', dec);
|
||||
// console.log('PIN: ', pin());
|
||||
// console.log('DECODE: ', decryptWithPin);
|
||||
return dec;
|
||||
};
|
||||
|
||||
const onConfirm = async() => {
|
||||
if (!isValidPin) return;
|
||||
|
||||
// Decrypt private key
|
||||
const enc = await decWithPin();
|
||||
|
||||
try {
|
||||
const decoded = nip19.decode(enc);
|
||||
|
||||
if (decoded.type !== 'nsec' || !decoded.data) {
|
||||
throw('invalid-nsec-decoded');
|
||||
}
|
||||
|
||||
// Save PIN for the session
|
||||
setCurrentPin(pin());
|
||||
|
||||
// Execute callback
|
||||
props.onSuccess && props.onSuccess(enc);
|
||||
} catch(e) {
|
||||
console.log('Failed to decode nsec: ', e);
|
||||
toast?.sendWarning('PIN is incorrect');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (props.open) {
|
||||
pinInput?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const isValidPin = () => {
|
||||
return pin().length > 3;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Modal open={props.open}>
|
||||
<div id={props.id} class={styles.modal}>
|
||||
<button class={styles.xClose} onClick={props.onAbort}>
|
||||
<div class={styles.iconClose}></div>
|
||||
</button>
|
||||
<div class={styles.title}>
|
||||
{intl.formatMessage(tPin.enterTitle)}
|
||||
</div>
|
||||
<div class={styles.inputs}>
|
||||
<TextInput
|
||||
type="password"
|
||||
ref={pinInput}
|
||||
value={pin()}
|
||||
onChange={(val: string) => setPin(val)}
|
||||
label={intl.formatMessage(tPin.enter)}
|
||||
validationState={pin().length === 0 || isValidPin() ? 'valid' : 'invalid'}
|
||||
errorMessage={intl.formatMessage(tPin.invalidRePin)}
|
||||
/>
|
||||
</div>
|
||||
<div class={styles.actions}>
|
||||
<ButtonPrimary
|
||||
onClick={onConfirm}
|
||||
disabled={!isValidPin()}
|
||||
>
|
||||
{intl.formatMessage(tActions.createPin)}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(EnterPinModal);
|
@ -33,7 +33,6 @@ const LoginModal: Component<{
|
||||
if (!isValidNsec()) return;
|
||||
|
||||
account?.actions.setSec(sec);
|
||||
storeSec(sec);
|
||||
setStep(() => 'pin');
|
||||
};
|
||||
|
||||
@ -57,7 +56,6 @@ const LoginModal: Component<{
|
||||
|
||||
if (key.startsWith('nsec')) {
|
||||
try {
|
||||
console.log('KEY: ', key)
|
||||
const decoded = nip19.decode(key);
|
||||
|
||||
return decoded.type === 'nsec' && decoded.data;
|
||||
|
@ -321,3 +321,6 @@ export const algoNpub ='npub1tkpg9lyfgy83c4mgrgkrhzl90t732ekzvt73m6658xva88g5rj6
|
||||
export const specialAlgos = ['primal_spam', 'primal_nsfw'];
|
||||
|
||||
export const profileContactListPage = 50;
|
||||
|
||||
export const pinEncodePrefix = 'prpec';
|
||||
export const pinEncodeIVSeparator = '?iv=';
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
PrimalNote,
|
||||
PrimalUser,
|
||||
} from '../types/primal';
|
||||
import { Kind, relayConnectingTimeout } from "../constants";
|
||||
import { Kind, pinEncodePrefix, relayConnectingTimeout } from "../constants";
|
||||
import { isConnected, refreshSocketListeners, removeSocketListeners, socket, subscribeTo, reset } from "../sockets";
|
||||
import { sendContacts, sendLike, sendMuteList, triggerImportEvents } from "../lib/notes";
|
||||
// @ts-ignore Bad types in nostr-tools
|
||||
@ -29,6 +29,7 @@ import { getStorage, readSecFromStorage, saveFollowing, saveLikes, saveMuted, sa
|
||||
import { connectRelays, connectToRelay, getDefaultRelays, getPreConfiguredRelays } from "../lib/relays";
|
||||
import { getPublicKey } from "../lib/nostrAPI";
|
||||
import { generateKeys } from "../lib/PrimalNostr";
|
||||
import EnterPinModal from "../components/EnterPinModal/EnterPinModal";
|
||||
|
||||
export type AccountContextStore = {
|
||||
likes: string[],
|
||||
@ -54,6 +55,7 @@ export type AccountContextStore = {
|
||||
allowlist: string[],
|
||||
allowlistSince: number,
|
||||
sec: string | undefined,
|
||||
showPin: string,
|
||||
actions: {
|
||||
showNewNoteForm: () => void,
|
||||
hideNewNoteForm: () => void,
|
||||
@ -103,6 +105,7 @@ const initialData = {
|
||||
allowlist: [],
|
||||
allowlistSince: 0,
|
||||
sec: undefined,
|
||||
showPin: '',
|
||||
};
|
||||
|
||||
export const AccountContext = createContext<AccountContextStore>();
|
||||
@ -124,8 +127,8 @@ export function AccountProvider(props: { children: JSXElement }) {
|
||||
updateStore('sec', () => sec);
|
||||
const pubkey = nostrGetPubkey(decoded.data);
|
||||
setPublicKey(pubkey);
|
||||
getUserProfiles([pubkey], `user_profile_${APP_ID}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const setPublicKey = (pubkey: string | undefined) => {
|
||||
@ -268,7 +271,12 @@ export function AccountProvider(props: { children: JSXElement }) {
|
||||
const sec = readSecFromStorage();
|
||||
|
||||
if (sec) {
|
||||
setSec(sec);
|
||||
if (sec.startsWith(pinEncodePrefix)) {
|
||||
updateStore('showPin', () => sec);
|
||||
}
|
||||
else {
|
||||
setSec(sec);
|
||||
}
|
||||
} else {
|
||||
updateStore('publicKey', () => undefined);
|
||||
}
|
||||
@ -1213,6 +1221,15 @@ const [store, updateStore] = createStore<AccountContextStore>({
|
||||
return (
|
||||
<AccountContext.Provider value={store}>
|
||||
{props.children}
|
||||
<EnterPinModal
|
||||
open={store.showPin.length > 0}
|
||||
valueToDecrypt={store.showPin}
|
||||
onSuccess={(sec: string) => {
|
||||
setSec(sec);
|
||||
updateStore('showPin', () => '');
|
||||
}}
|
||||
onAbort={() => updateStore('showPin', () => '')}
|
||||
/>
|
||||
</AccountContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,13 @@
|
||||
// @ts-ignore Bad types in nostr-tools
|
||||
import { generatePrivateKey, getPublicKey, nip04, getSignature, getEventHash, validateEvent, verifySignature } from 'nostr-tools';
|
||||
import { generatePrivateKey, getPublicKey, nip04, getSignature, getEventHash, validateEvent, verifySignature, nip19 } from 'nostr-tools';
|
||||
import { NostrExtension, NostrRelayEvent, NostrRelays, NostrRelaySignedEvent } from '../types/primal';
|
||||
import { readSecFromStorage, storeSec } from './localStore';
|
||||
import { base64 } from '@scure/base';
|
||||
import { pinEncodeIVSeparator, pinEncodePrefix } from '../constants';
|
||||
import { createSignal } from 'solid-js';
|
||||
|
||||
|
||||
export const [currentPin, setCurrentPin] = createSignal('');
|
||||
|
||||
export const generateKeys = (forceNewKey?: boolean) => {
|
||||
const sec = forceNewKey ?
|
||||
@ -12,64 +18,137 @@ export const generateKeys = (forceNewKey?: boolean) => {
|
||||
return { sec, pubkey };
|
||||
};
|
||||
|
||||
export const encryptWithPin = async (pin: string, text: string) => {
|
||||
try {
|
||||
const crypto = window.crypto;
|
||||
|
||||
if (!crypto) {
|
||||
throw('not-secure-env');
|
||||
}
|
||||
|
||||
const utf8Encoder = new TextEncoder();
|
||||
|
||||
const key = await crypto.subtle.digest('SHA-256', utf8Encoder.encode(pin));
|
||||
|
||||
let iv = Uint8Array.from(crypto.getRandomValues(new Uint8Array(16)));
|
||||
let plaintext = utf8Encoder.encode(text)
|
||||
let cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt'])
|
||||
let ciphertext = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, plaintext)
|
||||
let ctb64 = base64.encode(new Uint8Array(ciphertext))
|
||||
let ivb64 = base64.encode(new Uint8Array(iv.buffer))
|
||||
|
||||
return `${pinEncodePrefix}${ctb64}${pinEncodeIVSeparator}${ivb64}`
|
||||
} catch(e) {
|
||||
console.log('Failed to encrypt with PIN: ', e);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const decryptWithPin = async (pin: string, cipher: string) => {
|
||||
try {
|
||||
if (!cipher.startsWith(pinEncodePrefix)) {
|
||||
throw('bad-cipher');
|
||||
}
|
||||
|
||||
const crypto = window.crypto;
|
||||
|
||||
if (!crypto) {
|
||||
throw('not-secure-env');
|
||||
}
|
||||
const utf8Encoder = new TextEncoder();
|
||||
const utf8Decoder = new TextDecoder('utf-8');
|
||||
|
||||
const data = cipher.slice(pinEncodePrefix.length);
|
||||
const key = await crypto.subtle.digest('SHA-256', utf8Encoder.encode(pin));
|
||||
|
||||
let [ctb64, ivb64] = data.split(pinEncodeIVSeparator)
|
||||
|
||||
let cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt'])
|
||||
let ciphertext = base64.decode(ctb64)
|
||||
let iv = base64.decode(ivb64)
|
||||
|
||||
let plaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, ciphertext)
|
||||
|
||||
let text = utf8Decoder.decode(plaintext)
|
||||
return text
|
||||
} catch(e) {
|
||||
console.log('Failed to decrypt with PIN: ', e);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const PrimalNostr: (pk?: string) => NostrExtension = (pk?: string) => {
|
||||
let sec: string = pk || readSecFromStorage() || generatePrivateKey();
|
||||
const getSec = async () => {
|
||||
let sec: string = pk || readSecFromStorage() || generatePrivateKey();
|
||||
|
||||
let pubkey: string = getPublicKey(sec);
|
||||
if (sec.startsWith(pinEncodePrefix)) {
|
||||
sec = await decryptWithPin(currentPin(), sec);
|
||||
}
|
||||
const decoded = nip19.decode(sec);
|
||||
|
||||
storeSec(sec);
|
||||
if (decoded.type !== 'nsec' || !decoded.data) {
|
||||
throw('invalid-nsec');
|
||||
}
|
||||
|
||||
sec = decoded.data;
|
||||
|
||||
return sec.length > 0 ? sec : undefined;
|
||||
}
|
||||
|
||||
const gPk: () => Promise<string> = async () => {
|
||||
const sec = await getSec();
|
||||
if (!sec) throw('pubkey-no-nsec');
|
||||
|
||||
return await getPublicKey(sec);
|
||||
};
|
||||
|
||||
const gPk: () => Promise<string> = () => new Promise<string>(r => r(getPublicKey(sec)));
|
||||
const gRl: () => Promise<NostrRelays> = () => new Promise<NostrRelays>((resolve) => {resolve({})});
|
||||
|
||||
const encrypt: (pubkey: string, message: string) => Promise<string> =
|
||||
(pubkey, message) => new Promise((rs, rj) => {
|
||||
try {
|
||||
rs(nip04.encrypt(sec, pubkey, message));
|
||||
} catch(e) {
|
||||
console.log('Failed to encript (PrimalNostr): ', e);
|
||||
rj();
|
||||
}
|
||||
});
|
||||
async (pubkey, message) => {
|
||||
const sec = await getSec();
|
||||
if (!sec) throw('encrypt-no-nsec');
|
||||
|
||||
return await nip04.encrypt(sec, pubkey, message);
|
||||
};
|
||||
|
||||
const decrypt: (pubkey: string, message: string) => Promise<string> =
|
||||
(pubkey, message) => new Promise((rs, rj) => {
|
||||
try {
|
||||
rs(nip04.decrypt(sec, pubkey, message));
|
||||
} catch(e) {
|
||||
console.log('Failed to decrypt (PrimalNostr): ', e);
|
||||
rj();
|
||||
}
|
||||
});
|
||||
async (pubkey, message) => {
|
||||
const sec = await getSec();
|
||||
if (!sec) throw('decrypt-no-nsec');
|
||||
|
||||
return await nip04.decrypt(sec, pubkey, message);
|
||||
};
|
||||
|
||||
const signEvent = async (event: NostrRelayEvent) => {
|
||||
const sec = await getSec();
|
||||
if (!sec) throw('sign-no-nsec');
|
||||
|
||||
const pubkey: string = await gPk();
|
||||
|
||||
let evt = { ...event, pubkey };
|
||||
|
||||
// @ts-ignore
|
||||
evt.id = getEventHash(evt);
|
||||
// @ts-ignore
|
||||
evt.sig = getSignature(evt, sec);
|
||||
|
||||
const isValid = validateEvent(evt);
|
||||
const isVerified = verifySignature(evt);
|
||||
|
||||
if (!isValid) throw('event-not-valid');
|
||||
if (!isVerified) throw('event-sig-not-verified');
|
||||
|
||||
return evt as NostrRelaySignedEvent;
|
||||
};
|
||||
|
||||
return {
|
||||
sec,
|
||||
pubkey,
|
||||
getPublicKey: gPk,
|
||||
getRelays: gRl,
|
||||
nip04: {
|
||||
encrypt,
|
||||
decrypt,
|
||||
},
|
||||
signEvent: (event: NostrRelayEvent) => {
|
||||
return new Promise<NostrRelaySignedEvent>((resolve, reject) => {
|
||||
try {
|
||||
const id = getEventHash(event);
|
||||
const sig = getSignature(event, sec);
|
||||
|
||||
const signed: NostrRelaySignedEvent = { ...event, id, sig, pubkey };
|
||||
|
||||
const isValid = validateEvent(signed);
|
||||
const isVerified = verifySignature(signed);
|
||||
|
||||
if (!isValid) throw('event-not-valid');
|
||||
if (!isVerified) throw('event-sig-not-verified');
|
||||
|
||||
resolve(signed);
|
||||
} catch(e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
signEvent,
|
||||
};
|
||||
};
|
||||
|
@ -48,13 +48,23 @@ export const pin = {
|
||||
title: {
|
||||
id: 'pin.title',
|
||||
defaultMessage: 'Create Pin',
|
||||
description: 'Create Pin ',
|
||||
description: 'Create Pin modal title',
|
||||
},
|
||||
description: {
|
||||
id: 'pin.description',
|
||||
defaultMessage: 'Create a PIN to secure your account. You will need to enter this PIN every time you login to the Primal web app:',
|
||||
description: 'Label describing what the pin is used for',
|
||||
},
|
||||
enter: {
|
||||
id: 'pin.enter',
|
||||
defaultMessage: 'Enter your PIN to login: ',
|
||||
description: 'Label instructing the user to enter the pin',
|
||||
},
|
||||
enterTitle: {
|
||||
id: 'pin.enterTitle',
|
||||
defaultMessage: 'Enter Pin',
|
||||
description: 'Enter Pin modal title',
|
||||
},
|
||||
reEnter: {
|
||||
id: 'pin.reEnter',
|
||||
defaultMessage: 'Re-type your PIN:',
|
||||
|
2
src/types/primal.d.ts
vendored
2
src/types/primal.d.ts
vendored
@ -334,8 +334,6 @@ interface SendPaymentResponse {
|
||||
}
|
||||
|
||||
export type NostrExtension = {
|
||||
sec?: string,
|
||||
pubkey?: string,
|
||||
getPublicKey: () => Promise<string>,
|
||||
getRelays: () => Promise<NostrRelays>,
|
||||
signEvent: (event: NostrRelayEvent) => Promise<NostrRelaySignedEvent>,
|
||||
|
Loading…
Reference in New Issue
Block a user