fix(Update fees periodically): Implementing periodic fee update for airdrops, minting, self destruct and burning transactions

This commit is contained in:
Alex Jbanca 2023-09-01 12:27:44 +03:00 committed by Alex Jbanca
parent f9e7265447
commit 624b758c85
30 changed files with 1119 additions and 422 deletions

View File

@ -100,8 +100,9 @@ const asyncGetRemoteBurnFeesTask: Task = proc(argEncoded: string) {.gcsafe, nimc
"gasTable": tableToJsonArray(gasTable),
"chainId": arg.chainId,
"addressFrom": arg.addressFrom,
"error": "",
"requestId": arg.requestId,
"error": "" })
})
except Exception as e:
arg.finish(%* {
"error": e.msg,
@ -130,8 +131,9 @@ const asyncGetBurnFeesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.}
"gasTable": tableToJsonArray(gasTable),
"chainId": arg.chainId,
"addressFrom": arg.addressFrom,
"requestId": arg.requestId,
"error": "" })
"error": "",
"requestId": arg.requestId
})
except Exception as e:
arg.finish(%* {
"error": e.msg,
@ -163,15 +165,16 @@ const asyncGetMintFeesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.}
arg.walletAddresses, collectibleAndAmount.amount).result.getInt
gasTable[(chainId, collectibleAndAmount.communityToken.address)] = gas
arg.finish(%* {
"requestId": arg.requestId,
"feeTable": tableToJsonArray(feeTable),
"gasTable": tableToJsonArray(gasTable),
"addressFrom": arg.addressFrom,
"error": "" })
"error": "",
"requestId": arg.requestId
})
except Exception as e:
let output = %* {
"requestId": arg.requestId,
"error": e.msg
"error": e.msg,
"requestId": arg.requestId
}
arg.finish(output)

View File

@ -656,8 +656,10 @@ QtObject:
)
self.threadpool.start(arg)
except Exception as e:
#TODO: handle error - emit error signal
error "Error loading airdrop fees", msg = e.msg
var dataToEmit = AirdropFeesArgs()
dataToEmit.errorCode = ComputeFeeErrorCode.Other
self.events.emit(SIGNAL_COMPUTE_AIRDROP_FEE, dataToEmit)
proc getFiatValue*(self: Service, cryptoBalance: float, cryptoSymbol: string): float =
if (cryptoSymbol == ""):

View File

@ -68,24 +68,27 @@ SplitView {
onBurnClicked: logs.logEvent("BurnTokensPopup::onBurnClicked --> Burn amount: " + burnAmount)
onCancelClicked: logs.logEvent("BurnTokensPopup::onCancelClicked")
feeText: "0.0015 ETH ($75.43)"
feeErrorText: ""
isFeeLoading: false
onBurnFeesRequested: {
feeText = ""
feeErrorText = ""
isFeeLoading = true
feeCalculationTimer.restart()
onSelectedAccountAddressChanged: {
burnTokensPopup.isFeeLoading = true
timer.delay(2000, () => burnTokensPopup.isFeeLoading = false)
}
onAmountToBurnChanged: {
burnTokensPopup.isFeeLoading = true
timer.delay(2000, () => burnTokensPopup.isFeeLoading = false)
}
}
Timer {
id: feeCalculationTimer
interval: 1000
onTriggered: {
burnTokensPopup.feeText = "0.0015 ETH ($75.43)"
burnTokensPopup.isFeeLoading = false
id: timer
function delay(ms, callback) {
timer.interval = ms
timer.repeat = false
timer.triggered.connect(callback)
timer.start()
}
}
}

View File

@ -62,6 +62,9 @@ SplitView {
token: tokenObject
tokenOwnersModel: TokenHoldersModel {}
feeText: "0.01"
feeErrorText: ""
isFeeLoading: false
accounts: WalletAccountsModel {}

View File

@ -53,7 +53,7 @@ SplitView {
onMintClicked: logs.logEvent("EditOwnerTokenView::onMintClicked")
onDeployFeesRequested: {
Component.onCompleted: {
feeText = ""
feeErrorText = ""
isFeeLoading = true

View File

@ -30,13 +30,12 @@ SplitView {
}
Timer {
id: feesTimer
interval: 1000
onTriggered: {
panel.isFeeLoading = false
panel.feeText = "0,0002 ETH (123,15 USD)"
id: timer
function delay(delayTime, cb) {
timer.interval = delayTime;
timer.repeat = false;
timer.triggered.connect(cb);
timer.start();
}
}
@ -48,6 +47,24 @@ SplitView {
MintTokensSettingsPanel {
id: panel
readonly property var singleTransactionFee: {
"ethCurrency": {
"objectName":"",
"amount":0.000007900500349933282,
"symbol":"ETH",
"displayDecimals":4,
"stripTrailingZeroes":false
},
"fiatCurrency": {
"objectName":"",
"amount":0.012852533720433712,
"symbol":"USD",
"displayDecimals":2,
"stripTrailingZeroes":false
},
"errorCode":0
}
MintedTokensModel {
id: mintedTokensModel
}
@ -105,14 +122,9 @@ SplitView {
onMintCollectible: logs.logEvent("CommunityMintTokensSettingsPanel::mintCollectible")
onMintAsset: logs.logEvent("CommunityMintTokensSettingsPanel::mintAssets")
onDeleteToken: logs.logEvent("CommunityMintTokensSettingsPanel::deleteToken: " + tokenKey)
onDeployFeesRequested: {
feeText = ""
feeErrorText = ""
isFeeLoading = true
feesTimer.restart()
}
onRegisterDeployFeesSubscriber: timer.delay(2000, () => feeSubscriber.feesResponse = panel.singleTransactionFee)
onRegisterSelfDestructFeesSubscriber: timer.delay(2000, () => feeSubscriber.feesResponse = panel.singleTransactionFee)
onRegisterBurnTokenFeesSubscriber: timer.delay(2000, () => feeSubscriber.feesResponse = panel.singleTransactionFee)
}
}

View File

@ -74,12 +74,6 @@ SplitView {
])
close()
}
onRemotelyDestructFeesRequested: {
logs.logEvent("RemoteSelfDestructPopup::onRemotelyDestructFeesRequested",
["walletsAndAmounts", "accountAddress"], [
JSON.stringify(walletsAndAmounts), accountAddress
])
}
Component.onCompleted: {
open()

View File

@ -0,0 +1,13 @@
import QtQuick 2.15
QtObject {
id: root
readonly property string subscriptionId: Utils.uuid()
property bool isReady: false
property int notificationInterval: 3000 // 1 notification every 3 seconds
//The topic to subscribe to
property string topic: ""
property var response: {}
}

View File

@ -0,0 +1,228 @@
import QtQuick 2.15
//This is a helper component that is used to batch requests and send them periodically
//It is used to reduce the number of requests sent to the server and notify different subscribers of the same request
//It is used by the Subscription component
QtObject {
id: root
signal request(string topic)
signal subscribed(string subscriptionId)
signal unsubscribed(string subscriptionId)
function response(topic, responseObj) {
d.onResponse(topic, responseObj)
}
function subscribe(subscription) {
d.subscribe(subscription)
}
function unsubscribe(subscription) {
d.unsubscribe(subscription)
}
property bool active: true
readonly property QtObject d: QtObject {
//Mapping subscriptionId to subscription object
//subscriptionId is a string and represents the id of the subscription
//E.g. "subscriptionId": {subscription: subscriptionObject, topic: "topic"}
//The purpose of this mapping is to keep track of the subscriptions and their topics
readonly property var managedSubscriptions: ({})
//Mapping topic to subscriptionIds and request data
//topic is a string and represents the topic of the subscription
//E.g. "topic": {nextRequestTimestamp: 0, requestInterval: 1000, subscriptions: ["subscriptionId1", "subscriptionId2"], response: null}
readonly property var topics: ({})
property int topicsCount: 0 //helper property to track change events
property bool requestIntervalTriggered: false
readonly property int requestInterval: {
//dependency:
d.topicsCount
d.requestIntervalTriggered
const topicInfos = Object.values(d.topics)
if(!topicsCount || topicInfos.length === 0)
return 0
const now = Date.now()
const interval = topicInfos.reduce((minInterval, topicInfo) => Math.max(0, Math.min(minInterval, topicInfo.nextRequestTimestamp - now)), Number.MAX_SAFE_INTEGER)
return interval > 0 ? interval : requestInterval
}
readonly property Timer requestTimer: Timer {
interval: d.requestInterval
repeat: true
running: interval > 0 && root.active
onTriggered: d.periodicRequest()
triggeredOnStart: true
}
function subscribe(subscription) {
if(!(subscription instanceof Subscription))
return
if(d.managedSubscriptions.hasOwnProperty(subscription.subscriptionId))
return
registerToManagedSubscriptions(subscription)
connectToSubscriptionEvents(subscription)
if(subscription.isReady && subscription.topic)
registerToTopic(subscription.topic, subscription.subscriptionId)
root.subscribed(subscription.subscriptionId)
}
function unsubscribe(subscriptionId) {
if(!subscriptionId || !d.managedSubscriptions.hasOwnProperty(subscriptionId))
return
releaseManagedSubscription(subscriptionId)
root.unsubscribed(subscriptionId)
}
function registerToManagedSubscriptions(subscriptionObject) {
d.managedSubscriptions[subscriptionObject.subscriptionId] = {
subscription: subscriptionObject,
topic: subscriptionObject.topic,
}
}
function releaseManagedSubscription(subscriptionId) {
if(!subscriptionId || !d.managedSubscriptions.hasOwnProperty(subscriptionId)) return
const subscriptionInfo = d.managedSubscriptions[subscriptionId]
unregisterFromTopic(subscriptionInfo.topic, subscriptionId)
delete d.managedSubscriptions[subscriptionId]
}
function connectToSubscriptionEvents(subscription) {
const subscriptionId = subscription.subscriptionId
const topic = subscription.topic
const onTopicChangeHandler = () => {
if(!subscription.isReady || !d.managedSubscriptions.hasOwnProperty(subscriptionId)) return
const newTopic = subscription.topic
const oldTopic = d.managedSubscriptions[subscriptionId].topic
if(newTopic === oldTopic) return
d.unregisterFromTopic(oldTopic, subscriptionId)
d.registerToTopic(newTopic, subscriptionId)
d.managedSubscriptions[subscriptionId].topic = newTopic
}
const onReadyChangeHandler = () => {
if(!d.managedSubscriptions.hasOwnProperty(subscriptionId)) return
if(subscription.isReady) {
d.registerToTopic(subscription.topic, subscription.subscriptionId)
} else {
const subscriptionTopic = d.managedSubscriptions[subscriptionId].topic
d.unregisterFromTopic(subscriptionTopic, subscriptionId)
}
}
const onUnsubscribedHandler = (subscriptionId) => {
if(subscriptionId !== subscription.subscriptionId)
return
subscription.Component.onDestruction.disconnect(onDestructionHandler)
subscription.isReadyChanged.disconnect(onReadyChangeHandler)
subscription.topicChanged.disconnect(onTopicChangeHandler)
}
const onDestructionHandler = () => {
if(!d.managedSubscriptions.hasOwnProperty(subscriptionId))
return
root.unsubscribed.disconnect(onUnsubscribedHandler) //object is destroyed, no need to listen to the signal anymore
unsubscribe(subscriptionId)
}
subscription.Component.onDestruction.connect(onDestructionHandler)
subscription.isReadyChanged.connect(onReadyChangeHandler)
subscription.topicChanged.connect(onTopicChangeHandler)
root.unsubscribed.connect(onUnsubscribedHandler)
}
function registerToTopic(topic, subscriptionId) {
if(!d.topics.hasOwnProperty(topic)) {
d.topics[topic] = {
requestInterval: d.managedSubscriptions[subscriptionId].subscription.notificationInterval,
nextRequestTimestamp: Date.now() + d.managedSubscriptions[subscriptionId].subscription.notificationInterval,
subscriptions: [],
response: null,
requestPending: false
}
d.topicsCount++
}
const index = d.topics[topic].subscriptions.indexOf(subscriptionId)
if(index !== -1) {
console.assert("Duplicate subscription: " + subscriptionId + " " + topic)
return
}
const subscriptionsCount = d.topics[topic].subscriptions.push(subscriptionId)
if(subscriptionsCount === 1 && root.active) {
d.request(topic)
}
d.managedSubscriptions[subscriptionId].subscription.response = d.topics[topic].response
}
function unregisterFromTopic(topic, subscriptionId) {
if(!d.topics.hasOwnProperty(topic)) return
const index = d.topics[topic].subscriptions.indexOf(subscriptionId)
if(index === -1) return
d.topics[topic].subscriptions.splice(index, 1)
if(d.topics[topic].subscriptions.length === 0) {
delete d.topics[topic]
d.topicsCount--
}
}
function periodicRequest() {
if(!d.topics || !d.topicsCount) return
Object.entries(d.topics).forEach(function(entry) {
const topic = entry[0]
const topicInfo = entry[1]
if(!topicInfo ||
!topicInfo.subscriptions ||
!topicInfo.subscriptions.length ||
topicInfo.requestPending ||
topicInfo.nextRequestTimestamp > Date.now())
return
d.request(topic)
})
d.requestIntervalTriggered = !d.requestIntervalTriggered
}
function request(topic) {
if(!d.topics.hasOwnProperty(topic)) return
d.topics[topic].requestPending = true
d.topics[topic].nextRequestTimestamp = Date.now() + d.topics[topic].requestInterval
root.request(topic)
}
function onResponse(topic, responseObj) {
if(!d.topics.hasOwnProperty(topic)) return
d.topics[topic].response = responseObj
d.topics[topic].subscriptions.forEach(function(subscriptionId) {
d.managedSubscriptions[subscriptionId].subscription.response = responseObj
})
d.topics[topic].requestPending = false
}
}
}

View File

@ -8,6 +8,8 @@ ModelChangeTracker 0.1 ModelChangeTracker.qml
ModelsComparator 0.1 ModelsComparator.qml
StackViewStates 0.1 StackViewStates.qml
StatesStack 0.1 StatesStack.qml
Subscription 0.1 Subscription.qml
SubscriptionBroker 0.1 SubscriptionBroker.qml
XSS 1.0 xss.js
singleton AmountsArithmetic 0.1 AmountsArithmetic.qml
singleton Emoji 0.1 Emoji.qml

View File

@ -226,5 +226,7 @@
<file>StatusQ/Controls/StatusBlockProgressBar.qml</file>
<file>StatusQ/Components/StatusInfoBoxPanel.qml</file>
<file>StatusQ/Controls/StatusWarningBox.qml</file>
<file>StatusQ/Core/Utils/Subscription.qml</file>
<file>StatusQ/Core/Utils/SubscriptionBroker.qml</file>
</qresource>
</RCC>

View File

@ -0,0 +1,277 @@
import QtQuick 2.15
import QtTest 1.0
import StatusQ.Core.Utils 0.1
import StatusQ.TestHelpers 0.1
TestCase {
id: testCase
name: "SubscriptionBroker"
Component {
id: subscriptionBrokerComponent
SubscriptionBroker {
id: subscriptionBroker
//Signal spies
readonly property SignalSpy requestSignalSpy: SignalSpy {
target: subscriptionBroker
signalName: "request"
}
readonly property SignalSpy subscribedSignalSpy: SignalSpy {
target: subscriptionBroker
signalName: "subscribed"
}
readonly property SignalSpy unsubscribedSignalSpy: SignalSpy {
target: subscriptionBroker
signalName: "unsubscribed"
}
}
}
Component {
id: subscriptionComponent
Subscription {
id: subscription
}
}
MonitorQtOutput {
id: qtOuput
}
function init() {
qtOuput.restartCapturing()
}
function test_new_instance() {
const subscriptionBroker = createTemporaryObject(subscriptionBrokerComponent, testCase)
verify(qtOuput.qtOuput().length === 0, `No output expected. Found:\n"${qtOuput.qtOuput()}"\n`)
verify(subscriptionBroker.active, "SubscriptionBroker should be active by default")
verify(subscriptionBroker.requestSignalSpy.valid == true, "request signal should be defined")
verify(subscriptionBroker.subscribedSignalSpy.valid == true, "subscribed signal should be defined")
verify(subscriptionBroker.unsubscribedSignalSpy.valid == true, "unsubscribed signal should be defined")
verify(subscriptionBroker.response != undefined, "response function should be defined")
verify(subscriptionBroker.subscribe != undefined, "subscribe function should be defined")
verify(subscriptionBroker.unsubscribe != undefined, "unsubscribe function should be defined")
}
function test_subscribe_invalid_subscription() {
const subscriptionBroker = createTemporaryObject(subscriptionBrokerComponent, testCase)
subscriptionBroker.subscribe(undefined)
compare(qtOuput.qtOuput().length, 0, `No output expected. Found:\n"${qtOuput.qtOuput()}"\n`)
compare(subscriptionBroker.requestSignalSpy.count, 0, "request signal should not be emitted")
compare(subscriptionBroker.subscribedSignalSpy.count, 0, "subscribed signal should not be emitted")
compare(subscriptionBroker.unsubscribedSignalSpy.count, 0, "unsubscribed signal should not be emitted")
const subscriptionAsEmptyObject = {}
subscriptionBroker.subscribe(subscriptionAsEmptyObject)
compare(qtOuput.qtOuput().length, 0, `No output expected. Found:\n"${qtOuput.qtOuput()}"\n`)
compare(subscriptionBroker.requestSignalSpy.count, 0, "request signal should not be emitted")
compare(subscriptionBroker.subscribedSignalSpy.count, 0, "subscribed signal should not be emitted")
compare(subscriptionBroker.unsubscribedSignalSpy.count, 0, "unsubscribed signal should not be emitted")
}
function test_subscribe_valid_subscription_object() {
const subscriptionBroker = createTemporaryObject(subscriptionBrokerComponent, testCase)
const subscription = createTemporaryObject(subscriptionComponent, testCase)
verify(subscription.subscriptionId != "", "subscription should have an id")
subscriptionBroker.subscribe(subscription)
compare(qtOuput.qtOuput().length, 0, `No output expected. Found:\n"${qtOuput.qtOuput()}"\n`)
compare(subscriptionBroker.subscribedSignalSpy.count, 1, "subscribed signal should be emitted")
compare(subscriptionBroker.subscribedSignalSpy.signalArguments[0][0], subscription.subscriptionId, "subscribed signal should be emitted with the subscription id")
compare(subscriptionBroker.unsubscribedSignalSpy.count, 0, "unsubscribed signal should not be emitted")
compare(subscriptionBroker.requestSignalSpy.count, 0, "request signal should not be emitted. Subscription is inactive by default. Broker is inactive by default.")
subscriptionBroker.unsubscribe(subscription.subscriptionId)
compare(subscriptionBroker.subscribedSignalSpy.count, 1, "subscribed signal should not be emitted for unsunbscribe")
compare(subscriptionBroker.unsubscribedSignalSpy.count, 1, "unsubscribed signal should be emitted")
compare(subscriptionBroker.unsubscribedSignalSpy.signalArguments[0][0], subscription.subscriptionId, "unsubscribed signal should be emitted with the subscription id")
compare(subscriptionBroker.requestSignalSpy.count, 0, "request signal should not be emitted")
}
function test_periodic_request_one_subscription() {
const subscriptionBroker = createTemporaryObject(subscriptionBrokerComponent, testCase)
const subscription = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic1", isReady: true, notificationInterval: 50})
//Enable broker and subscription
//Verify that request signal is emitted after subscription
subscriptionBroker.active = true
subscriptionBroker.subscribe(subscription)
compare(subscriptionBroker.subscribedSignalSpy.count, 1, "subscribed signal should be emitted")
compare(subscriptionBroker.requestSignalSpy.count, 1, "request signal should be emitted")
compare(subscriptionBroker.requestSignalSpy.signalArguments[0][0], subscription.topic, "request signal should be emitted with the subscription topic")
//Verify that request signal is emitted after notificationInterval
//The broker expects a response before sending another request
subscriptionBroker.response(subscription.topic, "responseAsString")
compare(subscription.response, "responseAsString", "subscription response should be updated")
//first interval - check for one request every 50ms:
compare(subscriptionBroker.requestSignalSpy.count, 1, "request signal should not be emitted")
tryCompare(subscriptionBroker.requestSignalSpy, "count", 2, 90 /*40ms error margin*/, "request signal should be emitted after 50ms. Actual signal count: " + subscriptionBroker.requestSignalSpy.count)
compare(subscriptionBroker.requestSignalSpy.signalArguments[1][0], subscription.topic, "request signal should be emitted with the subscription topic")
subscriptionBroker.response(subscription.topic, "responseAsString2")
compare(subscription.response, "responseAsString2", "subscription response should be updated")
//second interval - check for one request every 50ms:
compare(subscriptionBroker.requestSignalSpy.count, 2, "request was emitted before 50ms interval")
tryCompare(subscriptionBroker.requestSignalSpy, "count", 3, 90 /*40ms error margin*/, "request signal should be emitted after 50ms")
compare(subscriptionBroker.requestSignalSpy.signalArguments[2][0], subscription.topic, "request signal should be emitted with the subscription topic")
subscriptionBroker.response(subscription.topic, "responseAsString3")
//Verify the request is not sent again after unsubscribe
subscriptionBroker.unsubscribe(subscription.subscriptionId)
compare(subscriptionBroker.subscribedSignalSpy.count, 1, "subscribed signal should not be emitted for unsunbscribe")
compare(subscriptionBroker.unsubscribedSignalSpy.count, 1, "unsubscribed signal should be emitted")
compare(subscriptionBroker.unsubscribedSignalSpy.signalArguments[0][0], subscription.subscriptionId, "unsubscribed signal should be emitted with the subscription id")
compare(subscriptionBroker.requestSignalSpy.count, 3, "request signal should not be emitted on unsubscribe")
wait(90)/*40ms error margin*/
compare(subscriptionBroker.requestSignalSpy.count, 3, "request signal should not be emitted again after unsubscribe")
//Verify the request is not sent again after disabling the broker
subscriptionBroker.subscribe(subscription)
compare(subscriptionBroker.subscribedSignalSpy.count, 2, "subscribed signal should be emitted")
compare(subscriptionBroker.requestSignalSpy.count, 4, "request signal should be emitted on subscribe")
subscriptionBroker.response(subscription.topic, "responseAsString4")
tryCompare(subscriptionBroker.requestSignalSpy, "count", 5, 90 /*40ms error margin*/, "request signal should be emitted")
subscriptionBroker.response(subscription.topic, "responseAsString5")
subscriptionBroker.active = false
compare(subscriptionBroker.requestSignalSpy.count, 5, "request signal should not be emitted after disabling the broker")
wait(90)/*40ms error margin*/
compare(subscriptionBroker.requestSignalSpy.count, 5, "request signal should not be emitted again after disabling the broker")
//Verify the request can be unsubsribed with a disabled broker
subscriptionBroker.unsubscribe(subscription.subscriptionId)
compare(subscriptionBroker.subscribedSignalSpy.count, 2, "subscribed signal should not be emitted for unsunbscribe")
compare(subscriptionBroker.unsubscribedSignalSpy.count, 2, "unsubscribed signal should be emitted")
compare(subscriptionBroker.unsubscribedSignalSpy.signalArguments[1][0], subscription.subscriptionId, "unsubscribed signal should be emitted with the subscription id")
compare(subscriptionBroker.requestSignalSpy.count, 5, "request signal should not be emitted on unsubscribe")
//Verify the request can be subscribed with a disabled broker
subscriptionBroker.subscribe(subscription)
compare(subscriptionBroker.subscribedSignalSpy.count, 3, "subscribed signal should be emitted")
compare(subscriptionBroker.requestSignalSpy.count, 5, "request signal should not be emitted on subscribe")
wait(90)/*40ms error margin*/
compare(subscriptionBroker.requestSignalSpy.count, 5, "request signal should not be emitted with a disabled broker")
//verify the request is sent after enabling the broker
subscriptionBroker.active = true
tryCompare(subscriptionBroker.requestSignalSpy, "count", 6, 1/*allow the event loop to be processed and the subscriptionBroker.active = true to be processed */, "request signal should be emitted after enabling the broker")
}
function test_periodic_request_multiple_subscriptions() {
const subscriptionBroker = createTemporaryObject(subscriptionBrokerComponent, testCase)
const subscription1 = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic1", isReady: true, notificationInterval: 50}) //10 requests in 500ms
const subscription2 = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic2", isReady: true, notificationInterval: 90}) //5 requests in 500ms
const subscription3 = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic3", isReady: true, notificationInterval: 130}) //3 requests in 500ms
const subscription4 = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic4", isReady: true, notificationInterval: 170}) //2 requests in 500ms
const subscription5 = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic5", isReady: true, notificationInterval: 210}) //2 requests in 500ms
//TOTAL: 22 requests in 500ms
var subscription1RequestTimestamp = 0
var subscription2RequestTimestamp = 0
var subscription3RequestTimestamp = 0
var subscription4RequestTimestamp = 0
var subscription5RequestTimestamp = 0
var requestCount = 0
subscriptionBroker.request.connect(function(topic) {
//sending a unique response for each request
subscriptionBroker.response(topic, "responseAsString" + Date.now())
})
//Enable broker and subscription
subscriptionBroker.active = true
subscriptionBroker.subscribe(subscription1)
subscriptionBroker.subscribe(subscription2)
subscriptionBroker.subscribe(subscription3)
subscriptionBroker.subscribe(subscription4)
subscriptionBroker.subscribe(subscription5)
//Make sure the interval difference between the subscriptions is at least 30ms
//This is to make sure interval computation deffects are not hidden by the error margin
//The usual error margin on Timer is < 10 ms. Setting it to 30 ms should be enough
const requestIntervalErrorMargin = 30
subscription1.responseChanged.connect(function() {
if(subscription1RequestTimestamp !== 0)
fuzzyCompare(Date.now() - subscription1RequestTimestamp, subscription1.notificationInterval, requestIntervalErrorMargin, "subscription1 request should be sent after notificationInterval")
subscription1RequestTimestamp = Date.now()
})
subscription2.responseChanged.connect(function() {
if(subscription2RequestTimestamp !== 0)
fuzzyCompare(Date.now() - subscription2RequestTimestamp, subscription2.notificationInterval, requestIntervalErrorMargin, "subscription2 request should be sent after notificationInterval")
subscription2RequestTimestamp = Date.now()
})
subscription3.responseChanged.connect(function() {
if(subscription3RequestTimestamp !== 0)
fuzzyCompare(Date.now() - subscription3RequestTimestamp, subscription3.notificationInterval, requestIntervalErrorMargin, "subscription3 request should be sent after notificationInterval")
subscription3RequestTimestamp = Date.now()
})
subscription4.responseChanged.connect(function() {
if(subscription4RequestTimestamp !== 0)
fuzzyCompare(Date.now() - subscription4RequestTimestamp, subscription4.notificationInterval, requestIntervalErrorMargin, "subscription4 request should be sent after notificationInterval")
subscription4RequestTimestamp = Date.now()
})
subscription5.responseChanged.connect(function() {
if(subscription5RequestTimestamp !== 0)
fuzzyCompare(Date.now() - subscription5RequestTimestamp, subscription5.notificationInterval, requestIntervalErrorMargin, "subscription5 request should be sent after notificationInterval")
subscription5RequestTimestamp = Date.now()
})
///Verify the request is sent periodically for 500 ms
///The test is fuzzy because the timer is not precise
///After each wait() the test error margin increases
//We should have 27 requests in 500ms. Adding an error margin of 100ms => 600ms total
tryVerify(() => subscriptionBroker.requestSignalSpy.count > 26, 600, "request signal should be emitted more than 27 times. Actual: " + subscriptionBroker.requestSignalSpy.count)
//Disable one subscription and verify the request count is reduced
subscription5.isReady = false
subscription4.isReady = false
let previousRequestCount = subscriptionBroker.requestSignalSpy.count
//We should have 18 requests in 500ms. Adding an error margin of 100ms => 600ms total
tryVerify(() => subscriptionBroker.requestSignalSpy.count > previousRequestCount + 17/*fuzzy compare. Exact number should be 18*/, 600, "request signal should be emitted more than 14 times. Actual: " + subscriptionBroker.requestSignalSpy.count)
previousRequestCount = subscriptionBroker.requestSignalSpy.count
//Leave just one subscription and verify the request count is reduced
subscription3.isReady = false
subscription2.isReady = false
//We should have 10 requests in 500ms. Adding an error margin of 100ms => 600ms total
tryVerify(() => subscriptionBroker.requestSignalSpy.count > previousRequestCount + 9 /*fuzzy compare. Exact number should be 10*/, 600, "request signal should be emitted more than 8 times. Actual: " + subscriptionBroker.requestSignalSpy.count)
}
//Testing how the subscription broker handles the topic changes
function test_topic_changes() {
const subscriptionBroker = createTemporaryObject(subscriptionBrokerComponent, testCase)
const subscription1 = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic1", isReady: true, notificationInterval: 50}) //10 requests in 500ms
const subscription2 = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic2", isReady: true, notificationInterval: 90}) //5 requests in 500ms
const subscription3 = createTemporaryObject(subscriptionComponent, testCase, {topic: "topic3", isReady: true, notificationInterval: 130}) //3 requests in 500ms
subscriptionBroker.active = true
subscriptionBroker.subscribe(subscription1)
subscriptionBroker.subscribe(subscription2)
subscriptionBroker.subscribe(subscription3)
compare(subscriptionBroker.subscribedSignalSpy.count, 3, "subscribed signal should be emitted")
subscription1.topic = "topic1Changed"
compare(subscriptionBroker.requestSignalSpy.count, 4, "request signal should be emitted after changing the topic")
}
}

View File

@ -0,0 +1,15 @@
import QtQuick 2.15
/*!
\qmltype BurnTokenFeesSubscriber
\inherits QtObject
\brief Helper object that holds the subscriber properties and the published properties for the fee computation.
*/
SingleFeeSubscriber {
id: root
required property string tokenKey
required property string amount
required property string accountAddress
required property bool enabled
}

View File

@ -0,0 +1,16 @@
import QtQuick 2.15
/*!
\qmltype DeployFeesSubscriber
\inherits QtObject
\brief Helper object that holds the subscriber properties and the published properties for the fee computation.
*/
SingleFeeSubscriber {
id: root
required property int chainId
required property int tokenType
required property bool isOwnerDeployment
required property string accountAddress
required property bool enabled
}

View File

@ -0,0 +1,24 @@
import QtQuick 2.15
/*!
\qmltype SelfDestructFeesSubscriber
\inherits QtObject
\brief Helper object that holds the subscriber properties and the published properties for the fee computation.
*/
SingleFeeSubscriber {
id: root
required property string tokenKey
/**
* walletsAndAmounts - array of following structure is expected:
* [
* {
* walletAddress: string
* amount: int
* }
* ]
*/
required property var walletsAndAmounts
required property string accountAddress
required property bool enabled
}

View File

@ -0,0 +1,39 @@
import QtQuick 2.15
import StatusQ.Core 0.1
import utils 1.0
/*!
\qmltype SingleFeeSubscriber
\inherits QtObject
\brief Helper object that parses fees response and provides fee text and error text for single fee respnse
*/
QtObject {
id: root
// Published properties
property var feesResponse
// Internal properties based on response
readonly property string feeText: {
if (!feesResponse || !Object.values(feesResponse.ethCurrency).length || !Object.values(feesResponse.fiatCurrency).length) return ""
if (feesResponse.errorCode !== Constants.ComputeFeeErrorCode.Success && feesResponse.errorCode !== Constants.ComputeFeeErrorCode.Balance)
return ""
return LocaleUtils.currencyAmountToLocaleString(feesResponse.ethCurrency)
+ " (" + LocaleUtils.currencyAmountToLocaleString(feesResponse.fiatCurrency) + ")"
}
readonly property string feeErrorText: {
if (!feesResponse) return ""
if (feesResponse.errorCode === Constants.ComputeFeeErrorCode.Success) return ""
if (feesResponse.errorCode === Constants.ComputeFeeErrorCode.Balance)
return qsTr("Not enough funds to make transaction")
if (feesResponse.errorCode === Constants.ComputeFeeErrorCode.Infura)
return qsTr("Infura error")
return qsTr("Unknown error")
}
}

View File

@ -0,0 +1,190 @@
import QtQuick 2.15
import shared.stores 1.0
import utils 1.0
import StatusQ.Core.Utils 0.1
QtObject {
id: root
enum FeeType {
Airdrop,
Deploy,
SelfDestruct,
Burn
}
property CommunityTokensStore communityTokensStore
property QtObject d: QtObject {
id: internal
component AirdropFeeSubscription: Subscription {
required property AirdropFeesSubscriber subscriber
readonly property var requestArgs: ({
type: TransactionFeesBroker.FeeType.Airdrop,
communityId: subscriber.communityId,
contractKeysAndAmounts: subscriber.contractKeysAndAmounts,
addressesToAirdrop: subscriber.addressesToAirdrop,
feeAccountAddress: subscriber.feeAccountAddress
})
isReady: !!subscriber.communityId &&
!!subscriber.contractKeysAndAmounts &&
!!subscriber.addressesToAirdrop &&
!!subscriber.feeAccountAddress &&
subscriber.contractKeysAndAmounts.length &&
subscriber.addressesToAirdrop.length &&
subscriber.enabled
topic: isReady ? JSON.stringify(requestArgs) : ""
onResponseChanged: subscriber.airdropFeesResponse = response
}
component DeployFeeSubscription: Subscription {
required property DeployFeesSubscriber subscriber
readonly property var requestArgs: ({
type: TransactionFeesBroker.FeeType.Deploy,
chainId: subscriber.chainId,
accountAddress: subscriber.accountAddress,
tokenType: subscriber.tokenType,
isOwnerDeployment: subscriber.isOwnerDeployment
})
isReady: !!subscriber.chainId &&
!!subscriber.accountAddress &&
!!subscriber.tokenType &&
subscriber.enabled
topic: isReady ? JSON.stringify(requestArgs) : ""
onResponseChanged: subscriber.feesResponse = response
}
component SelfDestructFeeSubscription: Subscription {
required property SelfDestructFeesSubscriber subscriber
readonly property var requestArgs: ({
type: TransactionFeesBroker.FeeType.SelfDestruct,
walletsAndAmounts:subscriber.walletsAndAmounts,
tokenKey: subscriber.tokenKey,
accountAddress: subscriber.accountAddress,
})
isReady: !!subscriber.walletsAndAmounts &&
!!subscriber.tokenKey &&
!!subscriber.accountAddress &&
subscriber.walletsAndAmounts.length &&
subscriber.enabled
topic: isReady ? JSON.stringify(requestArgs) : ""
onResponseChanged: subscriber.feesResponse = response
}
component BurnTokenFeeSubscription: Subscription {
required property BurnTokenFeesSubscriber subscriber
readonly property var requestArgs: ({
type: TransactionFeesBroker.FeeType.Burn,
tokenKey: subscriber.tokenKey,
amount: subscriber.amount,
accountAddress: subscriber.accountAddress
})
isReady: !!subscriber.tokenKey &&
!!subscriber.amount &&
!!subscriber.accountAddress &&
subscriber.enabled
topic: isReady ? JSON.stringify(requestArgs) : ""
onResponseChanged: subscriber.feesResponse = response
}
readonly property Component airdropFeeSubscriptionComponent: AirdropFeeSubscription {}
readonly property Component deployFeeSubscriptionComponent: DeployFeeSubscription {}
readonly property Component selfDestructFeeSubscriptionComponent: SelfDestructFeeSubscription {}
readonly property Component burnFeeSubscriptionComponent: BurnTokenFeeSubscription {}
readonly property SubscriptionBroker feesBroker: SubscriptionBroker {
active: Global.applicationWindow.active
onRequest: internal.computeFee(topic)
}
property Connections communityTokensStoreConnections: Connections {
target: communityTokensStore
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId) {
d.feesBroker.response(responseId, { ethCurrency: ethCurrency, fiatCurrency: fiatCurrency, errorCode: errorCode })
}
function onAirdropFeeUpdated(response) {
d.feesBroker.response(response.requestId, response)
}
function onSelfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId) {
d.feesBroker.response(responseId, { ethCurrency: ethCurrency, fiatCurrency: fiatCurrency, errorCode: errorCode })
}
function onBurnFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId) {
d.feesBroker.response(responseId, { ethCurrency: ethCurrency, fiatCurrency: fiatCurrency, errorCode: errorCode })
}
}
function computeFee(topic) {
const args = JSON.parse(topic)
switch (args.type) {
case TransactionFeesBroker.FeeType.Airdrop:
computeAirdropFee(args, topic)
break
case TransactionFeesBroker.FeeType.Deploy:
computeDeployFee(args, topic)
break
case TransactionFeesBroker.FeeType.SelfDestruct:
computeSelfDestructFee(args, topic)
break
case TransactionFeesBroker.FeeType.Burn:
computeBurnFee(args, topic)
break
default:
console.error("Unknown fee type: " + args.type)
}
}
function computeAirdropFee(args, topic) {
communityTokensStore.computeAirdropFee(
args.communityId,
args.contractKeysAndAmounts,
args.addressesToAirdrop,
args.feeAccountAddress,
topic)
}
function computeDeployFee(args, topic) {
communityTokensStore.computeDeployFee(args.chainId, args.accountAddress, args.tokenType, args.isOwnerDeployment, topic)
}
function computeSelfDestructFee(args, topic) {
communityTokensStore.computeSelfDestructFee(args.walletsAndAmounts, args.tokenKey, args.accountAddress, topic)
}
function computeBurnFee(args, topic) {
console.assert(typeof args.amount === "string")
communityTokensStore.computeBurnFee(args.tokenKey, args.amount, args.accountAddress, topic)
}
}
function registerAirdropFeesSubscriber(subscriberObj) {
const subscription = d.airdropFeeSubscriptionComponent.createObject(subscriberObj, { subscriber: subscriberObj })
d.feesBroker.subscribe(subscription)
}
function registerDeployFeesSubscriber(subscriberObj) {
const subscription = d.deployFeeSubscriptionComponent.createObject(subscriberObj, { subscriber: subscriberObj })
d.feesBroker.subscribe(subscription)
}
function registerSelfDestructFeesSubscriber(subscriberObj) {
const subscription = d.selfDestructFeeSubscriptionComponent.createObject(subscriberObj, { subscriber: subscriberObj })
d.feesBroker.subscribe(subscription)
}
function registerBurnFeesSubscriber(subscriberObj) {
const subscription = d.burnFeeSubscriptionComponent.createObject(subscriberObj, { subscriber: subscriberObj })
d.feesBroker.subscribe(subscription)
}
}

View File

@ -1,2 +1,8 @@
singleton PermissionsHelpers 1.0 PermissionsHelpers.qml
AirdropFeesSubscriber 1.0 AirdropFeesSubscriber.qml
BurnTokenFeesSubscriber 1.0 BurnTokenFeesSubscriber.qml
DeployFeesSubscriber 1.0 DeployFeesSubscriber.qml
SelfDestructFeesSubscriber 1.0 SelfDestructFeesSubscriber.qml
SingleFeeSubscriber 1.0 SingleFeeSubscriber.qml
TokenObject 1.0 TokenObject.qml
TransactionFeesBroker 1.0 TransactionFeesBroker.qml

View File

@ -20,7 +20,6 @@ StackView {
required property bool isTokenMasterOwner
required property bool isAdmin
readonly property bool isPrivilegedTokenOwnerProfile: root.isOwner || root.isTokenMasterOwner
readonly property alias airdropFeesSubscriber: d.aidropFeeSubscriber
// Owner and TMaster token related properties:
readonly property bool arePrivilegedTokensDeployed: root.isOwnerTokenDeployed && root.isTMasterTokenDeployed
@ -38,8 +37,8 @@ StackView {
property string previousPageName: depth > 1 ? qsTr("Airdrops") : ""
signal airdropClicked(var airdropTokens, var addresses, string feeAccountAddress)
signal airdropFeesRequested(var contractKeysAndAmounts, var addresses, string feeAccountAddress)
signal navigateToMintTokenSettings(bool isAssetType)
signal registerAirdropFeeSubscriber(var feeSubscriber)
function navigateBack() {
pop(StackView.Immediate)
@ -132,7 +131,6 @@ StackView {
Component.onCompleted: {
d.selectToken.connect(view.selectToken)
d.addAddresses.connect(view.addAddresses)
d.aidropFeeSubscriber = feesSubscriber
}
AirdropFeesSubscriber {
@ -142,6 +140,7 @@ StackView {
contractKeysAndAmounts: view.selectedContractKeysAndAmounts
addressesToAirdrop: view.selectedAddressesToAirdrop
feeAccountAddress: view.selectedFeeAccount
Component.onCompleted: root.registerAirdropFeeSubscriber(feesSubscriber)
}
}
}

View File

@ -50,11 +50,6 @@ StackView {
property var tokensModelWallet
property var accounts // Expected roles: address, name, color, emoji, walletType
// Transaction related properties:
property string feeText
property string feeErrorText
property bool isFeeLoading: true
// Network related properties:
property var layer1Networks
property var layer2Networks
@ -67,25 +62,15 @@ StackView {
signal kickUserRequested(string contactId)
signal banUserRequested(string contactId)
signal deployFeesRequested(int chainId, string accountAddress, int tokenType)
signal burnFeesRequested(string tokenKey, string amount, string accountAddress)
signal remotelyDestructFeesRequest(var walletsAndAmounts, // { [walletAddress (string), amount (int)] }
string tokenKey,
string accountAddress)
signal remotelyDestructCollectibles(var walletsAndAmounts, // { [walletAddress (string), amount (int)] }
string tokenKey,
string accountAddress)
signal signBurnTransactionOpened(string tokenKey, string amount, string accountAddress)
signal burnToken(string tokenKey, string amount, string accountAddress)
signal airdropToken(string tokenKey, string amount, int type, var addresses)
signal deleteToken(string tokenKey)
function setFeeLoading() {
root.isFeeLoading = true
root.feeText = ""
root.feeErrorText = ""
}
signal registerDeployFeesSubscriber(var feeSubscriber)
signal registerSelfDestructFeesSubscriber(var feeSubscriber)
signal registerBurnTokenFeesSubscriber(var feeSubscriber)
function navigateBack() {
pop(StackView.Immediate)
@ -138,7 +123,6 @@ StackView {
root.push(ownerTokenEditViewComponent, properties,
StackView.Immediate)
}
}
initialItem: SettingsPage {
@ -243,31 +227,36 @@ StackView {
allNetworks: root.allNetworks
accounts: root.accounts
feeText: feeSubscriber.feeText
feeErrorText: feeSubscriber.feeErrorText
isFeeLoading: !feeSubscriber.feesResponse
onMintClicked: signMintPopup.open()
onDeployFeesRequested: root.deployFeesRequested(
ownerToken.chainId,
ownerToken.accountAddress,
Constants.TokenType.ERC721)
feeText: root.feeText
feeErrorText: root.feeErrorText
isFeeLoading: root.isFeeLoading
DeployFeesSubscriber {
id: feeSubscriber
chainId: editOwnerTokenView.ownerToken.chainId
tokenType: editOwnerTokenView.ownerToken.type
isOwnerDeployment: editOwnerTokenView.ownerToken.isPrivilegedToken
accountAddress: editOwnerTokenView.ownerToken.accountAddress
enabled: editOwnerTokenView.visible || signMintPopup.visible
Component.onCompleted: root.registerDeployFeesSubscriber(feeSubscriber)
}
SignMultiTokenTransactionsPopup {
id: signMintPopup
title: qsTr("Sign transaction - Mint %1 tokens").arg(
editOwnerTokenView.communityName)
totalFeeText: root.isFeeLoading ?
"" : root.feeText
errorText: root.feeErrorText
totalFeeText: editOwnerTokenView.isFeeLoading ?
"" : editOwnerTokenView.feeText
errorText: editOwnerTokenView.feeErrorText
accountName: editOwnerTokenView.ownerToken.accountName
model: QtObject {
readonly property string title: editOwnerTokenView.feeLabel
readonly property string feeText: signMintPopup.totalFeeText
readonly property bool error: root.feeErrorText !== ""
readonly property bool error: editOwnerTokenView.feeErrorText !== ""
}
onSignTransactionClicked: editOwnerTokenView.signMintTransaction()
@ -335,7 +324,7 @@ StackView {
validationMode: !newTokenPage.isAssetView
? newTokenPage.validationMode
: StatusInput.ValidationMode.OnlyWhenDirty
collectible: newTokenPage.collectible
token: newTokenPage.collectible
}
CustomEditCommunityTokenView {
@ -345,10 +334,12 @@ StackView {
validationMode: newTokenPage.isAssetView
? newTokenPage.validationMode
: StatusInput.ValidationMode.OnlyWhenDirty
asset: newTokenPage.asset
token: newTokenPage.asset
}
component CustomEditCommunityTokenView: EditCommunityTokenView {
id: editView
viewWidth: root.viewWidth
layer1Networks: root.layer1Networks
layer2Networks: root.layer2Networks
@ -361,28 +352,27 @@ StackView {
referenceName: newTokenPage.referenceName
referenceSymbol: newTokenPage.referenceSymbol
feeText: root.feeText
feeErrorText: root.feeErrorText
isFeeLoading: root.isFeeLoading
feeText: deployFeeSubscriber.feeText
feeErrorText: deployFeeSubscriber.feeErrorText
isFeeLoading: !deployFeeSubscriber.feesResponse
onPreviewClicked: {
const properties = {
token: isAssetView ? asset : collectible
token: token
}
root.push(previewTokenViewComponent, properties,
StackView.Immediate)
}
onDeployFeesRequested: {
if (isAssetView)
root.deployFeesRequested(asset.chainId,
asset.accountAddress,
Constants.TokenType.ERC20)
else
root.deployFeesRequested(collectible.chainId,
collectible.accountAddress,
Constants.TokenType.ERC721)
DeployFeesSubscriber {
id: deployFeeSubscriber
chainId: editView.token.chainId
tokenType: editView.token.type
isOwnerDeployment: editView.token.isPrivilegedToken
accountAddress: editView.token.accountAddress
enabled: editView.visible
Component.onCompleted: root.registerDeployFeesSubscriber(deployFeeSubscriber)
}
}
}
@ -407,20 +397,14 @@ StackView {
viewWidth: root.viewWidth
preview: true
feeText: root.feeText
feeErrorText: root.feeErrorText
isFeeLoading: root.isFeeLoading
accounts: root.accounts
onDeployFeesRequested: root.deployFeesRequested(
token.chainId, token.accountAddress,
token.type)
feeText: feeSubscriber.feeText
feeErrorText: feeSubscriber.feeErrorText
isFeeLoading: !feeSubscriber.feesResponse
onMintClicked: signMintPopup.open()
function signMintTransaction() {
root.setFeeLoading()
if(preview.isAssetView)
root.mintAsset(token)
else
@ -429,18 +413,28 @@ StackView {
root.resetNavigation()
}
DeployFeesSubscriber {
id: feeSubscriber
chainId: preview.token.chainId
tokenType: preview.token.type
isOwnerDeployment: preview.token.isPrivilegedToken
accountAddress: preview.token.accountAddress
enabled: preview.visible || signMintPopup.visible
Component.onCompleted: root.registerDeployFeesSubscriber(feeSubscriber)
}
SignMultiTokenTransactionsPopup {
id: signMintPopup
title: qsTr("Sign transaction - Mint %1 token").arg(
preview.token.name)
totalFeeText: root.isFeeLoading ? "" : root.feeText
totalFeeText: preview.isFeeLoading ? "" : preview.feeText
accountName: preview.token.accountName
model: QtObject {
readonly property string title: preview.feeLabel
readonly property string feeText: signMintPopup.totalFeeText
readonly property bool error: root.feeErrorText !== ""
readonly property bool error: preview.feeErrorText !== ""
}
onSignTransactionClicked: preview.signMintTransaction()
@ -452,12 +446,11 @@ StackView {
component TokenViewPage: SettingsPage {
id: tokenViewPage
readonly property alias token: view.token
property TokenObject token: TokenObject {}
readonly property bool deploymentFailed: view.deployState === Constants.ContractTransactionStatus.Failed
property alias tokenOwnersModel: view.tokenOwnersModel
property alias airdropKey: view.airdropKey
property var tokenOwnersModel
property string airdropKey
// Owner and TMaster related props
readonly property bool isPrivilegedTokenItem: isOwnerTokenItem || isTMasterTokenItem
readonly property bool isOwnerTokenItem: token.privilegesLevel === Constants.TokenPrivilegesLevel.Owner
@ -517,11 +510,12 @@ StackView {
contentItem: CommunityTokenView {
id: view
property string airdropKey // TO REMOVE: Temporal property until airdrop backend is not ready to use token key instead of symbol
property string airdropKey: tokenViewPage.airdropKey // TO REMOVE: Temporal property until airdrop backend is not ready to use token key instead of symbol
viewWidth: root.viewWidth
token: TokenObject {}
token: tokenViewPage.token
tokenOwnersModel: tokenViewPage.tokenOwnersModel
onGeneralAirdropRequested: {
root.airdropToken(view.airdropKey,
@ -538,7 +532,7 @@ StackView {
onRemoteDestructRequested: {
if (token.isPrivilegedToken) {
tokenMasterActionPopup.openPopup(
TokenMasterActionPopup.ActionType.RemotelyDestruct, name)
TokenMasterActionPopup.ActionType.RemotelyDestruct, name, address)
} else {
remotelyDestructPopup.open()
// TODO: set the address selected in the popup's list
@ -572,16 +566,34 @@ StackView {
TokenMasterActionPopup {
id: tokenMasterActionPopup
property string address: ""
communityName: root.communityName
networkName: view.token.chainName
accountsModel: root.accounts
feeText: selfDestructFeesSubscriber.feeText
feeErrorText: selfDestructFeesSubscriber.feeErrorText
isFeeLoading: !selfDestructFeesSubscriber.feesResponse
function openPopup(type, userName) {
function openPopup(type, userName, address) {
tokenMasterActionPopup.actionType = type
tokenMasterActionPopup.userName = userName
tokenMasterActionPopup.address = address
open()
}
SelfDestructFeesSubscriber {
id: selfDestructFeesSubscriber
walletsAndAmounts: [{
walletAddress: tokenMasterActionPopup.address,
amount: 1
}]
accountAddress: tokenMasterActionPopup.selectedAccount
tokenKey: view.token.key
enabled: tokenMasterActionPopup.opened
Component.onCompleted: root.registerSelfDestructFeesSubscriber(selfDestructFeesSubscriber)
}
}
KickBanPopup {
@ -661,20 +673,16 @@ StackView {
RemotelyDestructPopup {
id: remotelyDestructPopup
property alias feeSubscriber: remotelyDestructFeeSubscriber
collectibleName: view.token.name
model: view.tokenOwnersModel || null
accounts: root.accounts
chainName: view.token.chainName
feeText: root.feeText
isFeeLoading: root.isFeeLoading
feeErrorText: root.feeErrorText
onRemotelyDestructFeesRequested: {
root.remotelyDestructFeesRequest(walletsAndAmounts,
view.token.key,
accountAddress)
}
feeText: remotelyDestructFeeSubscriber.feeText
feeErrorText: remotelyDestructFeeSubscriber.feeErrorText
isFeeLoading: !remotelyDestructFeeSubscriber.feesResponse
onRemotelyDestructClicked: {
remotelyDestructPopup.close()
@ -682,6 +690,15 @@ StackView {
footer.walletsAndAmounts = walletsAndAmounts
alertPopup.open()
}
SelfDestructFeesSubscriber {
id: remotelyDestructFeeSubscriber
walletsAndAmounts: remotelyDestructPopup.selectedWalletsAndAmounts
accountAddress: remotelyDestructPopup.selectedAccount
tokenKey: view.token.key
enabled: remotelyDestructPopup.tokenCount > 0 && accountAddress !== "" && (remotelyDestructPopup.opened || signTransactionPopup.opened)
Component.onCompleted: root.registerSelfDestructFeesSubscriber(remotelyDestructFeeSubscriber)
}
}
AlertPopup {
@ -702,11 +719,13 @@ StackView {
id: signTransactionPopup
property bool isRemotelyDestructTransaction
property var feeSubscriber: isRemotelyDestructTransaction
? remotelyDestructPopup.feeSubscriber
: burnTokensPopup.feeSubscriber
readonly property string tokenKey: tokenViewPage.token.key
function signTransaction() {
root.setFeeLoading()
if(signTransactionPopup.isRemotelyDestructTransaction)
root.remotelyDestructCollectibles(footer.walletsAndAmounts,
tokenKey, footer.accountAddress)
@ -723,23 +742,17 @@ StackView {
tokenName: footer.token.name
accountName: footer.token.accountName
networkName: footer.token.chainName
feeText: root.feeText
isFeeLoading: root.isFeeLoading
errorText: root.feeErrorText
feeText: feeSubscriber.feeText
isFeeLoading: feeSubscriber.feeText === "" && feeSubscriber.feeErrorText === ""
errorText: feeSubscriber.feeErrorText
onOpened: {
root.setFeeLoading()
signTransactionPopup.isRemotelyDestructTransaction
? root.remotelyDestructFeesRequest(footer.walletsAndAmounts, tokenKey, footer.accountAddress)
: root.signBurnTransactionOpened(tokenKey, footer.burnAmount, footer.accountAddress)
}
onSignTransactionClicked: signTransaction()
}
BurnTokensPopup {
id: burnTokensPopup
property alias feeSubscriber: burnTokensFeeSubscriber
communityName: root.communityName
tokenName: footer.token.name
remainingTokens: footer.token.remainingTokens
@ -747,12 +760,12 @@ StackView {
tokenSource: footer.token.artworkSource
chainName: footer.token.chainName
feeText: root.feeText
feeErrorText: root.feeErrorText
isFeeLoading: root.isFeeLoading
accounts: root.accounts
onAmountToBurnChanged: burnTokensFeeSubscriber.updateAmount()
onBurnFeesRequested: root.burnFeesRequested(footer.token.key, burnAmount, accountAddress)
feeText: burnTokensFeeSubscriber.feeText
feeErrorText: burnTokensFeeSubscriber.feeErrorText
isFeeLoading: burnTokensFeeSubscriber.feeText === "" && burnTokensFeeSubscriber.feeErrorText === ""
accounts: root.accounts
onBurnClicked: {
burnTokensPopup.close()
@ -761,13 +774,25 @@ StackView {
signTransactionPopup.isRemotelyDestructTransaction = false
signTransactionPopup.open()
}
BurnTokenFeesSubscriber {
id: burnTokensFeeSubscriber
readonly property var updateAmount: Backpressure.debounce(burnTokensFeeSubscriber, 500, () => {
burnTokensFeeSubscriber.amount = burnTokensPopup.amountToBurn
})
amount: ""
tokenKey: tokenViewPage.token.key
accountAddress: burnTokensPopup.selectedAccountAddress
enabled: burnTokensPopup.visible || signTransactionPopup.visible
Component.onCompleted: root.registerBurnTokenFeesSubscriber(burnTokensFeeSubscriber)
}
}
}
AlertPopup {
id: deleteTokenAlertPopup
readonly property alias tokenName: view.token.name
readonly property string tokenName: view.token.name
width: 521
title: qsTr("Delete %1").arg(tokenName)
@ -799,6 +824,7 @@ StackView {
}
delegate: TokenViewPage {
required property var model
implicitWidth: 0
anchors.fill: parent

View File

@ -27,6 +27,9 @@ StatusDialog {
property url tokenSource
property string chainName
readonly property alias amountToBurn: d.amountToBurn
readonly property alias selectedAccountAddress: d.accountAddress
// Fees related properties:
property string feeText
property string feeErrorText: ""
@ -38,7 +41,6 @@ StatusDialog {
signal burnClicked(string burnAmount, string accountAddress)
signal cancelClicked
signal burnFeesRequested(string burnAmount, string accountAddress)
QtObject {
id: d
@ -51,6 +53,8 @@ StatusDialog {
LocaleUtils.numberToLocaleString(remainingTokensFloat)
property string accountAddress
property string amountToBurn: !isFormValid ? "" :
specificAmountButton.checked ? amountInput.amount : root.remainingTokens
readonly property bool isFeeError: root.feeErrorText !== ""
@ -119,7 +123,7 @@ StatusDialog {
font.pixelSize: Style.current.primaryTextFontSize
ButtonGroup.group: radioGroup
onToggled: if(checked) amountToBurnInput.forceActiveFocus()
onToggled: if(checked) amountInput.forceActiveFocus()
}
AmountInput {
@ -164,18 +168,6 @@ StatusDialog {
FeesBox {
id: feesBox
readonly property bool triggerFeeReevaluation: {
specificAmountButton.checked
amountInput.amount
feesBox.accountsSelector.currentIndex
if (root.opened)
requestFeeDelayTimer.restart()
return true
}
Layout.fillWidth: true
placeholderText: qsTr("Choose number of tokens to burn to see gas fees")
@ -195,24 +187,6 @@ StatusDialog {
d.accountAddress = item.address
}
Timer {
id: requestFeeDelayTimer
interval: 500
onTriggered: {
if (specificAmountButton.checked) {
if (!amountInput.valid)
return
root.burnFeesRequested(amountInput.amount,
d.accountAddress)
} else {
root.burnFeesRequested(root.remainingTokens,
d.accountAddress)
}
}
}
QtObject {
id: singleFeeModel

View File

@ -27,11 +27,17 @@ StatusDialog {
property string feeLabel: qsTr("Remotely destruct %1 token on %2").arg(root.collectibleName).arg(root.chainName)
readonly property alias tokenCount: d.tokenCount
readonly property string selectedAccount: d.accountAddress
readonly property var selectedWalletsAndAmounts: {
//depedency
d.tokenCount
return ModelUtils.modelToArray(d.walletsAndAmountsList)
}
// Account expected roles: address, name, color, emoji, walletType
property var accounts
signal remotelyDestructClicked(var walletsAndAmounts, string accountAddress)
signal remotelyDestructFeesRequested(var walletsAndAmounts, string accountAddress)
QtObject {
id: d
@ -74,17 +80,6 @@ StatusDialog {
d.walletsAndAmountsList, "amount")
const sum = amounts.reduce((a, b) => a + b, 0)
d.tokenCount = sum
if (sum > 0)
sendFeeRequest()
}
function sendFeeRequest() {
const walletsAndAmounts = ModelUtils.modelToArray(
d.walletsAndAmountsList)
root.remotelyDestructFeesRequested(walletsAndAmounts,
d.accountAddress)
}
}
@ -126,10 +121,6 @@ StatusDialog {
const item = ModelUtils.get(accountsSelector.model,
accountsSelector.currentIndex)
d.accountAddress = item.address
// Whenever a change in the form happens, new fee calculation:
if (d.tokenCount > 0)
d.sendFeeRequest()
}
QtObject {

View File

@ -7,6 +7,7 @@ import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import AppLayouts.Communities.panels 1.0
@ -40,7 +41,10 @@ StatusDialog {
property string feeText
property string feeErrorText
property bool isFeeLoading
property var accountsModel
readonly property alias selectedAccount: d.accountAddress
readonly property string feeLabel: qsTr("Remotely destruct 1 TokenMaster token on %1").arg(
root.networkName)
@ -143,6 +147,15 @@ StatusDialog {
"" : root.feeText
readonly property bool error: root.feeErrorText !== ""
}
accountsSelector.onCurrentIndexChanged: {
if (accountsSelector.currentIndex < 0)
return
const item = ModelUtils.get(accountsSelector.model,
accountsSelector.currentIndex)
d.accountAddress = item.address
}
}
}
@ -156,7 +169,6 @@ StatusDialog {
}
StatusButton {
enabled: !root.isFeeLoading && root.feeErrorText === ""
&& root.feeText !== ""
text: {
if (root.actionType === TokenMasterActionPopup.ActionType.Ban)
return qsTr("Ban %1 and remotely destruct 1 token").arg(root.userName)
@ -179,4 +191,10 @@ StatusDialog {
}
}
}
QtObject {
id: d
property string accountAddress: ""
}
}

View File

@ -309,29 +309,6 @@ StatusSectionLayout {
readonly property CommunityTokensStore communityTokensStore:
rootStore.communityTokensStore
function setFeesInfo(ethCurrency, fiatCurrency, errorCode) {
if (errorCode === Constants.ComputeFeeErrorCode.Success
|| errorCode === Constants.ComputeFeeErrorCode.Balance) {
const valueStr = LocaleUtils.currencyAmountToLocaleString(ethCurrency)
+ "(" + LocaleUtils.currencyAmountToLocaleString(fiatCurrency) + ")"
mintPanel.feeText = valueStr
if (errorCode === Constants.ComputeFeeErrorCode.Balance)
mintPanel.feeErrorText = qsTr("Not enough funds to make transaction")
mintPanel.isFeeLoading = false
return
} else if (errorCode === Constants.ComputeFeeErrorCode.Infura) {
mintPanel.feeErrorText = qsTr("Infura error")
mintPanel.isFeeLoading = true
return
}
mintPanel.feeErrorText = qsTr("Unknown error")
mintPanel.isFeeLoading = true
}
// General community props
communityName: root.community.name
communityLogo: root.community.image
@ -356,22 +333,11 @@ StatusSectionLayout {
allNetworks: communityTokensStore.allNetworks
accounts: root.walletAccountsModel
onDeployFeesRequested: {
feeText = ""
feeErrorText = ""
isFeeLoading = true
onRegisterDeployFeesSubscriber: d.feesBroker.registerDeployFeesSubscriber(feeSubscriber)
communityTokensStore.computeDeployFee(
chainId, accountAddress, tokenType, !(mintPanel.isOwnerTokenDeployed && mintPanel.isTMasterTokenDeployed))
}
onRegisterSelfDestructFeesSubscriber: d.feesBroker.registerSelfDestructFeesSubscriber(feeSubscriber)
onBurnFeesRequested: {
feeText = ""
feeErrorText = ""
isFeeLoading = true
communityTokensStore.computeBurnFee(tokenKey, amount, accountAddress)
}
onRegisterBurnTokenFeesSubscriber: d.feesBroker.registerBurnFeesSubscriber(feeSubscriber)
onMintCollectible:
communityTokensStore.deployCollectible(
@ -384,17 +350,10 @@ StatusSectionLayout {
communityTokensStore.deployOwnerToken(
root.community.id, ownerToken, tMasterToken)
onRemotelyDestructFeesRequest:
communityTokensStore.computeSelfDestructFee(
walletsAndAmounts, tokenKey, accountAddress)
onRemotelyDestructCollectibles:
communityTokensStore.remoteSelfDestructCollectibles(
root.community.id, walletsAndAmounts, tokenKey, accountAddress)
onSignBurnTransactionOpened:
communityTokensStore.computeBurnFee(tokenKey, amount, accountAddress)
onBurnToken:
communityTokensStore.burnToken(root.community.id, tokenKey, amount, accountAddress)
@ -524,7 +483,6 @@ StatusSectionLayout {
membersModel: community.members
accountsModel: root.walletAccountsModel
onAirdropClicked: communityTokensStore.airdrop(
root.community.id, airdropTokens, addresses,
feeAccountAddress)
@ -534,10 +492,7 @@ StatusSectionLayout {
mintPanel.openNewTokenForm(isAssetType)
}
onAirdropFeesRequested:
communityTokensStore.computeAirdropFee(
root.community.id, contractKeysAndAmounts, addresses,
feeAccountAddress)
onRegisterAirdropFeeSubscriber: d.feesBroker.registerAirdropFeesSubscriber(feeSubscriber)
}
}
@ -556,6 +511,10 @@ StatusSectionLayout {
readonly property bool tokenMaster: root.community.memberRole === Constants.memberRole.tokenMaster
}
readonly property TransactionFeesBroker feesBroker: TransactionFeesBroker {
communityTokensStore: root.rootStore.communityTokensStore
}
function goTo(section: int, subSection: int) {
const stackContent = stackLayout.children
@ -705,22 +664,6 @@ StatusSectionLayout {
Connections {
target: rootStore.communityTokensStore
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
mintPanel.setFeesInfo(ethCurrency, fiatCurrency, errorCode)
}
function onSelfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
mintPanel.setFeesInfo(ethCurrency, fiatCurrency, errorCode)
}
function onBurnFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
mintPanel.setFeesInfo(ethCurrency, fiatCurrency, errorCode)
}
function onAirdropFeeUpdated(airdropFees) {
airdropPanel.airdropFees = airdropFees
}
function onRemoteDestructStateChanged(communityId, tokenName, status, url) {
if (root.community.id !== communityId)
return

View File

@ -24,6 +24,10 @@ StatusScrollView {
// https://bugreports.qt.io/browse/QTBUG-84269
/* required */ property TokenObject token
/* required */ property string feeText
/* required */ property string feeErrorText
/* required */ property bool isFeeLoading
readonly property bool isAssetView: token.type === Constants.TokenType.ERC20
@ -58,10 +62,6 @@ StatusScrollView {
// Required for preview mode:
property var accounts
property string feeText
property string feeErrorText
property bool isFeeLoading
signal mintClicked()
signal airdropRequested(string address)
@ -74,8 +74,6 @@ StatusScrollView {
signal kickRequested(string name, string contactId)
signal banRequested(string name, string contactId)
signal deployFeesRequested
QtObject {
id: d
@ -179,8 +177,6 @@ StatusScrollView {
accountsSelector.currentIndex)
token.accountAddress = item.address
token.accountName = item.name
root.deployFeesRequested()
})
}
}

View File

@ -59,13 +59,18 @@ StatusScrollView {
onFeesPerSelectedContractChanged: {
feesModel.clear()
feesPerSelectedContract.forEach(entry => {
let feeSource = feesPerSelectedContract
if(!feeSource || feeSource.length === 0) // if no fees are available, show the placeholder text based on selection
feeSource = ModelUtils.modelToArray(root.selectedHoldingsModel, ["contractUniqueKey"])
feeSource.forEach(entry => {
feesModel.append({
contractUniqueKey: entry.contractUniqueKey,
title: qsTr("Airdrop %1 on %2")
.arg(ModelUtils.getByKey(root.selectedHoldingsModel, "contractUniqueKey", entry.contractUniqueKey, "symbol"))
.arg(ModelUtils.getByKey(root.selectedHoldingsModel, "contractUniqueKey", entry.contractUniqueKey, "networkText")),
feeText: entry.feeText
feeText: entry.feeText ?? ""
})
})
}

View File

@ -24,15 +24,11 @@ StatusScrollView {
property int viewWidth: 560 // by design
property bool isAssetView: false
property int validationMode: StatusInput.ValidationMode.OnlyWhenDirty
property var tokensModel
property var tokensModelWallet
property TokenObject collectible: TokenObject {
type: Constants.TokenType.ERC721
}
property TokenObject asset: TokenObject{
type: Constants.TokenType.ERC20
property TokenObject token: TokenObject {
type: root.isAssetView ? Constants.TokenType.ERC20 : Constants.TokenType.ERC721
}
// Used for reference validation when editing a failed deployment
@ -53,12 +49,11 @@ StatusScrollView {
property bool isFeeLoading
readonly property string feeLabel:
isAssetView ? qsTr("Mint asset on %1").arg(asset.chainName)
: qsTr("Mint collectible on %1").arg(collectible.chainName)
isAssetView ? qsTr("Mint asset on %1").arg(root.token.chainName)
: qsTr("Mint collectible on %1").arg(root.token.chainName)
signal chooseArtWork
signal previewClicked
signal deployFeesRequested
QtObject {
id: d
@ -69,7 +64,7 @@ StatusScrollView {
&& symbolInput.valid
&& (unlimitedSupplyChecker.checked || (!unlimitedSupplyChecker.checked && parseInt(supplyInput.text) > 0))
&& (!root.isAssetView || (root.isAssetView && assetDecimalsInput.valid))
&& !root.isFeeLoading && root.feeErrorText === "" && !requestFeeDelayTimer.running
&& deployFeeSubscriber.feeText !== "" && deployFeeSubscriber.feeErrorText === ""
readonly property int imageSelectorRectWidth: root.isAssetView ? 128 : 290
@ -83,10 +78,7 @@ StatusScrollView {
contentHeight: mainLayout.height
Component.onCompleted: {
if(root.isAssetView)
networkSelector.setChain(asset.chainId)
else
networkSelector.setChain(collectible.chainId)
networkSelector.setChain(root.token.chainId)
}
ColumnLayout {
@ -106,8 +98,8 @@ StatusScrollView {
Layout.fillWidth: true
Layout.preferredHeight: d.imageSelectorRectWidth
dataImage: root.isAssetView ? asset.artworkSource : collectible.artworkSource
artworkSource: root.isAssetView ? asset.artworkSource : collectible.artworkSource
dataImage: root.token.artworkSource
artworkSource: root.token.artworkSource
editorAnchorLeft: false
editorRoundedImage: root.isAssetView
uploadTextLabel.uploadText: root.isAssetView ? qsTr("Upload") : qsTr("Drag and Drop or Upload Artwork")
@ -116,25 +108,15 @@ StatusScrollView {
editorTitle: root.isAssetView ? qsTr("Asset icon") : qsTr("Collectible artwork")
acceptButtonText: root.isAssetView ? qsTr("Upload asset icon") : qsTr("Upload collectible artwork")
onArtworkSourceChanged: {
if(root.isAssetView)
asset.artworkSource = artworkSource
else
collectible.artworkSource = artworkSource
}
onArtworkCropRectChanged: {
if(root.isAssetView)
asset.artworkCropRect = artworkCropRect
else
collectible.artworkCropRect = artworkCropRect
}
onArtworkSourceChanged: root.token.artworkSource = artworkSource
onArtworkCropRectChanged: root.token.artworkCropRect = artworkCropRect
}
CustomStatusInput {
id: nameInput
label: qsTr("Name")
text: root.isAssetView ? asset.name : collectible.name
text: root.token.name
charLimit: 15
placeholderText: qsTr("Name")
validationMode: root.validationMode
@ -144,7 +126,7 @@ StatusScrollView {
qsTr("Your token name contains invalid characters (use A-Z and 0-9, hyphens and underscores only)")
extraValidator.validate: function (value) {
// If minting failed, we can retry same deployment, so same name allowed
const allowRepeatedName = (root.isAssetView ? asset.deployState : collectible.deployState) === Constants.ContractTransactionStatus.Failed
const allowRepeatedName = root.token.deployState === Constants.ContractTransactionStatus.Failed
if(allowRepeatedName)
if(nameInput.text === root.referenceName)
return true
@ -154,19 +136,14 @@ StatusScrollView {
}
extraValidator.errorMessage: qsTr("You have used this token name before")
onTextChanged: {
if(root.isAssetView)
asset.name = text
else
collectible.name = text
}
onTextChanged: root.token.name = text
}
CustomStatusInput {
id: descriptionInput
label: qsTr("Description")
text: root.isAssetView ? asset.description : collectible.description
text: root.token.description
charLimit: 280
placeholderText: root.isAssetView ? qsTr("Describe your asset") : qsTr("Describe your collectible")
input.multiline: true
@ -179,19 +156,14 @@ StatusScrollView {
regexValidator.regularExpression: Constants.regularExpressions.ascii
regexValidator.errorMessage: qsTr("Only A-Z, 0-9 and standard punctuation allowed")
onTextChanged: {
if(root.isAssetView)
asset.description = text
else
collectible.description = text
}
onTextChanged: root.token.description
}
CustomStatusInput {
id: symbolInput
label: qsTr("Symbol")
text: root.isAssetView ? asset.symbol : collectible.symbol
text: root.token.symbol
charLimit: 6
placeholderText: root.isAssetView ? qsTr("e.g. ETH"): qsTr("e.g. DOODLE")
validationMode: root.validationMode
@ -201,7 +173,7 @@ StatusScrollView {
regexValidator.regularExpression: Constants.regularExpressions.capitalOnly
extraValidator.validate: function (value) {
// If minting failed, we can retry same deployment, so same symbol allowed
const allowRepeatedName = (root.isAssetView ? asset.deployState : collectible.deployState) === Constants.ContractTransactionStatus.Failed
const allowRepeatedName = root.token.deployState === Constants.ContractTransactionStatus.Failed
if(allowRepeatedName)
if(symbolInput.text.toUpperCase() === root.referenceSymbol.toUpperCase())
return true
@ -216,10 +188,7 @@ StatusScrollView {
onTextChanged: {
const cursorPos = input.edit.cursorPosition
const upperSymbol = text.toUpperCase()
if(root.isAssetView)
asset.symbol = upperSymbol
else
collectible.symbol = upperSymbol
root.token.symbol = upperSymbol
text = upperSymbol // breaking the binding on purpose but so does validate() and onTextChanged() internal handler
input.edit.cursorPosition = cursorPos
}
@ -237,15 +206,12 @@ StatusScrollView {
label: qsTr("Unlimited supply")
description: qsTr("Enable to allow the minting of additional tokens in the future. Disable to specify a finite supply")
checked: root.isAssetView ? asset.infiniteSupply : collectible.infiniteSupply
checked: root.token.infiniteSupply
onCheckedChanged: {
if(!checked) supplyInput.forceActiveFocus()
if(root.isAssetView)
asset.infiniteSupply = checked
else
collectible.infiniteSupply = checked
root.token.infiniteSupply = checked
}
}
@ -254,12 +220,8 @@ StatusScrollView {
visible: !unlimitedSupplyChecker.checked
label: qsTr("Total finite supply")
text: {
const token = root.isAssetView ? root.asset : root.collectible
return SQUtils.AmountsArithmetic.toNumber(token.supply,
token.multiplierIndex)
}
text: SQUtils.AmountsArithmetic.toNumber(root.token.supply,
root.token.multiplierIndex)
placeholderText: qsTr("e.g. 300")
minLengthValidator.errorMessage: qsTr("Please enter a total finite supply")
@ -274,9 +236,8 @@ StatusScrollView {
if (Number.isNaN(supplyNumber) || Object.values(errors).length)
return
const token = root.isAssetView ? root.asset : root.collectible
token.supply = SQUtils.AmountsArithmetic.fromNumber(
supplyNumber, token.multiplierIndex).toFixed(0)
supplyNumber, root.token.multiplierIndex).toFixed(0)
}
}
@ -286,9 +247,9 @@ StatusScrollView {
visible: !root.isAssetView
label: checked ? qsTr("Not transferable (Soulbound)") : qsTr("Transferable")
description: qsTr("If enabled, the token is locked to the first address it is sent to and can never be transferred to another address. Useful for tokens that represent Admin permissions")
checked: !collectible.transferable
checked: !root.token.transferable
onCheckedChanged: collectible.transferable = !checked
onCheckedChanged: root.token.transferable = !checked
}
CustomSwitchRowComponent {
@ -297,8 +258,8 @@ StatusScrollView {
visible: !root.isAssetView
label: qsTr("Remotely destructible")
description: qsTr("Enable to allow you to destroy tokens remotely. Useful for revoking permissions from individuals")
checked: !!collectible ? collectible.remotelyDestruct : true
onCheckedChanged: collectible.remotelyDestruct = checked
checked: !!root.token ? root.token.remotelyDestruct : true
onCheckedChanged: root.token.remotelyDestruct = checked
}
CustomStatusInput {
@ -309,7 +270,7 @@ StatusScrollView {
charLimit: 2
charLimitLabel: qsTr("Max 10")
placeholderText: "2"
text: !!asset ? asset.decimals : ""
text: root.token.decimals
validationMode: StatusInput.ValidationMode.Always
minLengthValidator.errorMessage: qsTr("Please enter how many decimals your token should have")
regexValidator.errorMessage: d.hasEmoji(text) ? qsTr("Your decimal amount is too cool (use 0-9 only)") :
@ -317,7 +278,7 @@ StatusScrollView {
regexValidator.regularExpression: Constants.regularExpressions.numerical
extraValidator.validate: function (value) { return parseInt(value) > 0 && parseInt(value) <= 10 }
extraValidator.errorMessage: qsTr("Enter a number between 1 and 10")
onTextChanged: asset.decimals = parseInt(text)
onTextChanged: root.token.decimals = parseInt(text)
}
FeesBox {
@ -338,44 +299,17 @@ StatusScrollView {
readonly property bool error: root.feeErrorText !== ""
}
Timer {
id: requestFeeDelayTimer
interval: 500
onTriggered: root.deployFeesRequested()
}
readonly property bool triggerFeeReevaluation: {
dropAreaItem.artworkSource
nameInput.text
descriptionInput.text
symbolInput.text
supplyInput.text
unlimitedSupplyChecker.checked
transferableChecker.checked
remotelyDestructChecker.checked
feesBox.accountsSelector.currentIndex
asset.chainId
collectible.chainId
requestFeeDelayTimer.restart()
return true
}
accountsSelector.model: root.accounts
readonly property TokenObject token: root.isAssetView ? root.asset
: root.collectible
// account can be changed also on preview page and it should be
// reflected in the form after navigating back
Connections {
target: feesBox.token
target: root.token
function onAccountAddressChanged() {
const idx = SQUtils.ModelUtils.indexOf(
feesBox.accountsSelector.model, "address",
feesBox.token.accountAddress)
root.token.accountAddress)
feesBox.accountsSelector.currentIndex = idx
}
@ -387,8 +321,8 @@ StatusScrollView {
const item = SQUtils.ModelUtils.get(
accountsSelector.model, accountsSelector.currentIndex)
token.accountAddress = item.address
token.accountName = item.name
root.token.accountAddress = item.address
root.token.accountName = item.name
}
}
@ -511,15 +445,9 @@ StatusScrollView {
multiSelection: false
onToggleNetwork: (network) => {
if(root.isAssetView) {
asset.chainId = network.chainId
asset.chainName = network.chainName
asset.chainIcon = network.iconUrl
} else {
collectible.chainId = network.chainId
collectible.chainName = network.chainName
collectible.chainIcon = network.iconUrl
}
root.token.chainId = network.chainId
root.token.chainName = network.chainName
root.token.chainIcon = network.iconUrl
}
}
}

View File

@ -69,7 +69,6 @@ StatusScrollView {
.arg(communityName).arg(ownerToken.chainName)
signal mintClicked
signal deployFeesRequested
QtObject {
id: d
@ -175,8 +174,6 @@ StatusScrollView {
onAddressChanged: {
ownerToken.accountAddress = address
tMasterToken.accountAddress = address
requestFeeDelayTimer.restart()
}
control.onDisplayTextChanged: {
ownerToken.accountName = control.displayText
@ -324,16 +321,7 @@ StatusScrollView {
tMasterToken.chainId = network.chainId
tMasterToken.chainName = network.chainName
tMasterToken.chainIcon = network.iconUrl
requestFeeDelayTimer.restart()
}
}
}
Timer {
id: requestFeeDelayTimer
interval: 500
onTriggered: root.deployFeesRequested()
}
}

View File

@ -12,10 +12,10 @@ QtObject {
property var enabledNetworks: networksModule.enabled
property var allNetworks: networksModule.all
signal deployFeeUpdated(var ethCurrency, var fiatCurrency, int error)
signal selfDestructFeeUpdated(var ethCurrency, var fiatCurrency, int error)
signal deployFeeUpdated(var ethCurrency, var fiatCurrency, int error, string responseId)
signal selfDestructFeeUpdated(var ethCurrency, var fiatCurrency, int error, string responseId)
signal airdropFeeUpdated(var airdropFees)
signal burnFeeUpdated(var ethCurrency, var fiatCurrency, int error)
signal burnFeeUpdated(var ethCurrency, var fiatCurrency, int error, string responseId)
signal deploymentStateChanged(string communityId, int status, string url)
signal ownerTokenDeploymentStateChanged(string communityId, int status, string url)
@ -63,18 +63,22 @@ QtObject {
readonly property Connections connections: Connections {
target: communityTokensModuleInst
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.deployFeeUpdated(ethCurrency, fiatCurrency, errorCode)
function onDeployFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId) {
root.deployFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId)
}
function onSelfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.selfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode)
function onSelfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId) {
root.selfDestructFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId)
}
function onAirdropFeesUpdated(jsonFees) {
root.airdropFeeUpdated(JSON.parse(jsonFees))
}
function onBurnFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId) {
root.burnFeeUpdated(ethCurrency, fiatCurrency, errorCode, responseId)
}
function onDeploymentStateChanged(communityId, status, url) {
root.deploymentStateChanged(communityId, status, url)
}
@ -98,14 +102,22 @@ QtObject {
function onBurnStateChanged(communityId, tokenName, status, url) {
root.burnStateChanged(communityId, tokenName, status, url)
}
function onBurnFeeUpdated(ethCurrency, fiatCurrency, errorCode) {
root.burnFeeUpdated(ethCurrency, fiatCurrency, errorCode)
}
}
function computeDeployFee(chainId, accountAddress, tokenType, isOwnerDeployment) {
communityTokensModuleInst.computeDeployFee(chainId, accountAddress, tokenType, isOwnerDeployment)
// Burn:
function computeBurnFee(tokenKey, amount, accountAddress, requestId) {
console.assert(typeof amount === "string")
communityTokensModuleInst.computeBurnFee(tokenKey, amount, accountAddress, requestId)
}
function computeAirdropFee(communityId, contractKeysAndAmounts, addresses, feeAccountAddress, requestId) {
communityTokensModuleInst.computeAirdropFee(
communityId, JSON.stringify(contractKeysAndAmounts),
JSON.stringify(addresses), feeAccountAddress, requestId)
}
function computeDeployFee(chainId, accountAddress, tokenType, isOwnerDeployment, requestId) {
communityTokensModuleInst.computeDeployFee(chainId, accountAddress, tokenType, isOwnerDeployment, requestId)
}
/**
@ -117,20 +129,14 @@ QtObject {
* }
* ]
*/
function computeSelfDestructFee(walletsAndAmounts, tokenKey, accountAddress) {
communityTokensModuleInst.computeSelfDestructFee(JSON.stringify(walletsAndAmounts), tokenKey, accountAddress)
function computeSelfDestructFee(walletsAndAmounts, tokenKey, accountAddress, requestId) {
communityTokensModuleInst.computeSelfDestructFee(JSON.stringify(walletsAndAmounts), tokenKey, accountAddress, requestId)
}
function remoteSelfDestructCollectibles(communityId, walletsAndAmounts, tokenKey, accountAddress) {
communityTokensModuleInst.selfDestructCollectibles(communityId, JSON.stringify(walletsAndAmounts), tokenKey, accountAddress)
}
// Burn:
function computeBurnFee(tokenKey, amount, accountAddress) {
console.assert(typeof amount === "string")
communityTokensModuleInst.computeBurnFee(tokenKey, amount, accountAddress)
}
function burnToken(communityId, tokenKey, burnAmount, accountAddress) {
console.assert(typeof burnAmount === "string")
communityTokensModuleInst.burnTokens(communityId, tokenKey, burnAmount, accountAddress)
@ -140,10 +146,4 @@ QtObject {
function airdrop(communityId, airdropTokens, addresses, feeAccountAddress) {
communityTokensModuleInst.airdropTokens(communityId, JSON.stringify(airdropTokens), JSON.stringify(addresses), feeAccountAddress)
}
function computeAirdropFee(communityId, contractKeysAndAmounts, addresses, feeAccountAddress) {
communityTokensModuleInst.computeAirdropFee(
communityId, JSON.stringify(contractKeysAndAmounts),
JSON.stringify(addresses), feeAccountAddress)
}
}