diff --git a/README.md b/README.md index 78480b4..65cb497 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ print(res) # >>> {'id': '0x51222328b7395860cb9cc6d69d822cf31056851b5694eeccc9f243021eecd547'} ``` -## Send VET and VTHO +## Send VET and VTHO (or any vip180 token) ```python from thor_requests.connect import Connect from thor_requests.wallet import Wallet @@ -270,6 +270,14 @@ connector.transfer_vtho( vtho_in_wei=3 * (10 ** 18) ) +# Transfer 3 OCE to 0x0000000000000000000000000000000000000000 +connector.transfer_token( + _wallet, + to='0x0000000000000000000000000000000000000000', + token_contract_addr='0x0ce6661b4ba86a0ea7ca2bd86a0de87b0b860f14', # OCE smart contract + amount_in_wei=3 * (10 ** 18) +) + # Check VET or VTHO balance of an address: 0x0000000000000000000000000000000000000000 amount_vet = connector.get_vet_balance('0x0000000000000000000000000000000000000000') amount_vtho = connector.get_vtho_balance('0x0000000000000000000000000000000000000000') @@ -277,6 +285,10 @@ amount_vtho = connector.get_vtho_balance('0x000000000000000000000000000000000000 ## VIP-191 Fee Delegation Feature (I) ```python +# Sign a local transaction if you have: +# 1) Wallet to originate the transaction +# 2) Wallet to pay for the gas fee + from thor_requests.connect import Connect from thor_requests.wallet import Wallet from thor_requests.contract import Contract @@ -310,9 +322,52 @@ print(res) ``` ## VIP-191 Fee Delegation Feature (II) +```python +# Sign a remotely received raw transaction if you have: +# 1) A judgment function to decide if you want to sign the raw tx or not. +# 2) A sponsor wallet to pay for the gas fee + +# On user's side, create a transaction body +delegated_body = { + "chainTag": 1, + "blockRef": "0x00000000aabbccdd", + "expiration": 32, + "clauses": [ + { + "to": "0x7567d83b7b8d80addcb281a71d54fc7b3364ffed", + "value": 10000, + "data": "0x000000606060" + } + ], + "gasPriceCoef": 128, + "gas": 21000, + "dependsOn": None, + "nonce": 12345678, + "reserved": { + "features": 1 + } +} + +delegated_tx = transaction.Transaction(delegated_body) +raw_tx = '0x' + delegated_tx.encode().hex() + +# We give an example judgment function that allows all transaction to be signed +def allPass(tx_payer:str, tx_origin:str, transaction): + ''' Run analyze on the given params, shall return bool, error_message ''' + return True, '' + +your_sponsor_wallet = ... # your local sponsor wallet +tx_origin_address = ... # '0x...' string, the transaction's originator +# Sign it with a local sponsor wallet +result = utils.sign_delegated_tx(your_sponsor_wallet, tx_origin_address, raw_tx, False, allPass) +print(result['signature']) # This is the sponsor signature you are looking for +``` + + +## VIP-191 Fee Delegation Feature (III) ```python -# Send VET or VTHO using fee delegation +# Quickly send VET or VTHO using fee delegation from thor_requests.connect import Connect from thor_requests.wallet import Wallet diff --git a/setup.py b/setup.py index 869f780..f90fed2 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setuptools.setup( name="thor-requests", - version="1.1.2", + version="1.2.0", author="laalaguer", author_email="laalaguer@gmail.com", description="Simple network VeChain SDK for human to interact with the blockchain", diff --git a/tests/test_fee_delegation.py b/tests/test_fee_delegation.py index 542158d..6565434 100644 --- a/tests/test_fee_delegation.py +++ b/tests/test_fee_delegation.py @@ -8,6 +8,7 @@ vtho_contract, ) from thor_requests import utils +from thor_devkit import transaction def test_vet_transfer(solo_connector, solo_wallet, clean_wallet): @@ -97,3 +98,74 @@ def test_transfer_vtho_easy( clean_wallet.getAddress() ) assert updated_balance == 0 + + +def test_sign_fee_delegation_allow( + solo_wallet, + clean_wallet +): + delegated_body = { + "chainTag": 1, + "blockRef": "0x00000000aabbccdd", + "expiration": 32, + "clauses": [ + { + "to": "0x7567d83b7b8d80addcb281a71d54fc7b3364ffed", + "value": 10000, + "data": "0x000000606060" + } + ], + "gasPriceCoef": 128, + "gas": 21000, + "dependsOn": None, + "nonce": 12345678, + "reserved": { + "features": 1 + } + } + + delegated_tx = transaction.Transaction(delegated_body) + raw_tx = '0x' + delegated_tx.encode().hex() + + def allPass(tx_payer:str, tx_origin:str, transaction): + return True, '' + + result = utils.sign_delegated_tx(solo_wallet, clean_wallet.getAddress(), raw_tx, False, allPass) + assert len(result['signature']) > 0 + assert len(result['error_message']) == 0 + + +def test_sign_fee_delegation_reject( + solo_wallet, + clean_wallet +): + ''' Reject the tx sign ''' + delegated_body = { + "chainTag": 1, + "blockRef": "0x00000000aabbccdd", + "expiration": 32, + "clauses": [ + { + "to": "0x7567d83b7b8d80addcb281a71d54fc7b3364ffed", + "value": 10000, + "data": "0x000000606060" + } + ], + "gasPriceCoef": 128, + "gas": 21000, + "dependsOn": None, + "nonce": 12345678, + "reserved": { + "features": 1 + } + } + + delegated_tx = transaction.Transaction(delegated_body) + raw_tx = '0x' + delegated_tx.encode().hex() + + def allPass(tx_payer:str, tx_origin:str, transaction): + return False, 'I just dont allow this tx to pass' + + result = utils.sign_delegated_tx(solo_wallet, clean_wallet.getAddress(), raw_tx, False, allPass) + assert len(result['signature']) == 0 + assert result['error_message'] == 'I just dont allow this tx to pass' \ No newline at end of file diff --git a/tests/test_vtho.py b/tests/test_vtho.py index a471ae8..908b90a 100644 --- a/tests/test_vtho.py +++ b/tests/test_vtho.py @@ -36,6 +36,31 @@ def test_transfer_vtho_easy( assert updated_balance == 3 * (10 ** 18) +def test_transfer_vip180_easy( + solo_connector, + solo_wallet, + clean_wallet, + vtho_contract_address, + vtho_contract +): + # Check the sender's vtho balance + sender_balance = solo_connector.get_vtho_balance(solo_wallet.getAddress()) + assert sender_balance > 0 + + # Check the receiver's vtho balance + receiver_balance = solo_connector.get_vtho_balance(clean_wallet.getAddress()) + assert receiver_balance == 0 + + # Do the vtho transfer (via common transfer token function) + res = solo_connector.transfer_token(solo_wallet, clean_wallet.getAddress(), vtho_contract_address, 3 * (10 ** 18)) + tx_id = res["id"] + receipt = solo_connector.wait_for_tx_receipt(tx_id) + + # Check the receiver's vtho balance + updated_balance = solo_connector.get_vtho_balance(clean_wallet.getAddress()) + assert updated_balance == 3 * (10 ** 18) + + def test_transfer_vtho_hard( solo_connector, solo_wallet, diff --git a/thor_requests/connect.py b/thor_requests/connect.py index 0d88a47..96beb87 100644 --- a/thor_requests/connect.py +++ b/thor_requests/connect.py @@ -673,3 +673,26 @@ def transfer_vtho(self, wallet: Wallet, to: str, vtho_in_wei: int = 0, gas_payer ''' _contract = Contract({"abi": json.loads(VTHO_ABI)}) return self.transact(wallet, _contract, 'transfer', [to, vtho_in_wei], VTHO_ADDRESS, gas_payer=gas_payer) + + def transfer_token(self, wallet: Wallet, to: str, token_contract_addr: str, amount_in_wei: int = 0, gas_payer: Wallet = None) -> dict: + ''' + Convenient function: do a pure vip180 token transfer + + Parameters + ---------- + wallet : Wallet + The sender's wallet + to : str + The receiver's address + token_contract_addr : str + The token's smart contract address + amount_in_wei : int, optional + Amount of token (in Wei) to send to receiver, by default 0 + + Returns + ------- + dict + See post_tx() + ''' + _contract = Contract({"abi": json.loads(VTHO_ABI)}) + return self.transact(wallet, _contract, 'transfer', [to, amount_in_wei], token_contract_addr, gas_payer=gas_payer) \ No newline at end of file diff --git a/thor_requests/utils.py b/thor_requests/utils.py index f1814a8..7d7a309 100644 --- a/thor_requests/utils.py +++ b/thor_requests/utils.py @@ -9,7 +9,7 @@ import secrets -from typing import List, Union +from typing import Callable, List, Union from thor_devkit import abi, cry, transaction from thor_devkit.cry import address, secp256k1 @@ -341,4 +341,57 @@ def suggest_gas_for_tx(vm_gas: int, tx_body: dict) -> int: tx_obj = calc_tx_unsigned(tx_body) intrincis_gas = tx_obj.get_intrinsic_gas() supposed_safe_gas = calc_gas(vm_gas, intrincis_gas) - return supposed_safe_gas \ No newline at end of file + return supposed_safe_gas + + +def sign_delegated_tx(payer: Wallet, tx_origin: str, raw_tx: str, tx_origin_signed:bool, judgment_fn: Callable) -> dict: + ''' + Sign a remote extenal delegated fee transaction, according to VIP-191 and VIP-201 standard. + You should pass in a judgement function to decide whether sign the transaction or not. + + Parameters + ---------- + payer : Wallet + The gas payer + tx_origin : str + The transaction origin (address) + raw_tx : str + '0x' prefixed raw transaction. ('0x.....') + tx_origin_signed : bool + Whether or not origin has signed the tx (should match the raw_tx status you send in) + judgment_fn : Callable + Accepts the params of (tx_payer:str, tx_origin:str, transaction:Transaction) + And returns a result of (success:book, error_message:str) + Returns + ------- + dict + If successfully signed return {'signature': '0x...', 'error_message': ''} + If failed to sign return {'signature': '', 'error_message': 'some text here'} + + Raises + ------ + AttributeError + Origin address not valid + AttributeError + raw_tx is not delegated tx + Exception + If any error occurs during unpacking raw_tx + ''' + # Check tx_origin address + # Raise exception if not valid + if not address.is_address(tx_origin): + raise AttributeError(f'tx_origin: {tx_origin} not valid address') + # Decode raw_tx back to transaction object or body + # Raise exception if not valid + tx = transaction.Transaction.decode(raw=bytes.fromhex(raw_tx.lstrip('0x')), unsigned=(not tx_origin_signed)) + if not tx.is_delegated(): + raise AttributeError(f'raw_tx: is not delegated tx') + # judge the (tx_payer, tx_origin, transaction), decide if can sign or not + should_sign, error_message = judgment_fn(payer.getAddress(), tx_origin, tx) + + if should_sign == True: + payerHash = tx.get_signing_hash(delegate_for=tx_origin.lower()) + payerSigBytes = payer.sign(payerHash) + return { 'signature': '0x' + payerSigBytes.hex(), 'error_message': '' } + else: + return { 'signature': '', 'error_message': error_message }