kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'cashu' into 'main'
Draft: checkpoint: call '/api/v1/ditto/wallet/create' endpoint See merge request soapbox-pub/soapbox!3329merge-requests/3329/merge
commit
d33c8f6a32
|
@ -0,0 +1,69 @@
|
|||
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'] });
|
||||
},
|
||||
});
|
||||
|
||||
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'] });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: swapCashuToWallet } = useMutation({
|
||||
mutationFn: () => api.post('/api/v1/ditto/nutzap/swap_to_wallet'),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ['cashu', 'swap', 'wallet'] });
|
||||
},
|
||||
});
|
||||
|
||||
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'] });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: getQuoteState } = useMutation({
|
||||
mutationFn: (quote_id: string) => api.get(`/api/v1/ditto/cashu/quote/${quote_id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ['cashu', 'nutzap', 'info'] });
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
createNutzapInfo,
|
||||
swapCashuToWallet,
|
||||
};
|
||||
}
|
||||
|
||||
export { useCashu };
|
|
@ -0,0 +1,403 @@
|
|||
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' },
|
||||
nutzap_info: { id: 'cashu.nutzap.info', defaultMessage: 'Your nutzap info' },
|
||||
swap_cashu: { id: 'cashu.nutzap.swap', defaultMessage: 'Swap your Cashu' },
|
||||
mint_placeholder: { id: 'cashu.wallet.mint_placeholder', defaultMessage: 'https://<mint-url>' },
|
||||
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, mintTheMint, getWallet } = useCashu();
|
||||
|
||||
const [mints, setMints] = useState<string[]>([]);
|
||||
|
||||
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 handleCreateWalletSubmit: 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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSwapWalletSubmit: React.FormEventHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
swapCashuToWallet(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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<Form onSubmit={handleCreateWalletSubmit}>
|
||||
<Stack space={4}>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.mints)}
|
||||
component={CashuInput}
|
||||
values={mints}
|
||||
onChange={handleStreamItemChange()}
|
||||
onAddItem={handleAddMint}
|
||||
onRemoveItem={deleteStreamItem()}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button to='/settings' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Form>
|
||||
<Form onSubmit={handleCreateNutzapInfoSubmit}>
|
||||
<Stack space={4}>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.nutzap_info)}
|
||||
component={CashuInput}
|
||||
values={mints}
|
||||
onChange={handleStreamItemChange()}
|
||||
onAddItem={handleAddMint}
|
||||
onRemoveItem={deleteStreamItem()}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button to='/settings' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Form>
|
||||
<Form onSubmit={handleSwapWalletSubmit}>
|
||||
<Stack space={4}>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.swap_cashu)}
|
||||
component={CashuInput}
|
||||
values={mints}
|
||||
onChange={handleStreamItemChange()}
|
||||
onAddItem={handleAddMint}
|
||||
onRemoveItem={deleteStreamItem()}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button to='/settings' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Form>
|
||||
|
||||
<Form onSubmit={handleMintQuote}>
|
||||
<Stack space={4}>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.create_cashu_quote)}
|
||||
component={CashuInput}
|
||||
values={mints}
|
||||
onChange={handleStreamItemChange()}
|
||||
onAddItem={handleAddMint}
|
||||
onRemoveItem={deleteStreamItem()}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button to='/settings' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Form>
|
||||
|
||||
<Form onSubmit={handleGetMintQuoteState}>
|
||||
<Stack space={4}>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.get_cashu_quote_state)}
|
||||
component={CashuInput}
|
||||
values={mints}
|
||||
onChange={handleStreamItemChange()}
|
||||
onAddItem={handleAddMint}
|
||||
onRemoveItem={deleteStreamItem()}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button to='/settings' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Form>
|
||||
|
||||
<Form onSubmit={handleMintTheMintQuoteState}>
|
||||
<Stack space={4}>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.mint_the_mint)}
|
||||
component={CashuInput}
|
||||
values={mints}
|
||||
onChange={handleStreamItemChange()}
|
||||
onAddItem={handleAddMint}
|
||||
onRemoveItem={deleteStreamItem()}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button to='/settings' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Form>
|
||||
|
||||
<Form onSubmit={handleGetWallet}>
|
||||
<Stack space={4}>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.get_wallet)}
|
||||
component={CashuInput}
|
||||
values={mints}
|
||||
onChange={handleStreamItemChange()}
|
||||
onAddItem={handleAddMint}
|
||||
onRemoveItem={deleteStreamItem()}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button to='/settings' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Stack>
|
||||
</Form>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
type Mint = string
|
||||
|
||||
const CashuInput: StreamfieldComponent<Mint> = ({ value, onChange }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleChange = (): React.ChangeEventHandler<HTMLInputElement> => {
|
||||
return e => {
|
||||
onChange(e.currentTarget.value);
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack space={3} grow className='my-2'>
|
||||
|
||||
<FormGroup>
|
||||
<Input
|
||||
type='text'
|
||||
outerClassName='grow'
|
||||
value={value}
|
||||
onChange={handleChange()}
|
||||
placeholder={intl.formatMessage(messages.mint_placeholder)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cashu;
|
|
@ -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 = () => {
|
|||
</ListItem>
|
||||
)}
|
||||
{features.nostr && <ListItem label={intl.formatMessage(messages.editRelays)} to='/settings/relays' />}
|
||||
{features.nostr && <ListItem label={intl.formatMessage(messages.manageCashu)} to='/settings/cashu' />}
|
||||
</List>
|
||||
</CardBody>
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ import {
|
|||
LandingTimeline,
|
||||
EditIdentity,
|
||||
Domains,
|
||||
NostrRelays,
|
||||
Cashu,
|
||||
Bech32Redirect,
|
||||
Relays,
|
||||
ManageZapSplit,
|
||||
|
@ -153,6 +153,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';
|
||||
|
@ -328,6 +329,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
{features.accountMoving && <WrappedRoute path='/settings/migration' page={DefaultPage} component={Migration} content={children} />}
|
||||
{features.backups && <WrappedRoute path='/settings/backups' page={DefaultPage} component={Backups} content={children} />}
|
||||
<WrappedRoute path='/settings/relays' page={DefaultPage} component={NostrRelays} content={children} />
|
||||
<WrappedRoute path='/settings/cashu' page={DefaultPage} component={Cashu} content={children} />
|
||||
<WrappedRoute path='/settings/email' page={DefaultPage} component={EditEmail} content={children} />
|
||||
<WrappedRoute path='/settings/password' page={DefaultPage} component={EditPassword} content={children} />
|
||||
<WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} />
|
||||
|
|
|
@ -167,6 +167,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'));
|
||||
|
|
|
@ -1190,6 +1190,16 @@
|
|||
"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.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.quote.mint": "Mint the Mint",
|
||||
"cashu.wallet.mint_placeholder": "https://<mint-url>",
|
||||
"nostr_extension.found": "<link>Sign in</link> 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.",
|
||||
|
|
Ładowanie…
Reference in New Issue