t402 1.9.0__py3-none-any.whl → 1.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- t402/__init__.py +2 -1
- t402/a2a/__init__.py +73 -0
- t402/a2a/helpers.py +158 -0
- t402/a2a/types.py +145 -0
- t402/bridge/client.py +13 -5
- t402/bridge/constants.py +4 -2
- t402/bridge/router.py +1 -1
- t402/bridge/scan.py +3 -1
- t402/chains.py +268 -1
- t402/cli.py +31 -9
- t402/common.py +2 -0
- t402/cosmos_paywall_template.py +2 -0
- t402/django/__init__.py +42 -0
- t402/django/middleware.py +596 -0
- t402/encoding.py +9 -3
- t402/erc4337/accounts.py +56 -51
- t402/erc4337/bundlers.py +105 -99
- t402/erc4337/paymasters.py +100 -109
- t402/erc4337/types.py +39 -26
- t402/errors.py +213 -0
- t402/evm_paywall_template.py +1 -1
- t402/facilitator.py +125 -0
- t402/fastapi/middleware.py +1 -3
- t402/mcp/constants.py +3 -6
- t402/mcp/server.py +501 -84
- t402/mcp/web3_utils.py +493 -0
- t402/multisig/__init__.py +120 -0
- t402/multisig/constants.py +54 -0
- t402/multisig/safe.py +441 -0
- t402/multisig/signature.py +228 -0
- t402/multisig/transaction.py +238 -0
- t402/multisig/types.py +108 -0
- t402/multisig/utils.py +77 -0
- t402/near_paywall_template.py +2 -0
- t402/networks.py +34 -1
- t402/paywall.py +1 -3
- t402/schemes/__init__.py +143 -0
- t402/schemes/aptos/__init__.py +70 -0
- t402/schemes/aptos/constants.py +349 -0
- t402/schemes/aptos/exact_direct/__init__.py +44 -0
- t402/schemes/aptos/exact_direct/client.py +202 -0
- t402/schemes/aptos/exact_direct/facilitator.py +426 -0
- t402/schemes/aptos/exact_direct/server.py +272 -0
- t402/schemes/aptos/types.py +237 -0
- t402/schemes/cosmos/__init__.py +114 -0
- t402/schemes/cosmos/constants.py +211 -0
- t402/schemes/cosmos/exact_direct/__init__.py +21 -0
- t402/schemes/cosmos/exact_direct/client.py +198 -0
- t402/schemes/cosmos/exact_direct/facilitator.py +493 -0
- t402/schemes/cosmos/exact_direct/server.py +315 -0
- t402/schemes/cosmos/types.py +501 -0
- t402/schemes/evm/__init__.py +46 -1
- t402/schemes/evm/exact/__init__.py +11 -0
- t402/schemes/evm/exact/client.py +3 -1
- t402/schemes/evm/exact/facilitator.py +894 -0
- t402/schemes/evm/exact/server.py +1 -1
- t402/schemes/evm/exact_legacy/__init__.py +38 -0
- t402/schemes/evm/exact_legacy/client.py +291 -0
- t402/schemes/evm/exact_legacy/facilitator.py +777 -0
- t402/schemes/evm/exact_legacy/server.py +231 -0
- t402/schemes/evm/upto/__init__.py +12 -0
- t402/schemes/evm/upto/client.py +6 -2
- t402/schemes/evm/upto/facilitator.py +625 -0
- t402/schemes/evm/upto/server.py +243 -0
- t402/schemes/evm/upto/types.py +3 -1
- t402/schemes/interfaces.py +6 -2
- t402/schemes/near/__init__.py +137 -0
- t402/schemes/near/constants.py +189 -0
- t402/schemes/near/exact_direct/__init__.py +21 -0
- t402/schemes/near/exact_direct/client.py +204 -0
- t402/schemes/near/exact_direct/facilitator.py +455 -0
- t402/schemes/near/exact_direct/server.py +303 -0
- t402/schemes/near/types.py +419 -0
- t402/schemes/near/upto/__init__.py +54 -0
- t402/schemes/near/upto/types.py +272 -0
- t402/schemes/polkadot/__init__.py +72 -0
- t402/schemes/polkadot/constants.py +155 -0
- t402/schemes/polkadot/exact_direct/__init__.py +43 -0
- t402/schemes/polkadot/exact_direct/client.py +235 -0
- t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
- t402/schemes/polkadot/exact_direct/server.py +292 -0
- t402/schemes/polkadot/types.py +385 -0
- t402/schemes/registry.py +6 -2
- t402/schemes/stacks/__init__.py +68 -0
- t402/schemes/stacks/constants.py +122 -0
- t402/schemes/stacks/exact_direct/__init__.py +43 -0
- t402/schemes/stacks/exact_direct/client.py +222 -0
- t402/schemes/stacks/exact_direct/facilitator.py +424 -0
- t402/schemes/stacks/exact_direct/server.py +292 -0
- t402/schemes/stacks/types.py +380 -0
- t402/schemes/svm/__init__.py +44 -0
- t402/schemes/svm/exact/__init__.py +35 -0
- t402/schemes/svm/exact/client.py +23 -0
- t402/schemes/svm/exact/facilitator.py +24 -0
- t402/schemes/svm/exact/server.py +20 -0
- t402/schemes/svm/upto/__init__.py +23 -0
- t402/schemes/svm/upto/types.py +193 -0
- t402/schemes/tezos/__init__.py +84 -0
- t402/schemes/tezos/constants.py +372 -0
- t402/schemes/tezos/exact_direct/__init__.py +22 -0
- t402/schemes/tezos/exact_direct/client.py +226 -0
- t402/schemes/tezos/exact_direct/facilitator.py +491 -0
- t402/schemes/tezos/exact_direct/server.py +277 -0
- t402/schemes/tezos/types.py +220 -0
- t402/schemes/ton/__init__.py +24 -2
- t402/schemes/ton/exact/__init__.py +7 -0
- t402/schemes/ton/exact/facilitator.py +730 -0
- t402/schemes/ton/exact/server.py +1 -1
- t402/schemes/ton/upto/__init__.py +31 -0
- t402/schemes/ton/upto/types.py +215 -0
- t402/schemes/tron/__init__.py +28 -2
- t402/schemes/tron/exact/__init__.py +9 -0
- t402/schemes/tron/exact/facilitator.py +673 -0
- t402/schemes/tron/exact/server.py +1 -1
- t402/schemes/tron/upto/__init__.py +30 -0
- t402/schemes/tron/upto/types.py +213 -0
- t402/stacks_paywall_template.py +2 -0
- t402/starlette/__init__.py +38 -0
- t402/starlette/middleware.py +522 -0
- t402/svm.py +45 -11
- t402/svm_paywall_template.py +1 -1
- t402/ton.py +6 -2
- t402/ton_paywall_template.py +1 -192
- t402/tron.py +2 -0
- t402/tron_paywall_template.py +2 -0
- t402/types.py +103 -3
- t402/wdk/chains.py +1 -1
- t402/wdk/errors.py +15 -5
- t402/wdk/signer.py +11 -2
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/METADATA +42 -1
- t402-1.10.0.dist-info/RECORD +156 -0
- t402-1.9.0.dist-info/RECORD +0 -72
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/WHEEL +0 -0
- {t402-1.9.0.dist-info → t402-1.10.0.dist-info}/entry_points.txt +0 -0
t402/mcp/web3_utils.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Web3 utilities for T402 MCP Server blockchain interactions."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from web3 import Web3
|
|
7
|
+
from web3.contract import Contract
|
|
8
|
+
from web3.types import TxReceipt
|
|
9
|
+
|
|
10
|
+
# Minimal ERC-20 ABI for balanceOf, decimals, symbol, transfer, approve, allowance
|
|
11
|
+
ERC20_ABI = [
|
|
12
|
+
{
|
|
13
|
+
"inputs": [{"name": "account", "type": "address"}],
|
|
14
|
+
"name": "balanceOf",
|
|
15
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
16
|
+
"stateMutability": "view",
|
|
17
|
+
"type": "function",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"inputs": [],
|
|
21
|
+
"name": "decimals",
|
|
22
|
+
"outputs": [{"name": "", "type": "uint8"}],
|
|
23
|
+
"stateMutability": "view",
|
|
24
|
+
"type": "function",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"inputs": [],
|
|
28
|
+
"name": "symbol",
|
|
29
|
+
"outputs": [{"name": "", "type": "string"}],
|
|
30
|
+
"stateMutability": "view",
|
|
31
|
+
"type": "function",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"inputs": [
|
|
35
|
+
{"name": "to", "type": "address"},
|
|
36
|
+
{"name": "amount", "type": "uint256"},
|
|
37
|
+
],
|
|
38
|
+
"name": "transfer",
|
|
39
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
40
|
+
"stateMutability": "nonpayable",
|
|
41
|
+
"type": "function",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"inputs": [
|
|
45
|
+
{"name": "spender", "type": "address"},
|
|
46
|
+
{"name": "amount", "type": "uint256"},
|
|
47
|
+
],
|
|
48
|
+
"name": "approve",
|
|
49
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
50
|
+
"stateMutability": "nonpayable",
|
|
51
|
+
"type": "function",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"inputs": [
|
|
55
|
+
{"name": "owner", "type": "address"},
|
|
56
|
+
{"name": "spender", "type": "address"},
|
|
57
|
+
],
|
|
58
|
+
"name": "allowance",
|
|
59
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
60
|
+
"stateMutability": "view",
|
|
61
|
+
"type": "function",
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# OFT ABI for LayerZero quoteSend and send
|
|
66
|
+
OFT_ABI = [
|
|
67
|
+
{
|
|
68
|
+
"inputs": [
|
|
69
|
+
{
|
|
70
|
+
"components": [
|
|
71
|
+
{"name": "dstEid", "type": "uint32"},
|
|
72
|
+
{"name": "to", "type": "bytes32"},
|
|
73
|
+
{"name": "amountLD", "type": "uint256"},
|
|
74
|
+
{"name": "minAmountLD", "type": "uint256"},
|
|
75
|
+
{"name": "extraOptions", "type": "bytes"},
|
|
76
|
+
{"name": "composeMsg", "type": "bytes"},
|
|
77
|
+
{"name": "oftCmd", "type": "bytes"},
|
|
78
|
+
],
|
|
79
|
+
"name": "_sendParam",
|
|
80
|
+
"type": "tuple",
|
|
81
|
+
},
|
|
82
|
+
{"name": "_payInLzToken", "type": "bool"},
|
|
83
|
+
],
|
|
84
|
+
"name": "quoteSend",
|
|
85
|
+
"outputs": [
|
|
86
|
+
{
|
|
87
|
+
"components": [
|
|
88
|
+
{"name": "nativeFee", "type": "uint256"},
|
|
89
|
+
{"name": "lzTokenFee", "type": "uint256"},
|
|
90
|
+
],
|
|
91
|
+
"name": "msgFee",
|
|
92
|
+
"type": "tuple",
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"stateMutability": "view",
|
|
96
|
+
"type": "function",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"inputs": [
|
|
100
|
+
{
|
|
101
|
+
"components": [
|
|
102
|
+
{"name": "dstEid", "type": "uint32"},
|
|
103
|
+
{"name": "to", "type": "bytes32"},
|
|
104
|
+
{"name": "amountLD", "type": "uint256"},
|
|
105
|
+
{"name": "minAmountLD", "type": "uint256"},
|
|
106
|
+
{"name": "extraOptions", "type": "bytes"},
|
|
107
|
+
{"name": "composeMsg", "type": "bytes"},
|
|
108
|
+
{"name": "oftCmd", "type": "bytes"},
|
|
109
|
+
],
|
|
110
|
+
"name": "_sendParam",
|
|
111
|
+
"type": "tuple",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"components": [
|
|
115
|
+
{"name": "nativeFee", "type": "uint256"},
|
|
116
|
+
{"name": "lzTokenFee", "type": "uint256"},
|
|
117
|
+
],
|
|
118
|
+
"name": "_fee",
|
|
119
|
+
"type": "tuple",
|
|
120
|
+
},
|
|
121
|
+
{"name": "_refundAddress", "type": "address"},
|
|
122
|
+
],
|
|
123
|
+
"name": "send",
|
|
124
|
+
"outputs": [
|
|
125
|
+
{
|
|
126
|
+
"components": [
|
|
127
|
+
{"name": "guid", "type": "bytes32"},
|
|
128
|
+
{"name": "nonce", "type": "uint64"},
|
|
129
|
+
{
|
|
130
|
+
"components": [
|
|
131
|
+
{"name": "nativeFee", "type": "uint256"},
|
|
132
|
+
{"name": "lzTokenFee", "type": "uint256"},
|
|
133
|
+
],
|
|
134
|
+
"name": "fee",
|
|
135
|
+
"type": "tuple",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
"name": "msgReceipt",
|
|
139
|
+
"type": "tuple",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"components": [
|
|
143
|
+
{"name": "amountSentLD", "type": "uint256"},
|
|
144
|
+
{"name": "amountReceivedLD", "type": "uint256"},
|
|
145
|
+
],
|
|
146
|
+
"name": "oftReceipt",
|
|
147
|
+
"type": "tuple",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
"stateMutability": "payable",
|
|
151
|
+
"type": "function",
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
# OFTSent event signature: OFTSent(bytes32,uint32,address,uint256,uint256)
|
|
156
|
+
OFT_SENT_EVENT_SIGNATURE = "OFTSent(bytes32,uint32,address,uint256,uint256)"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_web3_provider(rpc_url: str) -> Web3:
|
|
160
|
+
"""Create a Web3 instance connected to the given RPC URL.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
rpc_url: The RPC endpoint URL
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Connected Web3 instance
|
|
167
|
+
"""
|
|
168
|
+
return Web3(Web3.HTTPProvider(rpc_url))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_erc20_contract(w3: Web3, token_address: str) -> Contract:
|
|
172
|
+
"""Get an ERC-20 contract instance.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
w3: Web3 instance
|
|
176
|
+
token_address: Token contract address
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Contract instance
|
|
180
|
+
"""
|
|
181
|
+
return w3.eth.contract(
|
|
182
|
+
address=Web3.to_checksum_address(token_address), abi=ERC20_ABI
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_oft_contract(w3: Web3, token_address: str) -> Contract:
|
|
187
|
+
"""Get an OFT contract instance for LayerZero operations.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
w3: Web3 instance
|
|
191
|
+
token_address: OFT contract address
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Contract instance
|
|
195
|
+
"""
|
|
196
|
+
return w3.eth.contract(
|
|
197
|
+
address=Web3.to_checksum_address(token_address), abi=OFT_ABI
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_erc20_balance(w3: Web3, token_address: str, owner_address: str) -> int:
|
|
202
|
+
"""Query ERC-20 token balance for an address.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
w3: Web3 instance
|
|
206
|
+
token_address: Token contract address
|
|
207
|
+
owner_address: Wallet address to check
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Raw token balance (integer, not formatted)
|
|
211
|
+
"""
|
|
212
|
+
contract = get_erc20_contract(w3, token_address)
|
|
213
|
+
return contract.functions.balanceOf(
|
|
214
|
+
Web3.to_checksum_address(owner_address)
|
|
215
|
+
).call()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_erc20_decimals(w3: Web3, token_address: str) -> int:
|
|
219
|
+
"""Query ERC-20 token decimals.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
w3: Web3 instance
|
|
223
|
+
token_address: Token contract address
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Token decimals
|
|
227
|
+
"""
|
|
228
|
+
contract = get_erc20_contract(w3, token_address)
|
|
229
|
+
return contract.functions.decimals().call()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_erc20_symbol(w3: Web3, token_address: str) -> str:
|
|
233
|
+
"""Query ERC-20 token symbol.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
w3: Web3 instance
|
|
237
|
+
token_address: Token contract address
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Token symbol
|
|
241
|
+
"""
|
|
242
|
+
contract = get_erc20_contract(w3, token_address)
|
|
243
|
+
return contract.functions.symbol().call()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_native_balance(w3: Web3, address: str) -> int:
|
|
247
|
+
"""Get native token balance for an address.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
w3: Web3 instance
|
|
251
|
+
address: Wallet address
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Native balance in wei
|
|
255
|
+
"""
|
|
256
|
+
return w3.eth.get_balance(Web3.to_checksum_address(address))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def transfer_erc20(
|
|
260
|
+
w3: Web3,
|
|
261
|
+
private_key: str,
|
|
262
|
+
token_address: str,
|
|
263
|
+
to: str,
|
|
264
|
+
amount: int,
|
|
265
|
+
) -> TxReceipt:
|
|
266
|
+
"""Build, sign, send, and wait for an ERC-20 transfer transaction.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
w3: Web3 instance
|
|
270
|
+
private_key: Sender's private key (hex with 0x prefix)
|
|
271
|
+
token_address: Token contract address
|
|
272
|
+
to: Recipient address
|
|
273
|
+
amount: Raw token amount to transfer
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Transaction receipt
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
ValueError: If balance is insufficient or transaction fails
|
|
280
|
+
"""
|
|
281
|
+
contract = get_erc20_contract(w3, token_address)
|
|
282
|
+
account = w3.eth.account.from_key(private_key)
|
|
283
|
+
from_address = account.address
|
|
284
|
+
|
|
285
|
+
# Check balance
|
|
286
|
+
balance = contract.functions.balanceOf(from_address).call()
|
|
287
|
+
if balance < amount:
|
|
288
|
+
raise ValueError(
|
|
289
|
+
f"Insufficient token balance: have {balance}, need {amount}"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Build transaction
|
|
293
|
+
tx = contract.functions.transfer(
|
|
294
|
+
Web3.to_checksum_address(to), amount
|
|
295
|
+
).build_transaction(
|
|
296
|
+
{
|
|
297
|
+
"from": from_address,
|
|
298
|
+
"nonce": w3.eth.get_transaction_count(from_address),
|
|
299
|
+
"gas": 0, # Will be estimated
|
|
300
|
+
"gasPrice": w3.eth.gas_price,
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Estimate gas with 20% buffer
|
|
305
|
+
gas_estimate = w3.eth.estimate_gas(tx)
|
|
306
|
+
tx["gas"] = int(gas_estimate * 1.2)
|
|
307
|
+
|
|
308
|
+
# Sign and send
|
|
309
|
+
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
|
|
310
|
+
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
311
|
+
|
|
312
|
+
# Wait for receipt
|
|
313
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
314
|
+
|
|
315
|
+
if receipt["status"] != 1:
|
|
316
|
+
raise ValueError(f"Transaction failed: {tx_hash.hex()}")
|
|
317
|
+
|
|
318
|
+
return receipt
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def format_wei_to_ether(wei_amount: int) -> str:
|
|
322
|
+
"""Format a wei amount to ether string.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
wei_amount: Amount in wei
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Formatted ether string
|
|
329
|
+
"""
|
|
330
|
+
return str(Web3.from_wei(wei_amount, "ether"))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def address_to_bytes32(address: str) -> bytes:
|
|
334
|
+
"""Convert an Ethereum address to bytes32 (left-padded).
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
address: Ethereum address with 0x prefix
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
32-byte array with address right-aligned
|
|
341
|
+
"""
|
|
342
|
+
addr = address.lower().removeprefix("0x")
|
|
343
|
+
addr_bytes = bytes.fromhex(addr)
|
|
344
|
+
return b"\x00" * 12 + addr_bytes
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def quote_bridge_fee(
|
|
348
|
+
w3: Web3,
|
|
349
|
+
oft_address: str,
|
|
350
|
+
dst_eid: int,
|
|
351
|
+
recipient: str,
|
|
352
|
+
amount: int,
|
|
353
|
+
min_amount: int,
|
|
354
|
+
) -> tuple[int, int]:
|
|
355
|
+
"""Query LayerZero OFT quoteSend for bridge fee.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
w3: Web3 instance
|
|
359
|
+
oft_address: USDT0 OFT contract address
|
|
360
|
+
dst_eid: Destination LayerZero endpoint ID
|
|
361
|
+
recipient: Recipient address on destination chain
|
|
362
|
+
amount: Amount to send (raw, 6 decimals)
|
|
363
|
+
min_amount: Minimum amount to receive
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Tuple of (native_fee, lz_token_fee) in wei
|
|
367
|
+
"""
|
|
368
|
+
contract = get_oft_contract(w3, oft_address)
|
|
369
|
+
|
|
370
|
+
send_param = (
|
|
371
|
+
dst_eid,
|
|
372
|
+
address_to_bytes32(recipient),
|
|
373
|
+
amount,
|
|
374
|
+
min_amount,
|
|
375
|
+
b"", # extraOptions
|
|
376
|
+
b"", # composeMsg
|
|
377
|
+
b"", # oftCmd
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
result = contract.functions.quoteSend(send_param, False).call()
|
|
381
|
+
|
|
382
|
+
# Result is a tuple (nativeFee, lzTokenFee)
|
|
383
|
+
if isinstance(result, (list, tuple)):
|
|
384
|
+
return int(result[0]), int(result[1])
|
|
385
|
+
return int(result["nativeFee"]), int(result["lzTokenFee"])
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def execute_bridge_send(
|
|
389
|
+
w3: Web3,
|
|
390
|
+
private_key: str,
|
|
391
|
+
oft_address: str,
|
|
392
|
+
dst_eid: int,
|
|
393
|
+
recipient: str,
|
|
394
|
+
amount: int,
|
|
395
|
+
min_amount: int,
|
|
396
|
+
native_fee: int,
|
|
397
|
+
) -> TxReceipt:
|
|
398
|
+
"""Execute a LayerZero OFT bridge send transaction.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
w3: Web3 instance
|
|
402
|
+
private_key: Sender's private key (hex with 0x prefix)
|
|
403
|
+
oft_address: USDT0 OFT contract address
|
|
404
|
+
dst_eid: Destination LayerZero endpoint ID
|
|
405
|
+
recipient: Recipient address on destination chain
|
|
406
|
+
amount: Amount to send (raw, 6 decimals)
|
|
407
|
+
min_amount: Minimum amount to receive
|
|
408
|
+
native_fee: Native fee from quoteSend (with buffer)
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Transaction receipt
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
ValueError: If transaction fails
|
|
415
|
+
"""
|
|
416
|
+
contract = get_oft_contract(w3, oft_address)
|
|
417
|
+
account = w3.eth.account.from_key(private_key)
|
|
418
|
+
from_address = account.address
|
|
419
|
+
|
|
420
|
+
send_param = (
|
|
421
|
+
dst_eid,
|
|
422
|
+
address_to_bytes32(recipient),
|
|
423
|
+
amount,
|
|
424
|
+
min_amount,
|
|
425
|
+
b"", # extraOptions
|
|
426
|
+
b"", # composeMsg
|
|
427
|
+
b"", # oftCmd
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
fee_param = (native_fee, 0) # (nativeFee, lzTokenFee)
|
|
431
|
+
|
|
432
|
+
tx = contract.functions.send(
|
|
433
|
+
send_param, fee_param, from_address
|
|
434
|
+
).build_transaction(
|
|
435
|
+
{
|
|
436
|
+
"from": from_address,
|
|
437
|
+
"value": native_fee,
|
|
438
|
+
"nonce": w3.eth.get_transaction_count(from_address),
|
|
439
|
+
"gas": 0,
|
|
440
|
+
"gasPrice": w3.eth.gas_price,
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
gas_estimate = w3.eth.estimate_gas(tx)
|
|
445
|
+
tx["gas"] = int(gas_estimate * 1.2)
|
|
446
|
+
|
|
447
|
+
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
|
|
448
|
+
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
449
|
+
|
|
450
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
451
|
+
|
|
452
|
+
if receipt["status"] != 1:
|
|
453
|
+
raise ValueError(f"Bridge transaction failed: {tx_hash.hex()}")
|
|
454
|
+
|
|
455
|
+
return receipt
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def extract_message_guid_from_receipt(receipt: TxReceipt) -> Optional[str]:
|
|
459
|
+
"""Extract LayerZero message GUID from OFTSent event in receipt logs.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
receipt: Transaction receipt
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Message GUID hex string, or None if not found
|
|
466
|
+
"""
|
|
467
|
+
oft_sent_topic = Web3.keccak(text=OFT_SENT_EVENT_SIGNATURE).hex()
|
|
468
|
+
|
|
469
|
+
for log in receipt.get("logs", []):
|
|
470
|
+
topics = log.get("topics", [])
|
|
471
|
+
if len(topics) >= 2:
|
|
472
|
+
topic0 = topics[0].hex() if isinstance(topics[0], bytes) else topics[0]
|
|
473
|
+
if topic0.lower() == oft_sent_topic.lower():
|
|
474
|
+
guid = topics[1].hex() if isinstance(topics[1], bytes) else topics[1]
|
|
475
|
+
if not guid.startswith("0x"):
|
|
476
|
+
guid = "0x" + guid
|
|
477
|
+
return guid
|
|
478
|
+
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
async def run_sync_in_executor(func: Any, *args: Any) -> Any:
|
|
483
|
+
"""Run a synchronous function in an executor for async compatibility.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
func: Synchronous function to call
|
|
487
|
+
*args: Arguments to pass to the function
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Result of the function
|
|
491
|
+
"""
|
|
492
|
+
loop = asyncio.get_event_loop()
|
|
493
|
+
return await loop.run_in_executor(None, func, *args)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
T402 Multi-sig (Safe) Support
|
|
3
|
+
|
|
4
|
+
Provides Safe multi-sig smart account support for T402 payments.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
from t402.multisig import SafeClient, SafeConfig, TransactionBuilder
|
|
9
|
+
|
|
10
|
+
# Create client
|
|
11
|
+
config = SafeConfig(
|
|
12
|
+
address="0x...",
|
|
13
|
+
rpc_url="https://arb1.arbitrum.io/rpc",
|
|
14
|
+
)
|
|
15
|
+
client = SafeClient(config)
|
|
16
|
+
|
|
17
|
+
# Get Safe info
|
|
18
|
+
info = await client.get_info()
|
|
19
|
+
print(f"Owners: {info.owners}, Threshold: {info.threshold}")
|
|
20
|
+
|
|
21
|
+
# Create and propose a transaction
|
|
22
|
+
tx = TransactionBuilder().to("0x...").value(1000000).build()
|
|
23
|
+
request = await client.propose_transaction(tx)
|
|
24
|
+
|
|
25
|
+
# Sign with owner keys
|
|
26
|
+
sig1 = await client.sign_transaction_async(request.transaction, "0x...")
|
|
27
|
+
client.add_signature(request, sig1)
|
|
28
|
+
|
|
29
|
+
# Execute when threshold met
|
|
30
|
+
if request.is_ready():
|
|
31
|
+
result = await client.execute_transaction(request, executor_key)
|
|
32
|
+
```
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from .types import (
|
|
36
|
+
SignatureType,
|
|
37
|
+
OperationType,
|
|
38
|
+
SafeConfig,
|
|
39
|
+
SafeOwner,
|
|
40
|
+
SafeTransaction,
|
|
41
|
+
SafeSignature,
|
|
42
|
+
TransactionRequest,
|
|
43
|
+
SafeInfo,
|
|
44
|
+
ExecutionResult,
|
|
45
|
+
)
|
|
46
|
+
from .constants import (
|
|
47
|
+
SAFE_4337_MODULE,
|
|
48
|
+
SAFE_MODULE_SETUP,
|
|
49
|
+
SAFE_SINGLETON,
|
|
50
|
+
SAFE_PROXY_FACTORY,
|
|
51
|
+
SAFE_FALLBACK_HANDLER,
|
|
52
|
+
SAFE_ADD_MODULES_LIB,
|
|
53
|
+
SAFE_MULTISEND,
|
|
54
|
+
ENTRYPOINT_V07,
|
|
55
|
+
DEFAULT_REQUEST_EXPIRATION_SECONDS,
|
|
56
|
+
MIN_THRESHOLD,
|
|
57
|
+
MAX_OWNERS,
|
|
58
|
+
)
|
|
59
|
+
from .safe import SafeClient
|
|
60
|
+
from .signature import SignatureCollector
|
|
61
|
+
from .transaction import (
|
|
62
|
+
TransactionBuilder,
|
|
63
|
+
BatchTransactionBuilder,
|
|
64
|
+
erc20_transfer,
|
|
65
|
+
eth_transfer,
|
|
66
|
+
contract_call,
|
|
67
|
+
)
|
|
68
|
+
from .utils import (
|
|
69
|
+
generate_request_id,
|
|
70
|
+
current_timestamp,
|
|
71
|
+
sort_addresses,
|
|
72
|
+
is_valid_threshold,
|
|
73
|
+
are_addresses_unique,
|
|
74
|
+
get_owner_index,
|
|
75
|
+
combine_signatures,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
# Types
|
|
81
|
+
"SignatureType",
|
|
82
|
+
"OperationType",
|
|
83
|
+
"SafeConfig",
|
|
84
|
+
"SafeOwner",
|
|
85
|
+
"SafeTransaction",
|
|
86
|
+
"SafeSignature",
|
|
87
|
+
"TransactionRequest",
|
|
88
|
+
"SafeInfo",
|
|
89
|
+
"ExecutionResult",
|
|
90
|
+
# Constants
|
|
91
|
+
"SAFE_4337_MODULE",
|
|
92
|
+
"SAFE_MODULE_SETUP",
|
|
93
|
+
"SAFE_SINGLETON",
|
|
94
|
+
"SAFE_PROXY_FACTORY",
|
|
95
|
+
"SAFE_FALLBACK_HANDLER",
|
|
96
|
+
"SAFE_ADD_MODULES_LIB",
|
|
97
|
+
"SAFE_MULTISEND",
|
|
98
|
+
"ENTRYPOINT_V07",
|
|
99
|
+
"DEFAULT_REQUEST_EXPIRATION_SECONDS",
|
|
100
|
+
"MIN_THRESHOLD",
|
|
101
|
+
"MAX_OWNERS",
|
|
102
|
+
# Client
|
|
103
|
+
"SafeClient",
|
|
104
|
+
# Signature collector
|
|
105
|
+
"SignatureCollector",
|
|
106
|
+
# Transaction builders
|
|
107
|
+
"TransactionBuilder",
|
|
108
|
+
"BatchTransactionBuilder",
|
|
109
|
+
"erc20_transfer",
|
|
110
|
+
"eth_transfer",
|
|
111
|
+
"contract_call",
|
|
112
|
+
# Utilities
|
|
113
|
+
"generate_request_id",
|
|
114
|
+
"current_timestamp",
|
|
115
|
+
"sort_addresses",
|
|
116
|
+
"is_valid_threshold",
|
|
117
|
+
"are_addresses_unique",
|
|
118
|
+
"get_owner_index",
|
|
119
|
+
"combine_signatures",
|
|
120
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants for T402 Multi-sig (Safe) support.
|
|
3
|
+
|
|
4
|
+
Safe 4337 module addresses (v0.3.0) deployed on all major EVM chains.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Safe 4337 Module address
|
|
8
|
+
SAFE_4337_MODULE = "0xa581c4A4DB7175302464fF3C06380BC3270b4037"
|
|
9
|
+
|
|
10
|
+
# Safe Module Setup address
|
|
11
|
+
SAFE_MODULE_SETUP = "0x2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47"
|
|
12
|
+
|
|
13
|
+
# Safe Singleton address
|
|
14
|
+
SAFE_SINGLETON = "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762"
|
|
15
|
+
|
|
16
|
+
# Safe Proxy Factory address
|
|
17
|
+
SAFE_PROXY_FACTORY = "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67"
|
|
18
|
+
|
|
19
|
+
# Safe Fallback Handler address
|
|
20
|
+
SAFE_FALLBACK_HANDLER = "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99"
|
|
21
|
+
|
|
22
|
+
# Add Modules Lib address
|
|
23
|
+
SAFE_ADD_MODULES_LIB = "0x8EcD4ec46D4D2a6B64fE960B3D64e8B94B2234eb"
|
|
24
|
+
|
|
25
|
+
# MultiSend library address
|
|
26
|
+
SAFE_MULTISEND = "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526"
|
|
27
|
+
|
|
28
|
+
# ERC-4337 EntryPoint v0.7 address
|
|
29
|
+
ENTRYPOINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
|
|
30
|
+
|
|
31
|
+
# Default configuration values
|
|
32
|
+
DEFAULT_REQUEST_EXPIRATION_SECONDS = 3600 # 1 hour
|
|
33
|
+
DEFAULT_SALT_NONCE = 0
|
|
34
|
+
MAX_OWNERS = 10
|
|
35
|
+
MIN_THRESHOLD = 1
|
|
36
|
+
|
|
37
|
+
# Safe ABI method selectors (function selectors)
|
|
38
|
+
GET_OWNERS_SELECTOR = bytes.fromhex("a0e67e2b")
|
|
39
|
+
GET_THRESHOLD_SELECTOR = bytes.fromhex("e75b2357")
|
|
40
|
+
NONCE_SELECTOR = bytes.fromhex("affed0e0")
|
|
41
|
+
EXEC_TRANSACTION_SELECTOR = bytes.fromhex("6a761202")
|
|
42
|
+
GET_TRANSACTION_HASH_SELECTOR = bytes.fromhex("d8d11f78")
|
|
43
|
+
|
|
44
|
+
# EIP-712 domain type hash for Safe
|
|
45
|
+
SAFE_DOMAIN_SEPARATOR_TYPEHASH = "EIP712Domain(uint256 chainId,address verifyingContract)"
|
|
46
|
+
|
|
47
|
+
# Safe transaction type hash
|
|
48
|
+
SAFE_TX_TYPEHASH = "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)"
|
|
49
|
+
|
|
50
|
+
# ERC20 transfer selector
|
|
51
|
+
ERC20_TRANSFER_SELECTOR = bytes.fromhex("a9059cbb")
|
|
52
|
+
|
|
53
|
+
# MultiSend selector
|
|
54
|
+
MULTISEND_SELECTOR = bytes.fromhex("8d80ff0a")
|