diff --git a/locale/cnr/translation.toml b/locale/cnr/translation.toml index 90d2c3514..12dee5057 100644 --- a/locale/cnr/translation.toml +++ b/locale/cnr/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Interna greška, molimo pokušajte ponovo kasnije" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/de/translation.toml b/locale/de/translation.toml index 6c4c87d87..7a6226e42 100644 --- a/locale/de/translation.toml +++ b/locale/de/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Interner Fehler, bitte versuche es später erneut" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/en/translation.toml b/locale/en/translation.toml index d0956ed49..2ebb3a3c7 100644 --- a/locale/en/translation.toml +++ b/locale/en/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "Save Wallet File" # Save Wallet File proposalOverBudget = "Over Budget" # Over Budget badSaplingRoot = "There was an error while syncing. Resyncing from scratch (Bad sapling root)" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "Creating SHIELD transaction..." # Creating SHIELD transaction... +autoShieldTransaction = "Auto shield transaction" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Internal error, please try again later" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "Confirm this transaction matches the one on your {hardwareW CONFIRM_LEDGER_TX_OUT = "You will send {value} {ticker} to
{address}
" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "Balance is too small! Missing {sats} sats!" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "Shield is not enabled in this wallet!" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "Cannot auto shield to a shield address!" # Cannot auto shield to a shield address! diff --git a/locale/es-mx/translation.toml b/locale/es-mx/translation.toml index 8799397c0..557b5c2cb 100644 --- a/locale/es-mx/translation.toml +++ b/locale/es-mx/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Error interno, vuelve a intentarlo más tarde" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/fr/translation.toml b/locale/fr/translation.toml index 3ebe8c866..57788276e 100644 --- a/locale/fr/translation.toml +++ b/locale/fr/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Erreur interne, veuillez réessayer plus tard" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/it/translation.toml b/locale/it/translation.toml index 4af5510b4..5c92b05ee 100644 --- a/locale/it/translation.toml +++ b/locale/it/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Errore interno, rirova più tardi" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "Conferma che questa transazione combacia quella nel tuo {ha CONFIRM_LEDGER_TX_OUT = "Manderai {value} {ticker} a
{address}
" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "Il bilancio non è sufficiente! Mancano {sats} sats!" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "Lo Shield non è abilitato nel wallet!" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/nl/translation.toml b/locale/nl/translation.toml index e70dc7836..af2da80f8 100644 --- a/locale/nl/translation.toml +++ b/locale/nl/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Interne fout, probeer het later opnieuw" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/ph/translation.toml b/locale/ph/translation.toml index 36d2dba1f..8daedffc1 100644 --- a/locale/ph/translation.toml +++ b/locale/ph/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Internal error, Pakiusap uliting muli" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/pl/translation.toml b/locale/pl/translation.toml index 82bfc7344..63972985d 100644 --- a/locale/pl/translation.toml +++ b/locale/pl/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Błąd wewnętrzny, spróbuj ponownie później" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/pt-br/translation.toml b/locale/pt-br/translation.toml index b1ec19379..13ea80fc4 100644 --- a/locale/pt-br/translation.toml +++ b/locale/pt-br/translation.toml @@ -88,6 +88,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] STAKE_ADDR_SET = "Endereço de Cold Staking definido!
Ao fazer Stake no futuro este endereço irá ser usado." # Cold Address set!
Future stakes will use this address. @@ -132,3 +133,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/pt-pt/translation.toml b/locale/pt-pt/translation.toml index ea6d00b9a..9032e6fdf 100644 --- a/locale/pt-pt/translation.toml +++ b/locale/pt-pt/translation.toml @@ -88,6 +88,7 @@ saveWalletFile = "" # Save Wallet File proposalOverBudget = "" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] STAKE_ADDR_SET = "Endereço de Cold Staking definido!
Ao fazer Stake no futuro irá ser usado este endereço." # Cold Address set!
Future stakes will use this address. @@ -132,3 +133,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/locale/template/translation.toml b/locale/template/translation.toml index cd777e5b4..adc38913a 100644 --- a/locale/template/translation.toml +++ b/locale/template/translation.toml @@ -245,6 +245,7 @@ shieldAddress = "Shield address" cantShieldToExc = "This address does not support shield transfers" badSaplingRoot = "There was an error while syncing. Resyncing from scratch (Bad sapling root)" creatingShieldTransaction = "Creating SHIELD transaction..." +autoShieldTransaction = "Auto shield transaction" [ALERTS] INTERNAL_ERROR = "Internal error, please try again later" @@ -356,4 +357,5 @@ CONFIRM_POPUP_DELETE_ACCOUNT_TITLE = 'Are you sure?' CONFIRM_LEDGER_TX = 'Confirm this transaction matches the one on your {hardwareWallet}' CONFIRM_LEDGER_TX_OUT = 'You will send {value} {ticker} to
{address}
' MISSING_FUNDS = 'Balance is too small! Missing {sats} sats!' -MISSING_SHIELD = "Shield is not enabled in this wallet!" \ No newline at end of file +MISSING_SHIELD = "Shield is not enabled in this wallet!" +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "Cannot auto shield to a shield address!" \ No newline at end of file diff --git a/locale/uwu/translation.toml b/locale/uwu/translation.toml index 2ec8dde52..1a0aaab21 100644 --- a/locale/uwu/translation.toml +++ b/locale/uwu/translation.toml @@ -221,6 +221,7 @@ saveWalletFile = "Save Wawwet File" # Save Wallet File proposalOverBudget = "Over Budgey" # Over Budget badSaplingRoot = "" # There was an error while syncing. Resyncing from scratch (Bad sapling root) creatingShieldTransaction = "" # Creating SHIELD transaction... +autoShieldTransaction = "" # Auto shield transaction [ALERTS] INTERNAL_ERROR = "Internal error, pwease try again later" # Internal error, please try again later @@ -333,3 +334,4 @@ CONFIRM_LEDGER_TX = "" # Confirm this transaction matches the one on your {hardw CONFIRM_LEDGER_TX_OUT = "" # You will send {value} {ticker} to
{address}
MISSING_FUNDS = "" # Balance is too small! Missing {sats} sats! MISSING_SHIELD = "" # Shield is not enabled in this wallet! +AUTO_SHIELDING_TO_SHIELD_ADDRESS = "" # Cannot auto shield to a shield address! diff --git a/scripts/composables/use_wallet.js b/scripts/composables/use_wallet.js index bceb179bb..905d07ed6 100644 --- a/scripts/composables/use_wallet.js +++ b/scripts/composables/use_wallet.js @@ -73,6 +73,15 @@ export const useWallet = defineStore('wallet', () => { getEventEmitter().on('shield-loaded-from-disk', () => { hasShield.value = wallet.hasShield(); }); + const sendTransaction = async (network, tx) => { + const res = await network.sendTransaction(tx.serialize()); + if (res) { + await wallet.addTransaction(tx); + } else { + wallet.discardTransaction(tx); + } + return res; + }; const createAndSendTransaction = lockableFunction( async (network, address, value, opts) => { const tx = wallet.createTransaction(address, value, opts); @@ -81,15 +90,17 @@ export const useWallet = defineStore('wallet', () => { } else { await wallet.sign(tx); } - const res = await network.sendTransaction(tx.serialize()); - if (res) { - await wallet.addTransaction(tx); - } else { - wallet.discardTransaction(tx); - } - return res; + return await sendTransaction(network, tx); } ); + const createAutoshieldTransactions = async (network, address, value) => { + const txs = await wallet.createAutoshieldTransactions(address, value); + let res = true; + for (const tx of txs) { + res = res && (await sendTransaction(network, tx)); + } + return res; + }; const isCreatingTransaction = () => createAndSendTransaction.isLocked(); getEventEmitter().on('toggle-network', async () => { @@ -136,5 +147,6 @@ export const useWallet = defineStore('wallet', () => { createAndSendTransaction, loadFromDisk, coldBalance, + createAutoshieldTransactions, }; }); diff --git a/scripts/contacts-book.js b/scripts/contacts-book.js index 08b51564b..041301a31 100644 --- a/scripts/contacts-book.js +++ b/scripts/contacts-book.js @@ -7,6 +7,7 @@ import { createAlert, createQR, getImageFile, + isShieldAddress, isValidPIVXAddress, isXPub, sanitizeHTML, @@ -935,8 +936,12 @@ export async function guiSetAccountName(strDOM) { /** * Get the address color based on the validity of an address/contact * @param {string} address + * @param {boolean} [invalidateShieldAddresses] - if set to true, shield addresses will return as invalid color */ -export async function getAddressColor(address) { +export async function getAddressColor( + address, + invalidateShieldAddresses = false +) { // If the value is empty, we don't do any checks and simply reset the colourcoding if (!address) { return ''; @@ -955,6 +960,9 @@ export async function getAddressColor(address) { // Yep, nice! return 'green'; } + if (invalidateShieldAddresses && isShieldAddress(address)) { + return '#b20000'; + } if (isValidPIVXAddress(address)) { // Yep! return 'green'; diff --git a/scripts/dashboard/Dashboard.vue b/scripts/dashboard/Dashboard.vue index 0779fbb3f..35c369729 100644 --- a/scripts/dashboard/Dashboard.vue +++ b/scripts/dashboard/Dashboard.vue @@ -198,8 +198,9 @@ function lockWallet() { * Sends a transaction * @param {string} address - Address or contact to send to * @param {number} amount - Amount of PIVs to send + * @param {boolean} isAutoShield - Whether or not this is an autoshield transaction */ -async function send(address, amount, useShieldInputs) { +async function send(address, amount, useShieldInputs, isAutoShield) { // Ensure a wallet is unlocked if (wallet.isViewOnly && !wallet.isHardwareWallet) { if ( @@ -251,6 +252,10 @@ async function send(address, amount, useShieldInputs) { } } + if (isAutoShield && isShieldAddress(address)) { + return createAlert('warning', ALERTS.AUTO_SHIELDING_TO_SHIELD_ADDRESS); + } + // If this is an XPub, we'll fetch their last used 'index', and derive a new public key for enhanced privacy if (isXPub(address)) { const cNet = getNetwork(); @@ -325,9 +330,22 @@ async function send(address, amount, useShieldInputs) { // Create and send the TX try { - await wallet.createAndSendTransaction(getNetwork(), address, nValue, { - useShieldInputs, - }); + if (isAutoShield) { + await wallet.createAutoshieldTransactions( + getNetwork(), + address, + nValue + ); + } else { + await wallet.createAndSendTransaction( + getNetwork(), + address, + nValue, + { + useShieldInputs, + } + ); + } } catch (e) { console.error(e); createAlert('warning', e); diff --git a/scripts/dashboard/TransferMenu.vue b/scripts/dashboard/TransferMenu.vue index 943840073..b2829959c 100644 --- a/scripts/dashboard/TransferMenu.vue +++ b/scripts/dashboard/TransferMenu.vue @@ -17,8 +17,13 @@ const emit = defineEmits([ // Amount of PIVs to send in the selected currency (e.g. USD) const amountCurrency = ref(''); const useShieldInputs = ref(false); +const autoShieldTransaction = ref(false); const color = ref(''); +watch(useShieldInputs, (useShieldInputs) => { + if (!useShieldInputs) autoShieldTransaction.value = false; +}); + const props = defineProps({ show: Boolean, price: Number, @@ -35,9 +40,16 @@ const address = computed({ }, set(value) { emit('update:address', value); - getAddressColor(value).then((c) => (color.value = c)); + getAddressColor( + value, + autoShieldTransaction.value || !props.shieldEnabled + ).then((c) => (color.value = c)); }, }); +watch(autoShieldTransaction, () => { + // Trigger address setter so we can recolor shield addresses + address.value = address.value; +}); const amount = computed({ get() { return props.amount; @@ -55,7 +67,8 @@ function send() { 'send', sanitizeHTML(address.value), amount.value, - useShieldInputs.value + useShieldInputs.value, + autoShieldTransaction.value ); } @@ -254,6 +267,23 @@ async function selectContact() { }}
+
+
+ + +
+
+
diff --git a/scripts/transaction_builder.js b/scripts/transaction_builder.js index 4c726e82c..ebe5a3c58 100644 --- a/scripts/transaction_builder.js +++ b/scripts/transaction_builder.js @@ -4,6 +4,7 @@ import { OP } from './script.js'; import { hexToBytes, bytesToHex, dSHA256 } from './utils.js'; import { isShieldAddress, isExchangeAddress } from './misc.js'; import { SAPLING_TX_VERSION } from './chain_params.js'; +import { numToVarInt } from './encoding.js'; /** * @class Builds a non-signed transaction */ @@ -14,9 +15,9 @@ export class TransactionBuilder { // Part of the tx fee that has been already handled #handledFee = 0; - MIN_FEE_PER_BYTE = 10; + static MIN_FEE_PER_BYTE = 10; // This number is larger or equal than the max size of the script sig for a P2CS and P2PKH transaction - SCRIPT_SIG_MAX_SIZE = 108; + static SCRIPT_SIG_MAX_SIZE = 108; get valueIn() { return this.#valueIn; @@ -35,7 +36,9 @@ export class TransactionBuilder { */ isDust({ out, value }) { // Dust is a transaction such that its creation costs more than its value - return value < out.serialize().length * this.MIN_FEE_PER_BYTE; + return ( + value < out.serialize().length * TransactionBuilder.MIN_FEE_PER_BYTE + ); } constructor() { @@ -52,18 +55,37 @@ export class TransactionBuilder { return new TransactionBuilder(); } + /** + * @returns {number} Amount of fee in sats of a standard transaction based on the + * number of inputs and outputs + */ + static getStandardTxFee(nInputs, nOutputs) { + const inputVarIntLength = numToVarInt(nInputs).length; + const outputVarIntLength = numToVarInt(nOutputs).length; + return ( + (nInputs * (TransactionBuilder.SCRIPT_SIG_MAX_SIZE + 41) + + nOutputs * 34 + + 8 + + inputVarIntLength + + outputVarIntLength) * + TransactionBuilder.MIN_FEE_PER_BYTE + ); + } + getFee() { //TODO: find a cleaner way to add the dummy signature let scriptSig = []; for (let vin of this.#transaction.vin) { scriptSig.push(vin.scriptSig); // Insert a dummy signature just to compute fees - vin.scriptSig = bytesToHex(Array(this.SCRIPT_SIG_MAX_SIZE).fill(0)); + vin.scriptSig = bytesToHex( + Array(TransactionBuilder.SCRIPT_SIG_MAX_SIZE).fill(0) + ); } const fee = Math.ceil(this.#transaction.serialize().length / 2) * - this.MIN_FEE_PER_BYTE - + TransactionBuilder.MIN_FEE_PER_BYTE - this.#handledFee; // Re-insert whatever was inside before for (let i = 0; i < scriptSig.length; i++) { @@ -174,7 +196,8 @@ export class TransactionBuilder { if (this.isDust({ out, value }) && subtractFeeFromAmt) { return; } - const fee = out.serialize().length * this.MIN_FEE_PER_BYTE; + const fee = + out.serialize().length * TransactionBuilder.MIN_FEE_PER_BYTE; // We have subtracted fees from the value, mark this fee as handled (don't pay them again) if (subtractFeeFromAmt) { out.value -= fee; diff --git a/scripts/wallet.js b/scripts/wallet.js index fd864bb04..60b99cae0 100644 --- a/scripts/wallet.js +++ b/scripts/wallet.js @@ -5,8 +5,13 @@ import { beforeUnloadListener, blockCount } from './global.js'; import { getNetwork } from './network.js'; import { MAX_ACCOUNT_GAP, SHIELD_BATCH_SYNC_SIZE } from './chain_params.js'; import { HistoricalTx, HistoricalTxType } from './historical_tx.js'; -import { COutpoint, Transaction } from './transaction.js'; -import { confirmPopup, createAlert, isShieldAddress } from './misc.js'; +import { COutpoint, Transaction, UTXO } from './transaction.js'; +import { + confirmPopup, + createAlert, + isShieldAddress, + isValidPIVXAddress, +} from './misc.js'; import { cChainParams } from './chain_params.js'; import { COIN } from './chain_params.js'; import { ALERTS, tr, translation } from './i18n.js'; @@ -1102,6 +1107,50 @@ export class Wallet { } } + /** + * @param {string} address + * @param {number} value - Value to send in satoshi + * @returns {Transaction[]} Two transactions, the first one deshielding `value`, the second one sending `value` to `address` + */ + async createAutoshieldTransactions(address, value) { + if (!this.hasShield()) { + throw new Error( + 'trying to create a shield transaction without having shield enabled' + ); + } + if (isShieldAddress(address) || !isValidPIVXAddress(address)) + throw new Error('Invalid address'); + const [intermediaryAddress] = this.getNewAddress(1); + const firstTx = await this.sign( + this.createTransaction( + intermediaryAddress, + value + TransactionBuilder.getStandardTxFee(1, 1), + { + useShieldInputs: true, + } + ) + ); + const txBuilder = new TransactionBuilder() + .addUTXO( + new UTXO({ + outpoint: new COutpoint({ + txid: firstTx.txid, + n: 0, + }), + value: firstTx.vout[0].value, + script: firstTx.vout[0].script, + }) + ) + .addOutput({ + address, + value: firstTx.vout[0].value, + }); + txBuilder.equallySubtractAmt(txBuilder.getFee()); + const tx = txBuilder.build(); + await this.sign(tx); + return [firstTx, tx]; + } + /** * @param {import('./transaction.js').Transaction} transaction - transaction to sign * @throws {Error} if the wallet is view only diff --git a/tests/components/TransferMenu.test.js b/tests/components/TransferMenu.test.js index 9f4a7b28c..ba3cdedbb 100644 --- a/tests/components/TransferMenu.test.js +++ b/tests/components/TransferMenu.test.js @@ -63,7 +63,7 @@ it('Sends transaction correctly', async () => { expect(wrapper.emitted('send')).toBeUndefined(); await wrapper.find('[data-testid=sendButton]').trigger('click'); expect(wrapper.emitted('send')).toStrictEqual([ - ['DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bc', '60', false], + ['DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bc', '60', false, false], ]); }); @@ -74,6 +74,6 @@ it('Sends transaction with shield inputs', async () => { await wrapper.find('[data-testid=sendButton]').trigger('click'); expect(wrapper.emitted('send')).toStrictEqual([ - ['DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bc', '60', true], + ['DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bc', '60', true, false], ]); }); diff --git a/tests/unit/transaction_builder.spec.js b/tests/unit/transaction_builder.spec.js index 3e5c8d8c1..bf22fb3b7 100644 --- a/tests/unit/transaction_builder.spec.js +++ b/tests/unit/transaction_builder.spec.js @@ -7,6 +7,7 @@ import { UTXO, } from '../../scripts/transaction.js'; import { TransactionBuilder } from '../../scripts/transaction_builder.js'; +import { bytesToHex } from '../../scripts/utils'; describe('Transaction builder tests', () => { it('Builds a transaction correctly', () => { @@ -214,4 +215,80 @@ describe('Transaction builder tests', () => { }) ).toThrow(/address/); }); + + it('returns correct fee on standard tx', () => { + let tx = new TransactionBuilder().build(); + tx.version = 1; + expect(TransactionBuilder.getStandardTxFee(0, 0)).toBe( + (tx.serialize().length / 2) * TransactionBuilder.MIN_FEE_PER_BYTE + ); + tx = new TransactionBuilder() + .addOutput({ + address: 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', + value: 1000, + }) + .build(); + tx.version = 1; + expect(TransactionBuilder.getStandardTxFee(0, 1)).toBe( + (tx.serialize().length / 2) * TransactionBuilder.MIN_FEE_PER_BYTE + ); + tx = new TransactionBuilder() + .addOutput({ + address: 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', + value: 1000, + }) + .addOutput({ + address: 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', + value: 1000, + }) + .build(); + tx.version = 1; + expect(TransactionBuilder.getStandardTxFee(0, 2)).toBe( + (tx.serialize().length / 2) * TransactionBuilder.MIN_FEE_PER_BYTE + ); + tx = new TransactionBuilder() + .addUTXO( + new UTXO({ + outpoint: new COutpoint({ + txid: 'a4dce96d30fd6a5acb63dd25b4d59e4216824ec0bbabe54f237cf9754f9b62bc', + n: 4, + }), + script: bytesToHex( + Array(TransactionBuilder.SCRIPT_SIG_MAX_SIZE).fill(0) + ), + value: 5, + }) + ) + .build(); + tx.version = 1; + expect(TransactionBuilder.getStandardTxFee(1, 0)).toBe( + (tx.serialize().length / 2) * TransactionBuilder.MIN_FEE_PER_BYTE + ); + tx = new TransactionBuilder() + .addUTXO( + new UTXO({ + outpoint: new COutpoint({ + txid: 'a4dce96d30fd6a5acb63dd25b4d59e4216824ec0bbabe54f237cf9754f9b62bc', + n: 4, + }), + script: bytesToHex( + Array(TransactionBuilder.SCRIPT_SIG_MAX_SIZE).fill(0) + ), + value: 5, + }) + ) + .addOutput({ + address: 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', + value: 1000, + }) + .addOutput({ + address: 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', + value: 1000, + }) + .build(); + tx.version = 1; + expect(TransactionBuilder.getStandardTxFee(1, 2)).toBe( + (tx.serialize().length / 2) * TransactionBuilder.MIN_FEE_PER_BYTE + ); + }); }); diff --git a/tests/unit/wallet/transactions.spec.js b/tests/unit/wallet/transactions.spec.js index b682ef28e..4f87b6434 100644 --- a/tests/unit/wallet/transactions.spec.js +++ b/tests/unit/wallet/transactions.spec.js @@ -1,6 +1,6 @@ import { Wallet } from '../../../scripts/wallet.js'; import { Mempool } from '../../../scripts/mempool.js'; -import { setUpLegacyMainnetWallet } from '../../utils/test_utils'; +import { PIVXShield, setUpLegacyMainnetWallet } from '../../utils/test_utils'; import { describe, it, vi, afterAll, expect } from 'vitest'; import { COutpoint, @@ -11,6 +11,8 @@ import { import 'fake-indexeddb/auto'; import { TransactionBuilder } from '../../../scripts/transaction_builder.js'; +import { setUpHDMainnetWallet } from '../../utils/test_utils'; +import { COIN } from '../../../scripts/chain_params'; vi.stubGlobal('localStorage', { length: 0 }); vi.mock('../../../scripts/global.js'); @@ -36,8 +38,11 @@ async function checkFees(wallet, tx, feesPerBytes) { expect(fees).toBeLessThanOrEqual((feesPerBytes + 1) * nBytes); } describe('Wallet transaction tests', () => { + /** + * @type {Wallet} + */ let wallet; - const MIN_FEE_PER_BYTE = new TransactionBuilder().MIN_FEE_PER_BYTE; + const MIN_FEE_PER_BYTE = TransactionBuilder.MIN_FEE_PER_BYTE; beforeEach(async () => { wallet = await setUpLegacyMainnetWallet(); @@ -376,6 +381,68 @@ describe('Wallet transaction tests', () => { expect(spy).toBeCalledWith(tx); }); + it('throws when calling auto shield transaction on a legacy wallet', () => { + wallet.setShield(null); + expect( + wallet.createAutoshieldTransactions( + 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', + 1 + ) + ).rejects.toThrowError('shield enabled'); + }); + + it('creates auto shield transaction correctly', async () => { + wallet = await setUpHDMainnetWallet(true); + let shieldTx; + PIVXShield.prototype.createTransaction.mockImplementationOnce( + ({ address, amount }) => { + shieldTx = new TransactionBuilder() + .addOutput({ + address, + value: amount, + }) + .build(); + return { + hex: shieldTx.serialize(), + }; + } + ); + const txs = await wallet.createAutoshieldTransactions( + 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', + 1 * COIN + ); + expect(txs).toHaveLength(2); + expect(txs[0].serialize()).toBe(shieldTx.serialize()); + expect(txs[1].vin).toStrictEqual([ + new CTxIn({ + outpoint: new COutpoint({ + txid: shieldTx.txid, + n: 0, + }), + scriptSig: + '47304402203185d51275f4bf0057ffc9191b58d2e26052b67f7767542bbb7960cae3b26b97022037e86cc2a0c74f33e3490283c0cf7ab42414b9c7574c0a9defb8c21b32ad8a9b012102ecd478b763612a71351b16165dac15afd591c33e28cf7ca7b45b7189691b6373', + }), + ]); + expect(txs[1].vout).toStrictEqual([ + new CTxOut({ + script: '76a914a95cc6408a676232d61ec29dc56a180b5847835788ac', + value: 1 * COIN, + }), + ]); + }); + + it('throws when auto shielding to invalid or shield addresses', () => { + expect( + wallet.createAutoshieldTransactions('Invalidaddress', 1) + ).rejects.toThrowError('Invalid address'); + expect( + wallet.createAutoshieldTransactions( + 'ps1a0x2few52sy3t0nrdhun0re4c870e04w448qpa7c26qjw9ljs4quhja40hat95f7hy8tcuvcn2s', + 1 + ) + ).rejects.toThrowError('Invalid address'); + }); + afterAll(() => { vi.clearAllMocks(); });