feat(Community Airdrops): Dynamic fee calculation in the airdrop form

Closes: #10547
This commit is contained in:
Michał Cieślak 2023-06-14 17:25:48 +02:00 committed by Michał
parent d4ba22f7bb
commit e0dd0b82ce
8 changed files with 612 additions and 219 deletions

View File

@ -157,6 +157,10 @@ ListModel {
title: "SequenceColumnLayout"
section: "Panels"
}
ListElement {
title: "FeesPanel"
section: "Panels"
}
ListElement {
title: "BurnTokensPopup"
section: "Popups"

View File

@ -94,11 +94,58 @@ SplitView {
}
}
Timer {
id: feesCalculationTimer
interval: 2000
property var response
function requestMockedFees(contractKeysAndAmounts) {
const fees = []
function createAmount(amount, symbol, decimals) {
return {
amount, symbol,
displayDecimals: decimals, stripTrailingZeroes: false
}
}
contractKeysAndAmounts.forEach(entry => {
fees.push({
ethFee: createAmount(0.0002120115, "ETH", 4),
fiatFee: createAmount(123.15, "USD", 2),
errorCode: 0,
contractUniqueKey: entry.contractUniqueKey
})
})
response = {
fees, errorCode: feesErrorsButtonGroup.checkedButton.code,
totalEthFee: createAmount(0.0002120115 * fees.length, "ETH", 4),
totalFiatFee: createAmount(123.15 * fees.length, "USD", 2)
}
restart()
}
onTriggered: {
if (!loader.item)
return
const view = loader.item
view.airdropFees = response
}
}
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
Loader {
id: loader
anchors.fill: parent
active: globalUtilsReady && mainModuleReady
@ -123,6 +170,14 @@ SplitView {
name: "infiniteSupply"
expression: !(model.index % 4)
},
ExpressionRole {
name: "accountName"
expression: "StatusAccount"
},
ExpressionRole {
name: "contractUniqueKey"
expression: "contractUniqueKey_" + model.index
},
ExpressionRole {
name: "chainName"
expression: model.index ? "Optimism" : "Arbitrum"
@ -142,7 +197,6 @@ SplitView {
value: TokenCategories.Category.Community
}
Component.onCompleted: {
Qt.callLater(() => communityNewAirdropView.collectiblesModel = this)
}
@ -196,7 +250,17 @@ SplitView {
membersModel: members
onAirdropClicked: {
logs.logEvent("CommunityNewAirdropView::airdropClicked", ["airdropTokens", "addresses", "membersPubKeys"], arguments)
logs.logEvent("CommunityNewAirdropView::airdropClicked",
["airdropTokens", "addresses", "membersPubKeys"],
arguments)
}
onAirdropFeesRequested: {
logs.logEvent("CommunityNewAirdropView::airdropFeesRequested",
["contractKeysAndAmounts", "addresses"],
arguments)
feesCalculationTimer.requestMockedFees(contractKeysAndAmounts)
}
}
}
@ -211,13 +275,48 @@ SplitView {
logsView.logText: logs.logText
ColumnLayout {
MenuSeparator {}
TextEdit {
readOnly: true
selectByMouse: true
text: "valid address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4"
}
MenuSeparator {}
ButtonGroup {
id: feesErrorsButtonGroup
buttons: feesErrorsRow.children
}
RowLayout {
id: feesErrorsRow
Label {
text: "Fees calculation errors:"
}
RadioButton {
readonly property int code: Constants.ComputeFeeErrorCode.Success
text: `Success (${code})`
checked: true
}
RadioButton {
readonly property int code: Constants.ComputeFeeErrorCode.Infura
text: `Infura (${code})`
}
RadioButton {
readonly property int code: Constants.ComputeFeeErrorCode.Balance
text: `Balance (${code})`
}
RadioButton {
readonly property int code: Constants.ComputeFeeErrorCode.Other
text: `Other (${code})`
}
}
}
}
}

View File

@ -0,0 +1,117 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
import AppLayouts.Chat.panels.communities 1.0
SplitView {
Logs { id: logs }
SplitView {
orientation: Qt.Vertical
SplitView.fillWidth: true
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
Rectangle {
anchors.fill: feesPanel
anchors.margins: -15
border.color: "lightgray"
}
FeesPanel {
id: feesPanel
anchors.centerIn: parent
width: 500
model: ListModel {
ListElement {
account: "My Account 1"
network: "Optimism"
symbol: "TAT"
amount: 2
feeText: "0.0015 ($75.43)"
}
ListElement {
account: "My Account 2"
network: "Arbitrum"
symbol: "SNT"
amount: 34
feeText: "0.0085 ETH ($175.43)"
}
}
errorText: errorTextField.text
isFeeLoading: loadingSwitch.checked
showSummary: showSummarySwitch.checked
showAccounts: showAccountsSwitch.checked
totalFeeText: "0.01 ETH ($265.43)"
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 150
logsView.logText: logs.logText
}
}
Pane {
SplitView.minimumWidth: 300
SplitView.preferredWidth: 300
ColumnLayout {
anchors.fill: parent
Label {
Layout.fillWidth: true
text: "Error text"
}
TextField {
id: errorTextField
Layout.fillWidth: true
text: ""
}
Switch {
id: loadingSwitch
text: "Is fee loading"
checked: false
}
Switch {
id: showSummarySwitch
text: "Show summary"
checked: true
}
Switch {
id: showAccountsSwitch
text: "Show account names"
checked: true
}
Item {
Layout.fillHeight: true
}
}
}
}

View File

@ -0,0 +1,149 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import utils 1.0
Control {
id: root
// account, amount, symbol, network, feeText
property alias model: repeater.model
readonly property alias count: repeater.count
property alias showSummary: summaryRow.visible
property alias errorText: errorTxt.text
property alias totalFeeText: totalFeeText.text
property bool isFeeLoading: false
property bool showAccounts: true
QtObject {
id: d
readonly property int delegateHeightWhenAccountsHidden: 28
}
contentItem: ColumnLayout {
spacing: Style.current.padding
Repeater {
id: repeater
Item {
Layout.fillWidth: true
Layout.preferredHeight: root.showAccounts
? delegateColumn.implicitHeight
: Math.max(delegateColumn.implicitHeight,
d.delegateHeightWhenAccountsHidden)
ColumnLayout {
id: delegateColumn
width: parent.width
anchors.verticalCenter: parent.verticalCenter
RowLayout {
Layout.fillWidth: true
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Airdropping %1 %2 on %3")
.arg(model.amount).arg(model.symbol)
.arg(model.network)
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideRight
}
StatusDotsLoadingIndicator {
Layout.rightMargin: Style.current.padding
visible: root.isFeeLoading
}
StatusBaseText {
text: model.feeText
visible: !root.isFeeLoading
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
color: repeater.count === 1 ? Theme.palette.directColor1
: Theme.palette.baseColor1
}
}
StatusBaseText {
Layout.fillWidth: true
visible: root.showAccounts
text: qsTr("via %1").arg(model.account)
horizontalAlignment: Text.AlignLeft
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
color: Theme.palette.baseColor1
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
visible: summaryRow.visible
color: Theme.palette.baseColor2
}
RowLayout {
id: summaryRow
Layout.fillWidth: true
Layout.topMargin: Style.current.halfPadding
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Total")
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
}
StatusDotsLoadingIndicator {
visible: root.isFeeLoading
Layout.rightMargin: Style.current.padding
}
StatusBaseText {
id: totalFeeText
font.pixelSize: Style.current.primaryTextFontSize
visible: !root.isFeeLoading
}
}
StatusBaseText {
id: errorTxt
Layout.topMargin: Style.current.halfPadding
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignRight
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.dangerColor1
text: root.errorText
visible: root.errorText !== ""
}
}
}

View File

@ -4,6 +4,7 @@ CommunityMintTokensSettingsPanel 1.0 CommunityMintTokensSettingsPanel.qml
CommunityPermissionsSettingsPanel 1.0 CommunityPermissionsSettingsPanel.qml
CommunityProfilePopupInviteFriendsPanel 1.0 CommunityProfilePopupInviteFriendsPanel.qml
CommunityProfilePopupInviteMessagePanel 1.0 CommunityProfilePopupInviteMessagePanel.qml
FeesPanel 1.0 FeesPanel.qml
HidePermissionPanel 1.0 HidePermissionPanel.qml
JoinPermissionsOverlayPanel 1.0 JoinPermissionsOverlayPanel.qml
MintTokensFooterPanel 1.0 MintTokensFooterPanel.qml

View File

@ -1,25 +1,23 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
import AppLayouts.Chat.panels.communities 1.0
StatusDialog {
id: root
// account, amount, symbol, network, feeText
property alias model: repeater.model
property alias showSummary: summaryRow.visible
property alias errorText: errorTxt.text
property alias totalFeeText: totalFeeText.text
property alias model: feesPanel.model
property alias showSummary: feesPanel.showSummary
property alias errorText: feesPanel.errorText
property alias totalFeeText: feesPanel.totalFeeText
property bool isFeeLoading
property alias isFeeLoading: feesPanel.isFeeLoading
signal signTransactionClicked()
signal cancelClicked()
@ -32,126 +30,10 @@ StatusDialog {
implicitWidth: 600 // by design
topPadding: 2 * Style.current.padding // by design
bottomPadding: topPadding
bottomPadding: Style.current.bigPadding
contentItem: ColumnLayout {
id: column
spacing: Style.current.padding
Repeater {
id: repeater
Item {
Layout.fillWidth: true
implicitHeight: delegateColumn.implicitHeight
ColumnLayout {
id: delegateColumn
width: parent.width
RowLayout {
Layout.fillWidth: true
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Airdropping %1 %2 on %3")
.arg(model.amount).arg(model.symbol)
.arg(model.network)
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
}
StatusDotsLoadingIndicator {
visible: root.isFeeLoading
Layout.rightMargin: Style.current.padding
}
StatusBaseText {
text: model.feeText
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
color: Theme.palette.baseColor1
visible: !root.isFeeLoading
}
}
StatusBaseText {
Layout.fillWidth: true
text: qsTr("via %1").arg(model.account)
horizontalAlignment: Text.AlignLeft
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
color: Theme.palette.baseColor1
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Theme.palette.baseColor2
}
RowLayout {
id: summaryRow
Layout.fillHeight: false
Layout.fillWidth: true
Layout.topMargin: Style.current.halfPadding
Layout.bottomMargin: -Style.current.halfPadding
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Total")
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideMiddle
}
StatusDotsLoadingIndicator {
visible: root.isFeeLoading
Layout.rightMargin: Style.current.padding
}
StatusBaseText {
id: totalFeeText
font.pixelSize: Style.current.primaryTextFontSize
visible: !root.isFeeLoading
}
}
StatusBaseText {
id: errorTxt
Layout.topMargin: Style.current.halfPadding
Layout.bottomMargin: -Style.current.halfPadding
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignRight
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.dangerColor1
text: root.errorText
visible: root.errorText !== ""
}
contentItem: FeesPanel {
id: feesPanel
}
footer: StatusDialogFooter {

View File

@ -1,9 +1,11 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Utils 0.1
import utils 1.0
@ -48,6 +50,24 @@ StatusScrollView {
readonly property var selectedHoldingsModel: ListModel {}
ModelChangeTracker {
id: holdingsModelTracker
model: selectedHoldingsModel
}
ModelChangeTracker {
id: addressesModelTracker
model: addresses
}
ModelChangeTracker {
id: membersModelTracker
model: selectedMembersModel
}
readonly property bool isFullyFilled: tokensSelector.count > 0 &&
airdropRecipientsSelector.count > 0 &&
airdropRecipientsSelector.valid
@ -77,6 +97,51 @@ StatusScrollView {
addresses.addAddresses(_addresses)
}
onAirdropFeesChanged: {
if (root.airdropFees === null)
return
const fees = root.airdropFees.fees
const errorCode = root.airdropFees.errorCode
function buildFeeString(ethFee, fiatFee) {
return `${LocaleUtils.currencyAmountToLocaleString(ethFee)} (${LocaleUtils.currencyAmountToLocaleString(fiatFee)})`
}
d.feesError = ""
d.totalFee = ""
if (errorCode === Constants.ComputeFeeErrorCode.Infura) {
d.feesError = qsTr("Infura error")
return
}
if (errorCode === Constants.ComputeFeeErrorCode.Success ||
errorCode === Constants.ComputeFeeErrorCode.Balance) {
fees.forEach(fee => {
const idx = ModelUtils.indexOf(
feesModel, "contractUniqueKey",
fee.contractUniqueKey)
feesModel.set(idx, {
feeText: buildFeeString(fee.ethFee, fee.fiatFee)
})
})
d.totalFee = buildFeeString(
root.airdropFees.totalEthFee,
root.airdropFees.totalFiatFee)
if (errorCode === Constants.ComputeFeeErrorCode.Balance) {
d.feesError = qsTr("Your account does not have enough ETH to pay the gas fee for this airdrop. Try adding some ETH to your account.")
}
return
}
d.feesError = qsTr("Unknown error")
}
QtObject {
id: d
@ -84,14 +149,41 @@ StatusScrollView {
readonly property int dropdownHorizontalOffset: 4
readonly property int dropdownVerticalOffset: 1
property string feesError
property string totalFee
readonly property bool isFeeLoading:
root.airdropFees === null ||
(root.airdropFees.errorCode !== Constants.ComputeFeeErrorCode.Success &&
root.airdropFees.errorCode !== Constants.ComputeFeeErrorCode.Balance)
readonly property bool showFees: root.selectedHoldingsModel.count > 0
&& airdropRecipientsSelector.valid
&& airdropRecipientsSelector.count > 0
readonly property int totalRevision: holdingsModelTracker.revision
+ addressesModelTracker.revision
+ membersModelTracker.revision
+ (d.showFees ? 1 : 0)
onTotalRevisionChanged: {
Qt.callLater(() => {
if (!d.showFees)
return
d.resetFees()
})
}
function prepareEntry(key, amount, type) {
var tokenModel = null
let tokenModel = null
if(type === Constants.TokenType.ERC20)
tokenModel = root.assetsModel
else if (type === Constants.TokenType.ERC721)
tokenModel = root.collectiblesModel
const modelItem = CommunityPermissionsHelpers.getTokenByKey(tokenModel, key)
const modelItem = CommunityPermissionsHelpers.getTokenByKey(
tokenModel, key)
return {
key, amount, type,
@ -102,9 +194,59 @@ StatusScrollView {
supply: modelItem.supply,
infiniteSupply: modelItem.infiniteSupply,
contractUniqueKey: modelItem.contractUniqueKey,
accountName: modelItem.accountName
accountName: modelItem.accountName,
symbol: modelItem.symbol
}
}
function rebuildFeesModel() {
feesModel.clear()
const airdropTokens = ModelUtils.modelToArray(
root.selectedHoldingsModel,
["contractUniqueKey", "accountName",
"symbol", "amount", "tokenText",
"networkText"])
airdropTokens.forEach(entry => {
feesModel.append({
contractUniqueKey: entry.contractUniqueKey,
amount: entry.amount * addresses.count,
account: entry.accountName,
symbol: entry.symbol,
network: entry.networkText,
feeText: ""
})
})
}
function requestFees() {
const airdropTokens = ModelUtils.modelToArray(
root.selectedHoldingsModel,
["contractUniqueKey", "amount"])
const contractKeysAndAmounts = airdropTokens.map(item => ({
amount: item.amount,
contractUniqueKey: item.contractUniqueKey
}))
const addressesArray = ModelUtils.modelToArray(
addresses, ["address"]).map(e => e.address)
airdropFeesRequested(contractKeysAndAmounts, addressesArray)
}
function resetFees() {
root.airdropFees = null
d.feesError = ""
d.totalFee = ""
d.rebuildFeesModel()
d.requestFees()
}
}
ListModel {
id: feesModel
}
Instantiator {
@ -302,6 +444,8 @@ StatusScrollView {
recipientsCountInstantiator.maximumRecipientsCount
membersModel: SortFilterProxyModel {
id: selectedMembersModel
sourceModel: membersModel
filters: ExpressionFilter {
@ -432,7 +576,68 @@ StatusScrollView {
}
}
SequenceColumnLayout.Separator {}
StatusGroupBox {
id: feesBox
Layout.fillWidth: true
implicitWidth: 0
title: qsTr("Fees")
icon: Style.svg("gas")
Control {
id: feesControl
width: feesBox.availableWidth
padding: Style.current.padding
verticalPadding: 18
background: Rectangle {
radius: Style.current.radius
color: Theme.palette.indirectColor1
}
contentItem: Loader {
Component {
id: feesPanelComponent
FeesPanel {
width: feesControl.availableWidth
showAccounts: false
totalFeeText: d.totalFee
showSummary: count > 1
isFeeLoading: d.isFeeLoading
model: feesModel
}
}
Component {
id: placeholderComponent
StatusBaseText {
width: feesControl.availableWidth
text: qsTr("Add valid “What” and “To” values to see fees")
font.pixelSize: Style.current.primaryTextFontSize
elide: Text.ElideRight
color: Theme.palette.baseColor1
}
}
sourceComponent: d.showFees ? feesPanelComponent
: placeholderComponent
}
}
}
WarningPanel {
id: notEnoughTokensWarning
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
@ -442,13 +647,22 @@ StatusScrollView {
recipientsCountInstantiator.maximumRecipientsCount < airdropRecipientsSelector.count
}
WarningPanel {
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
text: d.feesError
visible: !notEnoughTokensWarning.visible && d.showFees && d.feesError
}
StatusButton {
Layout.preferredHeight: 44
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: Style.current.bigPadding
text: qsTr("Create airdrop")
enabled: root.isFullyFilled
enabled: root.isFullyFilled && !d.isFeeLoading && d.feesError === ""
onClicked: {
feesPopup.open()
@ -460,13 +674,12 @@ StatusScrollView {
destroyOnClose: false
model: ListModel {
id: feesModel
}
model: feesModel
isFeeLoading: root.airdropFees === null ||
(root.airdropFees.errorCode !== Constants.ComputeFeeErrorCode.Success &&
root.airdropFees.errorCode !== Constants.ComputeFeeErrorCode.Balance)
isFeeLoading: d.isFeeLoading
totalFeeText: d.totalFee
errorText: d.feesError
onOpened: {
const title1 = qsTr("Sign transaction - Airdrop %n token(s)", "",
@ -476,36 +689,7 @@ StatusScrollView {
title = `${title1} ${title2}`
root.airdropFees = null
errorText = ""
feesModel.clear()
const airdropTokens = ModelUtils.modelToArray(
root.selectedHoldingsModel,
["contractUniqueKey", "accountName",
"key", "amount", "tokenText",
"networkText"])
airdropTokens.forEach(entry => {
feesModel.append({
contractUniqueKey: entry.contractUniqueKey,
key: entry.key,
amount: entry.amount,
account: entry.accountName,
symbol: entry.key,
network: entry.networkText,
feeText: ""
})
})
const contractKeysAndAmounts = airdropTokens.map(item => ({
amount: item.amount,
contractUniqueKey: item.contractUniqueKey
}))
const addresses_ = ModelUtils.modelToArray(
addresses, ["address"]).map(e => e.address)
airdropFeesRequested(contractKeysAndAmounts, addresses_)
d.resetFees()
}
onSignTransactionClicked: {
@ -520,52 +704,6 @@ StatusScrollView {
root.airdropClicked(airdropTokens, addresses_, pubKeys)
}
Connections {
target: root
function onAirdropFeesChanged() {
if (root.airdropFees === null)
return
const fees = root.airdropFees.fees
const errorCode = root.airdropFees.errorCode
function buildFeeString(ethFee, fiatFee) {
return `${LocaleUtils.currencyAmountToLocaleString(ethFee)} (${LocaleUtils.currencyAmountToLocaleString(fiatFee)})`
}
if (errorCode === Constants.ComputeFeeErrorCode.Infura) {
feesPopup.errorText = qsTr("Infura error")
return
}
if (errorCode === Constants.ComputeFeeErrorCode.Success ||
errorCode === Constants.ComputeFeeErrorCode.Balance) {
fees.forEach(fee => {
const idx = ModelUtils.indexOf(
feesModel, "contractUniqueKey",
fee.contractUniqueKey)
feesPopup.model.set(idx, {
feeText: buildFeeString(fee.ethFee, fee.fiatFee)
})
})
feesPopup.totalFeeText = buildFeeString(
root.airdropFees.totalEthFee,
root.airdropFees.totalFiatFee)
if (errorCode === Constants.ComputeFeeErrorCode.Balance) {
feesPopup.errorText = qsTr("Not enough funds to make transaction")
}
return
}
feesPopup.errorText = qsTr("Unknown error")
}
}
}
}
}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75 3C6.67893 3 5 4.67893 5 6.75V18.75C5 19.7165 5.7835 20.5 6.75 20.5H12.75C13.7165 20.5 14.5 19.7165 14.5 18.75V13H14.75C15.4404 13 16 13.5596 16 14.25V17.75C16 19.2688 17.2312 20.5 18.75 20.5C20.2688 20.5 21.5 19.2688 21.5 17.75V9.1113C21.5 8.02994 21.0332 7.00122 20.2194 6.28914L17.2439 3.68557C16.9322 3.41281 16.4583 3.44439 16.1856 3.75612C15.9128 4.06785 15.9444 4.54167 16.2561 4.81443L17.744 6.11634C16.7328 6.43675 16 7.38279 16 8.5C16 9.88071 17.1193 11 18.5 11C19.0628 11 19.5822 10.814 20 10.5002V17.75C20 18.4404 19.4404 19 18.75 19C18.0596 19 17.5 18.4404 17.5 17.75V14.25C17.5 12.7312 16.2688 11.5 14.75 11.5H14.5V6.75C14.5 4.67893 12.8211 3 10.75 3H8.75ZM6.5 6.75C6.5 5.50736 7.50736 4.5 8.75 4.5H10.75C11.9926 4.5 13 5.50736 13 6.75V9.25L6.5 9.25V6.75ZM6.5 10.75V18.75C6.5 18.8881 6.61193 19 6.75 19H12.75C12.8881 19 13 18.8881 13 18.75V10.75L6.5 10.75ZM18.5 7.5C17.9477 7.5 17.5 7.94772 17.5 8.5C17.5 9.05228 17.9477 9.5 18.5 9.5C19.0523 9.5 19.5 9.05228 19.5 8.5C19.5 7.94772 19.0523 7.5 18.5 7.5Z" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB