From 7a09c7790288909f7f57736be2bf7ad630f392da Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Tue, 9 Apr 2024 13:19:43 +0200 Subject: [PATCH 1/3] Fix regression bug in reconnection to Nostr relays --- package.json | 2 +- src/services/nostrService.ts | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 2b9e5511..6d1f580c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.1.6-beta.47", + "version": "0.1.6-beta.48", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", diff --git a/src/services/nostrService.ts b/src/services/nostrService.ts index 14cd7472..757ba821 100644 --- a/src/services/nostrService.ts +++ b/src/services/nostrService.ts @@ -22,7 +22,7 @@ import {log} from './logService' import AppError, { Err } from '../utils/AppError' import { MinibitsClient } from './minibitsService' import { rootStoreInstance } from '../models' -import { Wallet } from './walletService' +import { WalletTask } from './walletService' export { NostrEvent, @@ -80,7 +80,7 @@ const reconnectToRelays = async function () { // recreate subscriptions if all relays down if(relaysStore.connectedCount === 0) { - Wallet.checkPendingReceived().catch(e => false) + WalletTask.handleSpentFromPending().catch(e => false) } const pool = getRelayPool() @@ -202,37 +202,37 @@ const decryptNip04 = async function( const publish = async function ( event: NostrUnsignedEvent, relays: string[], -): Promise { +): Promise { const keys: KeyPair = await getOrCreateKeyPair() - event.created_at = Math.floor(Date.now() / 1000) - event.id = getEventHash(event) - event.sig = getSignature(event, keys.privateKey) + const signed = {...event} as NostrEvent + + signed.created_at = Math.floor(Date.now() / 1000) + signed.id = getEventHash(event) + signed.sig = getSignature(event, keys.privateKey) if(!validateEvent(event)) { throw new AppError(Err.VALIDATION_ERROR, 'Event is invalid and could not be published', event) } - log.trace('Event to be published', event, 'publish') + log.trace('Event to be published', signed, 'publish') const pool = getRelayPool() - let pubs = pool.publish(relays, event) + let pubs = pool.publish(relays, signed) await delay(1000) // await Promise.all(pubs) const published: NostrEvent = await pool.get(relays, { - ids: [event.id] + ids: [signed.id] }) if(published) { - log.trace('Event successfully published', published, 'NostrClient.publish') - // pool.close(relays) + log.trace('Event successfully published', published, 'NostrClient.publish') return published } - - // pool.close(relays) + return undefined } @@ -258,10 +258,10 @@ const getEvent = async function ( const getEvents = async function ( relays: string[], filters: NostrFilter[] -): Promise { +): Promise { const pool = getRelayPool() - const events: Event[] = await pool.list(relays, filters) + const events: NostrEvent[] = await pool.list(relays, filters) if(events && events.length > 0) { return events @@ -357,7 +357,7 @@ const getProfileFromRelays = async function (pubkey: string, relays: string[]): kinds: [0], }] - const events: NostrEvent[] = await NostrClient.getEvents(relays, filters) + const events = await NostrClient.getEvents(relays, filters) if(!events || events.length === 0) { From 9a38bbb8261bf8dddc49e989df5bdec38994da54 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Tue, 9 Apr 2024 14:44:00 +0200 Subject: [PATCH 2/3] Fix regression bug in reconnection to Nostr relays --- package.json | 2 +- src/services/nostrService.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6d1f580c..651563a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.1.6-beta.48", + "version": "0.1.6-beta.49", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", diff --git a/src/services/nostrService.ts b/src/services/nostrService.ts index 757ba821..16664535 100644 --- a/src/services/nostrService.ts +++ b/src/services/nostrService.ts @@ -209,11 +209,11 @@ const publish = async function ( const signed = {...event} as NostrEvent signed.created_at = Math.floor(Date.now() / 1000) - signed.id = getEventHash(event) - signed.sig = getSignature(event, keys.privateKey) + signed.id = getEventHash(signed) + signed.sig = getSignature(signed, keys.privateKey) - if(!validateEvent(event)) { - throw new AppError(Err.VALIDATION_ERROR, 'Event is invalid and could not be published', event) + if(!validateEvent(signed)) { + throw new AppError(Err.VALIDATION_ERROR, 'Event is invalid and could not be published', signed) } log.trace('Event to be published', signed, 'publish') From 7c97e9032439d39bc70c0a4f86fcf1c034b4d151 Mon Sep 17 00:00:00 2001 From: KraXen72 Date: Mon, 22 Apr 2024 21:47:43 +0200 Subject: [PATCH 3/3] I18n improvements (#63) Co-authored-by: minibits-cash Co-authored-by: deerwhisper2310 <142937144+deerwhisper2310@users.noreply.github.com> --- README.md | 2 +- src/i18n/en.ts | 109 ++++- src/i18n/translate.ts | 35 +- src/screens/BackupScreen.tsx | 3 +- src/screens/Contacts/OwnName.tsx | 39 +- src/screens/Contacts/PrivateContacts.tsx | 245 +++++----- src/screens/Contacts/ProfileHeader.tsx | 3 +- src/screens/Contacts/PublicContacts.tsx | 41 +- src/screens/Contacts/RandomName.tsx | 239 +++++----- .../PaymentRequests/IncomingRequests.tsx | 2 +- .../PaymentRequests/OutgoingRequests.tsx | 17 +- .../PaymentRequestListItem.tsx | 445 +++++++++++------- .../Transactions/TransactionListItem.tsx | 419 ++++++++++------- src/screens/Wallet/FeeBadge.tsx | 87 ++-- 14 files changed, 1015 insertions(+), 671 deletions(-) diff --git a/README.md b/README.md index b988fbe9..49fc80b6 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Minibits wallet is in early beta and available as of now only for Android device Minibits is a bare React Native app written in Typescript. The project structure and code itself are intentionally verbose to support readability. Critical wallet code is reasonably documented. However, there is vast space for existing code improvements, refactoring, and bug fixing. This is an early beta software and the author does not code for a living. -The code is derived from Ignite template, however with many libraries, notably Expo, stripped down to achieve fast startup times. Performance bottleneck on some Android devices is react-native-keychain. To overcome this, it has been patched not to warm-up on startup, caching for wallet operatoions is in place and its use to encrypt storage is opt-in. +The code is derived from Ignite template, however with many libraries, notably Expo, stripped down to achieve fast startup times. Performance bottleneck on some Android devices is react-native-keychain. To overcome this, it has been patched not to warm-up on startup, caching for wallet operations is in place and its use to encrypt storage is opt-in. Wallet state is managed by mobx-state-tree and persisted in fast MMKV storage. Only the basic mobx concepts are in place, whole model could be improved. All critical wallet code is in services/walletService.ts and all ecash state changes are in models/ProofsStore.ts. Wallet communication with the mints is in services/cashuMintClient.ts and uses [cashu-ts](https://github.com/cashubtc/cashu-ts) library. diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 3f2f824d..26370082 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -7,11 +7,47 @@ const en = { paste: "Paste", scan: "Scan", copy: "Copy", + copyFailParam: "Couldn't copy %{param}", confirm: "Confirm", save: "Save", add: "Add", + success: "Success!", offline: "Offline", + qr: "QR Code", + sats: "SATS", + notCreated: 'Not yet created', + resetDefault: 'Reset to default', + share: "Share", }, + paymentCommon: { + pay: "Pay", + payMe: 'Pay me', + paySatsAmountTo: "Pay %{amount} SATS to", + payFromWallet: 'Pay from wallet', + receiveInPerson: "Receive in person" + }, + transactionCommon: { + from: "From %{sender}", + receivedFrom: "Received from %{sender}", + sentTo: "Sent to %{receiver}", + paidTo: "Paid to %{receiver}", + youReceived: 'You received', + youSent: 'You sent', + youPaid: 'You paid', + unknown: 'Unknown transaction', + status: { + completedFee: 'Completed %{fee}', + draft: 'Draft', + error: 'Error', + pending: 'Pending', + prepared: 'Prepared', + reverted: 'Reverted', + blocked: 'Blocked', + expired: 'Expired' + }, + tapToRedeem: 'Tap to redeem', + redeemOnline: 'Redeem online' + }, welcomeScreen: { page1: { heading: "Welcome", @@ -48,6 +84,10 @@ const en = { topUpWalletSubText: "Top up your balance by paying Bitcoin lightning invoice from another wallet", transferFromWallet: "Transfer from wallet", transferFromWalletSubText: "Debit your balance by paying Bitcoin lightning invoice from this wallet", + feeBadge: { + final: "+ final fee %{fee} SATS", + upto: "+ fee up to %{fee} SATS" + } }, receiveScreen: { title: "Receive", @@ -112,8 +152,75 @@ const en = { contactsScreen: { new: "New", scan: "Scan", - newTitle: "Add new contact", + newTitle: "Add new contact", + getInvoice: "Get invoice", + ownName: { + donationSuccess: "Thank you! Donation for %{receiver} has been successfully paid.", + tooShort: 'Write your wallet profile name to the text box, use min 2 characters.', + illegalChar: 'Do not use . or - characters at the beginning or the end of name.', + profileExists: 'This wallet name is already in use, choose another one.', + chooseOwnName: 'Choose your own name', + chooseOwnNameFooter: 'Use lowercase letters, numbers and .-_', + available: "%{name} is available!", + payToGetOwnName: "Pay the following lightning invoice and get your %{name} wallet name.", + ctaPay: "Pay from wallet", + insufficient: 'Your wallet balance is not enough to pay this invoice amount but you can still pay it from another wallet.', + betaWarning: 'Please accept this is an early beta software. Your data can still be lost due to a bug or unexpected data loss.' + }, + privateContacts: { + selectSendPaymentRequest: 'Select contact to send your payment request to.', + selectSendToken: 'Select contact to send your ecash to.', + selectSendLnURL: 'Select contact to send Lightning payment to.', + saveNewFormat: "Please enter a wallet address in name@domain.xyz format", + profileNotFound: "Profile name %{name} could not be found. Make sure the name is correct.", + noLightningAddress: 'This contact does not have a Lightning address, send ecash instead.', + explainerText: "Private contacts", + explainerSubText: "Add other Minibits users as your private contacts. Every user gets sharable @minibits.cash wallet address. You can pay privately to your contacts anytime even if they are offline.", + switchName: "Switch your wallet name and picture?", + switchNameSubText: 'Get cooler wallet name or profile picture. Select from an array of random names and images or opt for your own @minibits.cash wallet name.', + bottomModal: "Private contacts are unique identifiers of other Minibits wallets. You can use them to send or request ecash and you can safely share your own with others.", + domainMinibits: "Use minibits.cash domain", + domainExternal: "Use another domain" + }, + publicContacts: { + npubPasteError: 'Copy your NPUB key first, then paste', + relayurlPasteError: 'Copy your relay URL key first, then paste', + relayExists: 'Relay already exists.', + missingLightningAddress: 'This contact does not have a Lightning address, send ecash instead.', + nostrTip: 'Tip the people you follow', + nostrTipSubText: 'Add your NOSTR social network public key (npub) and tip or donate to your favourite people and projects directly from the minibits wallet.', + nostrSetPublicKey: 'Set your public key', + nostrSetPublicKeySubText: 'Add or change your NOSTR social network public key (npub).', + nostrSetRelay: 'Set relay', + nostrSetRelaySubText: 'Add or change your own relay if your profile and follows are not hosted on the default relays.', + nostrRemovePub: 'Remove your public key', + nostrRemovePubSubText: 'Remove your npub key and stop loading public contacts.', + addNpub: 'Add your npub key', + pasteDemoKey: 'Paste demo key', + setOwnRelay: 'Set your own relay' + }, + randomName: { + selectOneOfUsernames: 'Select one of the usernames' + } }, + paymentRequestScreen: { + incoming: { + noRequests: 'There are no incoming payment requests to be paid or they have already expired.' + }, + outgoing: { + noRequests: 'There are no outgoing payment requests to be paid or they have already expired.', + invoiceShared: 'Lightning invoice has been shared, waiting to be paid by receiver.', + sharingCancelled: 'Sharing cancelled', + scanAndPay: 'Scan and pay to top-up' + }, + // might move this to common later if it gets used in other screens + listItem: { + expires: "Expires %{datetime}", + expired: "Expired %{datetime}", + requestPaymentSuccess: 'Request has been paid' + } + }, + profileScreen: { changeAvatar: "Change picture", changeAvatarSubtext: "Select one from randomly generated pictures.", diff --git a/src/i18n/translate.ts b/src/i18n/translate.ts index ae6aebab..2a82e8fe 100644 --- a/src/i18n/translate.ts +++ b/src/i18n/translate.ts @@ -1,7 +1,6 @@ - -import { I18nOptions } from "i18n-js" -import { i18n } from "./i18n" -import { TxKeyPath } from "./i18n" +import {I18nOptions} from 'i18n-js' +import {i18n} from './i18n' +import {TxKeyPath} from './i18n' /** * Translates text. @@ -12,21 +11,29 @@ import { TxKeyPath } from "./i18n" * * @example * Translations: - * - * ```en.ts + * ```ts + * // en.ts * { - * "hello": "Hello, {{name}}!" + * "heading": "Welcome to the app!", + * "buttonText": "Donate", + * "greeting": "Hello, %{name}!" // parameter * } * ``` - * * Usage: - * ```ts - * import { translate } from "i18n-js" + * ```tsx + * import { translate } from "../../i18n"; + * // or whatever the path is, let it auto-import * - * translate("common.ok", { name: "world" }) - * // => "Hello world!" + * translate("heading") // => Welcome to the app! + * translate("greeting", { name: "world" }) // => Hello world! + *