Skip to content

Commit 938d1ea

Browse files
committed
Change method sendMany to receive main parameters explicit and optional as Map. Reinforce the validations of all parameters. Modified documentation to the new variables.
1 parent 2aa1c0c commit 938d1ea

File tree

3 files changed

+109
-72
lines changed

3 files changed

+109
-72
lines changed

bitgo-java-api/src/main/java/com/bitso/bitgo/v2/BitGoClient.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.bitso.bitgo.v2.entity.WalletTransferResponse;
66

77
import java.io.IOException;
8+
import java.math.BigDecimal;
89
import java.util.List;
910
import java.util.Map;
1011
import java.util.Optional;
@@ -37,24 +38,26 @@ Optional<String> login(String email, String password, String otp, boolean extens
3738
Optional<Wallet> getWalletByAddress(String coin, String waddress) throws IOException;
3839

3940
/**
40-
* Invokes the sendmany method see https://www.bitgo.com/api/v2/?shell#send-transaction-to-many
41-
* Required parameters are:
42-
* coin tbtc for test bitcoin, see full list at https://www.bitgo.com/api/v2/#coin-digital-currency-support
43-
* walletId The ID of the source wallet.
44-
* walletPass The wallet passphrase.
45-
* recipients A map with the recipients' addresses as keys and the corresponding
46-
* amounts as values. Amounts are in satoshis
47-
* sequenceId A unique identifier for this transaction (optional).
4841
*
49-
* Optional parameters are:
50-
* message Notes about the transaction (optional).
42+
* @param coin tbtc for test bitcoin, see full list at https://www.bitgo.com/api/v2/#coin-digital-currency-support
43+
* @param walletId The ID of the source wallet.
44+
* @param walletPass The wallet passphrase.
45+
* @param recipients A map with the recipients' addresses as keys and the corresponding
46+
* amounts as values. Amounts are in satoshis
47+
* @param sequenceId A unique identifier for this transaction (optional).
48+
* @param optionalParameters A map with optional extra parameters.
49+
* Some of the known optional parameters are:
50+
* message Notes about the transaction.
5151
* fee Fee (in satoshis), leave null for autodetect. Do not specify unless you are sure it is sufficient.
5252
* feeTxConfirmTarget Calculate fees per kilobyte, targeting transaction confirmation in this number of blocks. Default: 2, Minimum: 2, Maximum: 20
5353
* minConfirms only choose unspent inputs with a certain number of confirmations. We recommend setting this to 1 and using enforceMinConfirmsForChange
5454
* enforceMinConfirmsForChange Defaults to false. When constructing a transaction, minConfirms will only be enforced for unspents not originating from the wallet
5555
* @return A SendCoinsResponse, or empty if there was a problem (although more likely in case of a problem it will throw).
56+
* @throws IOException
5657
*/
57-
Optional<SendCoinsResponse> sendMany(Map<String, Object> parameters) throws IOException;
58+
Optional<SendCoinsResponse> sendMany(String coin, String walletId, String walletPass,
59+
Map<String, BigDecimal> recipients, String sequenceId,
60+
Map<String, Object> optionalParameters) throws IOException;
5861

5962
Optional<Map<String, Object>> getCurrentUserProfile() throws IOException;
6063

bitgo-java-api/src/main/java/com/bitso/bitgo/v2/BitGoClientImpl.java

Lines changed: 66 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.bitso.bitgo.v2.entity.WalletTransferResponse;
99
import com.fasterxml.jackson.core.type.TypeReference;
1010
import lombok.Getter;
11+
import lombok.NonNull;
1112
import lombok.Setter;
1213
import lombok.extern.slf4j.Slf4j;
1314

@@ -104,26 +105,25 @@ public Optional<Wallet> getWalletByAddress(String coin, String waddress) throws
104105
}
105106

106107
@Override
107-
public Optional<SendCoinsResponse> sendMany(Map<String, Object> parameters) throws IOException {
108-
// Get all needed parameters
109-
String coin = parameters.get("coin").toString();
110-
String walletId = parameters.get("walletId").toString();
111-
String walletPass = parameters.get("walletPass").toString();
112-
Map<String, Object> targets = (Map<String, Object>)parameters.get("recipients");
113-
HashMap<String, BigDecimal> recipients = new HashMap<String, BigDecimal>(targets.size());
114-
for (String address : targets.keySet()) {
115-
recipients.put(address, new BigDecimal(targets.get(address).toString()));
108+
public Optional<SendCoinsResponse> sendMany(@NonNull String coin, @NonNull String walletId, @NonNull String walletPass,
109+
@NonNull Map<String, BigDecimal> recipients, @NonNull String sequenceId,
110+
Map<String, Object> optionalParameters) throws IOException {
111+
// Validate all needed parameters
112+
if (!coin.matches("^(btc|ltc|bch|tbtc|tltc|tbch)$")) {
113+
throw new IOException("Invalid currency");
114+
}
115+
if (walletId.equals("")) {
116+
throw new IOException("WalletId can't be empty");
117+
}
118+
if (walletPass.equals("")) {
119+
throw new IOException("WalletPass can't be empty ");
120+
}
121+
if (sequenceId.equals("")) {
122+
throw new IOException("SequenceId can't be empty");
123+
}
124+
if (recipients.size() == 0) {
125+
throw new IOException("Transaction must have at least one address/amount tuple as destination");
116126
}
117-
String sequenceId = parameters.get("sequenceId").toString();
118-
119-
// Check for optional parameters or default them to null
120-
String message = parameters.containsKey("message") ? parameters.get("message").toString() : null;
121-
BigDecimal fee = parameters.containsKey("fee") ? new BigDecimal(parameters.get("fee").toString()) : null;
122-
BigDecimal feeTxConfirmTarget = parameters.containsKey("feeTxConfirmTarget") ?
123-
new BigDecimal(parameters.get("feeTxConfirmTarget").toString()) : null;
124-
int minConfirms = parameters.containsKey("minConfirms") ? Integer.parseInt(parameters.get("minConfirms").toString()) : null;
125-
boolean enforceMinConfirmsForChange = parameters.containsKey("enforceMinConfirmsForChange") ?
126-
Boolean.valueOf(parameters.get("enforceMinConfirmsForChange").toString()) : null;
127127

128128
String url = baseUrl + SEND_MANY_URL.replace("$COIN", coin).replace("$WALLET", walletId);
129129
final List<Map<String, Object>> addr = new ArrayList<>(recipients.size());
@@ -135,25 +135,56 @@ public Optional<SendCoinsResponse> sendMany(Map<String, Object> parameters) thro
135135
}
136136
final Map<String, Object> data = new HashMap<>();
137137
data.put("recipients", addr);
138-
if (message != null) {
139-
data.put("message", message);
140-
}
141-
if (sequenceId != null) {
142-
data.put("sequenceId", sequenceId);
143-
}
144-
if (fee != null) {
145-
data.put("fee", fee.longValue());
146-
}
147-
if (feeTxConfirmTarget != null) {
148-
data.put("feeTxConfirmTarget", feeTxConfirmTarget);
149-
}
150-
if (minConfirms > 0) {
151-
data.put("minConfirms", minConfirms);
138+
data.put("walletPassphrase", walletPass);
139+
data.put("sequenceId", sequenceId);
140+
141+
// Check for optional parameters or default them to null
142+
if (optionalParameters != null) {
143+
Object message = optionalParameters.containsKey("message") ? optionalParameters.get("message") : null;
144+
Object fee = optionalParameters.containsKey("fee") ? optionalParameters.get("fee") : null;
145+
Object feeTxConfirmTarget = optionalParameters.containsKey("feeTxConfirmTarget") ?
146+
optionalParameters.get("feeTxConfirmTarget") : null;
147+
Object minConfirms = optionalParameters.containsKey("minConfirms") ? optionalParameters.get("minConfirms") : null;
148+
Object enforceMinConfirmsForChange = optionalParameters.containsKey("enforceMinConfirmsForChange") ?
149+
optionalParameters.get("enforceMinConfirmsForChange") : null;
150+
if (message != null) {
151+
if (message.getClass().getName().equals(String.class.getName())) {
152+
data.put("message", message.toString());
153+
} else {
154+
throw new IOException("Message should be a String value");
155+
}
156+
}
157+
if (fee != null) {
158+
if (fee.getClass().getName().equals(BigDecimal.class.getName())) {
159+
data.put("fee", ((BigDecimal) fee).longValue());
160+
} else {
161+
throw new IOException("Fee should be a BigDecimal value");
162+
}
163+
}
164+
if (feeTxConfirmTarget != null) {
165+
if (feeTxConfirmTarget.getClass().getName().equals(BigDecimal.class.getName())) {
166+
data.put("feeTxConfirmTarget", feeTxConfirmTarget);
167+
} else {
168+
throw new IOException("FeeTxConfirmTarget should be a BigDecimal value");
169+
}
170+
}
171+
if (minConfirms != null) {
172+
if (minConfirms.getClass().getName().equals(Integer.class.getName()) && ((int) minConfirms > 0)) {
173+
data.put("minConfirms", minConfirms);
174+
} else {
175+
throw new IOException("MinConfirms should be an Integer value higher than 0");
176+
}
177+
}
178+
if (enforceMinConfirmsForChange != null) {
179+
if (enforceMinConfirmsForChange.getClass().getName().equals(Boolean.class.getName())) {
180+
data.put("enforceMinConfirmsForChange", enforceMinConfirmsForChange);
181+
} else {
182+
throw new IOException("EnforceMinConfirmsForChange should be a Boolean value");
183+
}
184+
}
152185
}
153-
data.put("enforceMinConfirmsForChange", enforceMinConfirmsForChange);
154186

155187
log.info("sendMany {}", data);
156-
data.put("walletPassphrase", walletPass);
157188
HttpURLConnection conn = httpPost(url, data);
158189
if (conn == null) {
159190
return Optional.empty();

bitgo-java-api/src/test/java/com/bitso/bitgo/util/TestClientV2.java

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import com.bitso.bitgo.v2.entity.Wallet;
77
import com.bitso.bitgo.v2.entity.WalletTransferResponse;
88
import lombok.extern.slf4j.Slf4j;
9-
import org.hamcrest.CoreMatchers;
109
import org.junit.Assert;
1110
import org.junit.Ignore;
1211
import org.junit.Test;
@@ -53,15 +52,11 @@ public void testSendMany() throws IOException {
5352
targets.put(TLTC_TEST_FAUCET_ADDRESS, amount);
5453
amount = amount.add(initAmount);
5554
}
56-
parameters.put("coin", COIN);
57-
parameters.put("walletId", WALLET_ID);
58-
parameters.put("walletPass", WALLET_PASSPHRASE);
59-
parameters.put("recipients", targets);
6055
parameters.put("message", "test");
6156
parameters.put("minConfirms", 1);
6257
parameters.put("enforceMinConfirmsForChange", true);
63-
parameters.put("sequenceId", ""); //Set it up for transaction to work
64-
Optional<SendCoinsResponse> resp = client.sendMany(parameters);
58+
String sequenceId = "btc35"; //Set it up for transaction to work
59+
Optional<SendCoinsResponse> resp = client.sendMany(COIN, WALLET_ID, WALLET_PASSPHRASE, targets, sequenceId, parameters);
6560
Assert.assertTrue(resp.isPresent());
6661
Assert.assertNotNull(resp.get().getTx());
6762

@@ -70,32 +65,40 @@ public void testSendMany() throws IOException {
7065
@Test
7166
@Ignore
7267
public void testSendManyFail() throws IOException {
73-
Map<String, Object> parameters = new HashMap<String, Object>();
7468
int unlockResult = client.unlock("0000000", TimeUnit.HOURS.toSeconds(1));
7569
System.out.println(unlockResult);
7670
assertTrue(unlockResult != 400 && unlockResult != 401);
7771
Map<String, BigDecimal> targets = new HashMap<>();
7872
BigDecimal initAmount = new BigDecimal("0.001").movePointRight(8);
7973
BigDecimal amount = initAmount;
8074
targets.put(TLTC_TEST_FAUCET_ADDRESS, amount);
81-
// avoid coin required parameter to test error handling
82-
parameters.put("coin", "");
83-
parameters.put("walletId", WALLET_ID);
84-
parameters.put("walletPass", WALLET_PASSPHRASE);
85-
parameters.put("recipients", targets);
86-
parameters.put("message", "test");
87-
parameters.put("minConfirms", 1);
88-
parameters.put("enforceMinConfirmsForChange", true);
89-
parameters.put("sequenceId", "btc34"); //Set it up for transaction to work
90-
Optional<SendCoinsResponse> resp = client.sendMany(parameters);
91-
// Coin is part of the URL of api, should not find the required page (also walletId is part of the URL)
92-
Assert.assertEquals(404, resp.get().getResponseCode());
93-
// Restart test swapping the wrong variable
94-
parameters.put("coin", COIN);
95-
parameters.put("sequenceId", "");
96-
resp = client.sendMany(parameters);
97-
Assert.assertEquals(400, resp.get().getResponseCode());
98-
Assert.assertThat(resp.get().getError(), CoreMatchers.containsString("invalid sequence id"));
75+
String sequenceId = "123";
76+
// Send required COIN parameter as empty
77+
try {
78+
client.sendMany("", WALLET_ID, WALLET_PASSPHRASE, targets, sequenceId, null);
79+
} catch (Exception e) {
80+
Assert.assertEquals("Invalid currency", e.getMessage());
81+
}
82+
// Send COIN parameter misformed
83+
try {
84+
client.sendMany("abc", WALLET_ID, WALLET_PASSPHRASE, targets, sequenceId, null);
85+
} catch (Exception e) {
86+
Assert.assertEquals("Invalid currency", e.getMessage());
87+
}
88+
// Send required COIN parameter as null
89+
try {
90+
client.sendMany(null, WALLET_ID, WALLET_PASSPHRASE, targets, sequenceId, null);
91+
} catch (Exception e) {
92+
Assert.assertEquals("coin is marked @NonNull but is null", e.getMessage());
93+
}
94+
// Send an optional parameter with a wrong type
95+
try {
96+
Map<String, Object> parameters = new HashMap<String, Object>();
97+
parameters.put("fee", "0");
98+
client.sendMany(COIN, WALLET_ID, WALLET_PASSPHRASE, targets, sequenceId, parameters);
99+
} catch (Exception e) {
100+
Assert.assertEquals("Fee should be a BigDecimal value", e.getMessage());
101+
}
99102
}
100103

101104
@Test

0 commit comments

Comments
 (0)