-
Notifications
You must be signed in to change notification settings - Fork 27
Approval limit check #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Approval limit check #195
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||
| """OPG token Permit2 approval utilities for x402 payments.""" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| from eth_account.account import LocalAccount | ||||||||||||||||||||||||||||||
|
|
@@ -9,6 +10,9 @@ | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| BASE_OPG_ADDRESS = "0x240b09731D96979f50B2C649C9CE10FcF9C7987F" | ||||||||||||||||||||||||||||||
| BASE_SEPOLIA_RPC = "https://sepolia.base.org" | ||||||||||||||||||||||||||||||
| APPROVAL_TX_TIMEOUT = 120 | ||||||||||||||||||||||||||||||
| ALLOWANCE_CONFIRMATION_TIMEOUT = 120 | ||||||||||||||||||||||||||||||
| ALLOWANCE_POLL_INTERVAL = 1.0 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ERC20_ABI = [ | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
|
|
@@ -102,12 +106,24 @@ def ensure_opg_approval(wallet_account: LocalAccount, opg_amount: float) -> Perm | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| signed = wallet_account.sign_transaction(tx) # type: ignore[arg-type] | ||||||||||||||||||||||||||||||
| tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) | ||||||||||||||||||||||||||||||
| receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120) | ||||||||||||||||||||||||||||||
| receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=APPROVAL_TX_TIMEOUT) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if receipt.status != 1: # type: ignore[attr-defined] | ||||||||||||||||||||||||||||||
| raise RuntimeError(f"Permit2 approval transaction reverted: {tx_hash.hex()}") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| allowance_after = token.functions.allowance(owner, spender).call() | ||||||||||||||||||||||||||||||
| deadline = time.time() + ALLOWANCE_CONFIRMATION_TIMEOUT | ||||||||||||||||||||||||||||||
| allowance_after = allowance_before | ||||||||||||||||||||||||||||||
| while allowance_after < amount_base: | ||||||||||||||||||||||||||||||
| allowance_after = token.functions.allowance(owner, spender).call() | ||||||||||||||||||||||||||||||
| if allowance_after >= amount_base: | ||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||
| if time.time() >= deadline: | ||||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+120
|
||||||||||||||||||||||||||||||
| deadline = time.time() + ALLOWANCE_CONFIRMATION_TIMEOUT | |
| allowance_after = allowance_before | |
| while allowance_after < amount_base: | |
| allowance_after = token.functions.allowance(owner, spender).call() | |
| if allowance_after >= amount_base: | |
| break | |
| if time.time() >= deadline: | |
| deadline = time.monotonic() + ALLOWANCE_CONFIRMATION_TIMEOUT | |
| allowance_after = allowance_before | |
| while allowance_after < amount_base: | |
| allowance_after = token.functions.allowance(owner, spender).call() | |
| if allowance_after >= amount_base: | |
| break | |
| if time.monotonic() >= deadline: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,7 +77,6 @@ def test_zero_amount_with_zero_allowance_skips(self, mock_wallet, mock_web3): | |
|
|
||
| assert result.tx_hash is None | ||
|
|
||
|
|
||
| class TestEnsureOpgApprovalSendsTx: | ||
| """Cases where allowance is insufficient and a transaction is sent.""" | ||
|
|
||
|
|
@@ -150,6 +149,37 @@ def test_gas_estimate_has_20_percent_buffer(self, mock_wallet, mock_web3): | |
| tx_dict = approve_fn.build_transaction.call_args[0][0] | ||
| assert tx_dict["gas"] == int(50_000 * 1.2) | ||
|
|
||
| def test_waits_for_allowance_update_after_receipt(self, mock_wallet, mock_web3, monkeypatch): | ||
| """After a successful receipt, poll allowance until the updated value is visible.""" | ||
| monkeypatch.setattr("opengradient.client.opg_token.ALLOWANCE_POLL_INTERVAL", 0) | ||
| contract = _setup_allowance(mock_web3, 0) | ||
|
|
||
| approve_fn = MagicMock() | ||
| contract.functions.approve.return_value = approve_fn | ||
| approve_fn.estimate_gas.return_value = 50_000 | ||
| approve_fn.build_transaction.return_value = {"mock": "tx"} | ||
|
|
||
| mock_web3.eth.get_transaction_count.return_value = 0 | ||
| mock_web3.eth.gas_price = 1_000_000_000 | ||
| mock_web3.eth.chain_id = 84532 | ||
|
|
||
| signed = MagicMock() | ||
| signed.raw_transaction = b"\x00" | ||
| mock_wallet.sign_transaction.return_value = signed | ||
|
|
||
| tx_hash = MagicMock() | ||
| tx_hash.hex.return_value = "0xconfirmed" | ||
| mock_web3.eth.send_raw_transaction.return_value = tx_hash | ||
| mock_web3.eth.wait_for_transaction_receipt.return_value = SimpleNamespace(status=1, blockNumber=100) | ||
|
|
||
| amount_base = int(1.0 * 10**18) | ||
| contract.functions.allowance.return_value.call.side_effect = [0, 0, amount_base] | ||
|
Comment on lines
+152
to
+176
|
||
|
|
||
| result = ensure_opg_approval(mock_wallet, 1.0) | ||
|
|
||
| assert result.allowance_before == 0 | ||
| assert result.allowance_after == amount_base | ||
| assert result.tx_hash == "0xconfirmed" | ||
|
|
||
| class TestEnsureOpgApprovalErrors: | ||
| """Error handling paths.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import ordering here is inconsistent with other modules in this repo (e.g., stdlib
import ...lines come beforefrom dataclasses import dataclassinsrc/opengradient/client/llm.pyandsrc/opengradient/types.py). Reorder/group the stdlib imports (time,dataclasses,typing) to match the existing convention and avoid isort/ruff import-order noise.