Skip to content

Commit 6478cae

Browse files
authored
feat: add batch transfer builder (#182)
1 parent e99dca9 commit 6478cae

8 files changed

Lines changed: 248 additions & 0 deletions

File tree

crypto/enums/abi_function.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ class AbiFunction(Enum):
1111
UPDATE_VALIDATOR = 'updateValidator'
1212
TRANSFER = 'transfer'
1313
APPROVE = 'approve'
14+
BATCH_TRANSFER_FROM = 'batchTransferFrom'

crypto/enums/contract_abi_type.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ class ContractAbiType(Enum):
66
MULTIPAYMENT = 'multipayment'
77
TOKEN = 'token'
88
USERNAMES = 'usernames'
9+
ERC20BATCH_TRANSFER = 'erc20BatchTransfer'

crypto/enums/contract_addresses.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ class ContractAddresses(Enum):
44
CONSENSUS = '0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1'
55
MULTIPAYMENT = '0x00EFd0D4639191C49908A7BddbB9A11A994A8527'
66
USERNAMES = '0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6'
7+
BATCH_TRANSFER = '0x5a223F4434D5Bd8478100EEb3b0166a57A26350d'
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from crypto.enums.abi_function import AbiFunction
2+
from crypto.enums.contract_abi_type import ContractAbiType
3+
from crypto.enums.contract_addresses import ContractAddresses
4+
from crypto.transactions.builder.abstract_transaction_builder import (
5+
AbstractTransactionBuilder,
6+
)
7+
from crypto.transactions.types.evm_call import EvmCall
8+
from crypto.utils.abi_encoder import AbiEncoder
9+
from crypto.utils.transaction_utils import TransactionUtils
10+
11+
12+
class BatchTransferBuilder(AbstractTransactionBuilder):
13+
def __init__(self, data: dict):
14+
super().__init__(data)
15+
16+
self._token_address = None
17+
self._recipients = []
18+
self._amounts = []
19+
20+
self.to(ContractAddresses.BATCH_TRANSFER.value)
21+
22+
def token_address(self, token_address: str):
23+
self._token_address = token_address
24+
return self
25+
26+
def add_recipient(self, address: str, amount: int):
27+
self._recipients.append(address)
28+
self._amounts.append(amount)
29+
return self
30+
31+
def sign(self, passphrase: str):
32+
self._encode()
33+
return super().sign(passphrase)
34+
35+
def _encode(self):
36+
if len(self._recipients) == 0:
37+
raise Exception('Must add at least one recipient before encoding.')
38+
39+
if self._token_address is None:
40+
raise Exception('Must set tokenAddress before encoding.')
41+
42+
encoder = AbiEncoder(ContractAbiType.ERC20BATCH_TRANSFER)
43+
payload = encoder.encode_function_call(
44+
AbiFunction.BATCH_TRANSFER_FROM.value,
45+
[self._token_address, self._recipients, self._amounts],
46+
)
47+
self.transaction.data['data'] = TransactionUtils.parse_hex_from_str(
48+
payload
49+
)
50+
51+
def get_transaction_instance(self, data: dict):
52+
return EvmCall(data)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"_format": "hh-sol-artifact-1",
3+
"contractName": "ERC20BatchTransfer",
4+
"sourceName": "config/solidity/ERC20BatchTransfer.sol",
5+
"abi": [
6+
{
7+
"inputs": [],
8+
"name": "LengthMismatch",
9+
"type": "error"
10+
},
11+
{
12+
"inputs": [
13+
{
14+
"internalType": "contract IERC20",
15+
"name": "token",
16+
"type": "address"
17+
},
18+
{
19+
"internalType": "address[]",
20+
"name": "to",
21+
"type": "address[]"
22+
},
23+
{
24+
"internalType": "uint256[]",
25+
"name": "amount",
26+
"type": "uint256[]"
27+
}
28+
],
29+
"name": "batchTransferFrom",
30+
"outputs": [],
31+
"stateMutability": "nonpayable",
32+
"type": "function"
33+
},
34+
{
35+
"inputs": [
36+
{
37+
"internalType": "contract IERC20[]",
38+
"name": "tokens",
39+
"type": "address[]"
40+
},
41+
{
42+
"internalType": "address[]",
43+
"name": "to",
44+
"type": "address[]"
45+
},
46+
{
47+
"internalType": "uint256[]",
48+
"name": "amount",
49+
"type": "uint256[]"
50+
}
51+
],
52+
"name": "multiBatchTransferFrom",
53+
"outputs": [],
54+
"stateMutability": "nonpayable",
55+
"type": "function"
56+
}
57+
],
58+
"bytecode": "0x6080604052348015600e575f5ffd5b5061083e8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80634885b25414610038578063f053e99f14610054575b5f5ffd5b610052600480360381019061004d9190610491565b610070565b005b61006e60048036038101906100699190610577565b6101cd565b005b8181905084849050146100af576040517fff633a3800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f5b848490508110156101c5578573ffffffffffffffffffffffffffffffffffffffff166323b872dd338787858181106100ec576100eb610627565b5b9050602002016020810190610101919061067e565b86868681811061011457610113610627565b5b905060200201356040518463ffffffff1660e01b8152600401610139939291906106d0565b6020604051808303815f875af1158015610155573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610179919061073a565b6101b8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101af906107bf565b60405180910390fd5b80806001019150506100b1565b505050505050565b85859050848490501415806101e85750858590508282905014155b1561021f576040517fff633a3800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f5f90505b8686905081101561035f5786868281811061024257610241610627565b5b905060200201602081019061025791906107dd565b73ffffffffffffffffffffffffffffffffffffffff166323b872dd3387878581811061028657610285610627565b5b905060200201602081019061029b919061067e565b8686868181106102ae576102ad610627565b5b905060200201356040518463ffffffff1660e01b81526004016102d3939291906106d0565b6020604051808303815f875af11580156102ef573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610313919061073a565b610352576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610349906107bf565b60405180910390fd5b8080600101915050610224565b50505050505050565b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61039982610370565b9050919050565b5f6103aa8261038f565b9050919050565b6103ba816103a0565b81146103c4575f5ffd5b50565b5f813590506103d5816103b1565b92915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126103fc576103fb6103db565b5b8235905067ffffffffffffffff811115610419576104186103df565b5b602083019150836020820283011115610435576104346103e3565b5b9250929050565b5f5f83601f840112610451576104506103db565b5b8235905067ffffffffffffffff81111561046e5761046d6103df565b5b60208301915083602082028301111561048a576104896103e3565b5b9250929050565b5f5f5f5f5f606086880312156104aa576104a9610368565b5b5f6104b7888289016103c7565b955050602086013567ffffffffffffffff8111156104d8576104d761036c565b5b6104e4888289016103e7565b9450945050604086013567ffffffffffffffff8111156105075761050661036c565b5b6105138882890161043c565b92509250509295509295909350565b5f5f83601f840112610537576105366103db565b5b8235905067ffffffffffffffff811115610554576105536103df565b5b6020830191508360208202830111156105705761056f6103e3565b5b9250929050565b5f5f5f5f5f5f6060878903121561059157610590610368565b5b5f87013567ffffffffffffffff8111156105ae576105ad61036c565b5b6105ba89828a01610522565b9650965050602087013567ffffffffffffffff8111156105dd576105dc61036c565b5b6105e989828a016103e7565b9450945050604087013567ffffffffffffffff81111561060c5761060b61036c565b5b61061889828a0161043c565b92509250509295509295509295565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b61065d8161038f565b8114610667575f5ffd5b50565b5f8135905061067881610654565b92915050565b5f6020828403121561069357610692610368565b5b5f6106a08482850161066a565b91505092915050565b6106b28161038f565b82525050565b5f819050919050565b6106ca816106b8565b82525050565b5f6060820190506106e35f8301866106a9565b6106f060208301856106a9565b6106fd60408301846106c1565b949350505050565b5f8115159050919050565b61071981610705565b8114610723575f5ffd5b50565b5f8151905061073481610710565b92915050565b5f6020828403121561074f5761074e610368565b5b5f61075c84828501610726565b91505092915050565b5f82825260208201905092915050565b7f5452414e534645525f46524f4d5f4641494c45440000000000000000000000005f82015250565b5f6107a9601483610765565b91506107b482610775565b602082019050919050565b5f6020820190508181035f8301526107d68161079d565b9050919050565b5f602082840312156107f2576107f1610368565b5b5f6107ff848285016103c7565b9150509291505056fea264697066735822122072bc5bd8cacc8452edacb73e0f0321cdfbba275969c6c2476826da50766804ee64736f6c634300081e0033"
59+
}

crypto/utils/abi_base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,7 @@ def __contract_abi_path(self, abi_type: ContractAbiType, path: Optional[str] = N
7171
if abi_type == ContractAbiType.USERNAMES:
7272
return os.path.join(os.path.dirname(__file__), 'abi/json', 'Abi.Usernames.json')
7373

74+
if abi_type == ContractAbiType.ERC20BATCH_TRANSFER:
75+
return os.path.join(os.path.dirname(__file__), 'abi/json', 'Abi.ERC20BatchTransfer.json')
76+
7477
return path
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"data": {
3+
"nonce": "1",
4+
"gasPrice": "5000000000",
5+
"gasLimit": "21000",
6+
"to": "0x5a223F4434D5Bd8478100EEb3b0166a57A26350d",
7+
"value": "0",
8+
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
9+
"data": "4885b254000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000030d40",
10+
"v": 1,
11+
"r": "dc7cac17d9961b4730ec5426eecb94de93e6bf44a3c6d13b00413948646d18e5",
12+
"s": "35220c92a43698524f9023727b68cd6f01753bf6d3128f0af4b78343ff6897e6",
13+
"senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d",
14+
"hash": "59c1dcace1b2573921909d9ad1ca475f49b890f2cb147030462a5b9076b6a5dd"
15+
},
16+
"recipients": [
17+
{ "address": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", "amount": 100000 },
18+
{ "address": "0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763", "amount": 200000 }
19+
],
20+
"serialized": "f9018c0185012a05f200825208945a223f4434d5bd8478100eeb3b0166a57a26350d80b901244885b254000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000030d40825c6ca0dc7cac17d9961b4730ec5426eecb94de93e6bf44a3c6d13b00413948646d18e5a035220c92a43698524f9023727b68cd6f01753bf6d3128f0af4b78343ff6897e6"
21+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import pytest
2+
3+
from crypto.enums.contract_addresses import ContractAddresses
4+
from crypto.transactions.builder.batch_transfer_builder import (
5+
BatchTransferBuilder,
6+
)
7+
8+
RECIPIENT_A = '0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22'
9+
RECIPIENT_B = '0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763'
10+
TOKEN_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7'
11+
12+
13+
def test_it_should_default_to_the_batch_transfer_contract():
14+
builder = BatchTransferBuilder.new()
15+
16+
assert builder.transaction.data['to'] == ContractAddresses.BATCH_TRANSFER.value
17+
18+
19+
def test_it_should_sign_it_with_a_passphrase(passphrase, load_transaction_fixture):
20+
fixture = load_transaction_fixture('transactions/batch-transfer')
21+
22+
builder = (
23+
BatchTransferBuilder
24+
.new()
25+
.gas_price(fixture['data']['gasPrice'])
26+
.gas_limit(fixture['data']['gasLimit'])
27+
.nonce(fixture['data']['nonce'])
28+
.token_address(TOKEN_ADDRESS)
29+
.add_recipient(RECIPIENT_A, 100000)
30+
.add_recipient(RECIPIENT_B, 200000)
31+
.sign(passphrase)
32+
)
33+
34+
assert builder.transaction.data['gasPrice'] == int(fixture['data']['gasPrice'])
35+
assert builder.transaction.data['gasLimit'] == int(fixture['data']['gasLimit'])
36+
assert builder.transaction.data['nonce'] == fixture['data']['nonce']
37+
assert builder.transaction.data['value'] == 0
38+
assert builder.transaction.data['to'] == fixture['data']['to']
39+
assert builder.transaction.data['data'] == fixture['data']['data']
40+
assert builder.transaction.data['v'] == fixture['data']['v']
41+
assert builder.transaction.data['r'] == fixture['data']['r']
42+
assert builder.transaction.data['s'] == fixture['data']['s']
43+
assert builder.transaction.data['hash'] == fixture['data']['hash']
44+
45+
assert builder.transaction.serialize().hex() == fixture['serialized']
46+
assert builder.verify()
47+
48+
49+
def test_it_should_handle_single_recipient(passphrase, load_transaction_fixture):
50+
fixture = load_transaction_fixture('transactions/batch-transfer')
51+
52+
builder = (
53+
BatchTransferBuilder
54+
.new()
55+
.gas_price(fixture['data']['gasPrice'])
56+
.gas_limit(fixture['data']['gasLimit'])
57+
.nonce(fixture['data']['nonce'])
58+
.token_address(TOKEN_ADDRESS)
59+
.add_recipient(RECIPIENT_A, 100000)
60+
.sign(passphrase)
61+
)
62+
63+
assert builder.transaction.data['data'].startswith('4885b254')
64+
assert builder.transaction.data['value'] == 0
65+
assert builder.verify()
66+
67+
68+
def test_it_should_encode_large_amounts(passphrase, load_transaction_fixture):
69+
fixture = load_transaction_fixture('transactions/batch-transfer')
70+
71+
builder = (
72+
BatchTransferBuilder
73+
.new()
74+
.gas_price(fixture['data']['gasPrice'])
75+
.gas_limit(fixture['data']['gasLimit'])
76+
.nonce(fixture['data']['nonce'])
77+
.token_address(TOKEN_ADDRESS)
78+
.add_recipient(RECIPIENT_A, 1000000000000000000000)
79+
.sign(passphrase)
80+
)
81+
82+
assert builder.verify()
83+
84+
85+
def test_it_should_throw_when_signing_with_no_recipients(passphrase):
86+
builder = (
87+
BatchTransferBuilder
88+
.new()
89+
.gas_price('5000000000')
90+
.gas_limit('21000')
91+
.nonce('1')
92+
.token_address(TOKEN_ADDRESS)
93+
)
94+
95+
with pytest.raises(Exception, match='Must add at least one recipient before encoding.'):
96+
builder.sign(passphrase)
97+
98+
99+
def test_it_should_throw_when_signing_without_a_token_address(passphrase):
100+
builder = (
101+
BatchTransferBuilder
102+
.new()
103+
.gas_price('5000000000')
104+
.gas_limit('21000')
105+
.nonce('1')
106+
.add_recipient(RECIPIENT_A, 100000)
107+
)
108+
109+
with pytest.raises(Exception, match='Must set tokenAddress before encoding.'):
110+
builder.sign(passphrase)

0 commit comments

Comments
 (0)