From f38efc330dc10b0be5ee0bccab9ef2052031cc34 Mon Sep 17 00:00:00 2001 From: surangap Date: Tue, 25 May 2021 11:46:39 +0800 Subject: [PATCH] Add `wallet.sendMany` RPC (#269) --- .../__tests__/category/wallet.test.ts | 140 +++++++++++++++++- .../jellyfish-api-core/src/category/wallet.ts | 43 ++++++ website/docs/jellyfish/api/wallet.md | 23 +++ 3 files changed, 205 insertions(+), 1 deletion(-) diff --git a/packages/jellyfish-api-core/__tests__/category/wallet.test.ts b/packages/jellyfish-api-core/__tests__/category/wallet.test.ts index f91139a5c7..42d92b7fbf 100644 --- a/packages/jellyfish-api-core/__tests__/category/wallet.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/wallet.test.ts @@ -2,7 +2,7 @@ import { ContainerAdapterClient } from '../container_adapter_client' import { MasterNodeRegTestContainer, RegTestContainer } from '@defichain/testcontainers' import { BigNumber, wallet } from '../../src' import waitForExpect from 'wait-for-expect' -import { UTXO, ListUnspentOptions, WalletFlag, SendToAddressOptions, Mode } from '../../src/category/wallet' +import { UTXO, ListUnspentOptions, WalletFlag, SendToAddressOptions, Mode, SendManyOptions } from '../../src/category/wallet' describe('non masternode', () => { const container = new RegTestContainer() @@ -632,3 +632,141 @@ describe('masternode', () => { }) }) }) + +describe('sendMany', () => { + // NOTE(surangap): defid side(c++) does not have much tests for sendmany RPC atm. + const container = new MasterNodeRegTestContainer() + const client = new ContainerAdapterClient(container) + + beforeAll(async () => { + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + await container.waitForWalletBalanceGTE(101) + }) + + afterAll(async () => { + await container.stop() + }) + + // Returns matching utxos for given transaction id and address. + const getMatchingUTXO = async (txId: string, address: string): Promise => { + const options: ListUnspentOptions = { + addresses: [address] + } + + const utxos: UTXO[] = await client.wallet.listUnspent(1, 9999999, options) + return utxos.filter((utxo) => { + return (utxo.address === address) && (utxo.txid === txId) + }) + } + + it('should send one address using sendMany', async () => { + const amounts: Record = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001 } + const transactionId = await client.wallet.sendMany(amounts) + expect(typeof transactionId).toBe('string') + + // generate one block + await container.generate(1) + + // check the corresponding UTXO + const utxos = await getMatchingUTXO(transactionId, 'mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU') + // In this case the we should only have one matching utxo + expect(utxos.length).toStrictEqual(1) + utxos.forEach(utxo => { + expect(utxo.address).toStrictEqual('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU') + expect(utxo.amount).toStrictEqual(new BigNumber(0.00001)) + }) + }) + + it('should send multiple address using sendMany', async () => { + const amounts: Record = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 } + const transactionId = await client.wallet.sendMany(amounts) + expect(typeof transactionId).toBe('string') + + // generate one block + await container.generate(1) + + // check the corresponding UTXOs + const utxos = await getMatchingUTXO(transactionId, 'mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU') + // In this case the we should only have one matching utxo + expect(utxos.length).toStrictEqual(1) + utxos.forEach(utxo => { + expect(utxo.address).toStrictEqual('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU') + expect(utxo.amount).toStrictEqual(new BigNumber(0.00001)) + }) + + const utxos2 = await getMatchingUTXO(transactionId, 'mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy') + // In this case the we should only have one matching utxo + expect(utxos2.length).toStrictEqual(1) + utxos2.forEach(utxo => { + expect(utxo.address).toStrictEqual('mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy') + expect(utxo.amount).toStrictEqual(new BigNumber(0.00002)) + }) + }) + + it('should sendMany with comment', async () => { + const amounts: Record = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 } + const options: SendManyOptions = { + comment: 'test comment' + } + const transactionId = await client.wallet.sendMany(amounts, [], options) + expect(typeof transactionId).toBe('string') + }) + + it('should sendMany with replaceable', async () => { + const amounts: Record = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 } + const options: SendManyOptions = { + replaceable: true + } + const transactionId = await client.wallet.sendMany(amounts, [], options) + expect(typeof transactionId).toBe('string') + }) + + it('should sendMany with confTarget', async () => { + const amounts: Record = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 } + const options: SendManyOptions = { + confTarget: 60 + } + const transactionId = await client.wallet.sendMany(amounts, [], options) + expect(typeof transactionId).toBe('string') + }) + + it('should sendMany with estimateMode', async () => { + const amounts: Record = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 0.00002 } + const options: SendManyOptions = { + estimateMode: Mode.ECONOMICAL + } + const transactionId = await client.wallet.sendMany(amounts, [], options) + expect(typeof transactionId).toBe('string') + }) + + it('should sendMany with fee substracted from mentioned recipients', async () => { + const amounts: Record = { mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU: 0.00001, mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy: 10.5 } + const subtractFeeFrom: string [] = ['mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy'] + const transactionId = await client.wallet.sendMany(amounts, subtractFeeFrom) + expect(typeof transactionId).toBe('string') + + // generate one block + await container.generate(1) + + // check the corresponding UTXOs + const utxos = await getMatchingUTXO(transactionId, 'mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU') + // In this case the we should only have one matching utxo + expect(utxos.length).toStrictEqual(1) + utxos.forEach(utxo => { + expect(utxo.address).toStrictEqual('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU') + // amount should be equal to 0.00001 + expect(utxo.amount).toStrictEqual(new BigNumber(0.00001)) + }) + + const utxos2 = await getMatchingUTXO(transactionId, 'mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy') + // In this case the we should only have one matching utxo + expect(utxos2.length).toStrictEqual(1) + utxos2.forEach(utxo => { + expect(utxo.address).toStrictEqual('mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy') + // amount should be less than 10.5 + expect(utxo.amount.isLessThan(10.5)).toBe(true) + }) + }) +}) diff --git a/packages/jellyfish-api-core/src/category/wallet.ts b/packages/jellyfish-api-core/src/category/wallet.ts index ae5fcf5a34..a22748ee1e 100644 --- a/packages/jellyfish-api-core/src/category/wallet.ts +++ b/packages/jellyfish-api-core/src/category/wallet.ts @@ -213,6 +213,42 @@ export class Wallet { async listAddressGroupings (): Promise { return await this.client.call('listaddressgroupings', [], 'bignumber') } + + /** + * Send given amounts to multiple given address and return a transaction id. + * + * @param {Record} amounts Dictionary/map with individual addresses and amounts + * @param {string[]} subtractfeefrom Array of addresses from which fee needs to be deducted. + * @param {SendManyOptions} options + * @param {string} [options.comment] A comment + * @param {boolean} [options.replaceable] Allow this transaction to be replaced by a transaction with higher fees via BIP 125 + * @param {number} [options.confTarget] Confirmation target (in blocks) + * @param {Mode} [options.estimateMode] The fee estimate mode, must be one of (Mode.UNSET, Mode.ECONOMICAL, Mode.CONSERVATIVE) + * @return {Promise} hex string of the transaction + */ + async sendMany ( + amounts: Record, + subtractfeefrom: string [] = [], + options: SendManyOptions = {}): Promise { + const { + comment = '', + replaceable = false, + confTarget = 6, + estimateMode = Mode.UNSET + } = options + + const dummy: string = '' // Must be set to '' for backward compatibality. + const minconf: number = 0 // Ignored dummy value + + return await this.client.call( + 'sendmany', + [ + dummy, amounts, minconf, comment, subtractfeefrom, + replaceable, confTarget, estimateMode + ], + 'bignumber' + ) + } } export interface UTXO { @@ -263,6 +299,13 @@ export interface SendToAddressOptions { avoidReuse?: boolean } +export interface SendManyOptions { + comment?: string + replaceable?: boolean + confTarget?: number + estimateMode?: Mode +} + export interface CreateWalletResult { name: string warning: string diff --git a/website/docs/jellyfish/api/wallet.md b/website/docs/jellyfish/api/wallet.md index 4f98bb5f16..e0cad446c0 100644 --- a/website/docs/jellyfish/api/wallet.md +++ b/website/docs/jellyfish/api/wallet.md @@ -256,3 +256,26 @@ interface wallet { listAddressGroupings (): Promise } ``` + +## sendMany + +Send given amounts to multiple given address and return a transaction id. + +```ts title="client.wallet.sendMany()" +interface wallet { + async sendMany (amounts: Record , subtractfeefrom: string [] = [], options: SendManyOptions = {}): Promise +} + +interface SendManyOptions { + comment?: string + replaceable?: boolean + confTarget?: number + estimateMode?: Mode +} + +enum Mode { + UNSET = 'UNSET', + ECONOMICAL = 'ECONOMICAL', + CONSERVATIVE = 'CONSERVATIVE' +} +```