Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved error handling on ecash hooks #104

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/orange-rice-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fedimint/react': patch
---

Fixed behavior of useOpenWallet hook for concurrent usages.
22 changes: 14 additions & 8 deletions packages/core-web/src/services/MintService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@ import type {
Duration,
JSONObject,
JSONValue,
MintSpendNotesResponse,
MSats,
ReissueExternalNotesState,
SpendNotesState,
} from '../types'

export class MintService {
constructor(private client: WorkerClient) {}

/** https://web.fedimint.org/core/FedimintWallet/MintService/redeemEcash */
async redeemEcash(notes: string) {
await this.client.rpcSingle('mint', 'reissue_external_notes', {
oob_notes: notes, // "out of band notes"
extra_meta: null,
})
return await this.client.rpcSingle<string>(
'mint',
'reissue_external_notes',
{
oob_notes: notes, // "out of band notes"
extra_meta: null,
},
)
}

async reissueExternalNotes(oobNotes: string, extraMeta: JSONObject = {}) {
Expand All @@ -31,7 +37,7 @@ export class MintService {

subscribeReissueExternalNotes(
operationId: string,
onSuccess: (state: JSONValue) => void = () => {},
onSuccess: (state: ReissueExternalNotesState) => void = () => {},
onError: (error: string) => void = () => {},
) {
const unsubscribe = this.client.rpcStream<ReissueExternalNotesState>(
Expand Down Expand Up @@ -60,7 +66,7 @@ export class MintService {
? { nanos: 0, secs: tryCancelAfter }
: tryCancelAfter

const res = await this.client.rpcSingle<Array<string>>(
const res = await this.client.rpcSingle<MintSpendNotesResponse>(
'mint',
'spend_notes',
{
Expand Down Expand Up @@ -94,10 +100,10 @@ export class MintService {

subscribeSpendNotes(
operationId: string,
onSuccess: (state: JSONValue) => void = () => {},
onSuccess: (state: SpendNotesState) => void = () => {},
onError: (error: string) => void = () => {},
) {
return this.client.rpcStream(
return this.client.rpcStream<SpendNotesState>(
'mint',
'subscribe_spend_notes',
{ operation_id: operationId },
Expand Down
19 changes: 12 additions & 7 deletions packages/core-web/src/types/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { type MintService } from '../services'
import { MSats, Duration, JSONValue } from './utils'

const MODULE_KINDS = ['', 'ln', 'mint'] as const
Expand Down Expand Up @@ -81,13 +80,18 @@ type StreamResult<T extends JSONValue> =

type CancelFunction = () => void

type ReissueExternalNotesState =
| 'Created'
| 'Issuing'
| 'Done'
| { Failed: { error: string } }
type ReissueExternalNotesState = 'Created' | 'Issuing' | 'Done'
// | { Failed: { error: string } }

type MintSpendNotesResponse = Array<string>

type MintSpendNotesResponse = ReturnType<MintService['spendNotes']>
type SpendNotesState =
| 'Created'
| 'UserCanceledProcessing'
| 'UserCanceledSuccess'
| 'UserCanceledFailure'
| 'Success'
| 'Refunded'

export {
LightningGateway,
Expand All @@ -106,4 +110,5 @@ export {
CancelFunction,
ReissueExternalNotesState,
MintSpendNotesResponse,
SpendNotesState,
}
9 changes: 4 additions & 5 deletions packages/core-web/src/worker/WorkerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,10 @@ export class WorkerClient {
})
}

rpcSingle<Response extends JSONValue = JSONValue>(
module: ModuleKind,
method: string,
body: JSONValue,
) {
rpcSingle<
Response extends JSONValue = JSONValue,
Error extends string = string,
>(module: ModuleKind, method: string, body: JSONValue) {
logger.debug('WorkerClient - rpcSingle', module, method, body)
return new Promise<Response>((resolve, reject) => {
this.rpcStream<Response>(module, method, body, resolve, reject)
Expand Down
39 changes: 35 additions & 4 deletions packages/react/lib/contexts/FedimintWalletContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { FedimintWallet } from '@fedimint/core-web'
import { createContext, createElement } from 'react'
import {
createContext,
createElement,
useEffect,
useMemo,
useState,
} from 'react'

let wallet: FedimintWallet

Expand All @@ -8,6 +14,8 @@ type FedimintWalletConfig = {
debug?: boolean
}

export type WalletStatus = 'open' | 'closed' | 'opening'

export const setupFedimintWallet = (config: FedimintWalletConfig) => {
wallet = new FedimintWallet(!!config.lazy)
if (config.debug) {
Expand All @@ -16,22 +24,45 @@ export const setupFedimintWallet = (config: FedimintWalletConfig) => {
}

export const FedimintWalletContext = createContext<
{ wallet: FedimintWallet } | undefined
| {
wallet: FedimintWallet
walletStatus: WalletStatus
setWalletStatus: (status: WalletStatus) => void
}
| undefined
>(undefined)

export type FedimintWalletProviderProps = {}

export const FedimintWalletProvider = (
parameters: React.PropsWithChildren<FedimintWalletProviderProps>,
) => {
const [walletStatus, setWalletStatus] = useState<WalletStatus>('closed')
const { children } = parameters

if (!wallet)
throw new Error(
'You must call setupFedimintWallet() first. See the getting started guide.',
)

const props = { value: { wallet } }
const contextValue = useMemo(
() => ({
wallet,
walletStatus,
setWalletStatus,
}),
[walletStatus],
)

useEffect(() => {
wallet.waitForOpen().then(() => {
setWalletStatus('open')
})
}, [wallet])

return createElement(FedimintWalletContext.Provider, props, children)
return createElement(
FedimintWalletContext.Provider,
{ value: contextValue },
children,
)
}
32 changes: 14 additions & 18 deletions packages/react/lib/hooks/useOpenWallet.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { useCallback, useEffect, useState } from 'react'
import { useFedimintWallet } from '.'

type WalletStatus = 'open' | 'closed' | 'opening'
import { useCallback, useContext } from 'react'
import { FedimintWalletContext } from '../contexts/FedimintWalletContext'

export const useOpenWallet = () => {
const wallet = useFedimintWallet()
const [walletStatus, setWalletStatus] = useState<WalletStatus>()
const value = useContext(FedimintWalletContext)

if (!value) {
throw new Error(
'useOpenWallet must be used within a FedimintWalletProvider',
)
}

const { wallet, walletStatus, setWalletStatus } = value

const openWallet = useCallback(() => {
if (walletStatus === 'open') return
Expand All @@ -22,21 +27,12 @@ export const useOpenWallet = () => {

setWalletStatus('opening')

const res = await wallet.joinFederation(invite)
setWalletStatus(res ? 'open' : 'closed')
await wallet.joinFederation(invite).then((res) => {
setWalletStatus(res ? 'open' : 'closed')
})
},
[wallet],
)

useEffect(() => {
wallet.waitForOpen().then(() => {
setWalletStatus('open')
})

return () => {
setWalletStatus('closed')
}
}, [wallet])

return { walletStatus, openWallet, joinFederation }
}
27 changes: 16 additions & 11 deletions packages/react/lib/hooks/useReceiveEcash.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { useCallback, useEffect, useState } from 'react'
import { useFedimintWallet, useOpenWallet } from '.'
import { ReissueExternalNotesState } from '@fedimint/core-web'

export const useReceiveEcash = () => {
const wallet = useFedimintWallet()
const { walletStatus } = useOpenWallet()

const [operationId, setOperationId] = useState<string>()
const [notes, setNotes] = useState<string>()
const [state, setState] = useState<any>()
const [state, setState] = useState<ReissueExternalNotesState | 'Error'>()
const [error, setError] = useState<string>()

useEffect(() => {
if (!operationId) return

const unsubscribe = wallet.mint.subscribeReissueExternalNotes(
operationId,
(_state) => (_state ? setState(_state) : setState(undefined)),
(_state) => {
setState(_state)
},
(error) => {
console.error('ECASH SPEND STATE ERROR', error)
setError(error)
},
)

Expand All @@ -28,18 +31,20 @@ export const useReceiveEcash = () => {
const redeemEcash = useCallback(
async (notes: string) => {
if (walletStatus !== 'open') throw new Error('Wallet is not open')
const response = await wallet.mint.redeemEcash(notes)
console.error('REEDEEEM', response)
// setOperationId(response.operation_id)
// setNotes(response.notes)
return response
try {
const response = await wallet.mint.redeemEcash(notes)
setOperationId(response)
} catch (e) {
setState('Error')
setError(e as string)
}
},
[wallet],
[wallet, walletStatus],
)

return {
redeemEcash,
notes,
state,
error,
}
}
15 changes: 11 additions & 4 deletions packages/react/lib/hooks/useSpendEcash.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { useCallback, useEffect, useState } from 'react'
import { useFedimintWallet, useOpenWallet } from '.'
import { type SpendNotesState } from '@fedimint/core-web'

export const useSpendEcash = () => {
const wallet = useFedimintWallet()
const { walletStatus } = useOpenWallet()

const [operationId, setOperationId] = useState<string>()
const [notes, setNotes] = useState<string>()
const [state, setState] = useState<any>()

const [state, setState] = useState<SpendNotesState | 'Error'>()
const [error, setError] = useState<string>()

useEffect(() => {
if (!operationId) return

const unsubscribe = wallet.mint.subscribeSpendNotes(
operationId,
(_state) => (_state ? setState(_state) : setState(undefined)),
(_state) => {
setState(_state)
},
(error) => {
console.error('ECASH SPEND STATE ERROR', error)
setState('Error')
setError(error)
},
)

Expand All @@ -36,12 +42,13 @@ export const useSpendEcash = () => {
setNotes(response.notes)
return response.notes
},
[wallet],
[wallet, walletStatus],
)

return {
spendEcash,
notes,
state,
error,
}
}
2 changes: 2 additions & 0 deletions packages/react/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: all;
max-width: 200px;
}
Loading
Loading