Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public enum AbiFunction {
USERNAME_RESIGNATION("resignUsername"),
MULTIPAYMENT("pay"),
TRANSFER("transfer"),
APPROVE("approve");
APPROVE("approve"),
BATCH_TRANSFER_FROM("batchTransferFrom");

private final String functionName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
public enum ContractAddresses {
CONSENSUS("0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1"),
MULTIPAYMENT("0x00EFd0D4639191C49908A7BddbB9A11A994A8527"),
USERNAMES("0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6");
USERNAMES("0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6"),
BATCH_TRANSFER("0x5a223F4434D5Bd8478100EEb3b0166a57A26350d");

private final String address;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.arkecosystem.crypto.transactions.builder;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import org.arkecosystem.crypto.enums.AbiFunction;
import org.arkecosystem.crypto.enums.ContractAbiType;
import org.arkecosystem.crypto.enums.ContractAddresses;
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
import org.arkecosystem.crypto.transactions.types.EvmCall;
import org.arkecosystem.crypto.utils.AbiEncoder;

public class BatchTransferBuilder extends AbstractTransactionBuilder<BatchTransferBuilder> {
private String tokenAddress;
private final List<String> recipients = new ArrayList<>();
private final List<BigInteger> amounts = new ArrayList<>();

public BatchTransferBuilder() {
super();
this.transaction.recipientAddress = ContractAddresses.BATCH_TRANSFER.address();
}

public BatchTransferBuilder tokenAddress(String tokenAddress) {
this.tokenAddress = tokenAddress;
return this.instance();
}

public BatchTransferBuilder addRecipient(String address, BigInteger amount) {
this.recipients.add(address);
this.amounts.add(amount);
return this.instance();
}

@Override
public BatchTransferBuilder sign(String passphrase) {
this.encode();
return super.sign(passphrase);
}

private void encode() {
if (this.recipients.isEmpty()) {
throw new RuntimeException("Must add at least one recipient before encoding.");
}

if (this.tokenAddress == null) {
throw new RuntimeException("Must set tokenAddress before encoding.");
}

List<Object> args = new ArrayList<>();
args.add(this.tokenAddress);
args.add(new ArrayList<Object>(this.recipients));
args.add(new ArrayList<Object>(this.amounts));

try {
String payload =
new AbiEncoder(ContractAbiType.ERC20BATCH_TRANSFER)
.encodeFunctionCall(AbiFunction.BATCH_TRANSFER_FROM.toString(), args);

this.transaction.data = payload.replaceFirst("^0x", "");
} catch (Exception e) {
throw new RuntimeException("Error encoding batch transfer", e);
}
}

@Override
protected AbstractTransaction getTransactionInstance() {
return new EvmCall();
}

@Override
protected BatchTransferBuilder instance() {
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package org.arkecosystem.crypto.transactions.builder;

import static org.junit.jupiter.api.Assertions.*;

import java.math.BigInteger;
import java.util.Map;
import org.arkecosystem.crypto.AbstractTest;
import org.arkecosystem.crypto.encoding.Hex;
import org.arkecosystem.crypto.enums.ContractAddresses;
import org.arkecosystem.crypto.transactions.Deserializer;
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
import org.arkecosystem.crypto.transactions.types.EvmCall;
import org.junit.jupiter.api.Test;

public class BatchTransferBuilderTest extends AbstractTest {

private static final String BATCH_TRANSFER_SELECTOR = "4885b254";
private static final String RECIPIENT_A = "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22";
private static final String RECIPIENT_B = "0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763";
private static final String TOKEN_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7";

@Test
public void it_should_default_to_the_batch_transfer_well_known_contract() {
BatchTransferBuilder builder = new BatchTransferBuilder();

assertEquals(
ContractAddresses.BATCH_TRANSFER.address(), builder.transaction.recipientAddress);
}

@Test
public void it_should_encode_the_batch_transfer_payload() throws Exception {
Map<String, Object> fixture = loadFixture("batch-transfer");
Map<String, Object> data = (Map<String, Object>) fixture.get("data");

BatchTransferBuilder builder =
new BatchTransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21_000)
.nonce(1L)
.tokenAddress(TOKEN_ADDRESS)
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
.addRecipient(RECIPIENT_B, new BigInteger("200000"))
.sign(this.passphrase);

assertEquals(data.get("data"), builder.transaction.data);
assertTrue(builder.transaction.data.startsWith(BATCH_TRANSFER_SELECTOR));
assertEquals("0", builder.transaction.value);
}

@Test
public void it_should_sign_and_verify_a_batch_transfer_transaction() {
BatchTransferBuilder builder =
new BatchTransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21_000)
.nonce(1L)
.tokenAddress(TOKEN_ADDRESS)
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
.addRecipient(RECIPIENT_B, new BigInteger("200000"))
.sign(this.passphrase);

assertNotNull(builder.transaction.signature);
assertNotNull(builder.transaction.id);
assertTrue(builder.verify());
}

@Test
public void it_should_encode_a_single_recipient() {
BatchTransferBuilder builder =
new BatchTransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21_000)
.nonce(1L)
.tokenAddress(TOKEN_ADDRESS)
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
.sign(this.passphrase);

assertTrue(builder.transaction.data.startsWith(BATCH_TRANSFER_SELECTOR));
assertEquals("0", builder.transaction.value);
assertTrue(builder.verify());
}

@Test
public void it_should_encode_large_amounts() {
BatchTransferBuilder builder =
new BatchTransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21_000)
.nonce(1L)
.tokenAddress(TOKEN_ADDRESS)
.addRecipient(RECIPIENT_A, new BigInteger("1000000000000000000000"))
.sign(this.passphrase);

assertTrue(builder.verify());
}

@Test
public void it_should_round_trip_through_serialization() throws Exception {
BatchTransferBuilder builder =
new BatchTransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21_000)
.nonce(7L)
.tokenAddress(TOKEN_ADDRESS)
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
.addRecipient(RECIPIENT_B, new BigInteger("200000"))
.sign(this.passphrase);

String serialized = Hex.encode(builder.transaction.serialize());
AbstractTransaction restored = Deserializer.newDeserializer(serialized).deserialize();

assertInstanceOf(EvmCall.class, restored);
assertEquals(builder.transaction.id, restored.id);
assertEquals(builder.transaction.data, restored.data);
assertEquals(
builder.transaction.recipientAddress.toLowerCase(),
restored.recipientAddress.toLowerCase());
}

@Test
public void it_should_throw_when_signing_with_no_recipients() {
RuntimeException exception =
assertThrows(
RuntimeException.class,
() ->
new BatchTransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21_000)
.nonce(1L)
.tokenAddress(TOKEN_ADDRESS)
.sign(this.passphrase));

assertEquals("Must add at least one recipient before encoding.", exception.getMessage());
}

@Test
public void it_should_throw_when_signing_without_a_token_address() {
RuntimeException exception =
assertThrows(
RuntimeException.class,
() ->
new BatchTransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21_000)
.nonce(1L)
.addRecipient(RECIPIENT_A, new BigInteger("100000"))
.sign(this.passphrase));

assertEquals("Must set tokenAddress before encoding.", exception.getMessage());
}
}
15 changes: 15 additions & 0 deletions src/test/resources/transactions/batch-transfer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"data": {
"nonce": "1",
"gasPrice": 5000000000,
"gasLimit": 21000,
"value": "0",
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"recipientAddress": "0x5a223F4434D5Bd8478100EEb3b0166a57A26350d",
"data": "4885b254000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000030d40"
},
"recipients": [
{ "address": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", "amount": "100000" },
{ "address": "0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763", "amount": "200000" }
]
}
Loading