P2PK implementation

This commit is contained in:
minibits-cash
2025-04-09 00:00:03 +02:00
parent 3d774fa06b
commit 9ef894aa72
13 changed files with 498 additions and 132 deletions
+18 -12
View File
@@ -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>
+1
View File
@@ -20,3 +20,4 @@ export * from "./EurIcon"
export * from "./MintIcon"
export * from "./HeaderBg"
export * from "./NwcIcon"
+4 -1
View File
@@ -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.",
+6
View File
@@ -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
+9 -5
View File
@@ -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)
+1 -1
View File
@@ -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'
+12
View File
@@ -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'}
+5 -2
View File
@@ -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'
+44 -21
View File
@@ -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
View File
@@ -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),
+6 -4
View File
@@ -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 ? (
+23 -14
View File
@@ -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
+8 -4
View File
@@ -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
}
)