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
1 change: 1 addition & 0 deletions crypto/enums/abi_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ class AbiFunction(Enum):
UPDATE_VALIDATOR = 'updateValidator'
TRANSFER = 'transfer'
APPROVE = 'approve'
BATCH_TRANSFER_FROM = 'batchTransferFrom'
1 change: 1 addition & 0 deletions crypto/enums/contract_abi_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ class ContractAbiType(Enum):
MULTIPAYMENT = 'multipayment'
TOKEN = 'token'
USERNAMES = 'usernames'
ERC20BATCH_TRANSFER = 'erc20BatchTransfer'
1 change: 1 addition & 0 deletions crypto/enums/contract_addresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ class ContractAddresses(Enum):
CONSENSUS = '0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1'
MULTIPAYMENT = '0x00EFd0D4639191C49908A7BddbB9A11A994A8527'
USERNAMES = '0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6'
BATCH_TRANSFER = '0x5a223F4434D5Bd8478100EEb3b0166a57A26350d'
52 changes: 52 additions & 0 deletions crypto/transactions/builder/batch_transfer_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from crypto.enums.abi_function import AbiFunction
from crypto.enums.contract_abi_type import ContractAbiType
from crypto.enums.contract_addresses import ContractAddresses
from crypto.transactions.builder.abstract_transaction_builder import (
AbstractTransactionBuilder,
)
from crypto.transactions.types.evm_call import EvmCall
from crypto.utils.abi_encoder import AbiEncoder
from crypto.utils.transaction_utils import TransactionUtils


class BatchTransferBuilder(AbstractTransactionBuilder):
def __init__(self, data: dict):
super().__init__(data)

self._token_address = None
self._recipients = []
self._amounts = []

self.to(ContractAddresses.BATCH_TRANSFER.value)

def token_address(self, token_address: str):
self._token_address = token_address
return self

def add_recipient(self, address: str, amount: int):
self._recipients.append(address)
self._amounts.append(amount)
return self

def sign(self, passphrase: str):
self._encode()
return super().sign(passphrase)

def _encode(self):
if len(self._recipients) == 0:
raise Exception('Must add at least one recipient before encoding.')

if self._token_address is None:
raise Exception('Must set tokenAddress before encoding.')

encoder = AbiEncoder(ContractAbiType.ERC20BATCH_TRANSFER)
payload = encoder.encode_function_call(
AbiFunction.BATCH_TRANSFER_FROM.value,
[self._token_address, self._recipients, self._amounts],
)
self.transaction.data['data'] = TransactionUtils.parse_hex_from_str(
payload
)

def get_transaction_instance(self, data: dict):
return EvmCall(data)
59 changes: 59 additions & 0 deletions crypto/utils/abi/json/Abi.ERC20BatchTransfer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "ERC20BatchTransfer",
"sourceName": "config/solidity/ERC20BatchTransfer.sol",
"abi": [
{
"inputs": [],
"name": "LengthMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "contract IERC20",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "to",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amount",
"type": "uint256[]"
}
],
"name": "batchTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IERC20[]",
"name": "tokens",
"type": "address[]"
},
{
"internalType": "address[]",
"name": "to",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amount",
"type": "uint256[]"
}
],
"name": "multiBatchTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
],
"bytecode": "0x6080604052348015600e575f5ffd5b5061083e8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80634885b25414610038578063f053e99f14610054575b5f5ffd5b610052600480360381019061004d9190610491565b610070565b005b61006e60048036038101906100699190610577565b6101cd565b005b8181905084849050146100af576040517fff633a3800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f5b848490508110156101c5578573ffffffffffffffffffffffffffffffffffffffff166323b872dd338787858181106100ec576100eb610627565b5b9050602002016020810190610101919061067e565b86868681811061011457610113610627565b5b905060200201356040518463ffffffff1660e01b8152600401610139939291906106d0565b6020604051808303815f875af1158015610155573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610179919061073a565b6101b8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101af906107bf565b60405180910390fd5b80806001019150506100b1565b505050505050565b85859050848490501415806101e85750858590508282905014155b1561021f576040517fff633a3800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f5f90505b8686905081101561035f5786868281811061024257610241610627565b5b905060200201602081019061025791906107dd565b73ffffffffffffffffffffffffffffffffffffffff166323b872dd3387878581811061028657610285610627565b5b905060200201602081019061029b919061067e565b8686868181106102ae576102ad610627565b5b905060200201356040518463ffffffff1660e01b81526004016102d3939291906106d0565b6020604051808303815f875af11580156102ef573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610313919061073a565b610352576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610349906107bf565b60405180910390fd5b8080600101915050610224565b50505050505050565b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61039982610370565b9050919050565b5f6103aa8261038f565b9050919050565b6103ba816103a0565b81146103c4575f5ffd5b50565b5f813590506103d5816103b1565b92915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126103fc576103fb6103db565b5b8235905067ffffffffffffffff811115610419576104186103df565b5b602083019150836020820283011115610435576104346103e3565b5b9250929050565b5f5f83601f840112610451576104506103db565b5b8235905067ffffffffffffffff81111561046e5761046d6103df565b5b60208301915083602082028301111561048a576104896103e3565b5b9250929050565b5f5f5f5f5f606086880312156104aa576104a9610368565b5b5f6104b7888289016103c7565b955050602086013567ffffffffffffffff8111156104d8576104d761036c565b5b6104e4888289016103e7565b9450945050604086013567ffffffffffffffff8111156105075761050661036c565b5b6105138882890161043c565b92509250509295509295909350565b5f5f83601f840112610537576105366103db565b5b8235905067ffffffffffffffff811115610554576105536103df565b5b6020830191508360208202830111156105705761056f6103e3565b5b9250929050565b5f5f5f5f5f5f6060878903121561059157610590610368565b5b5f87013567ffffffffffffffff8111156105ae576105ad61036c565b5b6105ba89828a01610522565b9650965050602087013567ffffffffffffffff8111156105dd576105dc61036c565b5b6105e989828a016103e7565b9450945050604087013567ffffffffffffffff81111561060c5761060b61036c565b5b61061889828a0161043c565b92509250509295509295509295565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b61065d8161038f565b8114610667575f5ffd5b50565b5f8135905061067881610654565b92915050565b5f6020828403121561069357610692610368565b5b5f6106a08482850161066a565b91505092915050565b6106b28161038f565b82525050565b5f819050919050565b6106ca816106b8565b82525050565b5f6060820190506106e35f8301866106a9565b6106f060208301856106a9565b6106fd60408301846106c1565b949350505050565b5f8115159050919050565b61071981610705565b8114610723575f5ffd5b50565b5f8151905061073481610710565b92915050565b5f6020828403121561074f5761074e610368565b5b5f61075c84828501610726565b91505092915050565b5f82825260208201905092915050565b7f5452414e534645525f46524f4d5f4641494c45440000000000000000000000005f82015250565b5f6107a9601483610765565b91506107b482610775565b602082019050919050565b5f6020820190508181035f8301526107d68161079d565b9050919050565b5f602082840312156107f2576107f1610368565b5b5f6107ff848285016103c7565b9150509291505056fea264697066735822122072bc5bd8cacc8452edacb73e0f0321cdfbba275969c6c2476826da50766804ee64736f6c634300081e0033"
}
3 changes: 3 additions & 0 deletions crypto/utils/abi_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,7 @@ def __contract_abi_path(self, abi_type: ContractAbiType, path: Optional[str] = N
if abi_type == ContractAbiType.USERNAMES:
return os.path.join(os.path.dirname(__file__), 'abi/json', 'Abi.Usernames.json')

if abi_type == ContractAbiType.ERC20BATCH_TRANSFER:
return os.path.join(os.path.dirname(__file__), 'abi/json', 'Abi.ERC20BatchTransfer.json')

return path
21 changes: 21 additions & 0 deletions tests/fixtures/transactions/batch-transfer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"data": {
"nonce": "1",
"gasPrice": "5000000000",
"gasLimit": "21000",
"to": "0x5a223F4434D5Bd8478100EEb3b0166a57A26350d",
"value": "0",
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"data": "4885b254000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000030d40",
"v": 1,
"r": "dc7cac17d9961b4730ec5426eecb94de93e6bf44a3c6d13b00413948646d18e5",
"s": "35220c92a43698524f9023727b68cd6f01753bf6d3128f0af4b78343ff6897e6",
"senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d",
"hash": "59c1dcace1b2573921909d9ad1ca475f49b890f2cb147030462a5b9076b6a5dd"
},
"recipients": [
{ "address": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", "amount": 100000 },
{ "address": "0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763", "amount": 200000 }
],
"serialized": "f9018c0185012a05f200825208945a223f4434d5bd8478100eeb3b0166a57a26350d80b901244885b254000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000030d40825c6ca0dc7cac17d9961b4730ec5426eecb94de93e6bf44a3c6d13b00413948646d18e5a035220c92a43698524f9023727b68cd6f01753bf6d3128f0af4b78343ff6897e6"
}
110 changes: 110 additions & 0 deletions tests/transactions/builder/test_batch_transfer_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import pytest

from crypto.enums.contract_addresses import ContractAddresses
from crypto.transactions.builder.batch_transfer_builder import (
BatchTransferBuilder,
)

RECIPIENT_A = '0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22'
RECIPIENT_B = '0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763'
TOKEN_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7'


def test_it_should_default_to_the_batch_transfer_contract():
builder = BatchTransferBuilder.new()

assert builder.transaction.data['to'] == ContractAddresses.BATCH_TRANSFER.value


def test_it_should_sign_it_with_a_passphrase(passphrase, load_transaction_fixture):
fixture = load_transaction_fixture('transactions/batch-transfer')

builder = (
BatchTransferBuilder
.new()
.gas_price(fixture['data']['gasPrice'])
.gas_limit(fixture['data']['gasLimit'])
.nonce(fixture['data']['nonce'])
.token_address(TOKEN_ADDRESS)
.add_recipient(RECIPIENT_A, 100000)
.add_recipient(RECIPIENT_B, 200000)
.sign(passphrase)
)

assert builder.transaction.data['gasPrice'] == int(fixture['data']['gasPrice'])
assert builder.transaction.data['gasLimit'] == int(fixture['data']['gasLimit'])
assert builder.transaction.data['nonce'] == fixture['data']['nonce']
assert builder.transaction.data['value'] == 0
assert builder.transaction.data['to'] == fixture['data']['to']
assert builder.transaction.data['data'] == fixture['data']['data']
assert builder.transaction.data['v'] == fixture['data']['v']
assert builder.transaction.data['r'] == fixture['data']['r']
assert builder.transaction.data['s'] == fixture['data']['s']
assert builder.transaction.data['hash'] == fixture['data']['hash']

assert builder.transaction.serialize().hex() == fixture['serialized']
assert builder.verify()


def test_it_should_handle_single_recipient(passphrase, load_transaction_fixture):
fixture = load_transaction_fixture('transactions/batch-transfer')

builder = (
BatchTransferBuilder
.new()
.gas_price(fixture['data']['gasPrice'])
.gas_limit(fixture['data']['gasLimit'])
.nonce(fixture['data']['nonce'])
.token_address(TOKEN_ADDRESS)
.add_recipient(RECIPIENT_A, 100000)
.sign(passphrase)
)

assert builder.transaction.data['data'].startswith('4885b254')
assert builder.transaction.data['value'] == 0
assert builder.verify()


def test_it_should_encode_large_amounts(passphrase, load_transaction_fixture):
fixture = load_transaction_fixture('transactions/batch-transfer')

builder = (
BatchTransferBuilder
.new()
.gas_price(fixture['data']['gasPrice'])
.gas_limit(fixture['data']['gasLimit'])
.nonce(fixture['data']['nonce'])
.token_address(TOKEN_ADDRESS)
.add_recipient(RECIPIENT_A, 1000000000000000000000)
.sign(passphrase)
)

assert builder.verify()


def test_it_should_throw_when_signing_with_no_recipients(passphrase):
builder = (
BatchTransferBuilder
.new()
.gas_price('5000000000')
.gas_limit('21000')
.nonce('1')
.token_address(TOKEN_ADDRESS)
)

with pytest.raises(Exception, match='Must add at least one recipient before encoding.'):
builder.sign(passphrase)


def test_it_should_throw_when_signing_without_a_token_address(passphrase):
builder = (
BatchTransferBuilder
.new()
.gas_price('5000000000')
.gas_limit('21000')
.nonce('1')
.add_recipient(RECIPIENT_A, 100000)
)

with pytest.raises(Exception, match='Must set tokenAddress before encoding.'):
builder.sign(passphrase)
Loading