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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,28 @@ async def main():
asyncio.run(main())
```

### Gas Sponsorship Modes

By default, write operations continue to use the existing gas station flow based on
`config.gas_station_api_key` / `config.gas_station_url`. No configuration changes are required.

To use a local fee payer account instead of gas station signing, pass
`fee_payer_account` in `BaseSDKOptions`:

```python
from aptos_sdk.account import Account
from decibel import BaseSDKOptions, DecibelWriteDex, TESTNET_CONFIG

sender = Account.generate()
fee_payer = Account.generate()

write = DecibelWriteDex(
TESTNET_CONFIG,
sender,
opts=BaseSDKOptions(fee_payer_account=fee_payer),
)
```

### WebSocket Streaming

```python
Expand Down
97 changes: 94 additions & 3 deletions src/decibel/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import TYPE_CHECKING, Any, cast

import httpx
from aptos_sdk.account_address import AccountAddress
from aptos_sdk.async_client import RestClient
from aptos_sdk.authenticator import (
AccountAuthenticator,
Expand All @@ -31,7 +32,6 @@

if TYPE_CHECKING:
from aptos_sdk.account import Account
from aptos_sdk.account_address import AccountAddress

from ._constants import DecibelConfig
from ._gas_price_manager import GasPriceManager, GasPriceManagerSync
Expand All @@ -49,12 +49,14 @@
DEFAULT_MAX_GAS_AMOUNT = 200_000
DEFAULT_GAS_ESTIMATE = 100
MAX_GAS_UNITS_LIMIT = 2_000_000
FEE_PAYER_PLACEHOLDER_ADDRESS = AccountAddress.from_str("0x0")


@dataclass
class BaseSDKOptions:
skip_simulate: bool = False
no_fee_payer: bool = False
fee_payer_account: Account | None = None
node_api_key: str | None = None
gas_price_manager: GasPriceManager | None = None
time_delta_ms: int = 0
Expand All @@ -64,6 +66,7 @@ class BaseSDKOptions:
class BaseSDKOptionsSync:
skip_simulate: bool = False
no_fee_payer: bool = False
fee_payer_account: Account | None = None
node_api_key: str | None = None
gas_price_manager: GasPriceManagerSync | None = None
time_delta_ms: int = 0
Expand All @@ -86,10 +89,14 @@ def __init__(
opts = opts or BaseSDKOptions()
self._skip_simulate = opts.skip_simulate
self._no_fee_payer = opts.no_fee_payer
self._fee_payer_account = opts.fee_payer_account
self._node_api_key = opts.node_api_key
self._gas_price_manager = opts.gas_price_manager
self._time_delta_ms = opts.time_delta_ms

if self._no_fee_payer and self._fee_payer_account is not None:
raise ValueError("no_fee_payer and fee_payer_account cannot be used together")

if config.chain_id is None:
logger.warning(
"Using default ABI for unknown chain_id, "
Expand Down Expand Up @@ -156,7 +163,7 @@ async def build_tx(
else:
gas_unit_price = await self._fetch_gas_price_estimation()

return build_simple_transaction_sync(
transaction = build_simple_transaction_sync(
sender=sender,
data=data,
chain_id=self._chain_id,
Expand All @@ -167,6 +174,8 @@ async def build_tx(
time_delta_ms=self._time_delta_ms,
max_gas_amount=max_gas_amount or DEFAULT_MAX_GAS_AMOUNT,
)
self._apply_fee_payer_address_override(transaction)
return transaction

async def submit_tx(
self,
Expand All @@ -175,12 +184,16 @@ async def submit_tx(
*,
txn_submit_timeout: float | None = None,
) -> PendingTransactionResponse:
self._validate_fee_payer_address(transaction)

if self._no_fee_payer:
return await self._submit_direct(transaction, sender_authenticator, txn_submit_timeout)
return await submit_fee_paid_transaction(
self._config,
transaction,
sender_authenticator,
fee_payer_account=self._fee_payer_account,
node_api_key=self._node_api_key,
txn_submit_timeout=txn_submit_timeout,
)

Expand Down Expand Up @@ -273,6 +286,40 @@ def _sign_transaction(
else:
return raw_txn.sign(signer.private_key)

def _apply_fee_payer_address_override(self, transaction: SimpleTransaction) -> None:
if self._fee_payer_account is None:
return

expected_fee_payer = self._fee_payer_account.address()
current_fee_payer = transaction.fee_payer_address

if current_fee_payer == expected_fee_payer:
return

if current_fee_payer == FEE_PAYER_PLACEHOLDER_ADDRESS:
transaction.fee_payer_address = expected_fee_payer
return

if current_fee_payer is None:
raise ValueError(
"transaction.fee_payer_address must be set when fee_payer_account is used"
)

raise ValueError("transaction.fee_payer_address does not match fee_payer_account")

def _validate_fee_payer_address(self, transaction: SimpleTransaction) -> None:
if self._fee_payer_account is None:
return

expected_fee_payer = self._fee_payer_account.address()
if transaction.fee_payer_address is None:
raise ValueError(
"transaction.fee_payer_address must be set when fee_payer_account is used"
)

if transaction.fee_payer_address != expected_fee_payer:
raise ValueError("transaction.fee_payer_address does not match fee_payer_account")

Comment thread
WGB5445 marked this conversation as resolved.
async def _fetch_gas_price_estimation(self) -> int:
url = f"{self._config.fullnode_url}/estimate_gas_price"
headers = self._build_node_headers()
Expand Down Expand Up @@ -458,11 +505,15 @@ def __init__(
opts = opts or BaseSDKOptionsSync()
self._skip_simulate = opts.skip_simulate
self._no_fee_payer = opts.no_fee_payer
self._fee_payer_account = opts.fee_payer_account
self._node_api_key = opts.node_api_key
self._gas_price_manager = opts.gas_price_manager
self._time_delta_ms = opts.time_delta_ms
self._http_client = opts.http_client

if self._no_fee_payer and self._fee_payer_account is not None:
raise ValueError("no_fee_payer and fee_payer_account cannot be used together")

if config.chain_id is None:
logger.warning(
"Using default ABI for unknown chain_id, "
Expand Down Expand Up @@ -525,7 +576,7 @@ def build_tx(
else:
gas_unit_price = self._fetch_gas_price_estimation()

return build_simple_transaction_sync(
transaction = build_simple_transaction_sync(
sender=sender,
data=data,
chain_id=self._chain_id,
Expand All @@ -536,6 +587,8 @@ def build_tx(
time_delta_ms=self._time_delta_ms,
max_gas_amount=max_gas_amount or DEFAULT_MAX_GAS_AMOUNT,
)
self._apply_fee_payer_address_override(transaction)
return transaction

def submit_tx(
self,
Expand All @@ -544,12 +597,16 @@ def submit_tx(
*,
txn_submit_timeout: float | None = None,
) -> PendingTransactionResponse:
self._validate_fee_payer_address(transaction)

if self._no_fee_payer:
return self._submit_direct(transaction, sender_authenticator, txn_submit_timeout)
return submit_fee_paid_transaction_sync(
self._config,
transaction,
sender_authenticator,
fee_payer_account=self._fee_payer_account,
node_api_key=self._node_api_key,
txn_submit_timeout=txn_submit_timeout,
)

Expand Down Expand Up @@ -640,6 +697,40 @@ def _sign_transaction(
else:
return raw_txn.sign(signer.private_key)

def _apply_fee_payer_address_override(self, transaction: SimpleTransaction) -> None:
if self._fee_payer_account is None:
return

expected_fee_payer = self._fee_payer_account.address()
current_fee_payer = transaction.fee_payer_address

if current_fee_payer == expected_fee_payer:
return

if current_fee_payer == FEE_PAYER_PLACEHOLDER_ADDRESS:
transaction.fee_payer_address = expected_fee_payer
return

if current_fee_payer is None:
raise ValueError(
"transaction.fee_payer_address must be set when fee_payer_account is used"
)

raise ValueError("transaction.fee_payer_address does not match fee_payer_account")

def _validate_fee_payer_address(self, transaction: SimpleTransaction) -> None:
if self._fee_payer_account is None:
return

expected_fee_payer = self._fee_payer_account.address()
if transaction.fee_payer_address is None:
raise ValueError(
"transaction.fee_payer_address must be set when fee_payer_account is used"
)

if transaction.fee_payer_address != expected_fee_payer:
raise ValueError("transaction.fee_payer_address does not match fee_payer_account")

Comment thread
WGB5445 marked this conversation as resolved.
def _fetch_gas_price_estimation(self) -> int:
url = f"{self._config.fullnode_url}/estimate_gas_price"
headers = self._build_node_headers()
Expand Down
Loading