diff --git a/src/daemon/firod.ts b/src/daemon/firod.ts index 6b7c3e59..5f271dad 100644 --- a/src/daemon/firod.ts +++ b/src/daemon/firod.ts @@ -198,6 +198,8 @@ export interface ApiStatusData { hasSentInitialStateWallet: boolean; latestBlockTimestamp: number; newLogMessages: string[]; + isSpark: boolean; + lelantusGracefulPeriod: number; } export interface ApiStatus { @@ -211,7 +213,7 @@ export interface ApiStatus { export interface TxOut { scriptType: 'pay-to-public-key' | 'pay-to-public-key-hash' | 'pay-to-script-hash' | 'pay-to-witness-script-hash' | 'zerocoin-mint' | 'zerocoin-remint' | 'zerocoin-spend' | 'sigma-spend' | 'sigma-mint' | 'lelantus-mint' | - 'lelantus-jmint' | 'lelantus-joinsplit' | 'elysium' | 'unknown'; + 'lelantus-jmint' | 'lelantus-joinsplit' | 'elysium' | 'spark-mint'| 'spark-smint' | 'spark-spend' | 'unknown'; amount: bigint; isChange: boolean; isLocked: boolean; @@ -220,6 +222,7 @@ export interface TxOut { isElysiumReferenceOutput: boolean; destination?: string; lelantusSerialHash?: string; + sparkInputLTagHashes?: string; } type ElysiumPropertyLelantusStatus = "SoftDisabled" | "SoftEnabled" | "HardDisabled" | "HardEnabled"; @@ -263,13 +266,14 @@ export interface ElysiumData { export interface Transaction { txid: string; - inputType: 'public' | 'mined' | 'zerocoin' | 'sigma' | 'lelantus'; + inputType: 'public' | 'mined' | 'zerocoin' | 'sigma' | 'lelantus' | 'sparkmint' | 'sparkspend'; isFromMe: boolean; firstSeenAt: number; fee: bigint; outputs: TxOut[]; publicInputs: CoinControl; lelantusInputSerialHashes: string[]; + sparkInputLTagHashes: string[]; elysium?: ElysiumData; // blockHash MAY be set without blockHeight or blockTime, in which case the transaction is from an orphaned block. @@ -289,6 +293,7 @@ export interface TransactionInput { } export interface AddressBookItem { + addressType: string; address: string; label: string; createdAt?: number; // UNIX timestamp in milliseconds @@ -298,6 +303,7 @@ export interface AddressBookItem { function isValidAddressBookItem(x: any): x is AddressBookItem { const r = x !== null && typeof x === 'object' && + typeof x.addressType === 'string' && typeof x.address === 'string' && typeof x.label === 'string' && typeof x.purpose === 'string'; @@ -1477,6 +1483,7 @@ export class Firod { async addAddressBookItem(item: AddressBookItem): Promise { await this.send('', 'create', 'editAddressBook', { + addresstype: item.addressType, address: item.address, label: item.label, purpose: item.purpose, @@ -1488,6 +1495,7 @@ export class Firod { async updateAddressBookItem(item: AddressBookItem, newLabel: string) : Promise { await this.send('', 'create', 'editAddressBook', { + addresstype: item.addressType, address: item.address, label: item.label, purpose: item.purpose, @@ -1500,7 +1508,8 @@ export class Firod { createdAt: item.createdAt, address: item.address, purpose: item.purpose, - label: newLabel + label: newLabel, + addressType: item.addressType, }; } @@ -1516,8 +1525,8 @@ export class Firod { } // Get an unused address with no associated label. - async getUnusedAddress(): Promise { - const data = await this.send(null, 'none', 'paymentRequestAddress', null); + async getUnusedAddress(addresstype: string = 'Transparent'): Promise { + const data = await this.send(null, 'none', 'paymentRequestAddress', {addressType: addresstype}); if (typeof data === 'object' && typeof data['address'] === 'string') { return data['address']; @@ -1537,6 +1546,15 @@ export class Firod { return data; } + async mintAllSpark(auth: string): Promise { + const data = await this.send(auth, 'create', 'autoMintSpark', null); + if (typeof data !== 'object') throw new UnexpectedFirodResponse('create/mint', data); + for (const x in data) { + if (typeof x !== 'string') throw new UnexpectedFirodResponse('create/mint', data); + } + return data; + } + async mintElysium(auth: string, address: string, propertyId: number): Promise<{txids: string[]}> { return await this.send(auth, null, 'mintElysium', { address, @@ -1727,4 +1745,78 @@ export class Firod { const d = (await this.apiStatus()).data; return d.disabledSporks && !d.disabledSporks.includes("lelantus"); } + + async isSparkAllowed(): Promise { + const d = (await this.apiStatus()).data; + return d.isSpark; + } + + async mintSpark(auth: string, label: string, recipient: string, amount: number, feePerKb: number, + subtractFeeFromAmount: boolean, coinControl?: CoinControl): Promise<{txids: string[]}> { + const data = await this.send(auth, 'create', 'mintSpark', { + label, + recipient, + amount, + subtractFeeFromAmount, + feePerKb, + coinControl: { + selected: coinControlToString(coinControl) + } + }); + function isValidResponse(x: any): x is {txids: string[]} { + if (x === null || + typeof x !== 'object') { + return false; + } + for (const v of x.txids) { + if (typeof v !== 'string') { + return false; + } + } + return true; + } + if (isValidResponse(data)) { + return data; + } else { + throw new UnexpectedFirodResponse('create/mintSpark', data); + } + } + + async spendSpark(auth: string, label: string, recipient: string, amount: number, feePerKb: number, + subtractFeeFromAmount: boolean, coinControl?: CoinControl): Promise<{txid: string}> { + const data = await this.send(auth, 'create', 'spendSpark', { + label, + recipient, + amount, + subtractFeeFromAmount, + feePerKb, + coinControl: { + selected: coinControlToString(coinControl) + } + }); + function isValidResponse(x: any): x is {txid: string} { + return x !== null && typeof x === 'object' && typeof x.txid === 'string'; + } + if (isValidResponse(data)) { + return data; + } else { + throw new UnexpectedFirodResponse('create/spendSpark', data); + } + } + + async lelantusToSpark(auth: string) { + await this.send(auth, 'create', 'lelantusToSpark', {}); + } + + async validateSparkAddress(address: string): Promise<{valid: boolean}> { + const data = await this.send('', 'create', 'validateSparkAddress', {address}); + function isValidResponse(x: any): x is {valid: boolean} { + return x !== null && typeof x === 'object' && typeof x.valid === 'boolean'; + } + if (isValidResponse(data)) { + return data; + } else { + throw new UnexpectedFirodResponse('create/validateSparkAddress', data); + } + } } diff --git a/src/daemon/modules/transaction.ts b/src/daemon/modules/transaction.ts index 7e8ddb14..3afd5da6 100644 --- a/src/daemon/modules/transaction.ts +++ b/src/daemon/modules/transaction.ts @@ -1,5 +1,8 @@ import { Firod } from '../firod'; -export function handleEvent(store, firo: Firod, eventData: any) { +export async function handleEvent(store, firod: Firod, eventData: any) { store.commit('Transactions/setWalletState', [eventData]); + if(eventData.inputType == "sparkspend") { + store.commit('Transactions/setWalletState', await firod.getStateWallet()); + } } diff --git a/src/renderer/assets/FiroInfo.vue b/src/renderer/assets/FiroInfo.vue new file mode 100644 index 00000000..4db59f1b --- /dev/null +++ b/src/renderer/assets/FiroInfo.vue @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/renderer/assets/FiroWarning.vue b/src/renderer/assets/FiroWarning.vue new file mode 100644 index 00000000..028f118c --- /dev/null +++ b/src/renderer/assets/FiroWarning.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/src/renderer/components/AnimatedTable/AddressBookItemAddress.vue b/src/renderer/components/AnimatedTable/AddressBookItemAddress.vue index 68d571b0..4a39f3b9 100644 --- a/src/renderer/components/AnimatedTable/AddressBookItemAddress.vue +++ b/src/renderer/components/AnimatedTable/AddressBookItemAddress.vue @@ -4,7 +4,9 @@ - {{ rowData.address }} +
+ {{ rowData.address }} +
@@ -21,4 +23,16 @@ export default { \ No newline at end of file diff --git a/src/renderer/components/AnimatedTable/AddressBookItemAddressType.vue b/src/renderer/components/AnimatedTable/AddressBookItemAddressType.vue new file mode 100644 index 00000000..484b2d5e --- /dev/null +++ b/src/renderer/components/AnimatedTable/AddressBookItemAddressType.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/renderer/components/AnimatedTable/AnimatedTableInputPrivacy.vue b/src/renderer/components/AnimatedTable/AnimatedTableInputPrivacy.vue new file mode 100644 index 00000000..499991a6 --- /dev/null +++ b/src/renderer/components/AnimatedTable/AnimatedTableInputPrivacy.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/renderer/components/AnimatedTable/TxAmount.vue b/src/renderer/components/AnimatedTable/TxAmount.vue index 2a6394a7..ba46a6d2 100644 --- a/src/renderer/components/AnimatedTable/TxAmount.vue +++ b/src/renderer/components/AnimatedTable/TxAmount.vue @@ -1,6 +1,9 @@ @@ -10,7 +13,7 @@ import VuetableFieldMixin from 'vue3-vuetable/src/components/VuetableFieldMixin. import {bigintToString} from "lib/convert"; export default { - name: 'TxAmount', + name: 'Amount', mixins: [ VuetableFieldMixin @@ -24,4 +27,7 @@ export default { \ No newline at end of file diff --git a/src/renderer/components/AnimatedTable/TxId.vue b/src/renderer/components/AnimatedTable/TxId.vue index 71a0e818..b1e258fa 100644 --- a/src/renderer/components/AnimatedTable/TxId.vue +++ b/src/renderer/components/AnimatedTable/TxId.vue @@ -1,7 +1,11 @@ @@ -9,7 +13,7 @@ import VuetableFieldMixin from 'vue3-vuetable/src/components/VuetableFieldMixin.vue' export default { - name: 'UTXOSelector', + name: 'TxId', mixins: [ VuetableFieldMixin @@ -25,4 +29,13 @@ export default { family: "Robot Mono"; } } + +.disable-txt { + color: rgba(0,0,0,0.3); + user-select: text; + font: { + size: 0.8em; + family: "Robot Mono"; + } +} \ No newline at end of file diff --git a/src/renderer/components/AnimatedTable/UTXOLocker.vue b/src/renderer/components/AnimatedTable/UTXOLocker.vue new file mode 100644 index 00000000..566a032f --- /dev/null +++ b/src/renderer/components/AnimatedTable/UTXOLocker.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/renderer/components/AnimatedTable/UTXOSelector.vue b/src/renderer/components/AnimatedTable/UTXOSelector.vue index e38154b1..193be976 100644 --- a/src/renderer/components/AnimatedTable/UTXOSelector.vue +++ b/src/renderer/components/AnimatedTable/UTXOSelector.vue @@ -1,7 +1,8 @@ @@ -15,26 +16,10 @@ export default { VuetableFieldMixin ], - data() { - return { - checkbox: false - } - }, - computed: { txidIndex() { return `${this.rowData.txid}-${this.rowData.index}`; } - }, - - watch: { - rowData() { - this.checkbox = !!this.vuetable.globalData[this.txidIndex]; - }, - - checkbox() { - this.vuetable.globalData[this.txidIndex] = this.checkbox; - } } } @@ -44,4 +29,9 @@ export default { td { padding-right: var(--padding-base); } +input[type=checkbox].disable-checkbox { + padding-right: var(--padding-base); + background-color: rgba(0,0,0,0.1); + border: 1px solid rgba(0,0,0,0.1); +} \ No newline at end of file diff --git a/src/renderer/components/AnonymizeDialog.vue b/src/renderer/components/AnonymizeDialog.vue index a24c7141..600bad38 100644 --- a/src/renderer/components/AnonymizeDialog.vue +++ b/src/renderer/components/AnonymizeDialog.vue @@ -29,11 +29,14 @@ export default { }; }, - computed: mapGetters({ - availablePublic: 'Balance/availablePublic', - tokensNeedingAnonymization: 'Elysium/tokensNeedingAnonymization', - tokenData: 'Elysium/tokenData' - }), + computed: { + ...mapGetters({ + availablePublic: 'Balance/availablePublic', + tokensNeedingAnonymization: 'Elysium/tokensNeedingAnonymization', + tokenData: 'Elysium/tokenData', + isSparkAllowed: 'ApiStatus/isSparkAllowed' + }) + }, methods: { cancel() { @@ -69,7 +72,11 @@ export default { } try { - await $daemon.mintAllLelantus(passphrase); + if(this.isSparkAllowed) { + await $daemon.mintAllSpark(passphrase); + } else { + await $daemon.mintAllLelantus(passphrase); + } } catch (e) { if (e instanceof FirodErrorResponse) { errors.unshift(e.errorMessage); diff --git a/src/renderer/components/AwaitingAnonymizationHeader.vue b/src/renderer/components/AwaitingAnonymizationHeader.vue index e8f550c1..deb91382 100644 --- a/src/renderer/components/AwaitingAnonymizationHeader.vue +++ b/src/renderer/components/AwaitingAnonymizationHeader.vue @@ -1,7 +1,11 @@ + + + + diff --git a/src/renderer/components/ReceivePage.vue b/src/renderer/components/ReceivePage.vue index 2ab63306..002318e4 100644 --- a/src/renderer/components/ReceivePage.vue +++ b/src/renderer/components/ReceivePage.vue @@ -22,6 +22,12 @@ +
+ +
@@ -56,7 +62,7 @@ import InputFrame from "renderer/components/shared/InputFrame"; import RefreshAddressIcon from "renderer/components/Icons/RefreshAddressIcon"; import CopyAddressIcon from "renderer/components/Icons/CopyAddressIcon"; import Popup from "renderer/components/shared/Popup"; -import {IncorrectPassphrase} from "daemon/firod"; +import AddressBookItemAddressType from "renderer/components/AnimatedTable/AddressBookItemAddressType"; export default { name: "ReceivePage", @@ -71,7 +77,7 @@ export default { data() { return { - address: ($store.getters['AddressBook/receiveAddresses'][0] || {address: null}).address, + address: ($store.getters['AddressBook/receiveAddresses'].filter(a => a.addressType === this.selectOption ? this.selectOption : 'Spark')[0] || {address: null}).address, label: '', _quickLabel: null, qrCode: null, @@ -79,11 +85,14 @@ export default { show: '', error: '', passphrase: '', + selectOption: 'Spark', + option: '', tableFields: [ {name: markRaw(CurrentAddressIndicator)}, {name: markRaw(AddressBookItemLabel)}, - {name: markRaw(AddressBookItemAddress)} + {name: markRaw(AddressBookItemAddress)}, + {name: markRaw(AddressBookItemAddressType)} ] }; }, @@ -97,8 +106,9 @@ export default { tableData() { this.$nextTick(() => this.$refs.animatedTable.reload()); - return this.receiveAddresses.map(addr => ({isSelected: addr.address === this.address, ...addr})); - } + this.selectOptionChange() + return this.receiveAddresses.map(addr => ({isSelected: addr.address === this.address, ...addr})).filter(a => a.addressType === this.selectOption); + }, }, destroyed() { @@ -165,6 +175,14 @@ export default { setAddressBook: 'AddressBook/setAddressBook' }), + selectOptionChange() { + if(this.option !== this.selectOption) { + let check = this.selectOption ? this.selectOption : 'Spark'; + this.address = $store.getters['AddressBook/receiveAddresses'].filter(a => a.addressType === check)[0].address; + this.option = this.selectOption; + } + }, + async changeLabel(ev) { if (!this.address) return; @@ -181,9 +199,11 @@ export default { }, async refreshAddress() { - const address = await $daemon.getUnusedAddress(); + const addresstype = this.selectOption; + const address = await $daemon.getUnusedAddress(addresstype); await $daemon.addAddressBookItem({ + addressType: addresstype, address, label: '', purpose: 'receive' @@ -260,6 +280,10 @@ export default { } } } + + .extra-select-option { + margin-top:12px; + } } .qr-code-container { diff --git a/src/renderer/components/SendPage.vue b/src/renderer/components/SendPage.vue index 50619b14..d35a0c81 100644 --- a/src/renderer/components/SendPage.vue +++ b/src/renderer/components/SendPage.vue @@ -3,6 +3,13 @@
+
+ +
+ +
+
+ + +
+
+ + +
+
+
@@ -158,6 +180,9 @@ :computed-tx-fee="transactionFee" :subtract-fee-from-amount="subtractFeeFromAmount" :coin-control="coinControl" + :isSparkAllowed="isSparkAllowed" + :isSparkAddr="isSparkAddr" + :isTransparentAddress="() => isTransparentAddress()" @success="() => cleanupForm()" @reset="() => cleanupForm()" /> @@ -192,6 +217,8 @@ import SearchInput from "renderer/components/shared/SearchInput"; import InputFrame from "renderer/components/shared/InputFrame"; import PlusButton from "renderer/components/shared/PlusButton"; import FiroSymbol from "renderer/assets/CoinIcons/FIRO.svg.data"; +import AddressBookItemAddressType from "renderer/components/AnimatedTable/AddressBookItemAddressType"; +import FiroWarning from 'renderer/assets/FiroWarning'; export default { name: 'SendPage', @@ -209,7 +236,8 @@ export default { Popup, InputFrame, SearchInput, - TransactionInfo + TransactionInfo, + FiroWarning }, data () { @@ -227,13 +255,16 @@ export default { tableFields: [ {name: markRaw(CurrentAddressIndicator)}, {name: markRaw(AddressBookItemLabel)}, - {name: markRaw(AddressBookItemAddress)} + {name: markRaw(AddressBookItemAddress)}, + {name: markRaw(AddressBookItemAddressType)} ], // This is the search term to filter addresses by. filter: '', selectedAsset: 'FIRO', showConnectionTransaction: false, - connectionTransaction: null + connectionTransaction: null, + selectOption: 'Spark', + isSparkAddr: null } }, @@ -242,6 +273,7 @@ export default { enableElysium: 'App/enableElysium', network: 'ApiStatus/network', isLelantusAllowed: 'ApiStatus/isLelantusAllowed', + isSparkAllowed: 'ApiStatus/isSparkAllowed', isBlockchainSynced: 'ApiStatus/isBlockchainSynced', availablePrivate: 'Balance/availablePrivate', availablePublic: 'Balance/availablePublic', @@ -326,7 +358,7 @@ export default { return (value) => { if (!value) return ''; - if (isValidAddress(value, this.network) || isValidPaymentCode(value, this.network)) + if (isValidAddress(value, this.network) || isValidPaymentCode(value, this.network) || this.validateSparkAddress()) return true; return 'The Firo address you entered is invalid'; @@ -372,7 +404,7 @@ export default { transactionFee() { if (this.selectedAsset != 'FIRO' || !this.satoshiAmount || !this.available || this.available < this.satoshiAmount) return undefined; - return this.calculateTransactionFee(this.isPrivate, this.satoshiAmount, this.txFeePerKb, this.subtractFeeFromAmount, this.customInputs.length ? this.customInputs : undefined) || 0n; + return this.calculateTransactionFee(this.isPrivate, this.isSparkAllowed, isValidAddress(this.address, this.network), this.satoshiAmount, this.txFeePerKb, this.subtractFeeFromAmount, this.customInputs.length ? this.customInputs : undefined) || 0n; }, formDisabled() { @@ -390,12 +422,12 @@ export default { filteredSendAddresses () { this.$nextTick(() => this.$refs.animatedTable && this.$refs.animatedTable.reload()); return this.sendAddresses - .filter(address => address.label.includes(this.filter) || address.address.includes(this.filter)) - .map(address => ({isSelected: address.address === this.address, ...address})); + .filter(address => (address.label.includes(this.filter) || address.address.includes(this.filter)) && address.addressType === this.selectOption) + .map(address => ({ isSelected: address.address === this.address, ...address })); }, showAddToAddressBook () { - return !this.formDisabled && isValidAddress(this.address, this.network) && !this.addressBook[this.address]; + return !this.formDisabled && (isValidAddress(this.address, this.network) || this.isSparkAddr) && !this.addressBook[this.address]; }, coinControl () { @@ -480,6 +512,7 @@ export default { }, address() { + this.validateSparkAddress(); const a = this.addressBook[this.address]; if (a && a.purpose === 'send' && a.label !== this.label) { this.addToAddressBook(); @@ -508,21 +541,34 @@ export default { }, async addToAddressBook() { - if (!isValidAddress(this.address, this.network) || !this.label) return; + this.validateSparkAddress(); + if ((!isValidAddress(this.address, this.network) && !this.isSparkAddr) || !this.label) return; if (this.addressBook[this.address]) { - const item = this.addressBook[this.address] + const item = this.addressBook[this.address]; await $daemon.updateAddressBookItem(item, this.label); $store.commit('AddressBook/updateAddress', {...item, label: this.label}); this.$refs.animatedTable.reload(); } else { - const item = { + if(isValidAddress(this.address, this.network)){ + const item = { + addressType: "Transparent", + address: this.address, + label: this.label, + purpose: 'send' + }; + $store.commit('AddressBook/updateAddress', { ...item, createdAt: Date.now() }); + await $daemon.addAddressBookItem(item); + } else { + const item = { + addressType: "Spark", address: this.address, label: this.label, purpose: 'send' - }; - $store.commit('AddressBook/updateAddress', {...item, createdAt: Date.now()}); - await $daemon.addAddressBookItem(item); + }; + $store.commit('AddressBook/updateAddress', { ...item, createdAt: Date.now() }); + await $daemon.addAddressBookItem(item); + } } }, @@ -539,7 +585,17 @@ export default { this.label = ''; this.amount = ''; this.address = ''; - } + }, + + isTransparentAddress() { + return isValidAddress(this.address, this.network); + }, + + async validateSparkAddress() { + let res = await $daemon.validateSparkAddress(this.address); + this.isSparkAddr = res.valid; + return this.isSparkAddr; + }, } } @@ -651,6 +707,27 @@ export default { justify-content: space-between; } } + + .warning-text { + margin-bottom: 6px; + color: #FFA800; + font-size: 10px; + + .warning-item { + display: flex; + align-items: center; + } + + .warning-icon { + height: 12px; + width: 12px; + margin-right: 5px; + } + + .warning-item-text { + margin-top: 8px; + } + } } .bottom { diff --git a/src/renderer/components/SendPage/GoPrivate.vue b/src/renderer/components/SendPage/GoPrivate.vue new file mode 100644 index 00000000..67c28993 --- /dev/null +++ b/src/renderer/components/SendPage/GoPrivate.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/renderer/components/SendPage/InputSelection.vue b/src/renderer/components/SendPage/InputSelection.vue index 19db5bb7..7d4590c4 100644 --- a/src/renderer/components/SendPage/InputSelection.vue +++ b/src/renderer/components/SendPage/InputSelection.vue @@ -29,6 +29,7 @@ import AnimatedTable from "renderer/components/AnimatedTable/AnimatedTable"; import UTXOSelector from "renderer/components/AnimatedTable/UTXOSelector"; import TxIdIndex from "renderer/components/AnimatedTable/TxId"; import TxAmount from "renderer/components/AnimatedTable/TxAmount"; +import UTXOLocker from 'renderer/components/AnimatedTable/UTXOLocker'; export default { name: "InputSelection", @@ -38,6 +39,7 @@ export default { UTXOSelector, TxIdIndex, TxAmount, + UTXOLocker }, props: { @@ -52,7 +54,8 @@ export default { fields: [ {name: markRaw(UTXOSelector)}, {name: markRaw(TxIdIndex)}, - {name: markRaw(TxAmount)} + {name: markRaw(TxAmount)}, + {name: markRaw(UTXOLocker)} ] } }, @@ -60,7 +63,8 @@ export default { computed: { ...mapGetters({ TXOMap: 'Transactions/TXOMap', - availableUTXOs: 'Transactions/availableUTXOs' + availableUTXOsWithLock: 'Transactions/availableUTXOsWithLock', + isSparkAllowed: 'ApiStatus/isSparkAllowed' }), showBreakingMasternodeWarning() { @@ -74,10 +78,9 @@ export default { }, ourUnspentUTXOs() { - return this.availableUTXOs + return this.availableUTXOsWithLock .filter(tx => tx.isPrivate === this.isPrivate) - .sort((a, b) => Number(b.amount - a.amount) || a.txid.localeCompare(b.txid) || a.index - b.index) - .map(tx => ({...tx, uniqId: `${tx.txid}-${tx.index}`})); + .sort((a, b) => Number(b.amount - a.amount) || a.txid.localeCompare(b.txid) || a.index - b.index); } }, diff --git a/src/renderer/components/SendPage/LelantusToSpark.vue b/src/renderer/components/SendPage/LelantusToSpark.vue new file mode 100644 index 00000000..a2439f15 --- /dev/null +++ b/src/renderer/components/SendPage/LelantusToSpark.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/renderer/components/SendPage/SendFlow.vue b/src/renderer/components/SendPage/SendFlow.vue index e72d69cc..d592ccb6 100644 --- a/src/renderer/components/SendPage/SendFlow.vue +++ b/src/renderer/components/SendPage/SendFlow.vue @@ -5,7 +5,15 @@ Reset - + + + +
@@ -31,6 +39,16 @@ @cancel="cancel()" @confirm="goToPassphraseStep()" /> + + @@ -49,6 +67,8 @@ import ConfirmStep from "./ConfirmStep"; import PassphraseInput from "../shared/PassphraseInput"; import ErrorStep from "./ErrorStep"; import WaitOverlay from "renderer/components/shared/WaitOverlay"; +import GoPrivate from "./GoPrivate"; +import LelantusToSpark from "./LelantusToSpark"; export default { name: "SendFlow", @@ -59,14 +79,18 @@ export default { ConfirmStep, PassphraseInput, ErrorStep, - WaitOverlay + WaitOverlay, + GoPrivate, + LelantusToSpark }, data() { return { error: null, show: 'button', - passphrase: '' + passphrase: '', + migrate: false, + goprivate: false } }, @@ -80,7 +104,10 @@ export default { subtractFeeFromAmount: Boolean, isPrivate: Boolean, coinControl: Array, - asset: String | Number + asset: String | Number, + isSparkAllowed: Boolean, + isSparkAddr: Boolean, + isTransparentAddress: Function, }, computed: { @@ -89,7 +116,10 @@ export default { lockedUTXOs: 'Transactions/lockedUTXOs', allowBreakingMasternodes: 'App/allowBreakingMasternodes', selectInputs: 'Transactions/selectInputs', - tokenData: 'Elysium/tokenData' + tokenData: 'Elysium/tokenData', + currentBlockHeight: 'ApiStatus/currentBlockHeight', + lelantusGracefulPeriod: 'ApiStatus/lelantusGracefulPeriod', + availableLelantus: 'Balance/availableLelantus', }), totalAmount() { @@ -102,8 +132,27 @@ export default { this.show = 'passphrase'; }, + goToMigrateStep() { + this.migrate = true; + this.show = 'passphrase'; + }, + + goToConfirmStepWithCheckingPrivate() { + if(!this.isPrivate && !this.isSparkAddr && this.isSparkAllowed && !this.goprivate) { + this.show = 'goprivate'; + this.goprivate = true; + } else { + this.show = 'confirm'; + } + }, + + goToConfirmStep() { + this.show = 'confirm'; + }, + cancel() { this.error = null; + this.goprivate = false; this.show = 'button'; }, @@ -116,9 +165,13 @@ export default { this.show = 'wait'; const passphrase = this.passphrase; this.passphrase = ''; - + this.goprivate = false; + const coinControl = this.coinControl || this.selectInputs(this.isPrivate, this.isSparkAllowed, this.isTransparentAddress(), this.amount, this.txFeePerKb, this.subtractFeeFromAmount); try { - if (this.asset != 'FIRO') { + if (this.migrate) { + await $daemon.lelantusToSpark(passphrase); + this.migrate = false; + } else if (this.asset != 'FIRO') { const id = this.tokenData[this.asset].id; try { @@ -129,12 +182,21 @@ export default { await $daemon.recoverElysium(passphrase); await $daemon.sendElysium(passphrase, id, this.address, this.amount); } - } else if (this.isPrivate) { + } else if (this.isPrivate && !this.isSparkAllowed) { // Under the hood we'll always use coin control because the daemon uses a very complex stochastic // algorithm that interferes with fee calculation. - const coinControl = this.coinControl || this.selectInputs(true, this.amount, this.txFeePerKb, this.subtractFeeFromAmount); await $daemon.sendLelantus(passphrase, this.address, this.amount, this.txFeePerKb, this.subtractFeeFromAmount, coinControl); + } else if (this.isPrivate && this.isSparkAllowed) { + // Under the hood we'll always use coin control because the daemon uses a very complex stochastic + // algorithm that interferes with fee calculation. + await $daemon.spendSpark(passphrase, this.label, this.address, this.amount, this.txFeePerKb, + this.subtractFeeFromAmount, coinControl); + } else if (!this.isPrivate && this.isSparkAllowed && this.isSparkAddr) { + // Under the hood we'll always use coin control because the daemon uses a very complex stochastic + // algorithm that interferes with fee calculation. + await $daemon.mintSpark(passphrase, this.label, this.address, this.amount, this.txFeePerKb, + this.subtractFeeFromAmount, coinControl); } else { let lockedCoins = []; if (this.coinControl && this.allowBreakingMasternodes) { @@ -146,7 +208,7 @@ export default { // Under the hood we'll always use coin control because the daemon uses a very complex stochastic // algorithm that interferes with fee calculation. - const coinControl = this.coinControl || this.selectInputs(false, this.amount, this.txFeePerKb, this.subtractFeeFromAmount); + const coinControl = this.coinControl || this.selectInputs(this.isPrivate, this.isSparkAllowed, this.isTransparentAddress(), this.amount, this.txFeePerKb, this.subtractFeeFromAmount); try { await $daemon.publicSend(passphrase, this.label, this.address, this.amount, this.txFeePerKb, this.subtractFeeFromAmount, coinControl); diff --git a/src/renderer/components/TransactionsPage.vue b/src/renderer/components/TransactionsPage.vue index b4439b2d..ea02ae23 100644 --- a/src/renderer/components/TransactionsPage.vue +++ b/src/renderer/components/TransactionsPage.vue @@ -4,6 +4,19 @@
+
+ +
+
The blockchain is not yet synced. Payment information may be incomplete or inaccurate.
@@ -45,11 +58,13 @@ import Label from 'renderer/components/AnimatedTable/AnimatedTableLabel'; import Popup from "renderer/components/shared/Popup"; import { bigintToString } from "lib/convert"; import SearchInput from "renderer/components/shared/SearchInput"; +import InputPrivacy from "renderer/components/AnimatedTable/AnimatedTableInputPrivacy"; const tableFields = [ {name: markRaw(RelativeDate), width: '160pt'}, {name: markRaw(Label)}, - {name: markRaw(Amount), width: '160pt'} + {name: markRaw(InputPrivacy)}, + {name: markRaw(Amount), width: '160pt'}, ]; export default { @@ -69,7 +84,8 @@ export default { tableData: [], newTableData: [], currentPage: 1, - selectedTx: null + selectedTx: null, + selectOption: 'all' } }, @@ -108,8 +124,8 @@ export default { latestTableData () { const tableData = []; - - for (const txo of this.userVisibleTransactions) { + let txos = this.userVisibleTransactions.filter(a => this.selectOption === 'all' ? true : a.inputPrivacy === this.selectOption); + for (const txo of txos) { tableData.push({ id: `${txo.blockHash}-${txo.txid}-${txo.index}`, label: (this.addressBook[txo.destination] || {}).label || txo.destination, diff --git a/src/renderer/components/shared/PrivatePublicBalance.vue b/src/renderer/components/shared/PrivatePublicBalance.vue index c43fb0f6..7cde9ece 100644 --- a/src/renderer/components/shared/PrivatePublicBalance.vue +++ b/src/renderer/components/shared/PrivatePublicBalance.vue @@ -36,6 +36,12 @@ export default { props: ['asset', 'modelValue', 'disabled'], + data() { + return { + isPrivate: true + }; + }, + computed: { ...mapGetters({ availablePrivateFiro: "Balance/availablePrivate", diff --git a/src/store/modules/ApiStatus.ts b/src/store/modules/ApiStatus.ts index 49f28b61..27b0ab54 100644 --- a/src/store/modules/ApiStatus.ts +++ b/src/store/modules/ApiStatus.ts @@ -44,6 +44,8 @@ const getters = { isLocked: (state): boolean | undefined => state.apiStatus.walletLock, isReindexing: (state): boolean => state.apiStatus.reindexing, isLelantusAllowed: (state): boolean => !state.apiStatus.disabledSporks.includes("lelantus"), + isSparkAllowed: (state): boolean => state.apiStatus.isSpark, + lelantusGracefulPeriod: (state): number => state.apiStatus.lelantusGracefulPeriod, smartFeePerKb: (state): bigint => state.apiStatus.smartFeePerKb, isBlockchainSynced: (state): boolean => ['regtest', 'regtest-ql'].includes(state.apiStatus.network) || state.apiStatus.synced, connections: (state): number => state.apiStatus.connections diff --git a/src/store/modules/Balance.ts b/src/store/modules/Balance.ts index 8245742a..c436db7e 100644 --- a/src/store/modules/Balance.ts +++ b/src/store/modules/Balance.ts @@ -3,7 +3,8 @@ import {TXO} from "./Transactions"; const getters = { balances: (state, getters, rootState, rootGetters) => { let [availablePrivate, unconfirmedPrivate, unconfirmedPrivateChange, availablePublic, unconfirmedPublic, - unconfirmedPublicChange, locked, immature] = [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]; + unconfirmedPublicChange, locked, immature, availableLelantus, unconfirmedLelantus, unconfirmedLelantusChange, + availableSpark, unconfirmedSpark, unconfirmedSparkChange] = [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]; let nextHeight: number = rootGetters['ApiStatus/currentBlockHeight'] + 1; for (const txo of rootGetters['Transactions/UTXOs']) { @@ -11,16 +12,24 @@ const getters = { else if (txo.isElysiumReferenceOutput) locked += txo.amount; else if (txo.isLocked) locked += txo.amount; else if (txo.inputPrivacy == 'mined' && txo.validAt > nextHeight) immature += txo.amount; - else if (txo.isPrivate && txo.validAt <= nextHeight) availablePrivate += txo.amount; - else if (txo.isPrivate && txo.isChange) unconfirmedPrivateChange += txo.amount; - else if (txo.isPrivate) unconfirmedPrivate += txo.amount; + else if (txo.isPrivate && txo.validAt <= nextHeight && (txo.scriptType === 'lelantus-mint' || txo.scriptType === 'lelantus-jmint')) availableLelantus += txo.amount; + else if (txo.isPrivate && txo.validAt <= nextHeight && (txo.scriptType === 'spark-mint' || txo.scriptType === 'spark-smint')) availableSpark += txo.amount; + else if (txo.isPrivate && txo.isChange && (txo.scriptType === 'lelantus-mint' || txo.scriptType === 'lelantus-jmint')) unconfirmedLelantusChange += txo.amount; + else if (txo.isPrivate && txo.isChange && (txo.scriptType === 'spark-mint' || txo.scriptType === 'spark-smint')) unconfirmedSparkChange += txo.amount; + else if (txo.isPrivate && (txo.scriptType === 'lelantus-mint' || txo.scriptType === 'lelantus-jmint')) unconfirmedLelantus += txo.amount; + else if (txo.isPrivate && (txo.scriptType === 'spark-mint' || txo.scriptType === 'spark-smint')) unconfirmedSpark += txo.amount; else if (txo.validAt <= nextHeight) availablePublic += txo.amount; else if (txo.isChange) unconfirmedPublicChange += txo.amount; else unconfirmedPublic += txo.amount; } + availablePrivate = availableSpark + availableLelantus; + unconfirmedPrivate = unconfirmedSpark + unconfirmedLelantus; + unconfirmedPrivateChange = unconfirmedSparkChange + unconfirmedLelantusChange; + return { availablePrivate, + availableLelantus, unconfirmedPrivate, unconfirmedPrivateChange, availablePublic, @@ -32,6 +41,7 @@ const getters = { }, availablePrivate: (state, getters) => getters.balances.availablePrivate, + availableLelantus: (state, getters) => getters.balances.availableLelantus, unconfirmedPrivate: (state, getters) => getters.balances.unconfirmedPrivate, availablePublic: (state, getters) => getters.balances.availablePublic, unconfirmedPublic: (state, getters) => getters.balances.unconfirmedPublic, @@ -39,7 +49,7 @@ const getters = { locked: (state, getters) => getters.balances.locked, immature: (state, getters) => getters.balances.immature, pendingChange: (state, getters) => getters.balances.unconfirmedPrivateChange + getters.balances.unconfirmedPrivate + getters.balances.unconfirmedPublicChange, - incoming: (dtate, getters) => getters.balances.unconfirmedPublic + incoming: (state, getters) => getters.balances.unconfirmedPublic, } export default { diff --git a/src/store/modules/Transactions.ts b/src/store/modules/Transactions.ts index b9769e95..e059a2bc 100644 --- a/src/store/modules/Transactions.ts +++ b/src/store/modules/Transactions.ts @@ -17,7 +17,7 @@ export interface TXO extends TxOut { // This indicates whether this input should be used for new private transactions. isPrivate: boolean; // This indicates whether the transaction as a whole was private. - inputPrivacy: 'public' | 'zerocoin' | 'sigma' | 'lelantus' | 'mined'; + inputPrivacy: 'public' | 'zerocoin' | 'sigma' | 'lelantus' | 'mined' | 'sparkmint' | 'sparkspend'; validAt: number; firstSeenAt: number; isFromMe: boolean; @@ -27,7 +27,7 @@ export interface TXO extends TxOut { lelantusInputSerialHashes?: string[]; } -function txosFromTx(tx: Transaction, spentSerialHashes: Set, spentPublicInputs: Set): TXO[] { +function txosFromTx(tx: Transaction, spentLTagHashes: Set, spentSerialHashes: Set, spentPublicInputs: Set): TXO[] { const txos: TXO[] = []; let index = -1; @@ -39,6 +39,11 @@ function txosFromTx(tx: Transaction, spentSerialHashes: Set, spentPublic let spendSize = undefined; switch (txout.scriptType) { + case "spark-mint": + case "spark-smint": + case "spark-spend": + spendSize = 2535; + break; case "lelantus-mint": case "lelantus-jmint": case "lelantus-joinsplit": @@ -67,7 +72,7 @@ function txosFromTx(tx: Transaction, spentSerialHashes: Set, spentPublic console.warn(`${tx.txid}-${index} has an unknown scriptType`); } - const isPrivate = ['lelantus-mint', 'lelantus-jmint', 'sigma-mint'].includes(txout.scriptType) + const isPrivate = ['lelantus-mint', 'lelantus-jmint', 'sigma-mint', "spark-mint", "spark-smint"].includes(txout.scriptType) let validAt = Infinity; if (!tx.blockHeight && !tx.isInstantSendLocked) validAt = Infinity; @@ -92,7 +97,8 @@ function txosFromTx(tx: Transaction, spentSerialHashes: Set, spentPublic elysium: tx.elysium, publicInputs: tx.publicInputs, lelantusInputSerialHashes: (tx).lelantusInputSerialHashes, - isSpent: txout.isSpent || (isPrivate ? spentSerialHashes.has(txout.lelantusSerialHash) : spentPublicInputs.has(`${tx.txid}-${index}`)), + sparkInputLTagHashes: (tx).sparkInputLTagHashes, + isSpent: txout.isSpent || (isPrivate ? (["spark-mint", "spark-smint"].includes(txout.scriptType) ? spentLTagHashes.has(txout.sparkInputLTagHashes) : spentSerialHashes.has(txout.lelantusSerialHash)) : spentPublicInputs.has(`${tx.txid}-${index}`)), ...txout }); } @@ -134,17 +140,46 @@ const mutations = { } }; -function selectUTXOs(isPrivate: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean, availableUTXOs: TXO[], coinControl: boolean): [bigint, TXO[]] { - const constantSize = isPrivate ? 1234n : 78n; +function selectUTXOs(isPrivate: boolean, isSpark: boolean, istransparentaddress: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean, availableUTXOs: TXO[], coinControl: boolean): [bigint, TXO[]] { + let constantSize = 0n; + if(!isPrivate && istransparentaddress) { + constantSize = 78n; + } else if (isPrivate && !isSpark) { + constantSize = 1234n; + } else if(isPrivate && isSpark && !istransparentaddress) { + constantSize = 1281n; + } else if(isPrivate && isSpark && istransparentaddress) { + constantSize = 1068n; + } else if (!isPrivate && isSpark && !istransparentaddress) { + constantSize = 348n; + } if (coinControl) { + let totalSize = 0n; + let gathered = 0n; + let fee = 0n; if (availableUTXOs.find(utxo => utxo.isPrivate != isPrivate)) return undefined; - // assume 5000 as the signature size for unknown outputs. - const totalSize = constantSize + availableUTXOs.reduce((a, utxo) => a + BigInt(utxo.spendSize) || 5000n, 0n); - const gathered = availableUTXOs.reduce((a, utxo) => a + utxo.amount, 0n); + if (!isPrivate && isSpark && !istransparentaddress) { + let destinations = []; + for (const utxo of availableUTXOs) { + if(!destinations.includes(utxo.destination)) { + totalSize+= constantSize + BigInt(utxo.spendSize) || 5000n; + destinations.push(utxo.destination); + } else { + totalSize += BigInt(utxo.spendSize) || 5000n; + } + gathered += utxo.amount; + if (gathered >= (subtractFeeFromAmount ? amount : amount + fee)) + break; + } + } else { + // assume 5000 as the signature size for unknown outputs. + totalSize = constantSize + availableUTXOs.reduce((a, utxo) => a + BigInt(utxo.spendSize) || 5000n, 0n); + gathered = availableUTXOs.reduce((a, utxo) => a + utxo.amount, 0n); + } - let fee = (totalSize * feePerKb) / 1000n ; + fee = (totalSize * feePerKb) / 1000n ; if (fee === 0n) fee = 1n; if (subtractFeeFromAmount && fee >= amount) return undefined; @@ -159,23 +194,46 @@ function selectUTXOs(isPrivate: boolean, amount: bigint, feePerKb: bigint, subtr .filter(utxo => utxo.isPrivate == isPrivate) .sort((a, b) => Number(b.amount - a.amount)); - let totalSize = constantSize; let gathered = 0n; - const selectedUTXOs = []; - while (utxos.length) { - const utxo = utxos.length % 2 ? utxos.shift() : utxos.pop(); - - gathered += utxo.amount; - totalSize += BigInt(utxo.spendSize); - selectedUTXOs.push(utxo); - - let fee = (totalSize * feePerKb) / 1000n; - if (fee === 0n) fee = 1n; - - if (subtractFeeFromAmount && amount <= fee) continue; - if (gathered >= (subtractFeeFromAmount ? amount : amount + fee)) { - return [fee, selectedUTXOs]; + if (!isPrivate && isSpark && !istransparentaddress) { + let destinations = []; + let totalSize = 0n; + while (utxos.length) { + const utxo = utxos.length % 2 ? utxos.shift() : utxos.pop(); + if(!destinations.includes(utxo.destination)) { + totalSize+= constantSize + BigInt(utxo.spendSize); + destinations.push(utxo.destination); + } else { + totalSize += BigInt(utxo.spendSize); + } + + gathered += utxo.amount; + selectedUTXOs.push(utxo); + + let fee = (totalSize * feePerKb) / 1000n; + if (fee === 0n) fee = 1n; + + if (subtractFeeFromAmount && amount <= fee) continue; + if (gathered >= (subtractFeeFromAmount ? amount : amount + fee)) { + return [fee, selectedUTXOs]; + } + } + } else { + let totalSize = constantSize; + while (utxos.length) { + const utxo = utxos.length % 2 ? utxos.shift() : utxos.pop(); + gathered += utxo.amount; + totalSize += BigInt(utxo.spendSize); + selectedUTXOs.push(utxo); + + let fee = (totalSize * feePerKb) / 1000n; + if (fee === 0n) fee = 1n; + + if (subtractFeeFromAmount && amount <= fee) continue; + if (gathered >= (subtractFeeFromAmount ? amount : amount + fee)) { + return [fee, selectedUTXOs]; + } } } @@ -186,10 +244,12 @@ const getters = { transactions: (state): {[txid: string]: Transaction} => state.transactions, spentSerialHashes: (state, getters): Set => new Set((Object.values(getters.transactions)).reduce((a, tx) => [...a, ...tx.lelantusInputSerialHashes], [])), + spentLTagHashes: (state, getters): Set => + new Set((Object.values(getters.transactions)).reduce((a, tx) => [...a, ...tx.sparkInputLTagHashes], [])), spentPublicInputs: (state, getters): Set => new Set((Object.values(getters.transactions)).reduce((a, tx) => [...a, ...tx.publicInputs], []).map(i => `${i[0]}-${i[1]}`)), allTXOs: (state, getters): TXO[] => (Object.values(getters.transactions)) - .reduce((a: TXO[], tx: Transaction): TXO[] => a.concat(txosFromTx(tx, getters.spentSerialHashes, getters.spentPublicInputs)), []), + .reduce((a: TXO[], tx: Transaction): TXO[] => a.concat(txosFromTx(tx, getters.spentLTagHashes, getters.spentSerialHashes, getters.spentPublicInputs)), []), TXOs: (state, getters): TXO[] => getters.allTXOs // Don't display orphaned mining transactions. .filter(txo => !(txo.blockHash && !txo.blockHeight && txo.inputPrivacy === 'mined')) @@ -199,18 +259,36 @@ const getters = { TXOMap: (state, getters): {[txidIndex: string]: TXO} => fromPairs(getters.allTXOs.map(txo => [`${txo.txid}-${txo.index}`, txo])), UTXOs: (state, getters): TXO[] => getters.TXOs.filter((txo: TXO) => !txo.isSpent && + !getters.spentLTagHashes.has(txo.sparkInputLTagHashes) && !getters.spentSerialHashes.has(txo.lelantusSerialHash) && !getters.spentPublicInputs.has(`${txo.txid}-${txo.index}`) ), - availableUTXOs: (state, getters, rootState, rootGetters): TXO[] => getters.UTXOs.filter((txo: TXO) => - txo.isToMe && - // Elysium has reference outputs that we should not allow the user to spend. - !txo.isElysiumReferenceOutput && - (rootGetters['App/allowBreakingMasternodes'] || !txo.isLocked) && - txo.spendSize && - txo.validAt <= rootGetters['ApiStatus/currentBlockHeight'] + 1 - ), + availableUTXOs: (state, getters, rootState, rootGetters): TXO[] => { + let isSparkAllowed: boolean = rootGetters['ApiStatus/isSparkAllowed']; + return getters.UTXOs.filter((txo: TXO) => + txo.isToMe && + // Elysium has reference outputs that we should not allow the user to spend. + !txo.isElysiumReferenceOutput && + (rootGetters['App/allowBreakingMasternodes'] || !txo.isLocked) && + txo.spendSize && + txo.validAt <= rootGetters['ApiStatus/currentBlockHeight'] + 1 && + txo.amount > 0 && + (txo.isPrivate ? (isSparkAllowed ? ["spark-mint", "spark-smint"].includes(txo.scriptType) : ["lelantus-mint", "lelantus-jmint"].includes(txo.scriptType)) : true) + ) + }, lockedUTXOs: (state, getters) => getters.UTXOs.filter((txo: TXO) => txo.isLocked), + availableUTXOsWithLock: (state, getters, rootState, rootGetters): TXO[] => { + let isSparkAllowed: boolean = rootGetters['ApiStatus/isSparkAllowed']; + return getters.UTXOs.filter((txo: TXO) => + txo.isToMe && + // Elysium has reference outputs that we should not allow the user to spend. + !txo.isElysiumReferenceOutput && + txo.spendSize && + txo.validAt <= rootGetters['ApiStatus/currentBlockHeight'] + 1 && + txo.amount > 0 && + (txo.isPrivate ? (isSparkAllowed ? ["spark-mint", "spark-smint"].includes(txo.scriptType) : ["lelantus-mint", "lelantus-jmint"].includes(txo.scriptType)) : true) + ) + }, // This will display: // 1) valid Elysium non-Lelantus Mint transactions @@ -236,23 +314,23 @@ const getters = { !(txo.isElysiumReferenceOutput && txo.elysium.property && !rootGetters['Elysium/selectedTokens'].includes(txo.elysium.property.creationTx)) && !((txo.blockHeight || !txo.isFromMe) && txo.elysium.valid === false) && !(txo.elysium.type === 'Lelantus Mint') && - (txo.isElysiumReferenceOutput || txo.destination) && + (txo.isElysiumReferenceOutput || txo.destination || (txo.inputPrivacy === 'sparkmint' || (txo.inputPrivacy === 'sparkspend'))) && (txo.isInstantSendLocked || txo.blockHeight || txo.isFromMe) && (txo.isFromMe || txo.isToMe || txo.elysium.isToMe) ) .sort((a, b) => b.firstSeenAt - a.firstSeenAt), - selectInputs: (state, getters): (isPrivate: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean) => CoinControl => { + selectInputs: (state, getters): (isPrivate: boolean, isSpark: boolean, istransparentaddress: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean) => CoinControl => { getters.availableUTXOs; - return (isPrivate: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean): CoinControl => { - return selectUTXOs(isPrivate, amount, feePerKb, subtractFeeFromAmount, getters.availableUTXOs, false)[1].map(utxo => [utxo.txid, utxo.index]); + return (isPrivate: boolean, isSpark: boolean, istransparentaddress: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean): CoinControl => { + return selectUTXOs(isPrivate, isSpark, istransparentaddress, amount, feePerKb, subtractFeeFromAmount, getters.availableUTXOs, false)[1].map(utxo => [utxo.txid, utxo.index]); } }, - calculateTransactionFee: (state, getters): (isPrivate: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean, coinControl?: TXO[]) => bigint => { + calculateTransactionFee: (state, getters): (isPrivate: boolean, isSpark: boolean, istransparentaddress: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean, coinControl?: TXO[]) => bigint => { getters.availableUTXOs; - return (isPrivate: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean, coinControl?: TXO[]): bigint => { - const x = selectUTXOs(isPrivate, amount, feePerKb, subtractFeeFromAmount, coinControl ? coinControl : getters.availableUTXOs, !!coinControl); + return (isPrivate: boolean, isSpark: boolean, istransparentaddress: boolean, amount: bigint, feePerKb: bigint, subtractFeeFromAmount: boolean, coinControl?: TXO[]): bigint => { + const x = selectUTXOs(isPrivate, isSpark, istransparentaddress, amount, feePerKb, subtractFeeFromAmount, coinControl ? coinControl : getters.availableUTXOs, !!coinControl); return x?.[0]; }; }