diff --git a/CHANGELOG.md b/CHANGELOG.md index a2b392a..aa16651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- added: Support for multiple spend targets. + ## 1.4.2 (2025-04-01) - fixed: Small internal implementation cleanup on how transactions are saved. diff --git a/src/MoneroEngine.ts b/src/MoneroEngine.ts index fd3385b..f7c598c 100644 --- a/src/MoneroEngine.ts +++ b/src/MoneroEngine.ts @@ -2,7 +2,7 @@ * Created by paul on 7/7/17. */ -import { div, eq, gte, lt, sub } from 'biggystring' +import { add, div, eq, gte, lt, sub } from 'biggystring' import type { Disklet } from 'disklet' import { EdgeCorePluginOptions, @@ -600,11 +600,15 @@ export class MoneroEngine implements EdgeCurrencyEngine { throw new TypeError('Missing destination address') } - const options = { - amount: '0', + const options: CreateTransactionOptions = { isSweepTx: true, priority: translateFee(edgeSpendInfo.networkFeeOption), - targetAddress: publicAddress + targets: [ + { + amount: '0', + targetAddress: publicAddress + } + ] } const result = await this.createMyMoneroTransaction(options, privateKeys) @@ -642,39 +646,45 @@ export class MoneroEngine implements EdgeCurrencyEngine { const { memos = [] } = edgeSpendInfo const privateKeys = asPrivateKeys(opts?.privateKeys) - // Monero can only have one output - // TODO: The new SDK fixes this! - if (edgeSpendInfo.spendTargets.length !== 1) { - throw new Error('Error: only one output allowed') - } + const { spendTargets } = edgeSpendInfo - const [spendTarget] = edgeSpendInfo.spendTargets - const { publicAddress, nativeAmount } = spendTarget - if (publicAddress == null) { - throw new TypeError('Missing destination address') - } - if (nativeAmount == null || eq(nativeAmount, '0')) { - throw new NoAmountSpecifiedError() - } + let totalAmount = '0' + const targets: CreateTransactionOptions['targets'] = [] - if ( - gte( - nativeAmount, - this.walletLocalData.totalBalances.get(PRIMARY_CURRENCY_TOKEN_ID) ?? '0' - ) - ) { - if (gte(this.walletLocalData.lockedXmrBalance, nativeAmount)) { - throw new PendingFundsError() - } else { - throw new InsufficientFundsError({ tokenId: PRIMARY_CURRENCY_TOKEN_ID }) + for (const spendTarget of spendTargets) { + const { publicAddress, nativeAmount } = spendTarget + if (publicAddress == null) { + throw new TypeError('Missing destination address') + } + if (nativeAmount == null || eq(nativeAmount, '0')) { + throw new NoAmountSpecifiedError() + } + totalAmount = add(totalAmount, nativeAmount) + if ( + gte( + totalAmount, + this.walletLocalData.totalBalances.get(PRIMARY_CURRENCY_TOKEN_ID) ?? + '0' + ) + ) { + if (gte(this.walletLocalData.lockedXmrBalance, totalAmount)) { + throw new PendingFundsError() + } else { + throw new InsufficientFundsError({ + tokenId: PRIMARY_CURRENCY_TOKEN_ID + }) + } } + targets.push({ + amount: div(nativeAmount, '1000000000000', 12), + targetAddress: publicAddress + }) } const options: CreateTransactionOptions = { - amount: div(nativeAmount, '1000000000000', 12), isSweepTx: false, priority: translateFee(edgeSpendInfo.networkFeeOption), - targetAddress: publicAddress + targets } this.log(`Creating transaction: ${JSON.stringify(options, null, 1)}`) diff --git a/src/MyMoneroApi.ts b/src/MyMoneroApi.ts index 62ac8aa..d07c9ee 100644 --- a/src/MyMoneroApi.ts +++ b/src/MyMoneroApi.ts @@ -44,11 +44,13 @@ export interface BalanceResults { } export interface CreateTransactionOptions { - amount: string + targets: Array<{ + amount: string + targetAddress: string + }> isSweepTx?: boolean paymentId?: string priority?: Priority - targetAddress: string } const asNumberBoolean: Cleaner = raw => { @@ -238,13 +240,7 @@ export class MyMoneroApi { opts: CreateTransactionOptions ): Promise { const { address, privateSpendKey, privateViewKey, publicSpendKey } = keys - const { - amount, - isSweepTx = false, - paymentId, - priority = 1, - targetAddress - } = opts + const { isSweepTx = false, paymentId, priority = 1, targets } = opts // Grab the UTXO set: const unspentOuts = await this.fetchPostMyMonero('get_unspent_outs', { @@ -268,14 +264,14 @@ export class MyMoneroApi { }) } + const destinations = targets.map(t => ({ + send_amount: t.amount, + to_address: t.targetAddress + })) + // Make the transaction: return await this.cppBridge.createTransaction({ - destinations: [ - { - send_amount: amount, - to_address: targetAddress - } - ], + destinations, priority, address, paymentId,