Skip to content

Commit

Permalink
feat: improved error handling on ecash hooks (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexlwn123 committed Dec 17, 2024
1 parent e98a1b8 commit e3400dd
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 84 deletions.
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.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export default defineConfig({
text: 'Contributing',
link: '/core/dev/contributing',
},
{
text: 'Awesome Fedimint Web SDK',
link: '/core/dev/awesome',
},
{
text: 'Discussions ',
link: 'https://chat.fedimint.org',
Expand Down
4 changes: 4 additions & 0 deletions docs/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export function getSidebar() {
return [
{
base: '/core/',
collapsed: false,
text: 'Introduction',
items: [
{ text: 'Overview', link: 'overview' },
Expand All @@ -14,6 +15,7 @@ export function getSidebar() {
...FedimintWalletSidebar,
{
base: '/examples/',
collapsed: false,
text: 'Examples',
items: [
{ text: 'Vite + React', link: 'vite-react' },
Expand All @@ -26,6 +28,7 @@ export function getSidebar() {
base: '/core/dev/',
items: [
{ text: 'Contributing', link: 'contributing' },
{ text: 'Awesome Projects', link: 'awesome' },
{ text: 'Testing', link: 'testing' },
],
},
Expand All @@ -35,6 +38,7 @@ export function getSidebar() {
const FedimintWalletSidebar = [
{
text: 'Core',
collapsed: true,
link: '.',
base: '/core/FedimintWallet/',
items: [
Expand Down
4 changes: 4 additions & 0 deletions docs/core/FedimintWallet/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

The `FedimintWallet` class serves as the main entry point for the library. It orchestrates the various services and the WorkerClient.

::: info
Check out the [Getting Started](../getting-started) guide to get started using the Fedimint Web SDK.
:::

<img src="/architecture-diagram.svg" alt="Architecture" />

## Properties
Expand Down
44 changes: 44 additions & 0 deletions docs/core/dev/awesome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Awesome Projects

A curated list of awesome projects, resources, and tools built with the **Fedimint Web SDK**.

## Applications

### Community Apps

- [Browsimint](https://github.com/IroncladDev/browsimint) - Browser extension lighting/ecash wallet. Beautiful UI. Supports joining multiple federations.
- [ATL Bitlab Wallet](https://ecashdemo.atlbitlab.com/) - Wallet for the Atlanta Bitcoin Community.
- [Bitsacco](https://bitsacco.com/) - Currently built using fedimint-clientd. Transitioning to use the Fedimint Web SDK.

### Demo Apps

- [Vite Core Example](https://github.com/fedimint/fedimint-web-sdk/tree/main/examples/vite-core) - Basic example using @fedimint/core-web
- [Bare JS Example](https://github.com/fedimint/fedimint-web-sdk/tree/main/examples/bare-js) - Minimal JavaScript implementation

## Templates

- [Create Fedimint App](https://github.com/fedimint/fedimint-web-sdk/tree/main/packages/create-fedimint-app) - Official project scaffolding tool
- Vite + React
- Vite + React + TypeScript
- Vite + React + TypeScript + Hooks

## Tools & Libraries

- [@fedimint/core-web](https://github.com/fedimint/fedimint-web-sdk/tree/main/packages/core-web) - Core Fedimint web utilities
- [@fedimint/react](https://github.com/fedimint/fedimint-web-sdk/tree/main/packages/react) - React contexts and hooks

## Contributing

Want to add your project to this list? Please submit a PR!

### Guidelines

- Ensure your project uses the Fedimint Web SDK
- Include a brief description
- Follow the existing format
- Add your project to the appropriate category

## Resources

- [Official Documentation](https://web.fedimint.org)
- [GitHub Repository](https://github.com/fedimint/fedimint-web-sdk)
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,
}
}
Loading

0 comments on commit e3400dd

Please sign in to comment.