Merge pull request #39 from Qortal/feature/sign-fees

Feature/sign fees
This commit is contained in:
Phillip 2025-05-05 09:40:21 +03:00 committed by GitHub
commit eeb66f4fa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 472 additions and 205 deletions

View File

@ -1028,12 +1028,7 @@ function App() {
const logoutFunc = useCallback(async () => { const logoutFunc = useCallback(async () => {
try { try {
if (hasSettingsChanged) { if (extState === 'authenticated') {
await showUnsavedChanges({
message:
'Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.',
}); // TODO translate
} else if (extState === 'authenticated') {
await showUnsavedChanges({ await showUnsavedChanges({
message: 'Are you sure you would like to logout?', message: 'Are you sure you would like to logout?',
}); });
@ -3014,13 +3009,16 @@ function App() {
})} })}
</TextP> </TextP>
<Spacer height="100px" /> <Spacer height="100px" />
<CustomButton <ButtonBase
autoFocus
onClick={() => { onClick={() => {
returnToMain(); returnToMain();
}} }}
> >
{t('core:action.continue', { postProcess: 'capitalize' })} <CustomButton>
</CustomButton> {t('core:action.continue', { postProcess: 'capitalize' })}
</CustomButton>
</ButtonBase>
</Box> </Box>
)} )}
{extState === 'transfer-success-request' && ( {extState === 'transfer-success-request' && (
@ -3221,7 +3219,7 @@ function App() {
onClick={onOkUnsavedChanges} onClick={onOkUnsavedChanges}
autoFocus autoFocus
> >
{t('core:action.decline', { {t('core:action.continue_logout', {
postProcess: 'capitalize', postProcess: 'capitalize',
})} })}
</Button> </Button>
@ -3270,6 +3268,8 @@ function App() {
lineHeight: 1.2, lineHeight: 1.2,
maxWidth: '90%', maxWidth: '90%',
textAlign: 'center', textAlign: 'center',
fontSize: '16px',
marginBottom: '10px',
}} }}
> >
{messageQortalRequestExtension?.text1} {messageQortalRequestExtension?.text1}
@ -3316,8 +3316,8 @@ function App() {
> >
{messageQortalRequestExtension?.text3} {messageQortalRequestExtension?.text3}
</TextP> </TextP>
<Spacer height="15px" />
</Box> </Box>
<Spacer height="15px" />
</> </>
)} )}
@ -3342,11 +3342,15 @@ function App() {
)} )}
{messageQortalRequestExtension?.html && ( {messageQortalRequestExtension?.html && (
<div <>
dangerouslySetInnerHTML={{ <Spacer height="15px" />
__html: messageQortalRequestExtension?.html,
}} <div
/> dangerouslySetInnerHTML={{
__html: messageQortalRequestExtension?.html,
}}
/>
</>
)} )}
<Spacer height="15px" /> <Spacer height="15px" />

View File

@ -3689,7 +3689,7 @@ export const checkThreads = async (bringBack) => {
dataToBringBack.push(thread); dataToBringBack.push(thread);
} }
} catch (error) { } catch (error) {
conosle.log({ error }); console.log({ error });
} }
} }

View File

@ -414,8 +414,23 @@ export const AppsDesktop = ({
setDesktopViewMode('dev'); setDesktopViewMode('dev');
}} }}
> >
<IconWrapper label="Dev" disableWidth> <IconWrapper
<AppsIcon height={30} /> color={
desktopViewMode === 'dev'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Dev"
disableWidth
>
<AppsIcon
color={
desktopViewMode === 'dev'
? theme.palette.text.primary
: theme.palette.text.secondary
}
height={30}
/>
</IconWrapper> </IconWrapper>
</ButtonBase> </ButtonBase>
)} )}

View File

@ -180,7 +180,7 @@ export const AppsDevModeNavBar = () => {
> >
<RefreshIcon <RefreshIcon
sx={{ sx={{
color: 'rgba(250, 250, 250, 0.5)', color: theme.palette.text.primary,
width: '40px', width: '40px',
height: 'auto', height: 'auto',
}} }}

View File

@ -253,6 +253,7 @@ export const listOfAllQortalRequests = [
'SELL_NAME', 'SELL_NAME',
'CANCEL_SELL_NAME', 'CANCEL_SELL_NAME',
'BUY_NAME', 'BUY_NAME',
'SIGN_FOREIGN_FEES',
'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA', 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
'TRANSFER_ASSET', 'TRANSFER_ASSET',
]; ];
@ -315,6 +316,7 @@ export const UIQortalRequests = [
'SELL_NAME', 'SELL_NAME',
'CANCEL_SELL_NAME', 'CANCEL_SELL_NAME',
'BUY_NAME', 'BUY_NAME',
'SIGN_FOREIGN_FEES',
'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA', 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
'TRANSFER_ASSET', 'TRANSFER_ASSET',
]; ];

View File

@ -159,6 +159,9 @@ export const AddGroup = ({ address, open, setOpen }) => {
}, },
...prev, ...prev,
]); ]);
setName('');
setDescription('');
setGroupType('1');
res(response); res(response);
return; return;
} }
@ -430,12 +433,12 @@ export const AddGroup = ({ address, open, setOpen }) => {
onChange={handleChangeApprovalThreshold} onChange={handleChangeApprovalThreshold}
> >
<MenuItem value={0}> <MenuItem value={0}>
{t('core.count.none', { {t('core:count.none', {
postProcess: 'capitalize', postProcess: 'capitalize',
})} })}
</MenuItem> </MenuItem>
<MenuItem value={1}> <MenuItem value={1}>
{t('core.count.one', { {t('core:count.one', {
postProcess: 'capitalize', postProcess: 'capitalize',
})} })}
</MenuItem> </MenuItem>
@ -454,7 +457,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
}} }}
> >
<Label> <Label>
{t('group.block_delay.minimum', { {t('group:block_delay.minimum', {
postProcess: 'capitalize', postProcess: 'capitalize',
})} })}
</Label> </Label>
@ -466,40 +469,40 @@ export const AddGroup = ({ address, open, setOpen }) => {
onChange={handleChangeMinBlock} onChange={handleChangeMinBlock}
> >
<MenuItem value={5}> <MenuItem value={5}>
{t('core.time.minute', { count: 5 })} {t('core:time.minute', { count: 5 })}
</MenuItem> </MenuItem>
<MenuItem value={10}> <MenuItem value={10}>
{t('core.time.minute', { count: 10 })} {t('core:time.minute', { count: 10 })}
</MenuItem> </MenuItem>
<MenuItem value={30}> <MenuItem value={30}>
{t('core.time.minute', { count: 30 })} {t('core:time.minute', { count: 30 })}
</MenuItem> </MenuItem>
<MenuItem value={60}> <MenuItem value={60}>
{t('core.time.hour', { count: 1 })} {t('core:time.hour', { count: 1 })}
</MenuItem> </MenuItem>
<MenuItem value={180}> <MenuItem value={180}>
{t('core.time.hour', { count: 3 })} {t('core:time.hour', { count: 3 })}
</MenuItem> </MenuItem>
<MenuItem value={300}> <MenuItem value={300}>
{t('core.time.hour', { count: 5 })} {t('core:time.hour', { count: 5 })}
</MenuItem> </MenuItem>
<MenuItem value={420}> <MenuItem value={420}>
{t('core.time.hour', { count: 7 })} {t('core:time.hour', { count: 7 })}
</MenuItem> </MenuItem>
<MenuItem value={720}> <MenuItem value={720}>
{t('core.time.hour', { count: 12 })} {t('core:time.hour', { count: 12 })}
</MenuItem> </MenuItem>
<MenuItem value={1440}> <MenuItem value={1440}>
{t('core.time.day', { count: 1 })} {t('core:time.day', { count: 1 })}
</MenuItem> </MenuItem>
<MenuItem value={4320}> <MenuItem value={4320}>
{t('core.time.day', { count: 3 })} {t('core:time.day', { count: 3 })}
</MenuItem> </MenuItem>
<MenuItem value={7200}> <MenuItem value={7200}>
{t('core.time.day', { count: 5 })} {t('core:time.day', { count: 5 })}
</MenuItem> </MenuItem>
<MenuItem value={10080}> <MenuItem value={10080}>
{t('core.time.day', { count: 7 })} {t('core:time.day', { count: 7 })}
</MenuItem> </MenuItem>
</Select> </Select>
</Box> </Box>
@ -511,7 +514,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
}} }}
> >
<Label> <Label>
{t('group.block_delay.maximum', { {t('group:block_delay.maximum', {
postProcess: 'capitalize', postProcess: 'capitalize',
})} })}
</Label> </Label>
@ -523,37 +526,37 @@ export const AddGroup = ({ address, open, setOpen }) => {
onChange={handleChangeMaxBlock} onChange={handleChangeMaxBlock}
> >
<MenuItem value={60}> <MenuItem value={60}>
{t('core.time.hour', { count: 1 })} {t('core:time.hour', { count: 1 })}
</MenuItem> </MenuItem>
<MenuItem value={180}> <MenuItem value={180}>
3{t('core.time.hour', { count: 3 })} 3{t('core:time.hour', { count: 3 })}
</MenuItem> </MenuItem>
<MenuItem value={300}> <MenuItem value={300}>
{t('core.time.hour', { count: 5 })} {t('core:time.hour', { count: 5 })}
</MenuItem> </MenuItem>
<MenuItem value={420}> <MenuItem value={420}>
{t('core.time.hour', { count: 7 })} {t('core:time.hour', { count: 7 })}
</MenuItem> </MenuItem>
<MenuItem value={720}> <MenuItem value={720}>
{t('core.time.hour', { count: 12 })} {t('core:time.hour', { count: 12 })}
</MenuItem> </MenuItem>
<MenuItem value={1440}> <MenuItem value={1440}>
{t('core.time.day', { count: 1 })} {t('core:time.day', { count: 1 })}
</MenuItem> </MenuItem>
<MenuItem value={4320}> <MenuItem value={4320}>
{t('core.time.day', { count: 3 })} {t('core:time.day', { count: 3 })}
</MenuItem> </MenuItem>
<MenuItem value={7200}> <MenuItem value={7200}>
{t('core.time.day', { count: 5 })} {t('core:time.day', { count: 5 })}
</MenuItem> </MenuItem>
<MenuItem value={10080}> <MenuItem value={10080}>
{t('core.time.day', { count: 7 })} {t('core:time.day', { count: 7 })}
</MenuItem> </MenuItem>
<MenuItem value={14400}> <MenuItem value={14400}>
{t('core.time.day', { count: 10 })} {t('core:time.day', { count: 10 })}
</MenuItem> </MenuItem>
<MenuItem value={21600}> <MenuItem value={21600}>
{t('core.time.day', { count: 15 })} {t('core:time.day', { count: 15 })}
</MenuItem> </MenuItem>
</Select> </Select>
</Box> </Box>
@ -570,7 +573,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
color="primary" color="primary"
onClick={handleCreateGroup} onClick={handleCreateGroup}
> >
{t('group.action.create', { {t('group:action.create_group', {
postProcess: 'capitalize', postProcess: 'capitalize',
})} })}
</Button> </Button>

View File

@ -97,16 +97,16 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
label={t('group:invitation_expiry', { postProcess: 'capitalize' })} label={t('group:invitation_expiry', { postProcess: 'capitalize' })}
onChange={handleChange} onChange={handleChange}
> >
<MenuItem value={10800}>{t('core.time.hour', { count: 3 })}</MenuItem> <MenuItem value={10800}>{t('core:time.hour', { count: 3 })}</MenuItem>
<MenuItem value={21600}>{t('core.time.hour', { count: 6 })}</MenuItem> <MenuItem value={21600}>{t('core:time.hour', { count: 6 })}</MenuItem>
<MenuItem value={43200}>{t('core.time.hour', { count: 12 })}</MenuItem> <MenuItem value={43200}>{t('core:time.hour', { count: 12 })}</MenuItem>
<MenuItem value={86400}>{t('core.time.day', { count: 1 })}</MenuItem> <MenuItem value={86400}>{t('core:time.day', { count: 1 })}</MenuItem>
<MenuItem value={259200}>{t('core.time.day', { count: 3 })}</MenuItem> <MenuItem value={259200}>{t('core:time.day', { count: 3 })}</MenuItem>
<MenuItem value={432000}>{t('core.time.day', { count: 5 })}</MenuItem> <MenuItem value={432000}>{t('core:time.day', { count: 5 })}</MenuItem>
<MenuItem value={604800}>{t('core.time.day', { count: 7 })}</MenuItem> <MenuItem value={604800}>{t('core:time.day', { count: 7 })}</MenuItem>
<MenuItem value={864000}>{t('core.time.day', { count: 10 })}</MenuItem> <MenuItem value={864000}>{t('core:time.day', { count: 10 })}</MenuItem>
<MenuItem value={1296000}>{t('core.time.day', { count: 15 })}</MenuItem> <MenuItem value={1296000}>{t('core:time.day', { count: 15 })}</MenuItem>
<MenuItem value={2592000}>{t('core.time.day', { count: 30 })}</MenuItem> <MenuItem value={2592000}>{t('core:time.day', { count: 30 })}</MenuItem>
</Select> </Select>
<Spacer height="20px" /> <Spacer height="20px" />
<LoadingButton <LoadingButton

View File

@ -158,6 +158,12 @@ export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => {
value={paymentPassword} value={paymentPassword}
onChange={(e) => setPaymentPassword(e.target.value)} onChange={(e) => setPaymentPassword(e.target.value)}
autoComplete="off" autoComplete="off"
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (isLoadingSendCoin) return;
sendCoinFunc();
}
}}
/> />
</Box> </Box>

View File

@ -27,15 +27,25 @@ export const ReactionPicker = ({ onReaction }) => {
if (showPicker) { if (showPicker) {
setShowPicker(false); setShowPicker(false);
} else { } else {
// Get the button's position
const buttonRect = buttonRef.current.getBoundingClientRect(); const buttonRect = buttonRef.current.getBoundingClientRect();
const pickerWidth = 350; const pickerWidth = 350;
const pickerHeight = 400; // Match Picker height prop
// Calculate position to align the right edge of the picker with the button's right edge // Initial position (below the button)
setPickerPosition({ let top = buttonRect.bottom + window.scrollY;
top: buttonRect.bottom + window.scrollY, // Position below the button let left = buttonRect.right + window.scrollX - pickerWidth;
left: buttonRect.right + window.scrollX - pickerWidth, // Align right edges
}); // If picker would overflow bottom, show it above the button
const overflowBottom =
top + pickerHeight > window.innerHeight + window.scrollY;
if (overflowBottom) {
top = buttonRect.top + window.scrollY - pickerHeight;
}
// Optional: prevent overflow on the left too
if (left < 0) left = 0;
setPickerPosition({ top, left });
setShowPicker(true); setShowPicker(true);
} }
}; };
@ -92,12 +102,13 @@ export const ReactionPicker = ({ onReaction }) => {
allowExpandReactions={true} allowExpandReactions={true}
autoFocusSearch={false} autoFocusSearch={false}
emojiStyle={EmojiStyle.NATIVE} emojiStyle={EmojiStyle.NATIVE}
height="450" height={400}
onEmojiClick={handlePicker} onEmojiClick={handlePicker}
onReactionClick={handleReaction} onReactionClick={handleReaction}
reactionsDefaultOpen={true} // reactionsDefaultOpen={true}
// open={true}
theme={Theme.DARK} theme={Theme.DARK}
width="350" width={350}
/> />
</div>, </div>,
document.body document.body

View File

@ -25,7 +25,7 @@ const ThemeContext = createContext({
toggleTheme: () => {}, toggleTheme: () => {},
userThemes: [defaultTheme], userThemes: [defaultTheme],
addUserTheme: (themes) => {}, addUserTheme: (themes) => {},
setUserTheme: (theme) => {}, setUserTheme: (theme, themes) => {},
currentThemeId: 'default', currentThemeId: 'default',
}); });
@ -83,13 +83,13 @@ export const ThemeProvider = ({ children }) => {
saveSettings(themes); saveSettings(themes);
}; };
const setUserTheme = (theme) => { const setUserTheme = (theme, themes) => {
if (theme.id === 'default') { if (theme.id === 'default') {
setCurrentThemeId('default'); setCurrentThemeId('default');
saveSettings(userThemes, themeMode, 'default'); saveSettings(themes || userThemes, themeMode, 'default');
} else { } else {
setCurrentThemeId(theme.id); setCurrentThemeId(theme.id);
saveSettings(userThemes, themeMode, theme.id); saveSettings(themes || userThemes, themeMode, theme.id);
} }
}; };

View File

@ -119,7 +119,7 @@ export default function ThemeManager() {
const newTheme = { ...themeDraft, id: uid.rnd() }; const newTheme = { ...themeDraft, id: uid.rnd() };
const updatedThemes = [...userThemes, newTheme]; const updatedThemes = [...userThemes, newTheme];
addUserTheme(updatedThemes); addUserTheme(updatedThemes);
setUserTheme(newTheme); setUserTheme(newTheme, updatedThemes);
} }
setOpenEditor(false); setOpenEditor(false);
}; };
@ -135,19 +135,22 @@ export default function ThemeManager() {
); );
if (defaultTheme) { if (defaultTheme) {
setUserTheme(defaultTheme); setUserTheme(defaultTheme, updatedThemes);
} else { } else {
// Emergency fallback // Emergency fallback
setUserTheme({ setUserTheme(
light: lightThemeOptions, {
dark: darkThemeOptions, light: lightThemeOptions,
}); dark: darkThemeOptions,
},
updatedThemes
);
} }
} }
}; };
const handleApplyTheme = (theme) => { const handleApplyTheme = (theme) => {
setUserTheme(theme); setUserTheme(theme, null);
}; };
const handleColorChange = (mode, fieldPath, color) => { const handleColorChange = (mode, fieldPath, color) => {
@ -210,7 +213,8 @@ export default function ThemeManager() {
const newTheme = { ...importedTheme, id: uid.rnd() }; const newTheme = { ...importedTheme, id: uid.rnd() };
const updatedThemes = [...userThemes, newTheme]; const updatedThemes = [...userThemes, newTheme];
addUserTheme(updatedThemes); addUserTheme(updatedThemes);
setUserTheme(newTheme);
setUserTheme(newTheme, updatedThemes);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -61,6 +61,7 @@ import {
buyNameRequest, buyNameRequest,
sellNameRequest, sellNameRequest,
cancelSellNameRequest, cancelSellNameRequest,
signForeignFees,
multiPaymentWithPrivateData, multiPaymentWithPrivateData,
transferAssetRequest, transferAssetRequest,
} from './qortalRequests/get'; } from './qortalRequests/get';
@ -754,7 +755,7 @@ function setupMessageListenerQortalRequest() {
case 'UPDATE_FOREIGN_FEE': { case 'UPDATE_FOREIGN_FEE': {
try { try {
const res = await updateForeignFee(request.payload); const res = await updateForeignFee(request.payload, isFromExtension);
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -806,7 +807,10 @@ function setupMessageListenerQortalRequest() {
case 'SET_CURRENT_FOREIGN_SERVER': { case 'SET_CURRENT_FOREIGN_SERVER': {
try { try {
const res = await setCurrentForeignServer(request.payload); const res = await setCurrentForeignServer(
request.payload,
isFromExtension
);
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -832,7 +836,7 @@ function setupMessageListenerQortalRequest() {
case 'ADD_FOREIGN_SERVER': { case 'ADD_FOREIGN_SERVER': {
try { try {
const res = await addForeignServer(request.payload); const res = await addForeignServer(request.payload, isFromExtension);
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -858,7 +862,10 @@ function setupMessageListenerQortalRequest() {
case 'REMOVE_FOREIGN_SERVER': { case 'REMOVE_FOREIGN_SERVER': {
try { try {
const res = await removeForeignServer(request.payload); const res = await removeForeignServer(
request.payload,
isFromExtension
);
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -1892,6 +1899,32 @@ function setupMessageListenerQortalRequest() {
} }
break; break;
} }
case 'SIGN_FOREIGN_FEES': {
try {
const res = await signForeignFees(request.payload, isFromExtension);
event.source.postMessage(
{
requestId: request.requestId,
action: request.action,
payload: res,
type: 'backgroundMessageResponse',
},
event.origin
);
} catch (error) {
event.source.postMessage(
{
requestId: request.requestId,
action: request.action,
error: error.message,
type: 'backgroundMessageResponse',
},
event.origin
);
}
break;
}
default: default:
break; break;
} }

View File

@ -1302,10 +1302,7 @@ export const publishMultipleQDNResources = async (
html: ` html: `
<div style="max-height: 30vh; overflow-y: auto;"> <div style="max-height: 30vh; overflow-y: auto;">
<style> <style>
body {
background-color: #121212;
color: #e0e0e0;
}
.resource-container { .resource-container {
display: flex; display: flex;
@ -1314,7 +1311,7 @@ export const publishMultipleQDNResources = async (
padding: 16px; padding: 16px;
margin: 8px 0; margin: 8px 0;
border-radius: 8px; border-radius: 8px;
background-color: #1e1e1e; background-color: var(--background-default);
} }
.resource-detail { .resource-detail {
@ -1323,7 +1320,7 @@ export const publishMultipleQDNResources = async (
.resource-detail span { .resource-detail span {
font-weight: bold; font-weight: bold;
color: #bb86fc; color: var(--text-primary);
} }
@media (min-width: 600px) { @media (min-width: 600px) {
@ -2658,7 +2655,12 @@ export const getForeignFee = async (data) => {
} }
}; };
export const updateForeignFee = async (data) => { function calculateRateFromFee(totalFee, sizeInBytes) {
const fee = (totalFee / sizeInBytes) * 1000;
return fee.toFixed(0);
}
export const updateForeignFee = async (data, isFromExtension) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error('This action cannot be done through a public node'); throw new Error('This action cannot be done through a public node');
@ -2679,33 +2681,52 @@ export const updateForeignFee = async (data) => {
} }
const { coin, type, value } = data; const { coin, type, value } = data;
const url = `/crosschain/${coin.toLowerCase()}/update${type}`;
try { const text3 =
const endpoint = await createEndpoint(url); type === 'feerequired' ? `${value} sats` : `${value} sats per kb`;
const response = await fetch(endpoint, { const text4 =
method: 'POST', type === 'feerequired'
headers: { ? `*The ${value} sats fee is derived from ${calculateRateFromFee(value, 300)} sats per kb, for a transaction that is approximately 300 bytes in size.`
Accept: '*/*', : '';
'Content-Type': 'application/json', const resPermission = await getUserPermission(
}, {
body: JSON.stringify({ value }), text1: `Do you give this application permission to update foreign fees on your node?`,
}); text2: `type: ${type === 'feerequired' ? 'unlocking' : 'locking'}`,
text3: `value: ${text3}`,
text4,
highlightedText: `Coin: ${coin}`,
},
isFromExtension
);
if (!response.ok) throw new Error('Failed to update foreign fee'); const { accepted } = resPermission;
let res; if (!accepted) {
try { throw new Error('User declined request');
res = await response.clone().json();
} catch (e) {
res = await response.text();
}
if (res?.error && res?.message) {
throw new Error(res.message);
}
return res; // Return full response here
} catch (error) {
throw new Error(error?.message || 'Error in update foreign fee');
} }
const url = `/crosschain/${coin.toLowerCase()}/update${type}`;
const valueStringified = JSON.stringify(+value);
const endpoint = await createEndpoint(url);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: valueStringified,
});
if (!response.ok) throw new Error('Failed to update foreign fee');
let res;
try {
res = await response.clone().json();
} catch (e) {
res = await response.text();
}
if (res?.error && res?.message) {
throw new Error(res.message);
}
return res; // Return full response here
}; };
export const getServerConnectionHistory = async (data) => { export const getServerConnectionHistory = async (data) => {
@ -2758,7 +2779,7 @@ export const getServerConnectionHistory = async (data) => {
} }
}; };
export const setCurrentForeignServer = async (data) => { export const setCurrentForeignServer = async (data, isFromExtension) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error('This action cannot be done through a public node'); throw new Error('This action cannot be done through a public node');
@ -2780,6 +2801,21 @@ export const setCurrentForeignServer = async (data) => {
} }
const { coin, host, port, type } = data; const { coin, host, port, type } = data;
const resPermission = await getUserPermission(
{
text1: `Do you give this application permission to set the current server?`,
text2: `type: ${type}`,
text3: `host: ${host}`,
highlightedText: `Coin: ${coin}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) {
throw new Error('User declined request');
}
const body = { const body = {
hostName: host, hostName: host,
port: port, port: port,
@ -2788,37 +2824,33 @@ export const setCurrentForeignServer = async (data) => {
const url = `/crosschain/${coin.toLowerCase()}/setcurrentserver`; const url = `/crosschain/${coin.toLowerCase()}/setcurrentserver`;
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
const response = await fetch(endpoint, {
method: 'POST',
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error('Failed to set current server');
let res;
try { try {
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available res = await response.clone().json();
const response = await fetch(endpoint, { } catch (e) {
method: 'POST', res = await response.text();
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error('Failed to set current server');
let res;
try {
res = await response.clone().json();
} catch (e) {
res = await response.text();
}
if (res?.error && res?.message) {
throw new Error(res.message);
}
return res; // Return the full response
} catch (error) {
throw new Error(error?.message || 'Error in set current server');
} }
if (res?.error && res?.message) {
throw new Error(res.message);
}
return res; // Return the full response
}; };
export const addForeignServer = async (data) => { export const addForeignServer = async (data, isFromExtension) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error('This action cannot be done through a public node'); throw new Error('This action cannot be done through a public node');
@ -2840,6 +2872,21 @@ export const addForeignServer = async (data) => {
} }
const { coin, host, port, type } = data; const { coin, host, port, type } = data;
const resPermission = await getUserPermission(
{
text1: `Do you give this application permission to add a server?`,
text2: `type: ${type}`,
text3: `host: ${host}`,
highlightedText: `Coin: ${coin}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) {
throw new Error('User declined request');
}
const body = { const body = {
hostName: host, hostName: host,
port: port, port: port,
@ -2848,37 +2895,33 @@ export const addForeignServer = async (data) => {
const url = `/crosschain/${coin.toLowerCase()}/addserver`; const url = `/crosschain/${coin.toLowerCase()}/addserver`;
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
const response = await fetch(endpoint, {
method: 'POST',
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error('Failed to add server');
let res;
try { try {
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available res = await response.clone().json();
const response = await fetch(endpoint, { } catch (e) {
method: 'POST', res = await response.text();
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error('Failed to add server');
let res;
try {
res = await response.clone().json();
} catch (e) {
res = await response.text();
}
if (res?.error && res?.message) {
throw new Error(res.message);
}
return res; // Return the full response
} catch (error) {
throw new Error(error.message || 'Error in adding server');
} }
if (res?.error && res?.message) {
throw new Error(res.message);
}
return res; // Return the full response
}; };
export const removeForeignServer = async (data) => { export const removeForeignServer = async (data, isFromExtension) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error('This action cannot be done through a public node'); throw new Error('This action cannot be done through a public node');
@ -2900,6 +2943,21 @@ export const removeForeignServer = async (data) => {
} }
const { coin, host, port, type } = data; const { coin, host, port, type } = data;
const resPermission = await getUserPermission(
{
text1: `Do you give this application permission to remove a server?`,
text2: `type: ${type}`,
text3: `host: ${host}`,
highlightedText: `Coin: ${coin}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) {
throw new Error('User declined request');
}
const body = { const body = {
hostName: host, hostName: host,
port: port, port: port,
@ -2908,34 +2966,30 @@ export const removeForeignServer = async (data) => {
const url = `/crosschain/${coin.toLowerCase()}/removeserver`; const url = `/crosschain/${coin.toLowerCase()}/removeserver`;
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available
const response = await fetch(endpoint, {
method: 'POST',
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error('Failed to remove server');
let res;
try { try {
const endpoint = await createEndpoint(url); // Assuming createEndpoint is available res = await response.clone().json();
const response = await fetch(endpoint, { } catch (e) {
method: 'POST', res = await response.text();
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error('Failed to remove server');
let res;
try {
res = await response.clone().json();
} catch (e) {
res = await response.text();
}
if (res?.error && res?.message) {
throw new Error(res.message);
}
return res; // Return the full response
} catch (error) {
throw new Error(error?.message || 'Error in removing server');
} }
if (res?.error && res?.message) {
throw new Error(res.message);
}
return res; // Return the full response
}; };
export const getDaySummary = async () => { export const getDaySummary = async () => {
@ -3493,6 +3547,35 @@ export const sendCoin = async (data, isFromExtension) => {
} }
}; };
function calculateFeeFromRate(feePerKb, sizeInBytes) {
return (feePerKb / 1000) * sizeInBytes;
}
const getBuyingFees = async (foreignBlockchain) => {
const ticker = sellerForeignFee[foreignBlockchain].ticker;
if (!ticker) throw new Error('invalid foreign blockchain');
const unlockFee = await getForeignFee({
coin: ticker,
type: 'feerequired',
});
const lockFee = await getForeignFee({
coin: ticker,
type: 'feekb',
});
return {
ticker: ticker,
lock: {
sats: lockFee,
fee: lockFee / QORT_DECIMALS,
},
unlock: {
sats: unlockFee,
fee: unlockFee / QORT_DECIMALS,
feePerKb: +calculateRateFromFee(+unlockFee, 300) / QORT_DECIMALS,
},
};
};
export const createBuyOrder = async (data, isFromExtension) => { export const createBuyOrder = async (data, isFromExtension) => {
const requiredFields = ['crosschainAtInfo', 'foreignBlockchain']; const requiredFields = ['crosschainAtInfo', 'foreignBlockchain'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -3528,6 +3611,7 @@ export const createBuyOrder = async (data, isFromExtension) => {
const crosschainAtInfo = await Promise.all(atPromises); const crosschainAtInfo = await Promise.all(atPromises);
try { try {
const buyingFees = await getBuyingFees(foreignBlockchain);
const resPermission = await getUserPermission( const resPermission = await getUserPermission(
{ {
text1: text1:
@ -3541,10 +3625,45 @@ export const createBuyOrder = async (data, isFromExtension) => {
return latest + +cur?.expectedForeignAmount; return latest + +cur?.expectedForeignAmount;
}, 0) }, 0)
)} )}
${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`, ${` ${buyingFees.ticker}`}`,
highlightedText: `Is using public node: ${isGateway}`, highlightedText: `Is using public node: ${isGateway}`,
fee: '', fee: '',
foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}`, html: `
<div style="max-height: 30vh; overflow-y: auto; font-family: sans-serif;">
<style>
.fee-container {
background-color: var(--background-default);
color: var(--text-primary);
border: 1px solid #444;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.fee-label {
font-weight: bold;
color: var(--text-primary);
margin-bottom: 4px;
}
.fee-description {
font-size: 14px;
color: var(--text-primary);
margin-bottom: 16px;
}
</style>
<div class="fee-container">
<div class="fee-label">Total Unlocking Fee:</div>
<div>${(+buyingFees?.unlock?.fee * atAddresses?.length)?.toFixed(8)} ${buyingFees.ticker}</div>
<div class="fee-description">
This fee is an estimate based on ${atAddresses?.length} ${atAddresses?.length > 1 ? 'orders' : 'order'}, assuming a 300-byte size at a rate of ${buyingFees?.unlock?.feePerKb?.toFixed(8)} ${buyingFees.ticker} per KB.
</div>
<div class="fee-label">Total Locking Fee:</div>
<div>${+buyingFees?.lock.fee.toFixed(8)} ${buyingFees.ticker} per kb</div>
</div>
</div>
`,
}, },
isFromExtension isFromExtension
); );
@ -4934,6 +5053,70 @@ export const buyNameRequest = async (data, isFromExtension) => {
} }
}; };
export const signForeignFees = async (data, isFromExtension) => {
const resPermission = await getUserPermission(
{
text1: `Do you give this application permission to sign the required fees for all your trade offers?`,
},
isFromExtension
);
const { accepted } = resPermission;
if (accepted) {
const wallet = await getSaveWallet();
const address = wallet.address0;
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey,
};
const unsignedFeesUrl = await createEndpoint(
`/crosschain/unsignedfees/${address}`
);
const unsignedFeesResponse = await fetch(unsignedFeesUrl);
const unsignedFees = await unsignedFeesResponse.json();
const signedFees = [];
unsignedFees.forEach((unsignedFee) => {
const unsignedDataDecoded = Base58.decode(unsignedFee.data);
const signature = nacl.sign.detached(
unsignedDataDecoded,
keyPair.privateKey
);
const signedFee = {
timestamp: unsignedFee.timestamp,
data: `${Base58.encode(signature)}`,
atAddress: unsignedFee.atAddress,
fee: unsignedFee.fee,
};
signedFees.push(signedFee);
});
const signedFeesUrl = await createEndpoint(`/crosschain/signedfees`);
await fetch(signedFeesUrl, {
method: 'POST',
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
},
body: `${JSON.stringify(signedFees)}`,
});
return true;
} else {
throw new Error('User declined request');
}
};
export const multiPaymentWithPrivateData = async (data, isFromExtension) => { export const multiPaymentWithPrivateData = async (data, isFromExtension) => {
const requiredFields = ['payments', 'assetId']; const requiredFields = ['payments', 'assetId'];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
@ -5075,19 +5258,15 @@ export const multiPaymentWithPrivateData = async (data, isFromExtension) => {
html: ` html: `
<div style="max-height: 30vh; overflow-y: auto;"> <div style="max-height: 30vh; overflow-y: auto;">
<style> <style>
body {
background-color: #121212;
color: #e0e0e0;
}
.resource-container { .resource-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid #444; border: 1px solid;
padding: 16px; padding: 16px;
margin: 8px 0; margin: 8px 0;
border-radius: 8px; border-radius: 8px;
background-color: #1e1e1e; background-color: var(--background-default);
} }
.resource-detail { .resource-detail {
@ -5096,7 +5275,7 @@ export const multiPaymentWithPrivateData = async (data, isFromExtension) => {
.resource-detail span { .resource-detail span {
font-weight: bold; font-weight: bold;
color: #bb86fc; color: var(--text-primary);
} }
@media (min-width: 600px) { @media (min-width: 600px) {

View File

@ -55,6 +55,11 @@ export const darkThemeOptions: ThemeOptions = {
'--bg-primary': 'rgba(31, 32, 35, 1)', '--bg-primary': 'rgba(31, 32, 35, 1)',
'--bg-2': 'rgb(39, 40, 44)', '--bg-2': 'rgb(39, 40, 44)',
'--primary-main': theme.palette.primary.main, '--primary-main': theme.palette.primary.main,
'--text-primary': theme.palette.text.primary,
'--text-secondary': theme.palette.text.secondary,
'--background-default': theme.palette.background.default,
'--background-paper': theme.palette.background.paper,
'--background-surface': theme.palette.background.surface,
}, },
'*, *::before, *::after': { '*, *::before, *::after': {
boxSizing: 'border-box', boxSizing: 'border-box',

View File

@ -55,6 +55,11 @@ export const lightThemeOptions: ThemeOptions = {
'--bg-primary': 'rgba(31, 32, 35, 1)', '--bg-primary': 'rgba(31, 32, 35, 1)',
'--bg-2': 'rgba(39, 40, 44, 1)', '--bg-2': 'rgba(39, 40, 44, 1)',
'--primary-main': theme.palette.primary.main, '--primary-main': theme.palette.primary.main,
'--text-primary': theme.palette.text.primary,
'--text-secondary': theme.palette.text.secondary,
'--background-default': theme.palette.background.default,
'--background-paper': theme.palette.background.paper,
'--background-surface': theme.palette.background.surface,
}, },
'*, *::before, *::after': { '*, *::before, *::after': {