Keysets v2 support

This commit is contained in:
minibits-cash
2026-04-12 23:06:28 +02:00
parent c0d425c0fd
commit 2c9d5c1cf9
10 changed files with 134 additions and 38 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "minibits_wallet", "name": "minibits_wallet",
"version": "0.4.2-beta.1", "version": "0.4.2-beta.2",
"private": true, "private": true,
"scripts": { "scripts": {
"android:clean": "cd android && ./gradlew clean", "android:clean": "cd android && ./gradlew clean",
-4
View File
@@ -16,10 +16,6 @@ import {
Mint as CashuMint, Mint as CashuMint,
GetKeysetsResponse, GetKeysetsResponse,
GetKeysResponse, GetKeysResponse,
MintActiveKeys,
MintAllKeysets,
type MintKeys as CashuMintKeys,
type MintKeyset as CashuMintKeyset
} from '@cashu/cashu-ts' } from '@cashu/cashu-ts'
import { MintUnit } from '../services/wallet/currency' import { MintUnit } from '../services/wallet/currency'
+22 -3
View File
@@ -4,7 +4,7 @@ import {
View, View,
TextStyle, TextStyle,
} from 'react-native' } from 'react-native'
import { PaymentRequest as CashuPaymentRequest, MeltQuoteBolt11Response, MeltQuoteResponse, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken } from '@cashu/cashu-ts' import { PaymentRequest as CashuPaymentRequest, MeltQuoteBolt11Response, MeltQuoteResponse, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken, getTokenMetadata } from '@cashu/cashu-ts'
import NfcManager, { Ndef, NfcEvents } from 'react-native-nfc-manager' import NfcManager, { Ndef, NfcEvents } from 'react-native-nfc-manager'
import { colors, spacing, typography, useThemeColor } from '../theme' import { colors, spacing, typography, useThemeColor } from '../theme'
import EventEmitter from '../utils/eventEmitter' import EventEmitter from '../utils/eventEmitter'
@@ -530,7 +530,16 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
// Check for NOSTR transport first // Check for NOSTR transport first
const nostrTransport = transports.find(t => t.type === PaymentRequestTransportType.NOSTR) const nostrTransport = transports.find(t => t.type === PaymentRequestTransportType.NOSTR)
const decodedTokenToSend = getDecodedToken(encodedTokenToSend)
// keysetsV2 support
const mintKeysetIds = mintsStore.findByUrl(result.mintUrl)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state, payment failed', {
mintUrl: result.mintUrl
})
}
const decodedTokenToSend = getDecodedToken(encodedTokenToSend, mintKeysetIds)
if (nostrTransport) { if (nostrTransport) {
const decoded = NostrClient.decodeNprofile(nostrTransport.target) const decoded = NostrClient.decodeNprofile(nostrTransport.target)
@@ -732,8 +741,18 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const handleCashuToken = async function (encodedToken: string) { const handleCashuToken = async function (encodedToken: string) {
// keysetsV2 support
const tokenInfo = getTokenMetadata(encodedToken)
const mintKeysetIds = mintsStore.findByUrl(tokenInfo.mint)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state, payment failed', {
mintUrl: tokenInfo.mint
})
}
const decoded = getDecodedToken(encodedToken, mintKeysetIds)
const decoded = getDecodedToken(encodedToken)
const amount = CashuUtils.getProofsAmount(decoded.proofs) const amount = CashuUtils.getProofsAmount(decoded.proofs)
const memo = decoded.memo || 'Received over NFC' const memo = decoded.memo || 'Received over NFC'
const result = await WalletTask.receiveQueueAwaitable( const result = await WalletTask.receiveQueueAwaitable(
+12 -2
View File
@@ -35,7 +35,7 @@ import { round, toNumber } from '../utils/number'
import numbro from 'numbro' import numbro from 'numbro'
import { TranItem } from './TranDetailScreen' import { TranItem } from './TranDetailScreen'
import { translate } from '../i18n' import { translate } from '../i18n'
import { Token, getDecodedToken } from '@cashu/cashu-ts' import { Token, getDecodedToken, getTokenMetadata } from '@cashu/cashu-ts'
import { RECEIVE_OFFLINE_PREPARE_TASK, RECEIVE_TASK } from '../services/wallet/receiveTask' import { RECEIVE_OFFLINE_PREPARE_TASK, RECEIVE_TASK } from '../services/wallet/receiveTask'
import { CurrencyAmount } from './Wallet/CurrencyAmount' import { CurrencyAmount } from './Wallet/CurrencyAmount'
@@ -134,8 +134,18 @@ export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) {
try { try {
//@ts-ignore //@ts-ignore
navigation.setParams({encodedToken: undefined}) navigation.setParams({encodedToken: undefined})
// keysetsV2 support
const tokenInfo = getTokenMetadata(encoded)
const mintKeysetIds = mintsStore.findByUrl(tokenInfo.mint)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state', {
mintUrl: tokenInfo.mint
})
}
const decoded = getDecodedToken(encoded, mintKeysetIds)
const decoded = getDecodedToken(encoded)
const tokenAmount = CashuUtils.getProofsAmount(decoded.proofs) const tokenAmount = CashuUtils.getProofsAmount(decoded.proofs)
const isLocked = CashuUtils.isTokenP2PKLocked(decoded) const isLocked = CashuUtils.isTokenP2PKLocked(decoded)
let isLockedToWallet = false let isLockedToWallet = false
+21 -3
View File
@@ -52,7 +52,7 @@ import { QRCodeBlock } from './Wallet/QRCode'
import numbro from 'numbro' import numbro from 'numbro'
import { TranItem } from './TranDetailScreen' import { TranItem } from './TranDetailScreen'
import { MemoInputCard } from '../components/MemoInputCard' import { MemoInputCard } from '../components/MemoInputCard'
import { PaymentRequest as CashuPaymentRequest, PaymentRequestTransport, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken } from '@cashu/cashu-ts' import { PaymentRequest as CashuPaymentRequest, PaymentRequestTransport, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken, getTokenMetadata } from '@cashu/cashu-ts'
import { ProfilePointer } from 'nostr-tools/nip19' import { ProfilePointer } from 'nostr-tools/nip19'
import { MINIBITS_NIP05_DOMAIN } from '@env' import { MINIBITS_NIP05_DOMAIN } from '@env'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
@@ -922,7 +922,16 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing payment request to pay.') throw new AppError(Err.VALIDATION_ERROR, 'Missing payment request to pay.')
} }
const decodedTokenToSend = getDecodedToken(encodedTokenToSend) // keysetsV2 support
const tokenInfo = getTokenMetadata(encodedTokenToSend)
const mintKeysetIds = mintsStore.findByUrl(tokenInfo.mint)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state', {
mintUrl: tokenInfo.mint
})
}
const decodedTokenToSend = getDecodedToken(encodedTokenToSend, mintKeysetIds)
messageContent = JSON.stringify({ messageContent = JSON.stringify({
id: decodedCashuPaymentRequest.id, id: decodedCashuPaymentRequest.id,
@@ -1004,7 +1013,16 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
setIsPostSending(true) setIsPostSending(true)
const decodedTokenToSend = getDecodedToken(encodedTokenToSend) // keysetsV2 support
const tokenInfo = getTokenMetadata(encodedTokenToSend)
const mintKeysetIds = mintsStore.findByUrl(tokenInfo.mint)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state', {
mintUrl: tokenInfo.mint
})
}
const decodedTokenToSend = getDecodedToken(encodedTokenToSend, mintKeysetIds)
const payload = { const payload = {
id: decodedCashuPaymentRequest.id, id: decodedCashuPaymentRequest.id,
+10 -2
View File
@@ -645,8 +645,16 @@ const ReceiveInfoBlock = function (props: {
setIsLoading(true) setIsLoading(true)
try { try {
const tokenToRetry = getDecodedToken(transaction.inputToken) // keysetsV2 support
const mintKeysetIds = mintsStore.findByUrl(transaction.mint)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state', {
mintUrl: transaction.mint
})
}
const tokenToRetry = getDecodedToken(transaction.inputToken, mintKeysetIds)
const amountToReceive = CashuUtils.getProofsAmount(tokenToRetry.proofs) const amountToReceive = CashuUtils.getProofsAmount(tokenToRetry.proofs)
const memo = tokenToRetry.memo || '' const memo = tokenToRetry.memo || ''
+17 -3
View File
@@ -12,11 +12,13 @@ import { colors, spacing, ThemeCode } from "../../theme"
import { translate, TxKeyPath } from "../../i18n" import { translate, TxKeyPath } from "../../i18n"
import { log } from "../../services" import { log } from "../../services"
import { MMKVStorage } from "../../services/mmkvStorage" import { MMKVStorage } from "../../services/mmkvStorage"
import { Token, getDecodedToken, getEncodedToken } from '@cashu/cashu-ts' import { Token, getDecodedToken, getEncodedToken, getTokenMetadata } from '@cashu/cashu-ts'
import { NfcService } from '../../services/nfcService'; import { NfcService } from '../../services/nfcService';
import { SvgXml } from 'react-native-svg'; import { SvgXml } from 'react-native-svg';
import { NfcIcon } from '../../components/NfcIcon'; import { NfcIcon } from '../../components/NfcIcon';
import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
import { useStores } from '../../models';
import AppError, { Err } from '../../utils/AppError';
export type QRCodeBlockTypes = 'EncodedV3Token' | 'EncodedV4Token' | 'Bolt11Invoice' | 'URL' | 'NWC' | 'PUBKEY' | 'PaymentRequest' export type QRCodeBlockTypes = 'EncodedV3Token' | 'EncodedV4Token' | 'Bolt11Invoice' | 'URL' | 'NWC' | 'PUBKEY' | 'PaymentRequest'
@@ -35,6 +37,8 @@ export const QRCodeBlock = function (props: {
) { ) {
const { qrCodeData, title, titleTx, type, size, startNfcOnLoad = false } = props const { qrCodeData, title, titleTx, type, size, startNfcOnLoad = false } = props
const {mintsStore} = useStores()
const [qrError, setQrError] = useState<Error | undefined>() const [qrError, setQrError] = useState<Error | undefined>()
const [encodedV3Token, setEncodedV3Token] = useState<string | undefined>() const [encodedV3Token, setEncodedV3Token] = useState<string | undefined>()
const [decodedToken, setDecodedToken] = useState<Token>() const [decodedToken, setDecodedToken] = useState<Token>()
@@ -100,10 +104,20 @@ export const QRCodeBlock = function (props: {
useEffect(() => { useEffect(() => {
const detectKeysetFormat = () => { const detectKeysetFormat = () => {
if(type === 'EncodedV4Token') { if(type === 'EncodedV4Token') {
const decoded = getDecodedToken(qrCodeData)
// keysetsV2 support
const tokenInfo = getTokenMetadata(qrCodeData)
const mintKeysetIds = mintsStore.findByUrl(tokenInfo.mint)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state', {
mintUrl: tokenInfo.mint
})
}
const decoded = getDecodedToken(qrCodeData, mintKeysetIds)
setDecodedToken(decoded) setDecodedToken(decoded)
if(decoded.proofs[0].id.startsWith('00')) { if(decoded.proofs[0].id.startsWith('00') || decoded.proofs[0].id.startsWith('01')) {
setKeysetFormat('hex') setKeysetFormat('hex')
} else { } else {
try { try {
+5 -4
View File
@@ -4,10 +4,11 @@ import type {
Proof as CashuProof, Proof as CashuProof,
PaymentRequest as CashuPaymentRequest, PaymentRequest as CashuPaymentRequest,
PaymentRequestPayload, PaymentRequestPayload,
TokenMetadata,
} from '@cashu/cashu-ts' } from '@cashu/cashu-ts'
import { bytesToHex } from '@noble/hashes/utils' import { bytesToHex } from '@noble/hashes/utils'
import AppError, {Err} from '../../utils/AppError' import AppError, {Err} from '../../utils/AppError'
import { getDecodedToken } from '@cashu/cashu-ts' import { getDecodedToken, getTokenMetadata } from '@cashu/cashu-ts'
import {Proof} from '../../models/Proof' import {Proof} from '../../models/Proof'
import { log } from '../logService' import { log } from '../logService'
import { decodePaymentRequest } from '@cashu/cashu-ts' import { decodePaymentRequest } from '@cashu/cashu-ts'
@@ -87,10 +88,10 @@ const extractEncodedCashuToken = function (maybeToken: string): string {
log.trace('[extractEncodedCashuToken] Extract token from', {maybeToken}) log.trace('[extractEncodedCashuToken] Extract token from', {maybeToken})
let encodedToken: string | undefined = undefined let encodedToken: string | undefined = undefined
let decoded: Token | undefined = undefined let tokenInfo: TokenMetadata | undefined = undefined
if (maybeToken && CASHU_TOKEN_PREFIXES.some(pref => maybeToken.startsWith(pref))) { if (maybeToken && CASHU_TOKEN_PREFIXES.some(pref => maybeToken.startsWith(pref))) {
decoded = getDecodedToken(maybeToken) // throws tokenInfo = getTokenMetadata(maybeToken) // throws
return maybeToken return maybeToken
} }
@@ -105,7 +106,7 @@ const extractEncodedCashuToken = function (maybeToken: string): string {
// try to decode // try to decode
if(encodedToken) { if(encodedToken) {
decoded = getDecodedToken(encodedToken) // throws tokenInfo = getTokenMetadata(encodedToken) // throws
return encodedToken return encodedToken
} }
+9 -1
View File
@@ -296,8 +296,16 @@ export const receiveOfflineCompleteTask = async function (
if (!transaction.inputToken) { if (!transaction.inputToken) {
throw new AppError(Err.VALIDATION_ERROR, 'Could not find ecash token to redeem', {caller: 'receiveOfflineComplete'}) throw new AppError(Err.VALIDATION_ERROR, 'Could not find ecash token to redeem', {caller: 'receiveOfflineComplete'})
} }
// keysetsV2 support
const mintKeysetIds = mintsStore.findByUrl(transaction.mint)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state', {
mintUrl: transaction.mint
})
}
const token = getDecodedToken(transaction.inputToken) const token = getDecodedToken(transaction.inputToken, mintKeysetIds)
mintToReceive = token.mint mintToReceive = token.mint
const unit = token.unit || 'sat' const unit = token.unit || 'sat'
+37 -15
View File
@@ -15,7 +15,7 @@ import {CashuProof, CashuUtils} from './cashu/cashuUtils'
import {LightningUtils} from './lightning/lightningUtils' import {LightningUtils} from './lightning/lightningUtils'
import AppError, {Err} from '../utils/AppError' import AppError, {Err} from '../utils/AppError'
import {MintBalance, MintStatus} from '../models/Mint' import {MintBalance, MintStatus} from '../models/Mint'
import {MeltQuoteBaseResponse, MeltQuoteBolt11Response, MeltQuoteResponse, MeltQuoteState, MintQuoteState, PaymentRequestPayload, Token, getDecodedToken, getEncodedToken} from '@cashu/cashu-ts' import {MeltQuoteBaseResponse, MeltQuoteBolt11Response, MeltQuoteResponse, MeltQuoteState, MintQuoteState, PaymentRequestPayload, Token, getDecodedToken, getEncodedToken, getTokenMetadata} from '@cashu/cashu-ts'
import {Mint} from '../models/Mint' import {Mint} from '../models/Mint'
import {pollerExists, stopPolling} from '../utils/poller' import {pollerExists, stopPolling} from '../utils/poller'
import { NostrClient, NostrEvent, NostrProfile } from './nostrService' import { NostrClient, NostrEvent, NostrProfile } from './nostrService'
@@ -692,7 +692,9 @@ const swapAllTask = async function (): Promise<WalletTaskResult> {
) )
const encodedTokenToReceive: string = sendResult.encodedTokenToSend const encodedTokenToReceive: string = sendResult.encodedTokenToSend
const tokenToReceive = getDecodedToken(encodedTokenToReceive)
// keysetsV2 support
const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs) const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
const receiveResult = await receiveBatchTask( const receiveResult = await receiveBatchTask(
@@ -727,7 +729,7 @@ const swapAllTask = async function (): Promise<WalletTaskResult> {
) )
const encodedTokenToReceive: string = sendResult.encodedTokenToSend const encodedTokenToReceive: string = sendResult.encodedTokenToSend
const tokenToReceive = getDecodedToken(encodedTokenToReceive) const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs) const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
const receiveResult = await receiveBatchTask( const receiveResult = await receiveBatchTask(
@@ -815,7 +817,7 @@ const swapByDenominationTask = async function (denomination: number): Promise<Wa
) )
const encodedTokenToReceive: string = sendResult.encodedTokenToSend const encodedTokenToReceive: string = sendResult.encodedTokenToSend
const tokenToReceive = getDecodedToken(encodedTokenToReceive) const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs) const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
const receiveResult = await receiveBatchTask( const receiveResult = await receiveBatchTask(
@@ -846,7 +848,7 @@ const swapByDenominationTask = async function (denomination: number): Promise<Wa
) )
const encodedTokenToReceive: string = sendResult.encodedTokenToSend const encodedTokenToReceive: string = sendResult.encodedTokenToSend
const tokenToReceive = getDecodedToken(encodedTokenToReceive) const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs) const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
const receiveResult = await receiveBatchTask( const receiveResult = await receiveBatchTask(
@@ -2383,7 +2385,16 @@ const handleClaimTask = async function (params: {
log.debug('[handleClaimTask] decrypted token', {encodedToken}) log.debug('[handleClaimTask] decrypted token', {encodedToken})
decoded = getDecodedToken(encodedToken) // keysetsV2 support
const tokenInfo = getTokenMetadata(encodedToken)
const mintKeysetIds = mintsStore.findByUrl(tokenInfo.mint)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state', {
mintUrl: tokenInfo.mint
})
}
decoded = getDecodedToken(encodedToken, mintKeysetIds)
const amountToReceive = CashuUtils.getProofsAmount(decoded.proofs) const amountToReceive = CashuUtils.getProofsAmount(decoded.proofs)
const memo = decoded.memo || 'Received to Lightning address' const memo = decoded.memo || 'Received to Lightning address'
@@ -2685,17 +2696,19 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro
// //
if(incoming.type === IncomingDataType.CASHU) { if(incoming.type === IncomingDataType.CASHU) {
const decoded = getDecodedToken(incoming.encoded) // keysetsV2 support
const amountToReceive = CashuUtils.getProofsAmount(decoded.proofs) const tokenInfo = getTokenMetadata(incoming.encoded)
const memo = decoded.memo || 'Received over Nostr' const amountToReceive = tokenInfo.amount
const memo = tokenInfo.memo || 'Received over Nostr'
const {unit, mint: mintUrl} = tokenInfo
// do not allow to receive automatically from unknown mints // do not allow to receive automatically from unknown mints
if(!mintsStore.mintExists(decoded.mint)) { if(!mintsStore.mintExists(mintUrl)) {
let message = 'Receiving ecash token over Nostr from unknown mint is not allowed.' let message = 'Receiving ecash token over Nostr from unknown mint is not allowed.'
const transactionData: TransactionData[] = [] const transactionData: TransactionData[] = []
let transaction: Transaction | undefined = undefined let transaction: Transaction | undefined = undefined
const {unit, mint} = decoded
transactionData.push({ transactionData.push({
status: TransactionStatus.ERROR, status: TransactionStatus.ERROR,
@@ -2711,7 +2724,7 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro
unit: unit as MintUnit, unit: unit as MintUnit,
data: JSON.stringify(transactionData), data: JSON.stringify(transactionData),
memo, memo,
mint, mint: mintUrl,
status: TransactionStatus.DRAFT, status: TransactionStatus.DRAFT,
} }
@@ -2720,13 +2733,22 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro
await _sendErrorReceiveNotification( await _sendErrorReceiveNotification(
amountToReceive, amountToReceive,
decoded.unit as MintUnit, unit as MintUnit,
decoded.mint mintUrl
) )
throw new AppError(Err.VALIDATION_ERROR, message, {decoded}) throw new AppError(Err.VALIDATION_ERROR, message, {tokenInfo})
} }
const mintKeysetIds = mintsStore.findByUrl(mintUrl)?.keysetIds
if(!mintKeysetIds || mintKeysetIds.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Missing keysetIds in the wallet state', {
mintUrl: tokenInfo.mint
})
}
const decoded = getDecodedToken(incoming.encoded, mintKeysetIds)
const {transaction, receivedAmount} = await receiveTask( const {transaction, receivedAmount} = await receiveTask(
decoded, decoded,
amountToReceive, amountToReceive,