From 545fbea2bff3def89132c61197f84fc5b7a5db6a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 6 Feb 2025 22:07:24 -0300 Subject: [PATCH 1/6] checkpoint: call '/api/v1/ditto/wallet/create' endpoint --- src/features/cashu/hooks/useCashu.ts | 21 ++++ src/features/cashu/index.tsx | 129 +++++++++++++++++++++++ src/features/settings/index.tsx | 2 + src/features/ui/index.tsx | 4 +- src/features/ui/util/async-components.ts | 1 + src/locales/en.json | 5 + 6 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/features/cashu/hooks/useCashu.ts create mode 100644 src/features/cashu/index.tsx diff --git a/src/features/cashu/hooks/useCashu.ts b/src/features/cashu/hooks/useCashu.ts new file mode 100644 index 000000000..46cd1fbd4 --- /dev/null +++ b/src/features/cashu/hooks/useCashu.ts @@ -0,0 +1,21 @@ +import { useMutation } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { queryClient } from 'soapbox/queries/client.ts'; + +function useCashu() { + const api = useApi(); + + const { mutate: createWallet } = useMutation({ + mutationFn: (data: {mints: string[]}) => api.post('/api/v1/ditto/wallet/create', data), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ['cashu', 'create', 'wallet'] }); + }, + }); + + return { + createWallet, + }; +} + +export { useCashu }; \ No newline at end of file diff --git a/src/features/cashu/index.tsx b/src/features/cashu/index.tsx new file mode 100644 index 000000000..0d65965a3 --- /dev/null +++ b/src/features/cashu/index.tsx @@ -0,0 +1,129 @@ +import { useState } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; + +import { HTTPError } from 'soapbox/api/HTTPError.ts'; +import Button from 'soapbox/components/ui/button.tsx'; +import { Column } from 'soapbox/components/ui/column.tsx'; +import FormActions from 'soapbox/components/ui/form-actions.tsx'; +import FormGroup from 'soapbox/components/ui/form-group.tsx'; +import Form from 'soapbox/components/ui/form.tsx'; +import Input from 'soapbox/components/ui/input.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Streamfield, { StreamfieldComponent } from 'soapbox/components/ui/streamfield.tsx'; +import { useCashu } from 'soapbox/features/cashu/hooks/useCashu.ts'; +import toast from 'soapbox/toast.tsx'; + +const messages = defineMessages({ + title: { id: 'cashu.wallet.create', defaultMessage: 'Create Cashu Wallet' }, + mints: { id: 'cashu.wallet.mints', defaultMessage: 'Your mints' }, + mint_placeholder: { id: 'cashu.wallet.mint_placeholder', defaultMessage: 'https://' }, + submit_success: { id: 'generic.saved', defaultMessage: 'Saved!' }, +}); + +const Cashu = () => { + const intl = useIntl(); + const { createWallet } = useCashu(); + + const [mints, setMints] = useState([]); + + const updateData = (values: string[]) => { + setMints(values); + }; + + const handleStreamItemChange = () => { + return (values: string[]) => { + updateData(values); + }; + }; + + const deleteStreamItem = () => { + return (i: number) => { + setMints(prevData => { + return [...prevData ].toSpliced(i, 1); + }); + }; + }; + + const handleAddMint = (): void => { + setMints(prevData => [...prevData, '']); + }; + + const handleSubmit: React.FormEventHandler = async (event) => { + event.preventDefault(); + createWallet({ mints }, { + onSuccess: async () => { + toast.success(messages.submit_success); + }, + onError: async (err) => { + if (err instanceof HTTPError) { + try { + const { error } = await err.response.json(); + if (typeof error === 'string') { + toast.error(error); + return; + } + } catch { /* empty */ } + } + toast.error(err.message); + }, + }); + }; + + return ( + +
+ + + + + + + + + + +
+
+ ); +}; + +type Mint = string + +const ScreenshotInput: StreamfieldComponent = ({ value, onChange }) => { + const intl = useIntl(); + + const handleChange = (): React.ChangeEventHandler => { + return e => { + onChange(e.currentTarget.value); + }; + }; + + return ( + + + + + + + + ); +}; + +export default Cashu; diff --git a/src/features/settings/index.tsx b/src/features/settings/index.tsx index 33ec3324b..962078379 100644 --- a/src/features/settings/index.tsx +++ b/src/features/settings/index.tsx @@ -32,6 +32,7 @@ const messages = defineMessages({ editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' }, editIdentity: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, editRelays: { id: 'nostr_relays.title', defaultMessage: 'Relays' }, + manageCashu: { id: 'settings.nostr.cashu', defaultMessage: 'Cashu' }, exportData: { id: 'column.export_data', defaultMessage: 'Export data' }, importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' }, mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' }, @@ -88,6 +89,7 @@ const Settings = () => { )} {features.nostr && } + {features.nostr && } diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index d6422624e..09932b714 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -142,7 +142,7 @@ import { LandingTimeline, EditIdentity, Domains, - NostrRelays, + Cashu, Bech32Redirect, Relays, ManageZapSplit, @@ -150,6 +150,7 @@ import { AdminNostrRelays, NostrBunkerLogin, ManageDittoServer, + NostrRelays, } from './util/async-components.ts'; import GlobalHotkeys from './util/global-hotkeys.tsx'; import { WrappedRoute } from './util/react-router-helpers.tsx'; @@ -325,6 +326,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.accountMoving && } {features.backups && } + diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index ff6b05aca..36905ae1d 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -168,6 +168,7 @@ export const EditIdentity = lazy(() => import('soapbox/features/edit-identity/in export const Domains = lazy(() => import('soapbox/features/admin/domains.tsx')); export const EditDomainModal = lazy(() => import('soapbox/features/ui/components/modals/edit-domain-modal.tsx')); export const NostrRelays = lazy(() => import('soapbox/features/nostr-relays/index.tsx')); +export const Cashu = lazy(() => import('soapbox/features/cashu/index.tsx')); export const Bech32Redirect = lazy(() => import('soapbox/features/nostr/Bech32Redirect.tsx')); export const ManageZapSplit = lazy(() => import('soapbox/features/admin/manage-zap-split.tsx')); export const ManageDittoServer = lazy(() => import('soapbox/features/admin/manage-ditto-server.tsx')); diff --git a/src/locales/en.json b/src/locales/en.json index 2b52b6c9e..4a9c7067a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1157,6 +1157,11 @@ "new_group_panel.action": "Create Group", "new_group_panel.subtitle": "Can't find what you're looking for? Start your own private or public group.", "new_group_panel.title": "Create Group", + "settings.nostr.cashu": "Cashu", + "cashu.title": "Cashu", + "cashu.wallet.create": "Create Cashu Wallet", + "cashu.wallet.mints": "Your mints", + "cashu.wallet.mint_placeholder": "https://", "nostr_extension.found": "Sign in with browser extension.", "nostr_extension.not_found": "Browser extension not found.", "nostr_extension.not_supported": "Browser extension not supported. Please upgrade to the latest version.", From 543a2d93bed0f856b1a169b1b450ca1d8955bc15 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 7 Feb 2025 18:12:13 -0300 Subject: [PATCH 2/6] checkpoint: be able to set nutzap information event --- src/features/cashu/hooks/useCashu.ts | 8 +++++ src/features/cashu/index.tsx | 51 ++++++++++++++++++++++++++-- src/locales/en.json | 1 + 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/features/cashu/hooks/useCashu.ts b/src/features/cashu/hooks/useCashu.ts index 46cd1fbd4..19999a4cf 100644 --- a/src/features/cashu/hooks/useCashu.ts +++ b/src/features/cashu/hooks/useCashu.ts @@ -13,8 +13,16 @@ function useCashu() { }, }); + const { mutate: createNutzapInfo } = useMutation({ + mutationFn: (data: {mints: string[]; relays: string[]}) => api.post('/api/v1/ditto/nutzap_information/create', data), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ['cashu', 'nutzap', 'info'] }); + }, + }); + return { createWallet, + createNutzapInfo, }; } diff --git a/src/features/cashu/index.tsx b/src/features/cashu/index.tsx index 0d65965a3..b54a5c573 100644 --- a/src/features/cashu/index.tsx +++ b/src/features/cashu/index.tsx @@ -16,13 +16,14 @@ import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ title: { id: 'cashu.wallet.create', defaultMessage: 'Create Cashu Wallet' }, mints: { id: 'cashu.wallet.mints', defaultMessage: 'Your mints' }, + nutzap_info: { id: 'cashu.nutzap.info', defaultMessage: 'Your nutzap info' }, mint_placeholder: { id: 'cashu.wallet.mint_placeholder', defaultMessage: 'https://' }, submit_success: { id: 'generic.saved', defaultMessage: 'Saved!' }, }); const Cashu = () => { const intl = useIntl(); - const { createWallet } = useCashu(); + const { createWallet, createNutzapInfo } = useCashu(); const [mints, setMints] = useState([]); @@ -48,7 +49,7 @@ const Cashu = () => { setMints(prevData => [...prevData, '']); }; - const handleSubmit: React.FormEventHandler = async (event) => { + const handleCreateWalletSubmit: React.FormEventHandler = async (event) => { event.preventDefault(); createWallet({ mints }, { onSuccess: async () => { @@ -69,9 +70,30 @@ const Cashu = () => { }); }; + const handleCreateNutzapInfoSubmit: React.FormEventHandler = async (event) => { + event.preventDefault(); + createNutzapInfo({ mints, relays: [] }, { + onSuccess: async () => { + toast.success(messages.submit_success); + }, + onError: async (err) => { + if (err instanceof HTTPError) { + try { + const { error } = await err.response.json(); + if (typeof error === 'string') { + toast.error(error); + return; + } + } catch { /* empty */ } + } + toast.error(err.message); + }, + }); + }; + return ( -
+ { onRemoveItem={deleteStreamItem()} /> + + + + + + +
+
+ + + + + + + + +
+
+ + + { type Mint = string -const ScreenshotInput: StreamfieldComponent = ({ value, onChange }) => { +const CashuInput: StreamfieldComponent = ({ value, onChange }) => { const intl = useIntl(); const handleChange = (): React.ChangeEventHandler => { diff --git a/src/locales/en.json b/src/locales/en.json index f345685d0..8a67eb586 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1161,6 +1161,7 @@ "cashu.title": "Cashu", "cashu.wallet.create": "Create Cashu Wallet", "cashu.nutzap.info": "Your nutzap info", + "cashu.nutzap.swap": "Swap your Cashu", "cashu.wallet.mints": "Your mints", "cashu.wallet.mint_placeholder": "https://", "nostr_extension.found": "Sign in with browser extension.", From 7048ba5caa0194de88ee15bf484b972dd723f1e9 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 17 Feb 2025 11:09:47 -0300 Subject: [PATCH 4/6] experiment: create mint quote --- src/features/cashu/hooks/useCashu.ts | 8 +++++ src/features/cashu/index.tsx | 48 +++++++++++++++++++++++++++- src/locales/en.json | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/features/cashu/hooks/useCashu.ts b/src/features/cashu/hooks/useCashu.ts index c2e53820f..ade89a571 100644 --- a/src/features/cashu/hooks/useCashu.ts +++ b/src/features/cashu/hooks/useCashu.ts @@ -27,7 +27,15 @@ function useCashu() { }, }); + const { mutate: createQuote } = useMutation({ + mutationFn: (data: {mint: string; amount: number}) => api.post('/api/v1/ditto/cashu/quote', data), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ['cashu', 'nutzap', 'info'] }); + }, + }); + return { + createQuote, createWallet, createNutzapInfo, swapCashuToWallet, diff --git a/src/features/cashu/index.tsx b/src/features/cashu/index.tsx index 75e4a9500..924a945c4 100644 --- a/src/features/cashu/index.tsx +++ b/src/features/cashu/index.tsx @@ -20,11 +20,12 @@ const messages = defineMessages({ swap_cashu: { id: 'cashu.nutzap.swap', defaultMessage: 'Swap your Cashu' }, mint_placeholder: { id: 'cashu.wallet.mint_placeholder', defaultMessage: 'https://' }, submit_success: { id: 'generic.saved', defaultMessage: 'Saved!' }, + create_cashu_quote: { id: 'cashu.quote', defaultMessage: 'Create Cashu Quote' }, }); const Cashu = () => { const intl = useIntl(); - const { createWallet, createNutzapInfo, swapCashuToWallet } = useCashu(); + const { createWallet, createNutzapInfo, swapCashuToWallet, createQuote } = useCashu(); const [mints, setMints] = useState([]); @@ -113,6 +114,27 @@ const Cashu = () => { }); }; + const handleMintQuote: React.FormEventHandler = async (event) => { + event.preventDefault(); + createQuote({ mint: mints[0], amount: 20 }, { + onSuccess: async () => { + toast.success(messages.submit_success); + }, + onError: async (err) => { + if (err instanceof HTTPError) { + try { + const { error } = await err.response.json(); + if (typeof error === 'string') { + toast.error(error); + return; + } + } catch { /* empty */ } + } + toast.error(err.message); + }, + }); + }; + return ( @@ -184,6 +206,30 @@ const Cashu = () => {
+ +
+ + + + + + + + + + +
); }; diff --git a/src/locales/en.json b/src/locales/en.json index 8a67eb586..585cdcc9a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1162,6 +1162,7 @@ "cashu.wallet.create": "Create Cashu Wallet", "cashu.nutzap.info": "Your nutzap info", "cashu.nutzap.swap": "Swap your Cashu", + "cashu.quote": "Create Cashu Quote", "cashu.wallet.mints": "Your mints", "cashu.wallet.mint_placeholder": "https://", "nostr_extension.found": "Sign in with browser extension.", From ecde1b9ebcbc61abb267d61f7c74e78ac36be277 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 17 Feb 2025 15:02:50 -0300 Subject: [PATCH 5/6] experiment: check mint quote state --- src/features/cashu/hooks/useCashu.ts | 8 +++++ src/features/cashu/index.tsx | 48 +++++++++++++++++++++++++++- src/locales/en.json | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/features/cashu/hooks/useCashu.ts b/src/features/cashu/hooks/useCashu.ts index ade89a571..628002aee 100644 --- a/src/features/cashu/hooks/useCashu.ts +++ b/src/features/cashu/hooks/useCashu.ts @@ -34,7 +34,15 @@ function useCashu() { }, }); + const { mutate: getQuoteState } = useMutation({ + mutationFn: (quote_id: string) => api.get(`/api/v1/ditto/cashu/quote/${quote_id}`), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ['cashu', 'nutzap', 'info'] }); + }, + }); + return { + getQuoteState, createQuote, createWallet, createNutzapInfo, diff --git a/src/features/cashu/index.tsx b/src/features/cashu/index.tsx index 924a945c4..0ba463df8 100644 --- a/src/features/cashu/index.tsx +++ b/src/features/cashu/index.tsx @@ -21,11 +21,12 @@ const messages = defineMessages({ mint_placeholder: { id: 'cashu.wallet.mint_placeholder', defaultMessage: 'https://' }, submit_success: { id: 'generic.saved', defaultMessage: 'Saved!' }, create_cashu_quote: { id: 'cashu.quote', defaultMessage: 'Create Cashu Quote' }, + get_cashu_quote_state: { id: 'cashu.quote.state', defaultMessage: 'Create Cashu Quote' }, }); const Cashu = () => { const intl = useIntl(); - const { createWallet, createNutzapInfo, swapCashuToWallet, createQuote } = useCashu(); + const { createWallet, createNutzapInfo, swapCashuToWallet, createQuote, getQuoteState } = useCashu(); const [mints, setMints] = useState([]); @@ -135,6 +136,27 @@ const Cashu = () => { }); }; + const handleGetMintQuoteState: React.FormEventHandler = async (event) => { + event.preventDefault(); + getQuoteState(mints[0], { + onSuccess: async () => { + toast.success(messages.submit_success); + }, + onError: async (err) => { + if (err instanceof HTTPError) { + try { + const { error } = await err.response.json(); + if (typeof error === 'string') { + toast.error(error); + return; + } + } catch { /* empty */ } + } + toast.error(err.message); + }, + }); + }; + return (
@@ -230,6 +252,30 @@ const Cashu = () => {
+ +
+ + + + + + + + + + +
); }; diff --git a/src/locales/en.json b/src/locales/en.json index 585cdcc9a..333dc2926 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1163,6 +1163,7 @@ "cashu.nutzap.info": "Your nutzap info", "cashu.nutzap.swap": "Swap your Cashu", "cashu.quote": "Create Cashu Quote", + "cashu.quote.state": "Get Cashu Quote State", "cashu.wallet.mints": "Your mints", "cashu.wallet.mint_placeholder": "https://", "nostr_extension.found": "Sign in with browser extension.", From 3fad16baf5a36479ae2d064dcccbcc515692c4c1 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 19 Feb 2025 13:28:58 -0300 Subject: [PATCH 6/6] mint the mint and get wallet --- src/features/cashu/hooks/useCashu.ts | 16 +++++ src/features/cashu/index.tsx | 94 +++++++++++++++++++++++++++- src/locales/en.json | 1 + 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/features/cashu/hooks/useCashu.ts b/src/features/cashu/hooks/useCashu.ts index 628002aee..5d13c4d9b 100644 --- a/src/features/cashu/hooks/useCashu.ts +++ b/src/features/cashu/hooks/useCashu.ts @@ -41,7 +41,23 @@ function useCashu() { }, }); + const { mutate: getWallet } = useMutation({ + mutationFn: () => api.get('/api/v1/ditto/cashu/wallet'), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ['cashu', 'nutzap', 'info'] }); + }, + }); + + const { mutate: mintTheMint } = useMutation({ + mutationFn: (quote_id: string) => api.post(`/api/v1/ditto/cashu/mint/${quote_id}`), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ['cashu', 'nutzap', 'info'] }); + }, + }); + return { + getWallet, + mintTheMint, getQuoteState, createQuote, createWallet, diff --git a/src/features/cashu/index.tsx b/src/features/cashu/index.tsx index 0ba463df8..6e3c4f66b 100644 --- a/src/features/cashu/index.tsx +++ b/src/features/cashu/index.tsx @@ -22,11 +22,13 @@ const messages = defineMessages({ submit_success: { id: 'generic.saved', defaultMessage: 'Saved!' }, create_cashu_quote: { id: 'cashu.quote', defaultMessage: 'Create Cashu Quote' }, get_cashu_quote_state: { id: 'cashu.quote.state', defaultMessage: 'Create Cashu Quote' }, + mint_the_mint: { id: 'cashu.quote.mint', defaultMessage: 'Mint the Mint' }, + get_wallet: { id: 'cashu.get_wallet', defaultMessage: 'Get wallet' }, }); const Cashu = () => { const intl = useIntl(); - const { createWallet, createNutzapInfo, swapCashuToWallet, createQuote, getQuoteState } = useCashu(); + const { createWallet, createNutzapInfo, swapCashuToWallet, createQuote, getQuoteState, mintTheMint, getWallet } = useCashu(); const [mints, setMints] = useState([]); @@ -157,6 +159,48 @@ const Cashu = () => { }); }; + const handleMintTheMintQuoteState: React.FormEventHandler = async (event) => { + event.preventDefault(); + mintTheMint(mints[0], { + onSuccess: async () => { + toast.success(messages.submit_success); + }, + onError: async (err) => { + if (err instanceof HTTPError) { + try { + const { error } = await err.response.json(); + if (typeof error === 'string') { + toast.error(error); + return; + } + } catch { /* empty */ } + } + toast.error(err.message); + }, + }); + }; + + const handleGetWallet: React.FormEventHandler = async (event) => { + event.preventDefault(); + getWallet(undefined, { + onSuccess: async () => { + toast.success(messages.submit_success); + }, + onError: async (err) => { + if (err instanceof HTTPError) { + try { + const { error } = await err.response.json(); + if (typeof error === 'string') { + toast.error(error); + return; + } + } catch { /* empty */ } + } + toast.error(err.message); + }, + }); + }; + return (
@@ -276,6 +320,54 @@ const Cashu = () => {
+ +
+ + + + + + + + + + +
+ +
+ + + + + + + + + + +
); }; diff --git a/src/locales/en.json b/src/locales/en.json index 333dc2926..e7b416bb8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1165,6 +1165,7 @@ "cashu.quote": "Create Cashu Quote", "cashu.quote.state": "Get Cashu Quote State", "cashu.wallet.mints": "Your mints", + "cashu.quote.mint": "Mint the Mint", "cashu.wallet.mint_placeholder": "https://", "nostr_extension.found": "Sign in with browser extension.", "nostr_extension.not_found": "Browser extension not found.",