mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-06 19:53:09 +00:00
P2PK implementation
This commit is contained in:
@@ -10,13 +10,14 @@ import { translate, TxKeyPath } from '../i18n';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface QRShareModalProps {
|
||||
url: string,
|
||||
data: string,
|
||||
isVisible?: boolean,
|
||||
onClose?: () => void,
|
||||
type: QRCodeBlockTypes
|
||||
shareModalTitle?: string,
|
||||
shareModalTx?: TxKeyPath,
|
||||
subHeading?: string,
|
||||
subHeadingTx?: TxKeyPath,
|
||||
label?: string
|
||||
labelTx?: TxKeyPath,
|
||||
size?: number,
|
||||
@@ -25,12 +26,13 @@ interface QRShareModalProps {
|
||||
export const QRShareModal = (props: QRShareModalProps) => {
|
||||
const labelText = useThemeColor('textDim')
|
||||
const {
|
||||
url,
|
||||
data,
|
||||
isVisible = true,
|
||||
onClose = () => {},
|
||||
shareModalTitle,
|
||||
shareModalTx,
|
||||
subHeading,
|
||||
subHeadingTx,
|
||||
type,
|
||||
label,
|
||||
labelTx,
|
||||
@@ -38,22 +40,26 @@ export const QRShareModal = (props: QRShareModalProps) => {
|
||||
} = props
|
||||
|
||||
let finalTx: TxKeyPath | undefined = shareModalTx
|
||||
if (!shareModalTx && !shareModalTitle) finalTx = 'common.share'
|
||||
|
||||
|
||||
return (
|
||||
<BottomModal
|
||||
isVisible={isVisible}
|
||||
ContentComponent={
|
||||
<>
|
||||
<Text
|
||||
tx={finalTx}
|
||||
text={shareModalTitle}
|
||||
style={{alignSelf: 'center', marginBottom: spacing.small}}
|
||||
/>
|
||||
<>
|
||||
{shareModalTx || shareModalTitle && (
|
||||
<Text
|
||||
tx={shareModalTx}
|
||||
text={shareModalTitle}
|
||||
style={{alignSelf: 'center', marginBottom: spacing.small}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<View style={$newContainer}>
|
||||
<QRCodeBlock
|
||||
qrCodeData={url.toString()}
|
||||
title={subHeading || shareModalTitle || translate('common.share')}
|
||||
qrCodeData={data.toString()}
|
||||
title={subHeading}
|
||||
titleTx={subHeadingTx}
|
||||
type={type}
|
||||
size={size}
|
||||
/>
|
||||
@@ -72,7 +78,7 @@ export const QRShareModal = (props: QRShareModalProps) => {
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
onPress={props.onClose}
|
||||
text='Close'
|
||||
tx='common.close'
|
||||
preset='tertiary'
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -20,3 +20,4 @@ export * from "./EurIcon"
|
||||
export * from "./MintIcon"
|
||||
export * from "./HeaderBg"
|
||||
export * from "./NwcIcon"
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
"copySuccessParam": "Copied %{param}",
|
||||
"hide": "Hide",
|
||||
"show": "Show",
|
||||
"update": "Update"
|
||||
"update": "Update",
|
||||
"none": ""
|
||||
},
|
||||
"confirmBackupRecovery": "Do you really want to recover ecash from the backup?",
|
||||
"confirmMintRemoval": "Do you really want to remove %{hostname} - %{shortname} from the wallet?",
|
||||
@@ -440,6 +441,8 @@
|
||||
"contactAddress": "Share contact address"
|
||||
},
|
||||
"shareWalletAddress": "Share wallet address",
|
||||
"shareWalletPubkey": "Share wallet public key (HEX)",
|
||||
"shareWalletNpub": "Share wallet public key (NPUB)",
|
||||
"showErrors": "Show errors",
|
||||
"showOnboarding": "Show onboarding again",
|
||||
"showOnboardingDesc": "Go through onboarding screens again next time you restart Minibits.",
|
||||
|
||||
@@ -138,6 +138,12 @@ export const ProofsStoreModel = types
|
||||
addProofs(newProofs: Proof[], isPending: boolean = false): {addedAmount: number, addedProofs: Proof[]} {
|
||||
try {
|
||||
const proofs = isPending ? self.pendingProofs : self.proofs
|
||||
|
||||
if(newProofs.length === 0) {
|
||||
log.error('[addProofs]', 'No proofs to add')
|
||||
return {addedAmount: 0, addedProofs: []}
|
||||
}
|
||||
|
||||
let addedAmount: number = 0
|
||||
let addedProofs: Proof[] = []
|
||||
const unit: MintUnit = newProofs[0].unit
|
||||
|
||||
@@ -65,6 +65,7 @@ export type SendParams = {
|
||||
offline?: boolean;
|
||||
includeFees?: boolean;
|
||||
includeDleq?: boolean;
|
||||
p2pk?: { pubkey: string; locktime?: number; refundKeys?: Array<string> }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +486,8 @@ export const WalletStoreModel = types
|
||||
transactionId: number,
|
||||
options?: {
|
||||
increaseCounterBy?: number,
|
||||
inFlightRequest?: InFlightRequest<SendParams>
|
||||
inFlightRequest?: InFlightRequest<SendParams>
|
||||
p2pk: { pubkey: string; locktime?: number; refundKeys?: Array<string> }
|
||||
}
|
||||
) {
|
||||
|
||||
@@ -517,8 +519,10 @@ export const WalletStoreModel = types
|
||||
proofs: proofsToSendFrom,
|
||||
options: {
|
||||
keysetId: cashuWallet.keysetId,
|
||||
counter: currentCounter.counter,
|
||||
includeFees: false // fee reserve needs to be already in proofsToSendFrom
|
||||
counter: options?.p2pk ? undefined : currentCounter.counter,
|
||||
includeFees: false, // fee reserve needs to be already in proofsToSendFrom
|
||||
p2pk: options?.p2pk
|
||||
// pubkey: options?.p2pk?.pubkey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,8 +540,8 @@ export const WalletStoreModel = types
|
||||
|
||||
currentCounter.removeInFlightRequest(transactionId)
|
||||
|
||||
log.trace(`[WalletStore.send] ${keep.length} returnedProofs`)
|
||||
log.trace(`[WalletStore.send] ${send.length} proofsToSend`)
|
||||
log.trace(`[WalletStore.send] ${keep.length} returnedProofs`, {keep})
|
||||
log.trace(`[WalletStore.send] ${send.length} proofsToSend`, {send})
|
||||
|
||||
const totalAmountToSendFrom: number = CashuUtils.getProofsAmount(proofsToSendFrom)
|
||||
const returnedAmount: number = CashuUtils.getProofsAmount(keep)
|
||||
|
||||
@@ -273,7 +273,7 @@ export const MintInfoScreen = observer(function MintInfoScreen({ route }: Props)
|
||||
|
||||
)}
|
||||
<QRShareModal
|
||||
url={route.params.mintUrl}
|
||||
data={route.params.mintUrl}
|
||||
shareModalTx='mintsScreen.share'
|
||||
subHeading={mintInfo?.name ?? translate('mintInfo.loadingNamePlaceholder')}
|
||||
type='URL'
|
||||
|
||||
@@ -16,8 +16,10 @@ export const MintBalanceSelector = observer(function (props: {
|
||||
unit: MintUnit
|
||||
title: string
|
||||
confirmTitle: string
|
||||
secondaryConfirmTitle?: string
|
||||
collapsible?: boolean
|
||||
onMintBalanceSelect: any
|
||||
onSecondaryMintBalanceSelect?: any
|
||||
onCancel: any
|
||||
onMintBalanceConfirm: any
|
||||
}) {
|
||||
@@ -116,11 +118,21 @@ export const MintBalanceSelector = observer(function (props: {
|
||||
<View style={$bottomContainer}>
|
||||
{!isKeyboardVisible && (
|
||||
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
|
||||
|
||||
{props.secondaryConfirmTitle && (
|
||||
<Button
|
||||
text={props.secondaryConfirmTitle}
|
||||
preset="secondary"
|
||||
onPress={props.onSecondaryMintBalanceSelect}
|
||||
style={{marginRight: spacing.medium}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
text={props.confirmTitle}
|
||||
onPress={props.onMintBalanceConfirm}
|
||||
style={{marginRight: spacing.medium}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.cancel'}
|
||||
|
||||
@@ -270,7 +270,10 @@ export const MintsScreen = observer(function MintsScreen({ route }: Props) {
|
||||
const onShare = function () {
|
||||
if (!selectedMint) return
|
||||
toggleMintMenuModal()
|
||||
toggleShareModal()
|
||||
setTimeout(() => {
|
||||
toggleShareModal()
|
||||
}, 500)
|
||||
|
||||
}
|
||||
|
||||
const handleError = function (e: AppError) {
|
||||
@@ -492,7 +495,7 @@ export const MintsScreen = observer(function MintsScreen({ route }: Props) {
|
||||
/>
|
||||
{selectedMint && (
|
||||
<QRShareModal
|
||||
url={selectedMint.mintUrl}
|
||||
data={selectedMint.mintUrl}
|
||||
shareModalTx='mintsScreen.share'
|
||||
subHeading={selectedMint?.shortname}
|
||||
type='URL'
|
||||
|
||||
@@ -9,9 +9,10 @@ import { ProfileHeader } from '../components/ProfileHeader'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import { log } from '../services/logService'
|
||||
import { KeyChain, MinibitsClient, NostrClient, NostrProfile } from '../services'
|
||||
import { translate } from '../i18n'
|
||||
import { translate, TxKeyPath } from '../i18n'
|
||||
import { CollapsibleText } from '../components/CollapsibleText'
|
||||
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
|
||||
import { QRShareModal } from '../components/QRShareModal'
|
||||
|
||||
type Props = StaticScreenProps<undefined>
|
||||
|
||||
@@ -25,6 +26,8 @@ export const ProfileScreen = observer(function ProfileScreen({ route }: Props) {
|
||||
)
|
||||
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState<boolean>(false)
|
||||
const [isShareModalVisible, setIsShareModalVisible] = useState<boolean>(false)
|
||||
const [isQrCodeModalVisible, setIsQrCodeModalVisible] = useState(false)
|
||||
const [qrCodeData, setQrCodeData] = useState<{title: TxKeyPath, data: string}>(undefined)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [info, setInfo] = useState('')
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
@@ -44,8 +47,8 @@ export const ProfileScreen = observer(function ProfileScreen({ route }: Props) {
|
||||
setIsUpdateModalVisible(previousState => !previousState)
|
||||
}
|
||||
|
||||
const toggleShareModal = () => {
|
||||
setIsShareModalVisible(previousState => !previousState)
|
||||
const toggleQrCodeModal = () => {
|
||||
setIsQrCodeModalVisible(previousState => !previousState)
|
||||
}
|
||||
|
||||
const gotoAvatar = function() {
|
||||
@@ -64,22 +67,33 @@ export const ProfileScreen = observer(function ProfileScreen({ route }: Props) {
|
||||
navigation.navigate('Privacy')
|
||||
}
|
||||
|
||||
const toggleShareModal = () => {
|
||||
setIsShareModalVisible(previousState => !previousState)
|
||||
}
|
||||
|
||||
const onCopyNpub = function () {
|
||||
try {
|
||||
Clipboard.setString(npub)
|
||||
} catch (e: any) {
|
||||
setInfo(translate('common.copyFailParam', { param: e.message }))
|
||||
}
|
||||
const onShareNpub = function () {
|
||||
toggleShareModal()
|
||||
setQrCodeData({
|
||||
title: 'shareWalletNpub',
|
||||
data: npub,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
toggleQrCodeModal()
|
||||
}, 500) // ios fix
|
||||
}
|
||||
|
||||
|
||||
const onCopyPubkey = function () {
|
||||
try {
|
||||
Clipboard.setString(pubkey)
|
||||
} catch (e: any) {
|
||||
setInfo(translate('common.copyFailParam', { param: e.message }))
|
||||
}
|
||||
const onSharePubkey = function () {
|
||||
toggleShareModal()
|
||||
setQrCodeData({
|
||||
title: 'shareWalletPubkey',
|
||||
data: pubkey,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
toggleQrCodeModal()
|
||||
}, 500) // ios fix
|
||||
}
|
||||
|
||||
const onCopyNip05 = function () {
|
||||
@@ -278,23 +292,32 @@ export const ProfileScreen = observer(function ProfileScreen({ route }: Props) {
|
||||
bottomSeparator={true}
|
||||
/>
|
||||
<ListItem
|
||||
text="Copy Nostr public key (NPUB)"
|
||||
tx="shareWalletNpub"
|
||||
subText={npub}
|
||||
leftIcon='faCopy'
|
||||
onPress={onCopyNpub}
|
||||
leftIcon='faQrcode'
|
||||
onPress={onShareNpub}
|
||||
bottomSeparator={true}
|
||||
/>
|
||||
<ListItem
|
||||
text="Copy Nostr public key (HEX)"
|
||||
tx="shareWalletPubkey"
|
||||
subText={pubkey}
|
||||
leftIcon='faCopy'
|
||||
onPress={onCopyPubkey}
|
||||
leftIcon='faQrcode'
|
||||
onPress={onSharePubkey}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleShareModal}
|
||||
onBackdropPress={toggleShareModal}
|
||||
/>
|
||||
{qrCodeData && (
|
||||
<QRShareModal
|
||||
data={qrCodeData.data}
|
||||
subHeadingTx={qrCodeData.title}
|
||||
type='PUBKEY'
|
||||
isVisible={isQrCodeModalVisible}
|
||||
onClose={toggleQrCodeModal}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <Loading />}
|
||||
{error && <ErrorModal error={error} />}
|
||||
{info && <InfoModal message={info} />}
|
||||
|
||||
+361
-68
@@ -10,8 +10,6 @@ import React, {
|
||||
} from 'react'
|
||||
import {StackActions, StaticScreenProps, useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
import {
|
||||
UIManager,
|
||||
Platform,
|
||||
TextInput,
|
||||
TextStyle,
|
||||
View,
|
||||
@@ -21,6 +19,7 @@ import {
|
||||
FlatList,
|
||||
ImageStyle,
|
||||
Image,
|
||||
Pressable,
|
||||
} from 'react-native'
|
||||
import {spacing, typography, useThemeColor, colors} from '../theme'
|
||||
import {
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
ErrorModal,
|
||||
BottomModal,
|
||||
Text,
|
||||
ListItem,
|
||||
} from '../components'
|
||||
import {TransactionStatus, Transaction} from '../models/Transaction'
|
||||
import {useStores} from '../models'
|
||||
@@ -63,6 +63,8 @@ import { SEND_TASK } from '../services/wallet/sendTask'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { CurrencyAmount } from './Wallet/CurrencyAmount'
|
||||
import { CashuUtils } from '../services/cashu/cashuUtils'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import { getUnixTime } from 'date-fns'
|
||||
|
||||
export enum SendOption {
|
||||
SEND_TOKEN = 'SEND_TOKEN',
|
||||
@@ -98,6 +100,7 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
|
||||
const amountInputRef = useRef<TextInput>(null)
|
||||
const memoInputRef = useRef<TextInput>(null)
|
||||
const pubkeyInputRef = useRef<TextInput>(null) // Initialize pubkeyInputRef
|
||||
const unitRef = useRef<MintUnit>('sat')
|
||||
|
||||
const [paymentOption, setPaymentOption] = useState<SendOption>(SendOption.SHOW_TOKEN)
|
||||
@@ -133,7 +136,10 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
|
||||
const [isNostrDMSending, setIsNostrDMSending] = useState(false)
|
||||
const [isNostrDMSuccess, setIsNostrDMSuccess] = useState(false)
|
||||
const [isLockedToPubkey, setIsLockedToPubkey] = useState(false)
|
||||
const [isLockedToPubkey, setIsLockedToPubkey] = useState(false)
|
||||
const [isPubkeySelectorModalVisible, setIsPubkeySelectorModalVisible] = useState(false)
|
||||
const [lockedPubkey, setLockedPubkey] = useState<string | undefined>() // Added lockedPubkey state
|
||||
const [lockTime, setLockTime] = useState<number | undefined>(1)
|
||||
|
||||
useEffect(() => {
|
||||
const focus = () => {
|
||||
@@ -172,6 +178,21 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
return () => {}
|
||||
}, [])
|
||||
|
||||
const getContactFrom = () => {
|
||||
const {
|
||||
pubkey,
|
||||
npub,
|
||||
name,
|
||||
picture,
|
||||
} = walletProfileStore
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
npub,
|
||||
name,
|
||||
picture
|
||||
} as Contact
|
||||
}
|
||||
|
||||
// Send to contact
|
||||
useFocusEffect(
|
||||
@@ -179,22 +200,6 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
|
||||
const {paymentOption, contact} = route.params
|
||||
|
||||
const getContactFrom = () => {
|
||||
const {
|
||||
pubkey,
|
||||
npub,
|
||||
name,
|
||||
picture,
|
||||
} = walletProfileStore
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
npub,
|
||||
name,
|
||||
picture
|
||||
} as Contact
|
||||
}
|
||||
|
||||
const prepareSendToContact = () => {
|
||||
try {
|
||||
let relays: string[] = []
|
||||
@@ -206,7 +211,7 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
relays = relaysStore.allUrls
|
||||
}
|
||||
|
||||
if (!relays) {
|
||||
if (relays.length === 0) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing NOSTR relays')
|
||||
}
|
||||
|
||||
@@ -527,6 +532,7 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
const toggleProofSelectorModal = () => setIsProofSelectorModalVisible(previousState => !previousState)
|
||||
const toggleResultModal = () => setIsResultModalVisible(previousState => !previousState)
|
||||
const toggleIsLockedToPubkey = () => setIsLockedToPubkey(previousState => !previousState)
|
||||
const togglePubkeySelectorModal = () => setIsPubkeySelectorModalVisible(previousState => !previousState)
|
||||
|
||||
const onAmountEndEditing = function () {
|
||||
try {
|
||||
@@ -599,6 +605,24 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
setMintBalanceToSendFrom(balance)
|
||||
}
|
||||
|
||||
|
||||
const onLockPubkeyStart = function () {
|
||||
togglePubkeySelectorModal()
|
||||
}
|
||||
|
||||
const onLockPubkeySelect = function () {
|
||||
if(!lockedPubkey) {
|
||||
return
|
||||
}
|
||||
setIsLockedToPubkey(true)
|
||||
togglePubkeySelectorModal()
|
||||
}
|
||||
|
||||
const onLockPubkeyCancel = function () {
|
||||
togglePubkeySelectorModal()
|
||||
setLockedPubkey(undefined)
|
||||
}
|
||||
|
||||
|
||||
const onMintBalanceConfirm = async function () {
|
||||
if (!mintBalanceToSendFrom) {
|
||||
@@ -606,16 +630,32 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setIsSendTaskSentToQueue(true)
|
||||
|
||||
const amountToSendInt = round(toNumber(amountToSend) * getCurrency(unitRef.current).precision, 0)
|
||||
|
||||
const p2pk: {
|
||||
pubkey: string;
|
||||
locktime?: number;
|
||||
refundKeys?: Array<string>
|
||||
} = {}
|
||||
|
||||
log.trace('[onMintBalanceConfirm] lockedPubkey', {lockedPubkey})
|
||||
|
||||
if(lockedPubkey) {
|
||||
p2pk.pubkey = '02'+lockedPubkey
|
||||
if(lockTime && lockTime > 0) {
|
||||
p2pk.locktime = getUnixTime(new Date(Date.now() + lockTime * 24 * 60 * 60))
|
||||
log.trace('[onMintBalanceConfirm] Locktime', {locktime: p2pk.locktime})
|
||||
}
|
||||
}
|
||||
|
||||
setIsSendTaskSentToQueue(true)
|
||||
WalletTask.sendQueue(
|
||||
mintBalanceToSendFrom as MintBalance,
|
||||
amountToSendInt,
|
||||
unitRef.current,
|
||||
memo,
|
||||
selectedProofs
|
||||
selectedProofs,
|
||||
p2pk
|
||||
)
|
||||
}
|
||||
|
||||
@@ -787,13 +827,41 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
onMintBalanceConfirm()
|
||||
}
|
||||
|
||||
|
||||
const gotoContacts = function () {
|
||||
//@ts-ignore
|
||||
navigation.navigate('ContactsNavigator', {
|
||||
screen: 'Contacts',
|
||||
params: {paymentOption: SendOption.SEND_TOKEN}
|
||||
})
|
||||
if(encodedTokenToSend &&
|
||||
lockedPubkey &&
|
||||
contactsStore.contacts.some(c => c.pubkey === lockedPubkey
|
||||
)) {
|
||||
|
||||
const contact = contactsStore.findByPubkey(lockedPubkey)
|
||||
let relays: string[] = []
|
||||
|
||||
if(contact?.type === ContactType.PUBLIC) {
|
||||
relays = relaysStore.allPublicUrls
|
||||
} else {
|
||||
relays = relaysStore.allUrls
|
||||
}
|
||||
|
||||
if (relays.length === 0) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing NOSTR relays')
|
||||
}
|
||||
|
||||
setPaymentOption(SendOption.SEND_TOKEN)
|
||||
setContactToSendFrom(getContactFrom())
|
||||
setContactToSendTo(contact)
|
||||
setRelaysToShareTo(relays)
|
||||
|
||||
if(encodedTokenToSend) {
|
||||
toggleNostrDMModal() // open if we already have a token
|
||||
}
|
||||
|
||||
} else {
|
||||
//@ts-ignore
|
||||
navigation.navigate('ContactsNavigator', {
|
||||
screen: 'Contacts',
|
||||
params: {paymentOption: SendOption.SEND_TOKEN}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -820,6 +888,9 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
setIsLoading(false)
|
||||
setResultModalInfo(undefined)
|
||||
setIsResultModalVisible(false)
|
||||
setLockTime(undefined)
|
||||
setIsLockedToPubkey(false)
|
||||
setLockedPubkey(undefined)
|
||||
}
|
||||
|
||||
|
||||
@@ -834,6 +905,24 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
|
||||
const headerBg = useThemeColor('header')
|
||||
const amountInputColor = useThemeColor('amountInput')
|
||||
const hintColor = useThemeColor('textDim')
|
||||
const inputText = useThemeColor('text')
|
||||
const inputBg = useThemeColor('background')
|
||||
const buttonBorder = useThemeColor('card')
|
||||
|
||||
|
||||
const onPasteLockedPubkey = async function () {
|
||||
try {
|
||||
const pastedText = await Clipboard.getString()
|
||||
setLockedPubkey(pastedText)
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
const onScanLockedPubkey = async function () {
|
||||
// TODO
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
@@ -862,7 +951,28 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
size='sm'
|
||||
text="Amount to send"
|
||||
style={{color: amountInputColor, textAlign: 'center'}}
|
||||
/>
|
||||
/>
|
||||
{lockedPubkey && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon="faLock"
|
||||
size={spacing.small}
|
||||
color={amountInputColor}
|
||||
/>
|
||||
<Text
|
||||
size='xxs'
|
||||
text="Locked"
|
||||
style={{color: amountInputColor, marginLeft: spacing.tiny}}
|
||||
/>
|
||||
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View style={$contentContainer}>
|
||||
@@ -883,7 +993,9 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
unit={unitRef.current}
|
||||
title='Send from mint'
|
||||
confirmTitle={isOfflineSend ? 'Send offline' : 'Create token'}
|
||||
secondaryConfirmTitle='Lock'
|
||||
onMintBalanceSelect={onMintBalanceSelect}
|
||||
onSecondaryMintBalanceSelect={onLockPubkeyStart}
|
||||
onCancel={onMintBalanceCancel}
|
||||
onMintBalanceConfirm={isOfflineSend ? onSelectProofsOffline : onMintBalanceConfirm}
|
||||
/>
|
||||
@@ -962,6 +1074,164 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
onBackButtonPress={toggleProofSelectorModal}
|
||||
onBackdropPress={toggleProofSelectorModal}
|
||||
/>
|
||||
<BottomModal
|
||||
isVisible={isPubkeySelectorModalVisible}
|
||||
ContentComponent={
|
||||
<View style={$bottomModal}>
|
||||
<Text text="Lock ecash" preset="subheading" />
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{color: hintColor}}
|
||||
text="Lock ecash token to one of your contacts or to a public key. Only the receiver will be able to unlock it."
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.small,
|
||||
}}>
|
||||
<TextInput
|
||||
ref={pubkeyInputRef}
|
||||
onChangeText={pubkey =>
|
||||
setLockedPubkey(pubkey)
|
||||
}
|
||||
value={lockedPubkey}
|
||||
autoCapitalize="none"
|
||||
keyboardType="default"
|
||||
maxLength={64}
|
||||
selectTextOnFocus={true}
|
||||
style={[
|
||||
$pubkeyInput,
|
||||
{backgroundColor: inputBg, color: inputText},
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
preset='secondary'
|
||||
tx='common.paste'
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
marginLeft: -spacing.small,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: buttonBorder
|
||||
}}
|
||||
onPress={onPasteLockedPubkey}
|
||||
/>
|
||||
<Button
|
||||
preset='secondary'
|
||||
tx="common.scan"
|
||||
style={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
marginHorizontal: 1,
|
||||
}}
|
||||
onPress={onScanLockedPubkey}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginTop: spacing.medium,
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: inputBg
|
||||
}}
|
||||
>
|
||||
<FlatList
|
||||
data={contactsStore.contacts}
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<ContactItem
|
||||
contact={item}
|
||||
onPress={() => setLockedPubkey(item.pubkey)}
|
||||
containerStyle={{
|
||||
paddingHorizontal: spacing.small,
|
||||
borderRadius: spacing.tiny,
|
||||
backgroundColor: lockedPubkey === item.pubkey ? inputBg : undefined,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
horizontal={true}
|
||||
keyExtractor={(item) => item.pubkey}
|
||||
style={{marginBottom: spacing.medium}}
|
||||
contentContainerStyle={{
|
||||
justifyContent: 'center', // Center items horizontally
|
||||
alignItems: 'center', // Center items vertically
|
||||
flexGrow: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
size="xxs"
|
||||
style={{color: hintColor, marginTop: spacing.small}}
|
||||
text="Lock for"
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
$buttonContainer,
|
||||
{
|
||||
marginVertical: spacing.small,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: inputBg,
|
||||
paddingBottom: spacing.small,
|
||||
alignSelf: 'stretch',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
preset={lockTime === 1 ? "secondary" : "tertiary"}
|
||||
text={"1 day"}
|
||||
onPress={() => setLockTime(1)}
|
||||
style={{
|
||||
minHeight: verticalScale(40),
|
||||
paddingVertical: verticalScale(spacing.tiny),
|
||||
marginRight: spacing.small
|
||||
}}
|
||||
textStyle={{fontSize: 14}}
|
||||
/>
|
||||
<Button
|
||||
preset={lockTime === 7 ? "secondary" : "tertiary"}
|
||||
text={"1 week"}
|
||||
onPress={() => setLockTime(7)}
|
||||
style={{
|
||||
minHeight: verticalScale(40),
|
||||
paddingVertical: verticalScale(spacing.tiny),
|
||||
marginRight: spacing.small
|
||||
}}
|
||||
textStyle={{fontSize: 14}}
|
||||
/>
|
||||
<Button
|
||||
preset={lockTime ? "tertiary" : "secondary"}
|
||||
text={"forever"}
|
||||
onPress={() => setLockTime(undefined)}
|
||||
style={{
|
||||
minHeight: verticalScale(40),
|
||||
paddingVertical: verticalScale(spacing.tiny),
|
||||
marginRight: spacing.small
|
||||
}}
|
||||
textStyle={{fontSize: 14}}
|
||||
/>
|
||||
</View>
|
||||
<View style={[$buttonContainer, {marginTop: spacing.medium}]}>
|
||||
<Button
|
||||
text="Lock"
|
||||
LeftAccessory={() => (<Icon icon="faLock" color="white" size={spacing.medium}/>)}
|
||||
onPress={onLockPubkeySelect}
|
||||
style={{marginRight: spacing.medium}}
|
||||
/>
|
||||
<Button
|
||||
style={{marginRight: spacing.medium}}
|
||||
preset="tertiary"
|
||||
tx="common.cancel"
|
||||
onPress={onLockPubkeyCancel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
onBackButtonPress={togglePubkeySelectorModal}
|
||||
onBackdropPress={togglePubkeySelectorModal}
|
||||
/>
|
||||
<BottomModal
|
||||
isVisible={isNostrDMModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
@@ -1294,6 +1564,44 @@ const SendAsNostrDMBlock = observer(function (props: {
|
||||
)
|
||||
})
|
||||
|
||||
const ContactItem = function (props: {
|
||||
contact: Contact
|
||||
onPress: any
|
||||
containerStyle?: ViewStyle
|
||||
}) {
|
||||
const textColor = useThemeColor('textDim')
|
||||
const tokenTextColor = useThemeColor('textDim')
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[{flexDirection: 'column', alignItems: 'center'}, props.containerStyle]}
|
||||
onPress={props.onPress}
|
||||
>
|
||||
{props.contact.picture ? (
|
||||
<View style={{borderRadius: 20, overflow: 'hidden'}}>
|
||||
<FastImage style={[
|
||||
$profileIcon, {
|
||||
width: 40,
|
||||
height: props.contact.isExternalDomain ? 40 : 43,
|
||||
borderRadius: props.contact.isExternalDomain ? 20 : 0,
|
||||
}] as import("react-native-fast-image").ImageStyle}
|
||||
source={{
|
||||
uri: getImageSource(props.contact.picture as string)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Icon
|
||||
icon='faCircleUser'
|
||||
size={38}
|
||||
color={tokenTextColor}
|
||||
/>
|
||||
)}
|
||||
<Text size='xxs' style={{color: tokenTextColor}} text={props.contact.name || props.contact.npub.slice(0, 10)}/>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const NostDMInfoBlock = observer(function (props: {
|
||||
contactToSendFrom: Contact
|
||||
amountToSend: string
|
||||
@@ -1309,52 +1617,26 @@ const NostDMInfoBlock = observer(function (props: {
|
||||
|
||||
return(
|
||||
<View style={{flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', marginBottom: spacing.medium}}>
|
||||
<View style={{flexDirection: 'column', alignItems: 'center', width: 100}}>
|
||||
<FastImage style={[
|
||||
$profileIcon, {
|
||||
width: 40,
|
||||
height: walletProfileStore.isOwnProfile ? 40 : 43,
|
||||
borderRadius: walletProfileStore.isOwnProfile ? 20 : 0,
|
||||
}] as import("react-native-fast-image").ImageStyle}
|
||||
source={{
|
||||
uri: getImageSource(props.contactToSendFrom.picture as string)
|
||||
}}
|
||||
/>
|
||||
<Text size='xxs' style={{color: tokenTextColor}} text={props.contactToSendFrom.name}/>
|
||||
</View>
|
||||
<ContactItem
|
||||
contact={props.contactToSendFrom}
|
||||
onPress={undefined}
|
||||
containerStyle={{height: 60}}
|
||||
/>
|
||||
<Text size='xxs' style={{color: tokenTextColor, textAlign: 'center', marginLeft: 30, marginBottom: 20}} text='...........' />
|
||||
<View style={{flexDirection: 'column', alignItems: 'center'}}>
|
||||
<Icon
|
||||
icon='faPaperPlane'
|
||||
icon='faPaperPlane'
|
||||
size={spacing.medium}
|
||||
color={tokenTextColor}
|
||||
/>
|
||||
<Text size='xxs' style={{color: tokenTextColor, marginBottom: -10}} text={`${amountToSendDisplay} ${getCurrency(props.unit).code}`} />
|
||||
</View>
|
||||
<Text size='xxs' style={{color: tokenTextColor, textAlign: 'center', marginRight: 30, marginBottom: 20}} text='...........' />
|
||||
<View style={{flexDirection: 'column', alignItems: 'center', width: 100}}>
|
||||
{props.contactToSendTo.picture ? (
|
||||
<View style={{borderRadius: 20, overflow: 'hidden'}}>
|
||||
<FastImage style={[
|
||||
$profileIcon, {
|
||||
width: 40,
|
||||
height: props.contactToSendTo.isExternalDomain ? 40 : 43,
|
||||
borderRadius: props.contactToSendTo.isExternalDomain ? 20 : 0,
|
||||
}] as import("react-native-fast-image").ImageStyle}
|
||||
source={{
|
||||
uri: getImageSource(props.contactToSendTo.picture as string)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Icon
|
||||
icon='faCircleUser'
|
||||
size={38}
|
||||
color={tokenTextColor}
|
||||
/>
|
||||
)}
|
||||
<Text size='xxs' style={{color: tokenTextColor}} text={props.contactToSendTo.name || props.contactToSendTo.npub.slice(0, 10)}/>
|
||||
</View>
|
||||
<ContactItem
|
||||
contact={props.contactToSendTo}
|
||||
onPress={undefined}
|
||||
containerStyle={{height: 60}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1373,11 +1655,22 @@ const $headerContainer: TextStyle = {
|
||||
|
||||
}
|
||||
|
||||
const $pubkeyInput: TextStyle = {
|
||||
flex: 1,
|
||||
// borderRadius: 0,
|
||||
borderRadius: spacing.extraSmall,
|
||||
fontSize: verticalScale(16),
|
||||
padding: spacing.small,
|
||||
alignSelf: 'stretch',
|
||||
textAlignVertical: 'top',
|
||||
// borderWidth: 1,
|
||||
}
|
||||
|
||||
const $amountContainer: ViewStyle = {
|
||||
}
|
||||
|
||||
const $amountInput: TextStyle = {
|
||||
borderRadius: spacing.small,
|
||||
borderRadius: spacing.small,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: verticalScale(48),
|
||||
|
||||
@@ -8,25 +8,26 @@ import Clipboard from "@react-native-clipboard/clipboard"
|
||||
import { moderateScale, verticalScale } from "@gocodingnow/rn-size-matters"
|
||||
import { colors, spacing } from "../../theme"
|
||||
import { useEffect, useState } from "react"
|
||||
import { translate } from "../../i18n"
|
||||
import { translate, TxKeyPath } from "../../i18n"
|
||||
import { CashuUtils, CashuProof } from "../../services/cashu/cashuUtils"
|
||||
import { log } from "../../services"
|
||||
import { Token, getDecodedToken, getEncodedToken } from '@cashu/cashu-ts';
|
||||
|
||||
export type QRCodeBlockTypes = 'EncodedV3Token' | 'EncodedV4Token' | 'Bolt11Invoice' | 'URL' | 'NWC'
|
||||
export type QRCodeBlockTypes = 'EncodedV3Token' | 'EncodedV4Token' | 'Bolt11Invoice' | 'URL' | 'NWC' | 'PUBKEY'
|
||||
|
||||
const ANIMATED_QR_FRAGMENT_LENGTH = 150
|
||||
const ANIMATED_QR_INTERVAL = 250
|
||||
|
||||
export const QRCodeBlock = function (props: {
|
||||
qrCodeData: string
|
||||
title: string
|
||||
title?: string
|
||||
titleTx?: TxKeyPath
|
||||
type: QRCodeBlockTypes
|
||||
size?: number
|
||||
}
|
||||
) {
|
||||
|
||||
const {qrCodeData, title, type, size} = props
|
||||
const {qrCodeData, title, titleTx, type, size} = props
|
||||
const [qrError, setQrError] = useState<Error | undefined>()
|
||||
const [encodedV3Token, setEncodedV3Token] = useState<string | undefined>()
|
||||
const [decodedToken, setDecodedToken] = useState<Token>()
|
||||
@@ -175,6 +176,7 @@ export const QRCodeBlock = function (props: {
|
||||
return (
|
||||
<Card
|
||||
heading={title}
|
||||
headingTx={titleTx}
|
||||
headingStyle={{textAlign: 'center', color: colors.light.text, marginBottom: spacing.extraSmall}}
|
||||
style={{backgroundColor: 'white', paddingBottom: spacing.small}}
|
||||
ContentComponent={qrError ? (
|
||||
|
||||
@@ -34,7 +34,8 @@ export const sendTask = async function (
|
||||
amountToSend: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
selectedProofs: Proof[]
|
||||
selectedProofs: Proof[],
|
||||
p2pk?: { pubkey: string; locktime?: number; refundKeys?: Array<string> }
|
||||
) : Promise<TransactionTaskResult> {
|
||||
|
||||
const mintUrl = mintBalanceToSendFrom.mintUrl
|
||||
@@ -84,6 +85,7 @@ export const sendTask = async function (
|
||||
unit,
|
||||
selectedProofs,
|
||||
transaction.id,
|
||||
p2pk
|
||||
)
|
||||
|
||||
// Update transaction status
|
||||
@@ -192,6 +194,7 @@ export const sendFromMintSync = async function (
|
||||
unit: MintUnit,
|
||||
selectedProofs: Proof[],
|
||||
transactionId: number,
|
||||
p2pk?: { pubkey: string; locktime?: number; refundKeys?: Array<string> }
|
||||
) {
|
||||
const mintUrl = mintBalance.mintUrl
|
||||
const mintInstance = mintsStore.findByUrl(mintUrl)
|
||||
@@ -306,8 +309,11 @@ export const sendFromMintSync = async function (
|
||||
let swapFeePaid: number = 0
|
||||
let proofsToSend: CashuProof[] | Proof[] = []
|
||||
let returnedProofs: CashuProof[] = []
|
||||
let isSwapNeeded: boolean = false
|
||||
|
||||
let isSwapNeeded: boolean = proofsToSendFromAmount - amountToSend > 0 ? true : false
|
||||
if(p2pk || proofsToSendFromAmount - amountToSend > 0) {
|
||||
isSwapNeeded = true
|
||||
}
|
||||
|
||||
log.trace('[sendFromMintSync]', {proofsToSendFromAmount, amountToSend})
|
||||
/*
|
||||
@@ -356,7 +362,8 @@ export const sendFromMintSync = async function (
|
||||
amountToSend,
|
||||
unit,
|
||||
proofsToSendFrom,
|
||||
transactionId
|
||||
transactionId,
|
||||
{p2pk}
|
||||
)
|
||||
} catch (e: any) {
|
||||
if(e.params && e.params.message.includes('outputs have already been signed before')) {
|
||||
@@ -367,7 +374,7 @@ export const sendFromMintSync = async function (
|
||||
unit,
|
||||
proofsToSendFrom,
|
||||
transactionId,
|
||||
{increaseCounterBy: 10}
|
||||
{p2pk, increaseCounterBy: 10}
|
||||
)
|
||||
} else {
|
||||
throw e
|
||||
@@ -380,16 +387,18 @@ export const sendFromMintSync = async function (
|
||||
|
||||
// add proofs returned by the mint after the split
|
||||
log.trace('[sendFromMintSync] add returned proofs to spendable')
|
||||
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
returnedProofs,
|
||||
{
|
||||
unit,
|
||||
transactionId,
|
||||
isPending: false
|
||||
}
|
||||
)
|
||||
|
||||
if(returnedProofs.length > 0) {
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
returnedProofs,
|
||||
{
|
||||
unit,
|
||||
transactionId,
|
||||
isPending: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
// SWAP is NOT needed, we've found denominations that match exact amount
|
||||
|
||||
@@ -115,7 +115,8 @@ type WalletTaskService = {
|
||||
amountToSend: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
selectedProofs: Proof[]
|
||||
selectedProofs: Proof[],
|
||||
p2pk?: { pubkey: string; locktime?: number; refundKeys?: Array<string> }
|
||||
) => Promise<void>
|
||||
swapAllQueue: () => Promise<void>
|
||||
topupQueue: (
|
||||
@@ -374,7 +375,8 @@ const sendQueue = async function (
|
||||
amountToSend: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
selectedProofs: Proof[]
|
||||
selectedProofs: Proof[],
|
||||
p2pk?: { pubkey: string; locktime?: number; refundKeys?: Array<string> }
|
||||
): Promise<void> {
|
||||
const now = new Date().getTime()
|
||||
SyncQueue.addPrioritizedTask(
|
||||
@@ -384,7 +386,8 @@ const sendQueue = async function (
|
||||
amountToSend,
|
||||
unit,
|
||||
memo,
|
||||
selectedProofs
|
||||
selectedProofs,
|
||||
p2pk
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -1220,7 +1223,8 @@ const handleInFlightByMintTask = async function (mint: Mint): Promise<WalletTask
|
||||
inFlight.request.proofs,
|
||||
transaction.id,
|
||||
{
|
||||
inFlightRequest: inFlight
|
||||
inFlightRequest: inFlight,
|
||||
p2pk: undefined
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user