Skip to content

Commit 0a48ab8

Browse files
authored
feat: add batch transfer builder (#207)
1 parent b3de6b4 commit 0a48ab8

5 files changed

Lines changed: 244 additions & 2 deletions

File tree

‎src/main/java/org/arkecosystem/crypto/enums/AbiFunction.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ public enum AbiFunction {
99
USERNAME_RESIGNATION("resignUsername"),
1010
MULTIPAYMENT("pay"),
1111
TRANSFER("transfer"),
12-
APPROVE("approve");
12+
APPROVE("approve"),
13+
BATCH_TRANSFER_FROM("batchTransferFrom");
1314

1415
private final String functionName;
1516

‎src/main/java/org/arkecosystem/crypto/enums/ContractAddresses.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
public enum ContractAddresses {
44
CONSENSUS("0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1"),
55
MULTIPAYMENT("0x00EFd0D4639191C49908A7BddbB9A11A994A8527"),
6-
USERNAMES("0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6");
6+
USERNAMES("0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6"),
7+
BATCH_TRANSFER("0x5a223F4434D5Bd8478100EEb3b0166a57A26350d");
78

89
private final String address;
910

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.arkecosystem.crypto.transactions.builder;
2+
3+
import java.math.BigInteger;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import org.arkecosystem.crypto.enums.AbiFunction;
7+
import org.arkecosystem.crypto.enums.ContractAbiType;
8+
import org.arkecosystem.crypto.enums.ContractAddresses;
9+
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
10+
import org.arkecosystem.crypto.transactions.types.EvmCall;
11+
import org.arkecosystem.crypto.utils.AbiEncoder;
12+
13+
public class BatchTransferBuilder extends AbstractTransactionBuilder<BatchTransferBuilder> {
14+
private String tokenAddress;
15+
private final List<String> recipients = new ArrayList<>();
16+
private final List<BigInteger> amounts = new ArrayList<>();
17+
18+
public BatchTransferBuilder() {
19+
super();
20+
this.transaction.recipientAddress = ContractAddresses.BATCH_TRANSFER.address();
21+
}
22+
23+
public BatchTransferBuilder tokenAddress(String tokenAddress) {
24+
this.tokenAddress = tokenAddress;
25+
return this.instance();
26+
}
27+
28+
public BatchTransferBuilder addRecipient(String address, BigInteger amount) {
29+
this.recipients.add(address);
30+
this.amounts.add(amount);
31+
return this.instance();
32+
}
33+
34+
@Override
35+
public BatchTransferBuilder sign(String passphrase) {
36+
this.encode();
37+
return super.sign(passphrase);
38+
}
39+
40+
private void encode() {
41+
if (this.recipients.isEmpty()) {
42+
throw new RuntimeException("Must add at least one recipient before encoding.");
43+
}
44+
45+
if (this.tokenAddress == null) {
46+
throw new RuntimeException("Must set tokenAddress before encoding.");
47+
}
48+
49+
List<Object> args = new ArrayList<>();
50+
args.add(this.tokenAddress);
51+
args.add(new ArrayList<Object>(this.recipients));
52+
args.add(new ArrayList<Object>(this.amounts));
53+
54+
try {
55+
String payload =
56+
new AbiEncoder(ContractAbiType.ERC20BATCH_TRANSFER)
57+
.encodeFunctionCall(AbiFunction.BATCH_TRANSFER_FROM.toString(), args);
58+
59+
this.transaction.data = payload.replaceFirst("^0x", "");
60+
} catch (Exception e) {
61+
throw new RuntimeException("Error encoding batch transfer", e);
62+
}
63+
}
64+
65+
@Override
66+
protected AbstractTransaction getTransactionInstance() {
67+
return new EvmCall();
68+
}
69+
70+
@Override
71+
protected BatchTransferBuilder instance() {
72+
return this;
73+
}
74+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package org.arkecosystem.crypto.transactions.builder;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.math.BigInteger;
6+
import java.util.Map;
7+
import org.arkecosystem.crypto.AbstractTest;
8+
import org.arkecosystem.crypto.encoding.Hex;
9+
import org.arkecosystem.crypto.enums.ContractAddresses;
10+
import org.arkecosystem.crypto.transactions.Deserializer;
11+
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
12+
import org.arkecosystem.crypto.transactions.types.EvmCall;
13+
import org.junit.jupiter.api.Test;
14+
15+
public class BatchTransferBuilderTest extends AbstractTest {
16+
17+
private static final String BATCH_TRANSFER_SELECTOR = "4885b254";
18+
private static final String RECIPIENT_A = "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22";
19+
private static final String RECIPIENT_B = "0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763";
20+
private static final String TOKEN_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
21+
22+
@Test
23+
public void it_should_default_to_the_batch_transfer_well_known_contract() {
24+
BatchTransferBuilder builder = new BatchTransferBuilder();
25+
26+
assertEquals(
27+
ContractAddresses.BATCH_TRANSFER.address(), builder.transaction.recipientAddress);
28+
}
29+
30+
@Test
31+
public void it_should_encode_the_batch_transfer_payload() throws Exception {
32+
Map<String, Object> fixture = loadFixture("batch-transfer");
33+
Map<String, Object> data = (Map<String, Object>) fixture.get("data");
34+
35+
BatchTransferBuilder builder =
36+
new BatchTransferBuilder()
37+
.gasPrice(5_000_000_000L)
38+
.gasLimit(21_000)
39+
.nonce(1L)
40+
.tokenAddress(TOKEN_ADDRESS)
41+
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
42+
.addRecipient(RECIPIENT_B, new BigInteger("200000"))
43+
.sign(this.passphrase);
44+
45+
assertEquals(data.get("data"), builder.transaction.data);
46+
assertTrue(builder.transaction.data.startsWith(BATCH_TRANSFER_SELECTOR));
47+
assertEquals("0", builder.transaction.value);
48+
}
49+
50+
@Test
51+
public void it_should_sign_and_verify_a_batch_transfer_transaction() {
52+
BatchTransferBuilder builder =
53+
new BatchTransferBuilder()
54+
.gasPrice(5_000_000_000L)
55+
.gasLimit(21_000)
56+
.nonce(1L)
57+
.tokenAddress(TOKEN_ADDRESS)
58+
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
59+
.addRecipient(RECIPIENT_B, new BigInteger("200000"))
60+
.sign(this.passphrase);
61+
62+
assertNotNull(builder.transaction.signature);
63+
assertNotNull(builder.transaction.id);
64+
assertTrue(builder.verify());
65+
}
66+
67+
@Test
68+
public void it_should_encode_a_single_recipient() {
69+
BatchTransferBuilder builder =
70+
new BatchTransferBuilder()
71+
.gasPrice(5_000_000_000L)
72+
.gasLimit(21_000)
73+
.nonce(1L)
74+
.tokenAddress(TOKEN_ADDRESS)
75+
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
76+
.sign(this.passphrase);
77+
78+
assertTrue(builder.transaction.data.startsWith(BATCH_TRANSFER_SELECTOR));
79+
assertEquals("0", builder.transaction.value);
80+
assertTrue(builder.verify());
81+
}
82+
83+
@Test
84+
public void it_should_encode_large_amounts() {
85+
BatchTransferBuilder builder =
86+
new BatchTransferBuilder()
87+
.gasPrice(5_000_000_000L)
88+
.gasLimit(21_000)
89+
.nonce(1L)
90+
.tokenAddress(TOKEN_ADDRESS)
91+
.addRecipient(RECIPIENT_A, new BigInteger("1000000000000000000000"))
92+
.sign(this.passphrase);
93+
94+
assertTrue(builder.verify());
95+
}
96+
97+
@Test
98+
public void it_should_round_trip_through_serialization() throws Exception {
99+
BatchTransferBuilder builder =
100+
new BatchTransferBuilder()
101+
.gasPrice(5_000_000_000L)
102+
.gasLimit(21_000)
103+
.nonce(7L)
104+
.tokenAddress(TOKEN_ADDRESS)
105+
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
106+
.addRecipient(RECIPIENT_B, new BigInteger("200000"))
107+
.sign(this.passphrase);
108+
109+
String serialized = Hex.encode(builder.transaction.serialize());
110+
AbstractTransaction restored = Deserializer.newDeserializer(serialized).deserialize();
111+
112+
assertInstanceOf(EvmCall.class, restored);
113+
assertEquals(builder.transaction.id, restored.id);
114+
assertEquals(builder.transaction.data, restored.data);
115+
assertEquals(
116+
builder.transaction.recipientAddress.toLowerCase(),
117+
restored.recipientAddress.toLowerCase());
118+
}
119+
120+
@Test
121+
public void it_should_throw_when_signing_with_no_recipients() {
122+
RuntimeException exception =
123+
assertThrows(
124+
RuntimeException.class,
125+
() ->
126+
new BatchTransferBuilder()
127+
.gasPrice(5_000_000_000L)
128+
.gasLimit(21_000)
129+
.nonce(1L)
130+
.tokenAddress(TOKEN_ADDRESS)
131+
.sign(this.passphrase));
132+
133+
assertEquals("Must add at least one recipient before encoding.", exception.getMessage());
134+
}
135+
136+
@Test
137+
public void it_should_throw_when_signing_without_a_token_address() {
138+
RuntimeException exception =
139+
assertThrows(
140+
RuntimeException.class,
141+
() ->
142+
new BatchTransferBuilder()
143+
.gasPrice(5_000_000_000L)
144+
.gasLimit(21_000)
145+
.nonce(1L)
146+
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
147+
.sign(this.passphrase));
148+
149+
assertEquals("Must set tokenAddress before encoding.", exception.getMessage());
150+
}
151+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"data": {
3+
"nonce": "1",
4+
"gasPrice": 5000000000,
5+
"gasLimit": 21000,
6+
"value": "0",
7+
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
8+
"recipientAddress": "0x5a223F4434D5Bd8478100EEb3b0166a57A26350d",
9+
"data": "4885b254000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000030d40"
10+
},
11+
"recipients": [
12+
{ "address": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", "amount": "100000" },
13+
{ "address": "0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763", "amount": "200000" }
14+
]
15+
}

0 commit comments

Comments
 (0)