cashu wallet (beta) (testnet)

This commit is contained in:
Danny Morabito 2025-03-25 22:30:00 +01:00
parent 9a125e3111
commit 985c1494b5
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
8 changed files with 637 additions and 24 deletions

View file

@ -1,8 +1,12 @@
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 } from 'lit/decorators.js';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ndk } from '@/ndk';
import '@components/MarkdownContent';
@customElement('arx-forum-post')
@ -14,6 +18,16 @@ 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 {
@ -171,14 +185,24 @@ export class ForumPost extends LitElement {
}
private _handleZap() {
alert('Zapping is not yet implemented');
this.dispatchEvent(
new CustomEvent('zap', {
detail: { postId: this.id, npub: this.npub },
bubbles: true,
composed: true,
}),
);
// 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;
}
private _handleDownzap() {
@ -206,6 +230,16 @@ export class ForumPost extends LitElement {
};
return html`
<arx-prompt
promptText="Zap amount"
placeholder="Enter zap amount"
@save=${this._doZap}
@cancel=${() => {
this.zapAmountDialogOpen = false;
}}
showInput
.open=${this.zapAmountDialogOpen}
></arx-prompt>
<div class=${classMap(postClasses)} id="post-${this.id}">
<div class="post__sidebar">
<arx-nostr-profile
@ -248,7 +282,7 @@ export class ForumPost extends LitElement {
></iconify-icon>
</arx-button>
<arx-button label="Zap" @click=${this._handleZap} disabled>
<arx-button label="Zap" @click=${this._handleZap}>
<iconify-icon
slot="prefix"
icon="mdi:lightning-bolt"

View file

@ -0,0 +1,100 @@
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`<div class="transaction ${this.transaction.direction}">
<span class="amount">
${this.transaction.direction === 'in' ? '+' : '-'} ${satsComma(this.transaction.amount)}
sats
</span>
<span class="date">${formatDateTime(this.transaction.created_at * 1000)}</span>
<div class="profiles">
<arx-nostr-profile
pubkey=${this.transaction.senderPubkey}
class="sender"
></arx-nostr-profile>
<arx-nostr-profile
pubkey=${this.transaction.recipientPubkey}
class="recipient"
></arx-nostr-profile>
</div>
</div>`;
}
}

View file

@ -0,0 +1,82 @@
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`
<div class="widget-header">
<h3 class="widget-title">
<span class="bitcoin-icon"></span> Bitcoin Wallet
</h3>
</div>
<arx-fieldset .legend=${when(
this.loading,
() => 'Loading...',
() => `${satsComma(wallet.balance)} sats`,
)}>
${when(
wallet.sortedHistory.length > 0,
() => html`
Latest Transaction:
<arx-wallet-transaction-line .transaction=${wallet.sortedHistory[0]}></arx-wallet-transaction-line>
`,
() => 'No transactions yet',
)}
</arx-fieldset>
`;
}
}