diff --git a/src/components/Arbor/ForumPost.ts b/src/components/Arbor/ForumPost.ts index 3558b53..280c5b1 100644 --- a/src/components/Arbor/ForumPost.ts +++ b/src/components/Arbor/ForumPost.ts @@ -1,12 +1,8 @@ -import { wallet } from '@/wallet'; -import { StateController } from '@lit-app/state'; -import type { NDKUser } from '@nostr-dev-kit/ndk'; import formatDateTime from '@utils/formatDateTime'; import { LitElement, css, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; -import { ndk } from '@/ndk'; import '@components/MarkdownContent'; @customElement('arx-forum-post') @@ -18,16 +14,6 @@ export class ForumPost extends LitElement { @property({ type: String }) content = ''; @property({ type: Boolean }) isHighlighted = false; - @state() private zapAmountDialogOpen = false; - @state() public authorProfile: NDKUser | undefined = undefined; - - override connectedCallback(): void { - super.connectedCallback(); - this.authorProfile = ndk.getUser({ pubkey: this.npub }); - new StateController(this, wallet); - wallet.loadWallet(); - } - static override styles = [ css` .post { @@ -185,24 +171,14 @@ export class ForumPost extends LitElement { } private _handleZap() { - // setting to false and then to true forces the dialog to open, even when it wasn't closed correctly - this.zapAmountDialogOpen = false; - setTimeout(() => { - this.zapAmountDialogOpen = true; - }, 0); - } - - private async _doZap(e: Event) { - if (!(e instanceof CustomEvent)) return; - e.preventDefault(); - const zapAmount = Number.parseInt(e.detail.value); - if (Number.isNaN(zapAmount) || zapAmount <= 10) { - alert('Zap amount must be greater or equal to 10'); - return; - } - await wallet.nutZap(zapAmount, this.authorProfile!, this.id); - - this.zapAmountDialogOpen = false; + alert('Zapping is not yet implemented'); + this.dispatchEvent( + new CustomEvent('zap', { + detail: { postId: this.id, npub: this.npub }, + bubbles: true, + composed: true, + }), + ); } private _handleDownzap() { @@ -230,16 +206,6 @@ export class ForumPost extends LitElement { }; return html` - { - this.zapAmountDialogOpen = false; - }} - showInput - .open=${this.zapAmountDialogOpen} - >
- + this.handleChange('locale', e)} + @input=${(e: Event) => this.handleChange('locale', e)} > diff --git a/src/components/General/Button.ts b/src/components/General/Button.ts index a1a5a64..526ced6 100644 --- a/src/components/General/Button.ts +++ b/src/components/General/Button.ts @@ -186,8 +186,8 @@ export class StyledButton extends LitElement { } private _handleClick(e: MouseEvent) { - e.preventDefault(); if (this.disabled || this.loading) { + e.preventDefault(); return; } diff --git a/src/components/General/Input.ts b/src/components/General/Input.ts index 3b20e82..2936518 100644 --- a/src/components/General/Input.ts +++ b/src/components/General/Input.ts @@ -1,35 +1,17 @@ -import { LitElement, type PropertyValues, css, html } from 'lit'; -import { customElement, property, query, state } from 'lit/decorators.js'; +import { LitElement, css, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; -export class ArxInputChangeEvent extends CustomEvent<{ value: string }> { - constructor(value: string) { - super('change', { detail: { value } }); - } -} - @customElement('arx-input') export class StyledInput extends LitElement { @property() placeholder = ''; @property() value = ''; @property({ type: Boolean }) disabled = false; - @property() type: 'text' | 'number' | 'password' = 'text'; + @property() type = 'text'; @property() name = ''; @property({ type: Boolean }) required = false; @property() label = ''; - @query('input') private _input!: HTMLInputElement; - - @state() private _value = ''; - - protected override firstUpdated(_changedProperties: PropertyValues): void { - this._value = this.value; - } - - protected override updated(changedProperties: PropertyValues): void { - if (changedProperties.has('value')) this._value = this.value; - } - static override styles = css` :host { display: inline-block; @@ -116,7 +98,7 @@ export class StyledInput extends LitElement { return html` ${when(this.label, () => html``)} ` diff --git a/src/components/General/Textarea.ts b/src/components/General/Textarea.ts index e799a8f..93e2791 100644 --- a/src/components/General/Textarea.ts +++ b/src/components/General/Textarea.ts @@ -1,7 +1,6 @@ import { LitElement, css, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; -import { ArxInputChangeEvent } from './Input'; @customElement('arx-textarea') export class StyledTextarea extends LitElement { @@ -202,6 +201,12 @@ export class StyledTextarea extends LitElement { private _handleInput(e: InputEvent) { const textarea = e.target as HTMLTextAreaElement; this.value = textarea.value; - this.dispatchEvent(new ArxInputChangeEvent(this.value)); + this.dispatchEvent( + new CustomEvent('input', { + detail: { value: this.value }, + bubbles: true, + composed: true, + }), + ); } } diff --git a/src/components/Header.ts b/src/components/Header.ts index e8745c9..4834ab3 100644 --- a/src/components/Header.ts +++ b/src/components/Header.ts @@ -1,5 +1,4 @@ import { ndk } from '@/ndk'; -import type { ArxInputChangeEvent } from '@components/General/Input'; import type { NDKEvent } from '@nostr-dev-kit/ndk'; import * as nip19 from '@nostr/tools/nip19'; import { LitElement, css, html } from 'lit'; @@ -9,8 +8,8 @@ import { keyed } from 'lit/directives/keyed.js'; import { map } from 'lit/directives/map.js'; import { when } from 'lit/directives/when.js'; -import '@components/General/Input'; import '@components/HeaderSugestion'; +import '@components/General/Input'; @customElement('arx-header') export class Header extends LitElement { @@ -50,10 +49,10 @@ export class Header extends LitElement { .nav-buttons { display: flex; gap: var(--space-xs, 0.5rem); - padding: 0 var(--space-xs, 0.5rem); + padding-right: var(--space-xs, 0.5rem); } - button { + .nav-buttons button { text-decoration: none; color: var(--color-primary-content); background: oklch(from var(--color-primary-content) l c h / 0.1); @@ -67,21 +66,23 @@ export class Header extends LitElement { align-items: center; justify-content: center; transition: all 0.2s ease; - &:hover { - background: oklch(from var(--color-primary-content) l c h / 0.2); - transform: translateY(-2px); - box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px) - calc(var(--depth) * 4px) - oklch(from var(--color-base-content) l c h / 0.15); - } - &:active { - transform: translateY(1px); - } + } - &.disabled { - opacity: 0.5; - pointer-events: none; - } + .nav-buttons button:hover { + background: oklch(from var(--color-primary-content) l c h / 0.2); + transform: translateY(-2px); + box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px) + calc(var(--depth) * 4px) + oklch(from var(--color-base-content) l c h / 0.15); + } + + .nav-buttons button:active { + transform: translateY(1px); + } + + .nav-buttons button.disabled { + opacity: 0.5; + pointer-events: none; } .search-container { @@ -152,7 +153,7 @@ export class Header extends LitElement { placeholder=${this.url} @keyup=${this._handleSearch} @focus=${this._handleFocus} - @change=${this._handleInput} + @input=${this._handleInput} > ${when( this.showSuggestions, @@ -174,25 +175,16 @@ export class Header extends LitElement { `, )}
- `; } - private _goToWallet() { - window.location.hash = 'wallet'; - } - private _handleFocus() { this.showSuggestions = true; } - private _handleInput(e: ArxInputChangeEvent) { - this.searchQuery = e.detail.value; + private _handleInput(e: InputEvent) { + this.searchQuery = (e.target as HTMLInputElement).value; if (this._debounceTimeout) { clearTimeout(this._debounceTimeout); } @@ -253,6 +245,8 @@ export class Header extends LitElement { private _handleSearch(e: KeyboardEvent) { if (e.key !== 'Enter') return; + const target = e.target as HTMLInputElement; + this.searchQuery = target.value; this.showSuggestions = false; if (this.searchQuery.startsWith('npub1')) { diff --git a/src/components/InitialSetup.ts b/src/components/InitialSetup.ts index 06210c9..a947959 100644 --- a/src/components/InitialSetup.ts +++ b/src/components/InitialSetup.ts @@ -1,5 +1,4 @@ import { ndk, setSigner } from '@/ndk'; -import type { ArxInputChangeEvent } from '@components/General/Input'; import { animate } from '@lit-labs/motion'; import { randomBytes } from '@noble/ciphers/webcrypto'; import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; @@ -11,10 +10,10 @@ import { encodeBase64 } from '@std/encoding/base64'; import { LitElement, css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import '@components/General/Button'; -import '@components/General/Fieldset'; -import '@components/General/Input'; import '@components/LoadingView'; +import '@components/General/Button'; +import '@components/General/Input'; +import '@components/General/Fieldset'; @customElement('arx-initial-setup') export class InitialSetup extends LitElement { @@ -202,8 +201,8 @@ export class InitialSetup extends LitElement { }, 300); } - private onSeedPhraseInput(event: ArxInputChangeEvent) { - this.seedPhrase = event.detail.value; + private onSeedPhraseInput(event: Event) { + this.seedPhrase = (event.target as HTMLInputElement).value; } private generateSeedPhrase() { @@ -301,7 +300,7 @@ export class InitialSetup extends LitElement {

) { - this.userName = e.detail.value; + private onUserNameInput(e: Event) { + this.userName = (e.target as HTMLInputElement).value; } - private onProfileImageInput(e: CustomEvent<{ value: string }>) { - this.profileImage = e.detail.value; + private onProfileImageInput(e: Event) { + this.profileImage = (e.target as HTMLInputElement).value; } - private onLightningAddressInput(e: CustomEvent<{ value: string }>) { - this.lightningAddress = e.detail.value; + private onLightningAddressInput(e: Event) { + this.lightningAddress = (e.target as HTMLInputElement).value; } private renderPageFour() { @@ -410,7 +409,7 @@ export class InitialSetup extends LitElement { id="username" type="text" .value=${this.userName} - @change=${this.onUserNameInput} + @input=${this.onUserNameInput} placeholder="Enter your name" > @@ -419,7 +418,7 @@ export class InitialSetup extends LitElement { id="profile-image" type="text" .value=${this.profileImage} - @change=${this.onProfileImageInput} + @input=${this.onProfileImageInput} placeholder="Enter image URL" > @@ -465,7 +464,7 @@ export class InitialSetup extends LitElement { id="lightning-address" type="text" .value=${this.lightningAddress} - @change=${this.onLightningAddressInput} + @input=${this.onLightningAddressInput} placeholder="your@lightning.address" /> diff --git a/src/components/WalletTransactionLine.ts b/src/components/WalletTransactionLine.ts deleted file mode 100644 index 687ba2f..0000000 --- a/src/components/WalletTransactionLine.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { StateController } from '@lit-app/state'; -import { LitElement, css, html } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -import { type WalletHistory, wallet } from '@/wallet'; - -import formatDateTime from '@/utils/formatDateTime'; -import satsComma from '@/utils/satsComma'; -import '@components/General/Card'; -import '@components/General/Input'; - -@customElement('arx-wallet-transaction-line') -export class WalletTransactionLine extends LitElement { - @property({ type: Object }) transaction!: WalletHistory; - - override connectedCallback(): void { - super.connectedCallback(); - new StateController(this, wallet); - } - - static override styles = css` - :host { - display: block; - width: 100%; - border-bottom: 1px solid var(--color-base-200); - container-type: inline-size; - } - - .transaction { - display: grid; - grid-template-columns: 200px 1fr auto; - justify-content: space-between; - align-items: center; - padding: 10px 0; - } - - .in { - color: var(--color-success); - } - - .out { - color: var(--color-error); - } - - .amount { - text-align: right; - font-family: var(--font-mono); - font-weight: bold; - margin-right: 10px; - } - - .date { - color: var(--color-secondary); - font-size: 0.9em; - } - - .profiles { - display: flex; - gap: 10px; - } - - .sender { - order: 1; - } - - .recipient { - order: 2; - } - - @container (max-width: 400px) { - .transaction { - grid-template-columns: 1fr; - } - - .amount { - text-align: left; - } - } - `; - - override render() { - return html`
- - ${this.transaction.direction === 'in' ? '+' : '-'} ${satsComma(this.transaction.amount)} - sats - - ${formatDateTime(this.transaction.created_at * 1000)} -
- - -
-
`; - } -} diff --git a/src/components/Widgets/WalletWidget.ts b/src/components/Widgets/WalletWidget.ts deleted file mode 100644 index 492b895..0000000 --- a/src/components/Widgets/WalletWidget.ts +++ /dev/null @@ -1,82 +0,0 @@ -import satsComma from '@/utils/satsComma'; -import { wallet } from '@/wallet'; -import '@components/General/Fieldset'; -import '@components/LoadingView'; -import '@components/WalletTransactionLine'; -import { StateController } from '@lit-app/state'; -import { LitElement, css, html } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import { when } from 'lit/directives/when.js'; - -@customElement('arx-wallet-widget') -export class WalletWidget extends LitElement { - @state() - public loading = false; - - override connectedCallback(): void { - super.connectedCallback(); - new StateController(this, wallet); - } - - override async firstUpdated() { - this.loading = true; - await wallet.loadWallet(); - this.loading = false; - } - - static override styles = [ - css` - :host { - display: block; - } - - .widget-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - border-bottom: var(--border) solid var(--color-base-300); - padding-bottom: 8px; - } - - .widget-title { - font-size: 1.2rem; - font-weight: 600; - margin: 0; - color: var(--color-base-content); - display: flex; - align-items: center; - gap: 8px; - } - - .bitcoin-icon { - color: var(--color-warning); - font-size: 1.4rem; - } - `, - ]; - - override render() { - return html` -
-

- Bitcoin Wallet -

-
- 'Loading...', - () => `${satsComma(wallet.balance)} sats`, - )}> - ${when( - wallet.sortedHistory.length > 0, - () => html` - Latest Transaction: - - `, - () => 'No transactions yet', - )} - - `; - } -} diff --git a/src/routes/Arbor/NewPost.ts b/src/routes/Arbor/NewPost.ts index ab2a6c3..8a3f8b5 100644 --- a/src/routes/Arbor/NewPost.ts +++ b/src/routes/Arbor/NewPost.ts @@ -1,4 +1,3 @@ -import type { ArxInputChangeEvent } from '@/components/General/Input'; import { getSigner, ndk } from '@/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk'; import { LitElement, css, html } from 'lit'; @@ -107,8 +106,8 @@ export class ArborPostCreator extends LitElement { } } - private handleContentInput(e: ArxInputChangeEvent) { - this.postContent = e.detail.value; + private handleContentInput(e: InputEvent) { + this.postContent = (e.target as HTMLTextAreaElement).value; if (this.error && this.postContent.length >= 10) { this.error = null; } @@ -122,7 +121,7 @@ export class ArborPostCreator extends LitElement { diff --git a/src/routes/Arbor/NewTopic.ts b/src/routes/Arbor/NewTopic.ts index 7228bee..23f3403 100644 --- a/src/routes/Arbor/NewTopic.ts +++ b/src/routes/Arbor/NewTopic.ts @@ -1,12 +1,11 @@ import { getSigner, ndk } from '@/ndk'; -import type { ArxInputChangeEvent } from '@components/General/Input'; import { NDKEvent } from '@nostr-dev-kit/ndk'; import { LitElement, css, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import '@components/General/Button'; import '@components/General/Input'; import '@components/General/Textarea'; +import '@components/General/Button'; @customElement('arx-arbor-topic-creator') export class ArborTopicCreator extends LitElement { @@ -87,12 +86,12 @@ export class ArborTopicCreator extends LitElement { } } - private handleTopicInput(e: ArxInputChangeEvent) { - this.newTopic = e.detail.value; + private handleTopicInput(e: InputEvent) { + this.newTopic = (e.target as HTMLInputElement).value; } - private handleContentInput(e: ArxInputChangeEvent) { - this.topicContent = e.detail.value; + private handleContentInput(e: InputEvent) { + this.topicContent = (e.target as HTMLTextAreaElement).value; } override render() { @@ -103,14 +102,14 @@ export class ArborTopicCreator extends LitElement { type="text" placeholder="New Topic" .value=${this.newTopic} - @change=${this.handleTopicInput} + @input=${this.handleTopicInput} ?disabled=${this.isCreating} > diff --git a/src/routes/Home.ts b/src/routes/Home.ts index 3950b8e..dc0736d 100644 --- a/src/routes/Home.ts +++ b/src/routes/Home.ts @@ -5,11 +5,10 @@ import { customElement, state } from 'lit/decorators.js'; import { map } from 'lit/directives/map.js'; import { html, literal } from 'lit/static-html.js'; +import '@widgets/BitcoinBlockWidget'; import '@components/AppGrid'; -import '@components/General/Card'; import '@components/NostrAvatar'; -import '@components/Widgets/BitcoinBlockWidget'; -import '@components/Widgets/WalletWidget'; +import '@components/General/Card'; @customElement('arx-eve-home') export class Home extends LitElement { @@ -84,10 +83,6 @@ export class Home extends LitElement { title: 'Bitcoin Block', content: literal`arx-bitcoin-block-widget`, }, - { - title: 'Bitcoin Wallet', - content: literal`arx-wallet-widget`, - }, ]; async loadProperties() { @@ -100,71 +95,60 @@ export class Home extends LitElement { static override styles = [ css` - ::-webkit-scrollbar { - width: 12px; - height: 12px; - } - - ::-webkit-scrollbar-track { - background: var(--color-base-200); - border-radius: var(--radius-field); - } - - ::-webkit-scrollbar-thumb { - background: var(--color-base-300); - border-radius: var(--radius-field); - border: 2px solid var(--color-base-200); - transition: var(--transition); - } - - ::-webkit-scrollbar-thumb:hover { - background: var(--color-neutral); - } - .content-wrapper { - margin: auto; - height: 90vh; - overflow: hidden; display: flex; - align-items: center; - justify-content: center; gap: 20px; width: 100%; max-width: 1200px; + margin: auto; + } + + .home { + min-height: calc(100vh - var(--font-2xl)); + width: 100%; + position: absolute; + right: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; } .home-container { flex: 1; padding: 0; - height: auto; } .widgets-container { - height: 75vh; - overflow: auto; width: 350px; - margin: 0 auto; + display: flex; + flex-direction: column; + gap: 20px; + + & > * { + flex: 1; + } } @media (max-width: 1024px) { .content-wrapper { - gap: 0; - height: auto; flex-direction: column; } - .home-container { - width: 100%; - } - .widgets-container { - overflow: unset; - height: auto; width: calc(100vw - 40px); + flex-direction: row; + overflow-x: auto; padding-bottom: 10px; } } + @media (max-width: 768px) { + .widgets-container { + flex-direction: column; + } + } + .welcome-section { display: flex; flex-direction: column; @@ -189,21 +173,23 @@ export class Home extends LitElement { override render() { return html` -
- - - -
-

Welcome, ${this.username}

-
+
+
+ + + +
+

Welcome, ${this.username}

+
+
+
- - -
- ${map(this.widgets, (widget) => html`<${widget.content}>`)} +
+ ${map(this.widgets, (widget) => html`<${widget.content}>`)} +
`; diff --git a/src/routes/Settings.ts b/src/routes/Settings.ts index 6e3ddf1..cdcb210 100644 --- a/src/routes/Settings.ts +++ b/src/routes/Settings.ts @@ -1,17 +1,16 @@ import defaultAvatar from '@/default-avatar.png'; import { getSigner, getUserProfile, ndk } from '@/ndk'; -import type { ArxInputChangeEvent } from '@components/General/Input'; import { NDKEvent, type NDKUserProfile } from '@nostr-dev-kit/ndk'; import { LitElement, css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; -import '@components/Breadcrumbs'; import '@components/DateTimeSettings'; -import '@components/General/Button'; -import '@components/General/Card'; -import '@components/General/Fieldset'; import '@components/General/Input'; +import '@components/General/Button'; +import '@components/General/Fieldset'; +import '@components/General/Card'; +import '@components/Breadcrumbs'; @customElement('arx-settings') export class EveSettings extends LitElement { @@ -74,10 +73,11 @@ export class EveSettings extends LitElement { } } - private handleInputChange(field: string, e: ArxInputChangeEvent) { + private handleInputChange(e: Event) { + const target = e.target as HTMLInputElement; this.profile = { ...this.profile, - [field]: e.detail.value, + [target.name]: target.value, }; } @@ -144,7 +144,7 @@ export class EveSettings extends LitElement { type="text" name="name" .value=${this.profile.name} - @change=${(e: ArxInputChangeEvent) => this.handleInputChange('name', e)} + @input=${this.handleInputChange} placeholder="Your display name" > @@ -153,7 +153,7 @@ export class EveSettings extends LitElement { type="text" name="image" .value=${this.profile.picture} - @change=${(e: ArxInputChangeEvent) => this.handleInputChange('picture', e)} + @input=${this.handleInputChange} placeholder="https://example.com/your-image.jpg" > @@ -162,7 +162,7 @@ export class EveSettings extends LitElement { type="text" name="banner" .value=${this.profile.banner} - @change=${(e: ArxInputChangeEvent) => this.handleInputChange('banner', e)} + @input=${this.handleInputChange} placeholder="https://example.com/your-image.jpg" > diff --git a/src/routes/Wallet.ts b/src/routes/Wallet.ts deleted file mode 100644 index 8d3d54d..0000000 --- a/src/routes/Wallet.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { wallet } from '@/wallet'; -import type { ArxInputChangeEvent } from '@components/General/Input'; -import { StateController } from '@lit-app/state'; -import { LitElement, css, html } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -import '@components/General/Card'; -import '@components/General/Input'; -import '@components/WalletTransactionLine'; - -@customElement('arx-wallet-route') -export class WalletRoute extends LitElement { - static override styles = css` - h1 { - text-align: center; - } - - .transaction-list { - background-color: var(--color-base-100); - border-radius: var(--radius-card); - padding: 20px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - } - - .transaction-list h3 { - color: var(--color-base-content); - margin-bottom: 20px; - } - - arx-wallet-transaction-line:last-child { - border-bottom: none; - } - `; - - private inputToken = ''; - - override async firstUpdated() { - await wallet.loadWallet(); - } - - async doReceiveToken() { - console.log('clicked', Date.now()); - await wallet.receiveToken(this.inputToken); - this.inputToken = ''; - } - - override connectedCallback(): void { - super.connectedCallback(); - new StateController(this, wallet); - } - - override render() { - return html` - -

Wallet

-
-

You have ${wallet.balance} sats

- { - if (!e.detail.value) return; - this.inputToken = e.detail.value; - }} - type="text" - id="token" - > - -
-

Transaction History

- ${wallet.sortedHistory.map( - (h) => html``, - )} -
-
- `; - } -} diff --git a/src/routes/router.ts b/src/routes/router.ts index ab7a796..235a129 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -1,13 +1,12 @@ -import '@components/InitialSetup'; import '@routes/404Page'; -import '@routes/Arbor/Home'; -import '@routes/Arbor/NewPost'; -import '@routes/Arbor/NewTopic'; -import '@routes/Arbor/TopicView'; import '@routes/Home'; import '@routes/Profile'; import '@routes/Settings'; -import '@routes/Wallet'; +import '@routes/Arbor/Home'; +import '@routes/Arbor/NewTopic'; +import '@routes/Arbor/TopicView'; +import '@routes/Arbor/NewPost'; +import '@components/InitialSetup'; import { spread } from '@open-wc/lit-helpers'; import { LitElement, css } from 'lit'; @@ -66,11 +65,6 @@ export default class EveRouter extends LitElement { params: {}, component: literal`arx-settings`, }, - { - pattern: 'wallet', - params: {}, - component: literal`arx-wallet-route`, - }, { pattern: '404', params: {}, @@ -98,27 +92,6 @@ export default class EveRouter extends LitElement { overflow: hidden; } - ::-webkit-scrollbar { - width: 12px; - height: 12px; - } - - ::-webkit-scrollbar-track { - background: var(--color-base-200); - border-radius: var(--radius-field); - } - - ::-webkit-scrollbar-thumb { - background: var(--color-base-300); - border-radius: var(--radius-field); - border: 2px solid var(--color-base-200); - transition: var(--transition); - } - - ::-webkit-scrollbar-thumb:hover { - background: var(--color-neutral); - } - .window { overflow: auto; } @@ -130,10 +103,6 @@ export default class EveRouter extends LitElement { margin: 0 auto; padding: 1rem; } - - .hide-overflow { - overflow: hidden; - } `; constructor() { @@ -299,7 +268,7 @@ export default class EveRouter extends LitElement { @go-forward=${this.goForward} title="Eve" > -
+
${keyed( this.currentRoute.params, diff --git a/src/style.css b/src/style.css index bb02569..a6a8efb 100644 --- a/src/style.css +++ b/src/style.css @@ -43,8 +43,6 @@ --border: 2px; --depth: 1; --noise: 1; - - --font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace; } body.dark { diff --git a/src/wallet.ts b/src/wallet.ts deleted file mode 100644 index 3d1fc09..0000000 --- a/src/wallet.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { CashuMint, CashuWallet, type Proof, type SendOptions } from '@cashu/cashu-ts'; -import { bytesToHex } from '@noble/ciphers/utils'; -import { secp256k1 } from '@noble/curves/secp256k1'; -import { NDKEvent, type NDKEventId, NDKKind, type NDKTag, type NDKUser } from '@nostr-dev-kit/ndk'; -import { getSigner, ndk } from './ndk'; - -import { State, property } from '@lit-app/state'; - -export type WalletHistory = { - id: string; - amount: number; - created_at: number; - direction: 'in' | 'out'; - tokenEventId: NDKEventId; - nutZapEventId?: NDKEventId; - senderPubkey?: string; - recipientPubkey: string; -}; - -class Wallet extends State { - static mintUrl = 'https://testnut.cashu.space'; // TODO: this should be user controlled - - private walletEvent: NDKEvent | null = null; - private cashuWallet: CashuWallet = new CashuWallet(new CashuMint(Wallet.mintUrl)); - - private proofs: Proof[] = []; - private eventIdToProofIdMap: Map = new Map(); - - private isHandlingZapEvent = false; - private zapEventsQueue: NDKEvent[] = []; - private latestHistoryTimestamp = 0; - - @property() public balance = 0; - @property() public history: WalletHistory[] = []; - @property() public sortedHistory: WalletHistory[] = []; - - async loadWallet() { - if (this.walletEvent) return; - await getSigner(); - const user = await ndk.signer!.user()!; - const privateWalletEvent = await ndk.fetchEvent({ - kinds: [17375 as NDKKind], - authors: [user.pubkey], - }); - if (!privateWalletEvent) { - const walletPrivateKey = secp256k1.utils.randomPrivateKey(); - const walletPublicKey = secp256k1.getPublicKey(walletPrivateKey); - const newPrivateWalletEvent = new NDKEvent(ndk); - newPrivateWalletEvent.kind = NDKKind.CashuWallet; - newPrivateWalletEvent.content = await ndk.signer!.encrypt( - user, - JSON.stringify([ - ['privkey', bytesToHex(walletPrivateKey)], - ['mint', Wallet.mintUrl], - ]), - 'nip44', - ); - const publicWalletEvent = new NDKEvent(ndk); - publicWalletEvent.kind = NDKKind.CashuMintList; - publicWalletEvent.content = ''; - publicWalletEvent.tags = [ - ['mint', Wallet.mintUrl, 'sat'], - ['pubkey', bytesToHex(walletPublicKey)], - ]; - await newPrivateWalletEvent.sign(); - await newPrivateWalletEvent.publish(); - await publicWalletEvent.sign(); - await publicWalletEvent.publish(); - this.walletEvent = newPrivateWalletEvent; - } else this.walletEvent = privateWalletEvent; - - const walletHistory = await ndk.fetchEvents({ - kinds: [NDKKind.CashuWalletTx], - authors: [user.pubkey], - since: this.walletEvent?.created_at, - }); - - for (const event of walletHistory) { - await this.processHistoryEvent(event); - } - - const proofsEvents = await ndk.fetchEvents({ - kinds: [NDKKind.CashuToken], - authors: [user.pubkey], - }); - - for (const proofsEvent of proofsEvents) { - const { - mint, - proofs = [], - del = [], - } = JSON.parse(await ndk.signer!.decrypt(user, proofsEvent.content, 'nip44')) as { - mint: string; - proofs: Proof[]; - del?: NDKEventId[]; - }; - - if (mint !== this.cashuWallet.mint.mintUrl) continue; - - for (const eventId of del) { - const toDeletes = this.eventIdToProofIdMap.get(eventId); - if (toDeletes) - for (const proof of toDeletes) - this.proofs = this.proofs.filter((p) => p.secret !== proof[0] || p.C !== proof[1]); - this.eventIdToProofIdMap.delete(eventId); - } - - for (const proof of proofs) this.proofs.push(proof); - - this.eventIdToProofIdMap.set( - proofsEvent.id, - proofs.map((p) => [p.secret, p.C]), - ); - } - - ndk - .subscribe({ - kinds: [NDKKind.Nutzap], - '#p': [user.pubkey], - since: this.latestHistoryTimestamp, - }) - .on('event', async (event) => { - this.zapEventsQueue.push(event); - this.handleZapEvent(); - }); - - ndk - .subscribe({ - kinds: [NDKKind.CashuWalletTx], - authors: [user.pubkey], - since: this.latestHistoryTimestamp, - }) - .on('event', async (event) => { - await this.processHistoryEvent(event); - }); - - this.recalculateBalance(); - } - - private async processHistoryEvent(event: NDKEvent) { - const user = await ndk.signer!.user()!; - const parsedContent = JSON.parse(await ndk.signer!.decrypt(user, event.content, 'nip44')); - const direction = parsedContent.find((p: [string, string]) => p[0] === 'direction')?.[1]; - const amount = parsedContent.find((p: [string, string]) => p[0] === 'amount')?.[1]; - const tokenEventId = parsedContent.find((p: [string, string]) => p[0] === 'e')?.[1]; - const nutZapEventId = event.tags.find((t: NDKTag) => t[0] === 'e')?.[1]; - const senderPubkey = event.tags.find((t: NDKTag) => t[0] === 'p')?.[1]; - if (!nutZapEventId || !senderPubkey) return; - this.history.push({ - id: event.id, - amount: Number.parseInt(amount), - created_at: event.created_at ?? 0, - direction: direction as 'in' | 'out', - tokenEventId, - nutZapEventId: nutZapEventId as NDKEventId, - senderPubkey, - recipientPubkey: event.pubkey, - }); - this.latestHistoryTimestamp = Math.max(this.latestHistoryTimestamp, event.created_at ?? 0); - this.sortedHistory = [...this.history.sort((a, b) => b.created_at - a.created_at)]; - } - - private async handleZapEvent() { - if (this.isHandlingZapEvent) return; - const event = this.zapEventsQueue.shift(); - if (!event) return; - this.isHandlingZapEvent = true; - const proofs = JSON.parse(event.tags.find((t) => t[0] === 'proof')?.[1] ?? '[]') as Proof[]; - this.proofs.push(...proofs); - - const newProofsEvent = await this.createProofsEvent(); - await this.createHistoryEvent( - newProofsEvent, - this.proofs.reduce((acc, curr) => acc + curr.amount, 0), - 'in', - ); - this.recalculateBalance(); - this.isHandlingZapEvent = false; - } - - private async createProofsEvent(): Promise { - const newProofsEvent = new NDKEvent(ndk); - newProofsEvent.kind = NDKKind.CashuToken; - newProofsEvent.content = await ndk.signer?.encrypt( - await ndk.signer!.user()!, - JSON.stringify({ - mint: this.cashuWallet.mint.mintUrl, - proofs: this.proofs, - del: Array.from(this.eventIdToProofIdMap.keys()), - }), - 'nip44', - )!; - await newProofsEvent.sign(); - await newProofsEvent.publish(); - return newProofsEvent; - } - - recalculateBalance() { - this.proofs = this.proofs.filter( - (p, index, self) => index === self.findIndex((t) => t.secret === p.secret && t.C === p.C), - ); - this.balance = this.proofs.reduce((acc, curr) => acc + curr.amount, 0); - } - - private async createHistoryEvent(newProofsEvent: NDKEvent, amount: number, direction: 'in' | 'out') { - const historyEvent = new NDKEvent(ndk); - historyEvent.kind = NDKKind.CashuWalletTx; - historyEvent.content = await ndk.signer?.encrypt( - await ndk.signer!.user()!, - JSON.stringify([ - ['direction', direction], - ['amount', amount.toString()], - ['e', newProofsEvent.id], - ]), - 'nip44', - )!; - historyEvent.tags = [ - ['e', newProofsEvent.id], - ['p', newProofsEvent.pubkey], - ]; - await historyEvent.sign(); - await historyEvent.publish(); - } - - /** @throws Error if token is invalid */ - async receiveToken(token: string) { - const proofs = await this.cashuWallet.receive(token); - if (proofs.length === 0) throw new Error('Invalid token'); - for (const proof of proofs) this.proofs.push(proof); - const newProofsEvent = await this.createProofsEvent(); - const amount = proofs.reduce((acc, curr) => acc + curr.amount, 0); - await this.createHistoryEvent(newProofsEvent, amount, 'in'); - this.recalculateBalance(); - } - - private async createToken(amount: number, recipient?: NDKUser) { - if (amount < 10) throw new Error('Amount must be greater than 10'); - const sendOptions: SendOptions = { - includeFees: true, - }; - if (recipient) { - const mintListEvent = await ndk.fetchEvent({ - kinds: [NDKKind.CashuMintList], - authors: [recipient.pubkey], - }); - if (!mintListEvent) throw new Error('Recipient has no mint list'); - const pubkey = mintListEvent.tags.find((t) => t[0] === 'pubkey')?.[1]; - if (!pubkey) throw new Error('Recipient has no mint list'); - sendOptions.pubkey = pubkey; - } - await this.cashuWallet.getKeySets(); - const sendResult = await this.cashuWallet.send(amount, this.proofs, sendOptions); - this.proofs = sendResult.keep; - const proofsEvent = new NDKEvent(ndk); - proofsEvent.kind = NDKKind.CashuToken; - proofsEvent.content = await ndk.signer?.encrypt( - await ndk.signer!.user()!, - JSON.stringify({ - mint: this.cashuWallet.mint.mintUrl, - proofs: this.proofs, - del: Array.from(this.eventIdToProofIdMap.keys()), - }), - 'nip44', - )!; - await proofsEvent.sign(); - await proofsEvent.publish(); - this.recalculateBalance(); - return sendResult.send; - } - - async nutZap(amount: number, recipient: NDKUser, eventId?: NDKEventId, message?: string) { - const token = await this.createToken(amount, recipient); - const zapEvent = new NDKEvent(ndk); - zapEvent.kind = NDKKind.Nutzap; - zapEvent.content = message ?? ''; - zapEvent.tags = [ - ['proof', JSON.stringify(token)], - ['u', Wallet.mintUrl], - ['p', recipient.pubkey], - ]; - if (eventId) zapEvent.tags.push(['e', eventId]); - await zapEvent.sign(); - await zapEvent.publish(); - - const spendingHistoryEvent = new NDKEvent(ndk); - spendingHistoryEvent.kind = NDKKind.CashuWalletTx; - const historyContent = [ - ['direction', 'out'], - ['amount', amount.toString()], - ]; - if (eventId) historyContent.push(['e', eventId]); - spendingHistoryEvent.content = await ndk.signer?.encrypt( - await ndk.signer!.user()!, - JSON.stringify(historyContent), - 'nip44', - )!; - spendingHistoryEvent.tags = [ - ['e', zapEvent.id], - ['p', recipient.pubkey], - ]; - await spendingHistoryEvent.sign(); - await spendingHistoryEvent.publish(); - } -} - -export const wallet = new Wallet();