py-near 1.1.49__py3-none-any.whl → 1.1.51__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.
- py_near/account.py +10 -11
- py_near/models.py +2 -0
- py_near/mpc/__init__.py +0 -0
- py_near/mpc/auth/__init__.py +0 -0
- py_near/mpc/auth/auth_2fa.py +97 -0
- py_near/mpc/auth/base.py +23 -0
- py_near/mpc/auth/core.py +0 -0
- py_near/mpc/auth/default_auth.py +29 -0
- py_near/mpc/auth/keys_auth.py +83 -0
- py_near/mpc/models.py +32 -0
- py_near/mpc/wallet.py +237 -0
- py_near/providers.py +12 -28
- py_near/transactions.py +5 -6
- {py_near-1.1.49.dist-info → py_near-1.1.51.dist-info}/METADATA +9 -5
- {py_near-1.1.49.dist-info → py_near-1.1.51.dist-info}/RECORD +17 -8
- {py_near-1.1.49.dist-info → py_near-1.1.51.dist-info}/LICENSE +0 -0
- {py_near-1.1.49.dist-info → py_near-1.1.51.dist-info}/WHEEL +0 -0
py_near/account.py
CHANGED
@@ -13,7 +13,7 @@ else:
|
|
13
13
|
from collections import MutableSet
|
14
14
|
|
15
15
|
import base58
|
16
|
-
import
|
16
|
+
from nacl import signing, encoding
|
17
17
|
from loguru import logger
|
18
18
|
from py_near_primitives import DelegateAction
|
19
19
|
|
@@ -85,10 +85,8 @@ class Account(object):
|
|
85
85
|
except UnicodeEncodeError:
|
86
86
|
logger.error(f"Can't decode private key {pk[:10]}")
|
87
87
|
continue
|
88
|
-
private_key =
|
89
|
-
public_key =
|
90
|
-
private_key.get_verifying_key().to_bytes()
|
91
|
-
).decode("utf-8")
|
88
|
+
private_key = signing.SigningKey(pk[:32], encoder=encoding.RawEncoder)
|
89
|
+
public_key = private_key.verify_key
|
92
90
|
self._signer_by_pk[public_key] = pk
|
93
91
|
self._free_signers.put_nowait(pk)
|
94
92
|
self._signers.append(pk)
|
@@ -206,11 +204,12 @@ class Account(object):
|
|
206
204
|
if pk is None:
|
207
205
|
pk = self._signers[0]
|
208
206
|
|
209
|
-
private_key =
|
210
|
-
public_key = private_key.
|
207
|
+
private_key = signing.SigningKey(pk[:32], encoder=encoding.RawEncoder)
|
208
|
+
public_key = private_key.verify_key
|
209
|
+
public_key.to_curve25519_public_key()
|
211
210
|
|
212
211
|
resp = await self._provider.get_access_key(
|
213
|
-
self.account_id, base58.b58encode(public_key.
|
212
|
+
self.account_id, base58.b58encode(public_key.encode()).decode("utf8")
|
214
213
|
)
|
215
214
|
if "error" in resp:
|
216
215
|
raise ValueError(resp["error"])
|
@@ -449,8 +448,8 @@ class Account(object):
|
|
449
448
|
access_key = await self.get_access_key(pk)
|
450
449
|
await self._update_last_block_hash()
|
451
450
|
|
452
|
-
private_key =
|
453
|
-
verifying_key = private_key.
|
451
|
+
private_key = signing.SigningKey(pk[:32], encoder=encoding.RawEncoder)
|
452
|
+
verifying_key = private_key.verify_key
|
454
453
|
return DelegateActionModel(
|
455
454
|
sender_id=self.account_id,
|
456
455
|
receiver_id=receiver_id,
|
@@ -479,7 +478,7 @@ class Account(object):
|
|
479
478
|
if public_key not in self._signer_by_pk:
|
480
479
|
raise ValueError(f"Public key {public_key} not found in signer list")
|
481
480
|
|
482
|
-
private_key =
|
481
|
+
private_key = signing.SigningKey(self._signer_by_pk[public_key], encoder=encoding.RawEncoder)
|
483
482
|
sign = private_key.sign(nep461_hash)
|
484
483
|
return base58.b58encode(sign).decode("utf-8")
|
485
484
|
|
py_near/models.py
CHANGED
@@ -48,8 +48,10 @@ class ReceiptOutcome:
|
|
48
48
|
tokens_burnt: str
|
49
49
|
executor_id: str
|
50
50
|
gas_burnt: int
|
51
|
+
receipt_id: str
|
51
52
|
|
52
53
|
def __init__(self, data):
|
54
|
+
self.receipt_id = data['id']
|
53
55
|
self.logs = data["outcome"]["logs"]
|
54
56
|
self.metadata = data["outcome"]["metadata"]
|
55
57
|
self.receipt_ids = data["outcome"]["receipt_ids"]
|
py_near/mpc/__init__.py
ADDED
File without changes
|
File without changes
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import datetime
|
2
|
+
from hashlib import sha256
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
import base58
|
6
|
+
from nacl import signing
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from py_near.account import Account
|
10
|
+
from py_near.mpc.auth.base import AuthContract
|
11
|
+
|
12
|
+
|
13
|
+
class Wallet2FA(BaseModel):
|
14
|
+
wallet_id: str
|
15
|
+
public_key: str
|
16
|
+
delay_to_remove: int
|
17
|
+
cancellation_at: int
|
18
|
+
conditions: Optional[str] = None
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def build(cls, data: dict, wallet_id):
|
22
|
+
return cls(
|
23
|
+
wallet_id=wallet_id,
|
24
|
+
public_key=data["public_key"],
|
25
|
+
cancellation_at=data["cancellation_at"],
|
26
|
+
delay_to_remove=data["delay_to_remove"],
|
27
|
+
conditions=data.get("conditions"),
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
class AuthContract2FA(AuthContract):
|
32
|
+
root_pk: signing.SigningKey
|
33
|
+
contract_id: str = "2fa.auth.hot.tg"
|
34
|
+
|
35
|
+
def __init__(self, root_pk: signing.SigningKey, near_account: Account = None):
|
36
|
+
self.root_pk = root_pk
|
37
|
+
self.near_account = near_account
|
38
|
+
super().__init__()
|
39
|
+
|
40
|
+
def generate_user_payload(self, msg_hash: bytes):
|
41
|
+
auth_signature = base58.b58encode(self.root_pk.sign(msg_hash).signature).decode(
|
42
|
+
"utf-8"
|
43
|
+
)
|
44
|
+
return auth_signature
|
45
|
+
|
46
|
+
async def get_2fa_for_wallet(self, wallet_id: str) -> Optional[Wallet2FA]:
|
47
|
+
if not self.near_account:
|
48
|
+
raise ValueError("Near account is required")
|
49
|
+
res = (
|
50
|
+
await self.near_account.view_function(
|
51
|
+
self.contract_id,
|
52
|
+
"get_wallet_2fa",
|
53
|
+
{
|
54
|
+
"wallet_id": wallet_id,
|
55
|
+
},
|
56
|
+
)
|
57
|
+
).result
|
58
|
+
if res:
|
59
|
+
return Wallet2FA.build(res, wallet_id)
|
60
|
+
|
61
|
+
async def cancel_2fa(
|
62
|
+
self, user_pk: signing.SigningKey, wallet_id: str, rule_id: int = 0
|
63
|
+
):
|
64
|
+
if not self.near_account:
|
65
|
+
raise ValueError("Near account is required")
|
66
|
+
two_fa = await self.get_2fa_for_wallet(wallet_id)
|
67
|
+
ts = int(datetime.datetime.utcnow().timestamp() * 10**9)
|
68
|
+
|
69
|
+
if two_fa.cancellation_at == 0:
|
70
|
+
msg_hash = sha256(f"CANCEL_2FA:{wallet_id}:{ts}".encode("utf-8")).digest()
|
71
|
+
auth_signature = base58.b58encode(user_pk.sign(msg_hash).signature).decode(
|
72
|
+
"utf-8"
|
73
|
+
)
|
74
|
+
return await self.near_account.function_call(
|
75
|
+
self.contract_id,
|
76
|
+
"cancel_2fa",
|
77
|
+
{
|
78
|
+
"wallet_id": wallet_id,
|
79
|
+
"user_signature": auth_signature,
|
80
|
+
"timestamp": ts,
|
81
|
+
},
|
82
|
+
included=True,
|
83
|
+
)
|
84
|
+
else:
|
85
|
+
if two_fa.cancellation_at < ts:
|
86
|
+
raise ValueError(
|
87
|
+
f"2FA already cancelled at {datetime.datetime.fromtimestamp(two_fa.cancellation_at / 10**9)}"
|
88
|
+
)
|
89
|
+
return await self.near_account.function_call(
|
90
|
+
self.contract_id,
|
91
|
+
"complete_cancel_2fa",
|
92
|
+
{
|
93
|
+
"wallet_id": wallet_id,
|
94
|
+
"rule_id": rule_id,
|
95
|
+
},
|
96
|
+
included=True,
|
97
|
+
)
|
py_near/mpc/auth/base.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
from py_near.account import Account
|
2
|
+
from py_near.mpc.models import WalletAccessModel
|
3
|
+
|
4
|
+
|
5
|
+
class AuthContract:
|
6
|
+
contract_id: str
|
7
|
+
|
8
|
+
def __init__(self, near_account: Account = None):
|
9
|
+
self.near_account = near_account
|
10
|
+
|
11
|
+
async def grant_access(
|
12
|
+
self,
|
13
|
+
wallet_id: str,
|
14
|
+
access: WalletAccessModel,
|
15
|
+
near_account,
|
16
|
+
wallet_auth_method=0,
|
17
|
+
):
|
18
|
+
raise NotImplementedError("Grant access is not implemented")
|
19
|
+
|
20
|
+
async def revoke_assess(
|
21
|
+
self, wallet_id: str, near_account: Account, access_id: int
|
22
|
+
):
|
23
|
+
raise NotImplementedError("Revoke access is not implemented")
|
py_near/mpc/auth/core.py
ADDED
File without changes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
import base58
|
4
|
+
from nacl import signing
|
5
|
+
|
6
|
+
from py_near.mpc.auth.base import AuthContract
|
7
|
+
|
8
|
+
|
9
|
+
class DefaultAuthContract(AuthContract):
|
10
|
+
root_pk: signing.SigningKey
|
11
|
+
contract_id: str = "default.auth.hot.tg"
|
12
|
+
|
13
|
+
def __init__(self, root_pk: signing.SigningKey):
|
14
|
+
self.root_pk = root_pk
|
15
|
+
super().__init__()
|
16
|
+
|
17
|
+
def generate_user_payload(self, msg_hash: bytes):
|
18
|
+
auth_signature = base58.b58encode(self.root_pk.sign(msg_hash).signature).decode(
|
19
|
+
"utf-8"
|
20
|
+
)
|
21
|
+
return json.dumps(
|
22
|
+
dict(
|
23
|
+
signature=auth_signature,
|
24
|
+
public_key=base58.b58encode(
|
25
|
+
bytes(self.root_pk.verify_key.encode())
|
26
|
+
).decode(),
|
27
|
+
)
|
28
|
+
)
|
29
|
+
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import json
|
2
|
+
from hashlib import sha256
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
import base58
|
6
|
+
from nacl import signing
|
7
|
+
|
8
|
+
from py_near.account import Account
|
9
|
+
from py_near.mpc.auth.base import AuthContract
|
10
|
+
from py_near.mpc.models import WalletAccessModel
|
11
|
+
|
12
|
+
|
13
|
+
class KeysAuthContract(AuthContract):
|
14
|
+
auth_keys: List[signing.SigningKey]
|
15
|
+
auth_method: int
|
16
|
+
contract_id: str = "keys.auth.hot.tg"
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
auth_keys: List[signing.SigningKey],
|
21
|
+
auth_method=0,
|
22
|
+
near_account: Account = None,
|
23
|
+
):
|
24
|
+
self.auth_keys = auth_keys
|
25
|
+
self.auth_method = auth_method
|
26
|
+
super().__init__(near_account)
|
27
|
+
|
28
|
+
def generate_user_payload(self, msg_hash: bytes):
|
29
|
+
signatures = []
|
30
|
+
for key in self.auth_keys:
|
31
|
+
signatures.append(
|
32
|
+
base58.b58encode(key.sign(msg_hash).signature).decode("utf-8")
|
33
|
+
)
|
34
|
+
return json.dumps(
|
35
|
+
dict(
|
36
|
+
signatures=signatures,
|
37
|
+
auth_method=0,
|
38
|
+
)
|
39
|
+
)
|
40
|
+
|
41
|
+
async def grant_access(
|
42
|
+
self,
|
43
|
+
wallet_id: str,
|
44
|
+
access: WalletAccessModel,
|
45
|
+
near_account,
|
46
|
+
wallet_auth_method=0,
|
47
|
+
):
|
48
|
+
access = access.model_dump()
|
49
|
+
proof_hash = sha256(
|
50
|
+
f"{json.dumps(access).replace(' ', '')}.{wallet_id}".encode("utf-8")
|
51
|
+
).digest()
|
52
|
+
signatures = []
|
53
|
+
for pk in self.auth_keys:
|
54
|
+
signature = base58.b58encode(pk.sign(proof_hash).signature).decode("utf-8")
|
55
|
+
signatures.append(signature)
|
56
|
+
|
57
|
+
res = await near_account.function_call(
|
58
|
+
self.contract_id,
|
59
|
+
"add_wallet_auth_account_id",
|
60
|
+
{
|
61
|
+
"wallet_id": wallet_id,
|
62
|
+
"access": access,
|
63
|
+
"signatures": signatures,
|
64
|
+
"wallet_auth_method": wallet_auth_method,
|
65
|
+
"auth_method": self.auth_method,
|
66
|
+
},
|
67
|
+
included=True,
|
68
|
+
)
|
69
|
+
return res
|
70
|
+
|
71
|
+
async def get_rules(self, wallet_id: str):
|
72
|
+
if not self.near_account:
|
73
|
+
raise ValueError("Near account is required")
|
74
|
+
res = (
|
75
|
+
await self.near_account.view_function(
|
76
|
+
self.contract_id,
|
77
|
+
"get_wallet",
|
78
|
+
{
|
79
|
+
"wallet_id": wallet_id,
|
80
|
+
},
|
81
|
+
)
|
82
|
+
).result
|
83
|
+
return res
|
py_near/mpc/models.py
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Optional, List
|
3
|
+
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
|
7
|
+
class CurveType(int, Enum):
|
8
|
+
SECP256K1 = 0
|
9
|
+
ED25519 = 1
|
10
|
+
|
11
|
+
|
12
|
+
_WALLET_REGISTER = "mpc.hot.tg"
|
13
|
+
|
14
|
+
|
15
|
+
class WalletAccessModel(BaseModel):
|
16
|
+
account_id: str
|
17
|
+
metadata: Optional[str] = None
|
18
|
+
chain_id: int
|
19
|
+
msg: Optional[str] = None
|
20
|
+
|
21
|
+
|
22
|
+
class WalletModel(BaseModel):
|
23
|
+
access_list: List[WalletAccessModel]
|
24
|
+
key_gen: int = 0
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def build(cls, data: dict):
|
28
|
+
return cls(
|
29
|
+
access_list=[WalletAccessModel(**x) for x in data["access_list"]],
|
30
|
+
block_height=data["block_height"],
|
31
|
+
key_gen=data["key_gen"],
|
32
|
+
)
|
py_near/mpc/wallet.py
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
from hashlib import sha256
|
2
|
+
from typing import List, Optional
|
3
|
+
|
4
|
+
import base58
|
5
|
+
import httpx
|
6
|
+
from eth_keys.datatypes import Signature
|
7
|
+
from eth_keys.exceptions import BadSignature
|
8
|
+
from eth_utils import keccak
|
9
|
+
from loguru import logger
|
10
|
+
from nacl import encoding, signing
|
11
|
+
|
12
|
+
from py_near.account import Account
|
13
|
+
from py_near.mpc.auth.auth_2fa import AuthContract2FA
|
14
|
+
from py_near.mpc.auth.base import AuthContract
|
15
|
+
from py_near.mpc.auth.default_auth import DefaultAuthContract
|
16
|
+
from py_near.mpc.auth.keys_auth import KeysAuthContract
|
17
|
+
from py_near.mpc.models import WalletAccessModel, WalletModel, CurveType
|
18
|
+
|
19
|
+
_WALLET_REGISTER = "mpc.hot.tg"
|
20
|
+
|
21
|
+
AUTH_CLASS = {
|
22
|
+
"default.auth.hot.tg": DefaultAuthContract,
|
23
|
+
"keys.auth.hot.tg": KeysAuthContract,
|
24
|
+
"2fa.auth.hot.tg": AuthContract2FA,
|
25
|
+
}
|
26
|
+
|
27
|
+
|
28
|
+
class MPCWallet:
|
29
|
+
derive: bytes
|
30
|
+
near_account: Account
|
31
|
+
hot_rpc: str
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
near_account: Account,
|
36
|
+
default_root_pk: Optional[bytes] = None,
|
37
|
+
hot_rpc: str = "https://rpc1.hotdao.ai",
|
38
|
+
derive: Optional[bytes] = None,
|
39
|
+
):
|
40
|
+
"""
|
41
|
+
:param near_account: Near account
|
42
|
+
:param derive: Derived key for wallet
|
43
|
+
:param hot_rpc: Hot RPC url
|
44
|
+
:param default_root_pk: Default auth key for HOT Protocol, if provided, derive = sha256(default_root_pk.public_key).digest()
|
45
|
+
"""
|
46
|
+
self.near_account = near_account
|
47
|
+
if len(default_root_pk) != 32:
|
48
|
+
raise ValueError("Default root pk should be 32 bytes")
|
49
|
+
if default_root_pk:
|
50
|
+
self.default_root_pk = default_root_pk
|
51
|
+
private_key_obj = self.derive_private_key(0)
|
52
|
+
derive = sha256(private_key_obj.verify_key.encode()).digest()
|
53
|
+
elif derive is None:
|
54
|
+
logger.warning(
|
55
|
+
"Its recomended to use default_root_pk to generate derives, v0 wallets support will be deprecated"
|
56
|
+
)
|
57
|
+
raise ValueError("Derive is required")
|
58
|
+
self.derive = derive
|
59
|
+
self.hot_rpc = hot_rpc
|
60
|
+
self._client = httpx.AsyncClient()
|
61
|
+
|
62
|
+
def derive_private_key(self, gen=0):
|
63
|
+
private_key = self.default_root_pk
|
64
|
+
for _ in range(gen):
|
65
|
+
private_key = sha256(private_key).digest()
|
66
|
+
return signing.SigningKey(
|
67
|
+
private_key,
|
68
|
+
encoder=encoding.RawEncoder,
|
69
|
+
)
|
70
|
+
|
71
|
+
@property
|
72
|
+
def wallet_id(self):
|
73
|
+
return base58.b58encode(sha256(self.derive).digest()).decode("utf-8")
|
74
|
+
|
75
|
+
async def get_wallet(self):
|
76
|
+
wallet = await self.near_account.view_function(
|
77
|
+
_WALLET_REGISTER, "get_wallet", args={"wallet_id": self.wallet_id}
|
78
|
+
)
|
79
|
+
if wallet.result:
|
80
|
+
return WalletModel.build(wallet.result)
|
81
|
+
|
82
|
+
async def create_wallet_with_keys_auth(self, public_key: bytes, key_gen=1):
|
83
|
+
"""
|
84
|
+
Create wallet with keys.auth.hot.tg auth method.
|
85
|
+
:param public_key: Public key for auth future signs on keys.auth.hot.tg
|
86
|
+
"""
|
87
|
+
wallet = await self.get_wallet()
|
88
|
+
if wallet and wallet.access_list[0].account_id != "default.auth.hot.tg":
|
89
|
+
raise ValueError(
|
90
|
+
"MPCWallet already exists with different auth method, please use another wallet_id"
|
91
|
+
)
|
92
|
+
root_pk = self.derive_private_key(0)
|
93
|
+
proof_hash = sha256(f"CREATE_WALLET:{self.wallet_id}".encode("utf-8")).digest()
|
94
|
+
signature = base58.b58encode(root_pk.sign(proof_hash).signature).decode("utf-8")
|
95
|
+
|
96
|
+
s = await self._client.post(
|
97
|
+
f"{self.hot_rpc}/create_wallet",
|
98
|
+
json=dict(
|
99
|
+
public_key=base58.b58encode(public_key).decode(),
|
100
|
+
wallet_id=self.wallet_id,
|
101
|
+
key_gen=key_gen,
|
102
|
+
signature=signature,
|
103
|
+
wallet_derive_public_key=base58.b58encode(
|
104
|
+
root_pk.verify_key.encode()
|
105
|
+
).decode(),
|
106
|
+
),
|
107
|
+
timeout=30,
|
108
|
+
)
|
109
|
+
return s.json()
|
110
|
+
|
111
|
+
async def get_ecdsa_public_key(self) -> bytes:
|
112
|
+
resp = (
|
113
|
+
await self._client.post(
|
114
|
+
f"{self.hot_rpc}/public_key",
|
115
|
+
json=dict(wallet_derive=base58.b58encode(self.derive).decode()),
|
116
|
+
timeout=10,
|
117
|
+
follow_redirects=True,
|
118
|
+
)
|
119
|
+
).json()
|
120
|
+
return bytes.fromhex(resp["ecdsa"])
|
121
|
+
|
122
|
+
@property
|
123
|
+
def public_key(self):
|
124
|
+
# TODO Calculate with Rust Code
|
125
|
+
raise NotImplementedError("Public key calculation is not implemented yet")
|
126
|
+
|
127
|
+
async def sign_message(
|
128
|
+
self,
|
129
|
+
message_body: bytes,
|
130
|
+
curve_type: CurveType = CurveType.SECP256K1,
|
131
|
+
auth_methods: List[AuthContract] = None,
|
132
|
+
):
|
133
|
+
if not self.default_root_pk:
|
134
|
+
raise ValueError("Default auth key is required")
|
135
|
+
wallet = await self.get_wallet()
|
136
|
+
user_payloads = []
|
137
|
+
msg_hash = keccak(message_body)
|
138
|
+
if len(auth_methods) != len(wallet.access_list):
|
139
|
+
raise ValueError("Auth methods count should be equal to wallet access list")
|
140
|
+
|
141
|
+
for auth_contract, auth_method in zip(auth_methods, wallet.access_list):
|
142
|
+
auth_class = AUTH_CLASS[auth_method.account_id]
|
143
|
+
if not isinstance(auth_contract, auth_class):
|
144
|
+
raise ValueError(
|
145
|
+
f"Auth method {auth_method.account_id} is not supported for this auth class"
|
146
|
+
)
|
147
|
+
user_payloads.append(auth_contract.generate_user_payload(msg_hash))
|
148
|
+
|
149
|
+
proof = {
|
150
|
+
"auth_id": 0,
|
151
|
+
"curve_type": curve_type,
|
152
|
+
"user_payloads": user_payloads,
|
153
|
+
"message_body": message_body.hex(),
|
154
|
+
}
|
155
|
+
|
156
|
+
resp = (
|
157
|
+
await self._client.post(
|
158
|
+
f"{self.hot_rpc}/sign_raw",
|
159
|
+
json=dict(
|
160
|
+
uid=self.derive.hex(),
|
161
|
+
message=msg_hash.hex(),
|
162
|
+
proof=proof,
|
163
|
+
key_type=curve_type,
|
164
|
+
),
|
165
|
+
timeout=10,
|
166
|
+
follow_redirects=True,
|
167
|
+
)
|
168
|
+
).json()
|
169
|
+
resp = resp["Ecdsa"]
|
170
|
+
r = int(resp["big_r"][2:], 16)
|
171
|
+
s = int(resp["signature"], 16)
|
172
|
+
|
173
|
+
pk = await self.get_ecdsa_public_key()
|
174
|
+
for v in (0, 1):
|
175
|
+
sig = Signature(vrs=(v, r, s))
|
176
|
+
recovered = sig.recover_public_key_from_msg_hash(msg_hash)
|
177
|
+
if recovered.to_compressed_bytes() == pk:
|
178
|
+
return (
|
179
|
+
r.to_bytes(32, "big") + s.to_bytes(32, "big") + bytes([v])
|
180
|
+
).hex()
|
181
|
+
|
182
|
+
raise BadSignature("Cannot recover public key from signature")
|
183
|
+
|
184
|
+
async def add_new_access_rule(
|
185
|
+
self, access: WalletAccessModel, auth_contracts: List[AuthContract]
|
186
|
+
):
|
187
|
+
wallet = await self.get_wallet()
|
188
|
+
access_json = access.model_dump_json()
|
189
|
+
message_body = f"ADD_AUTH_METHOD:{access_json}:{self.wallet_id}".encode("utf-8")
|
190
|
+
msg_hash = sha256(message_body).digest()
|
191
|
+
user_payloads = []
|
192
|
+
for auth_contract, auth_method in zip(auth_contracts, wallet.access_list):
|
193
|
+
auth_class = AUTH_CLASS[auth_method.account_id]
|
194
|
+
if not isinstance(auth_contract, auth_class):
|
195
|
+
raise ValueError(
|
196
|
+
f"Auth method {auth_method.account_id} is not supported for this auth class"
|
197
|
+
)
|
198
|
+
user_payloads.append(auth_contract.generate_user_payload(msg_hash))
|
199
|
+
return await self.near_account.function_call(
|
200
|
+
_WALLET_REGISTER,
|
201
|
+
"add_access_rule",
|
202
|
+
{
|
203
|
+
"wallet_id": self.wallet_id,
|
204
|
+
"rule": access.model_dump(),
|
205
|
+
"user_payloads": user_payloads,
|
206
|
+
},
|
207
|
+
included=True,
|
208
|
+
)
|
209
|
+
|
210
|
+
async def remove_access_rule(
|
211
|
+
self, access_id: int, auth_contracts: List[AuthContract]
|
212
|
+
):
|
213
|
+
wallet = await self.get_wallet()
|
214
|
+
message_body = f"REMOVE_AUTH_METHOD:{access_id}:{self.wallet_id}".encode(
|
215
|
+
"utf-8"
|
216
|
+
)
|
217
|
+
msg_hash = sha256(message_body).digest()
|
218
|
+
user_payloads = []
|
219
|
+
if len(auth_contracts) != len(wallet.access_list):
|
220
|
+
raise ValueError("Auth methods count should be equal to wallet access list")
|
221
|
+
for auth_contract, auth_method in zip(auth_contracts, wallet.access_list):
|
222
|
+
auth_class = AUTH_CLASS[auth_method.account_id]
|
223
|
+
if not isinstance(auth_contract, auth_class):
|
224
|
+
raise ValueError(
|
225
|
+
f"Auth method {auth_method.account_id} is not supported for this auth class"
|
226
|
+
)
|
227
|
+
user_payloads.append(auth_contract.generate_user_payload(msg_hash))
|
228
|
+
return await self.near_account.function_call(
|
229
|
+
_WALLET_REGISTER,
|
230
|
+
"remove_access_rule",
|
231
|
+
{
|
232
|
+
"wallet_id": self.wallet_id,
|
233
|
+
"rule_id": access_id,
|
234
|
+
"user_payloads": user_payloads,
|
235
|
+
},
|
236
|
+
included=True,
|
237
|
+
)
|
py_near/providers.py
CHANGED
@@ -6,6 +6,7 @@ from collections import Counter
|
|
6
6
|
from typing import Optional
|
7
7
|
|
8
8
|
import httpx
|
9
|
+
from httpx import Limits
|
9
10
|
from loguru import logger
|
10
11
|
|
11
12
|
from py_near.constants import TIMEOUT_WAIT_RPC
|
@@ -60,29 +61,13 @@ class JsonProvider(object):
|
|
60
61
|
self._last_rpc_addr_check = 0
|
61
62
|
self.allow_broadcast = allow_broadcast
|
62
63
|
self._timeout = timeout
|
63
|
-
self._client
|
64
|
+
self._client = httpx.AsyncClient(
|
65
|
+
limits=Limits(max_connections=1000, max_keepalive_connections=200)
|
66
|
+
)
|
64
67
|
|
65
68
|
async def shutdown(self):
|
66
69
|
pass
|
67
70
|
|
68
|
-
async def check_available_rpcs(self):
|
69
|
-
if (
|
70
|
-
self._last_rpc_addr_check < datetime.datetime.now().timestamp() - 30
|
71
|
-
or not self._available_rpcs
|
72
|
-
):
|
73
|
-
self._last_rpc_addr_check = datetime.datetime.now().timestamp()
|
74
|
-
asyncio.create_task(self._check_available_rpcs())
|
75
|
-
|
76
|
-
for _ in range(5):
|
77
|
-
if self._available_rpcs:
|
78
|
-
break
|
79
|
-
await self._check_available_rpcs()
|
80
|
-
await asyncio.sleep(3)
|
81
|
-
|
82
|
-
if not self._available_rpcs:
|
83
|
-
self._available_rpcs = self._rpc_addresses.copy()
|
84
|
-
logger.error("All RPCs are async, reset to default list")
|
85
|
-
|
86
71
|
async def _check_available_rpcs(self):
|
87
72
|
available_rpcs = []
|
88
73
|
for rpc_addr in self._rpc_addresses:
|
@@ -132,7 +117,7 @@ class JsonProvider(object):
|
|
132
117
|
except Exception as e:
|
133
118
|
if rpc_addr in self._available_rpcs:
|
134
119
|
logger.error(f"Remove rpc: {e}")
|
135
|
-
logger.
|
120
|
+
logger.exception(e)
|
136
121
|
self._available_rpcs = available_rpcs
|
137
122
|
|
138
123
|
@staticmethod
|
@@ -144,7 +129,6 @@ class JsonProvider(object):
|
|
144
129
|
async def call_rpc_request(
|
145
130
|
self, method, params, broadcast=False, threshold: int = 0
|
146
131
|
):
|
147
|
-
await self.check_available_rpcs()
|
148
132
|
j = {"method": method, "params": params, "id": "dontcare", "jsonrpc": "2.0"}
|
149
133
|
|
150
134
|
async def f(rpc_call_addr):
|
@@ -236,8 +220,11 @@ class JsonProvider(object):
|
|
236
220
|
break
|
237
221
|
if not body:
|
238
222
|
return error
|
239
|
-
|
240
|
-
|
223
|
+
if len(body) == 1 and list(body.keys())[0] in ERROR_CODE_TO_EXCEPTION:
|
224
|
+
key, body = list(body.items())[0]
|
225
|
+
if isinstance(body, str) and body in ERROR_CODE_TO_EXCEPTION:
|
226
|
+
key = body
|
227
|
+
body = {}
|
241
228
|
error = ERROR_CODE_TO_EXCEPTION[key](
|
242
229
|
body, error_json=content["error"]
|
243
230
|
)
|
@@ -325,7 +312,6 @@ class JsonProvider(object):
|
|
325
312
|
return await self.wait_for_trx(trx_hash, receiver_id)
|
326
313
|
|
327
314
|
async def get_status(self):
|
328
|
-
await self.check_available_rpcs()
|
329
315
|
for rpc_addr in self._available_rpcs.copy():
|
330
316
|
try:
|
331
317
|
data = {
|
@@ -340,9 +326,7 @@ class JsonProvider(object):
|
|
340
326
|
if "@" in rpc_addr:
|
341
327
|
auth_key = rpc_addr.split("//")[1].split("@")[0]
|
342
328
|
rpc_addr = rpc_addr.replace(auth_key + "@", "")
|
343
|
-
headers = {
|
344
|
-
"Authorization": f"Bearer {auth_key}"
|
345
|
-
}
|
329
|
+
headers = {"Authorization": f"Bearer {auth_key}"}
|
346
330
|
r = await self._client.post(rpc_addr, json=data, headers=headers)
|
347
331
|
if r.status_code == 200:
|
348
332
|
return json.loads(r.text)["result"]
|
@@ -462,4 +446,4 @@ class JsonProvider(object):
|
|
462
446
|
return await self.json_rpc("light_client_proof", params)
|
463
447
|
|
464
448
|
async def get_next_light_client_block(self, last_block_hash):
|
465
|
-
return await self.json_rpc("next_light_client_block", [last_block_hash])
|
449
|
+
return await self.json_rpc("next_light_client_block", [last_block_hash])
|
py_near/transactions.py
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
import base64
|
2
2
|
from typing import Union, List
|
3
|
-
|
3
|
+
from nacl import signing, encoding
|
4
4
|
import base58
|
5
|
-
import ed25519
|
6
5
|
from py_near_primitives import (
|
7
6
|
CreateAccountAction,
|
8
7
|
AddKeyAction,
|
@@ -34,11 +33,11 @@ def sign_and_serialize_transaction(
|
|
34
33
|
pk = base58.b58decode(private_key.replace("ed25519:", ""))
|
35
34
|
else:
|
36
35
|
pk = private_key
|
37
|
-
private_key =
|
36
|
+
private_key = signing.SigningKey(pk[:32], encoder=encoding.RawEncoder)
|
38
37
|
|
39
38
|
transaction = Transaction(
|
40
39
|
account_id,
|
41
|
-
private_key.
|
40
|
+
private_key.verify_key.encode(),
|
42
41
|
nonce,
|
43
42
|
receiver_id,
|
44
43
|
block_hash,
|
@@ -61,11 +60,11 @@ def calc_trx_hash(
|
|
61
60
|
pk = base58.b58decode(private_key.replace("ed25519:", ""))
|
62
61
|
else:
|
63
62
|
pk = private_key
|
64
|
-
private_key =
|
63
|
+
private_key = signing.SigningKey(pk[:32], encoder=encoding.RawEncoder)
|
65
64
|
|
66
65
|
transaction = Transaction(
|
67
66
|
account_id,
|
68
|
-
private_key.
|
67
|
+
private_key.verify_key.encode(),
|
69
68
|
nonce,
|
70
69
|
receiver_id,
|
71
70
|
block_hash,
|
@@ -1,20 +1,24 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: py-near
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.51
|
4
4
|
Summary: Pretty simple and fully asynchronous framework for working with NEAR blockchain
|
5
5
|
Author: pvolnov
|
6
6
|
Author-email: petr@herewallet.app
|
7
|
-
Requires-Python: >=3.
|
7
|
+
Requires-Python: >=3.8,<3.12
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: Programming Language :: Python :: 3.7
|
10
9
|
Classifier: Programming Language :: Python :: 3.8
|
11
10
|
Classifier: Programming Language :: Python :: 3.9
|
12
11
|
Classifier: Programming Language :: Python :: 3.10
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
14
|
-
|
15
|
-
Requires-Dist:
|
13
|
+
Requires-Dist: base58 (>=2.1.1,<3.0.0)
|
14
|
+
Requires-Dist: eth-keys (>=0.6.1,<0.7.0)
|
15
|
+
Requires-Dist: eth-utils (>=5.2.0,<6.0.0)
|
16
|
+
Requires-Dist: frozenlist (>=1.4.1,<2.0.0)
|
16
17
|
Requires-Dist: httpx (==0.26.0)
|
18
|
+
Requires-Dist: loguru (>=0.7.2,<0.8.0)
|
17
19
|
Requires-Dist: py-near-primitives (==0.2.3)
|
20
|
+
Requires-Dist: pydantic (>=2.6.2,<3.0.0)
|
21
|
+
Requires-Dist: pynacl (>=1.5.0,<2.0.0)
|
18
22
|
Description-Content-Type: text/markdown
|
19
23
|
|
20
24
|
# py-near
|
@@ -1,5 +1,5 @@
|
|
1
1
|
py_near/__init__.py,sha256=t5fAxjaU8dN8xpQR2vz0ZGhfTkdVy2RCbkhJhZFglk4,50
|
2
|
-
py_near/account.py,sha256=
|
2
|
+
py_near/account.py,sha256=ICLWOzRE7sfEU_ilLGPd0kyzhsMDmS1CWURFFCbk3Io,18200
|
3
3
|
py_near/constants.py,sha256=inaWIuwmF1EB5JSB0ynnZY5rKY_QsxhF9KuCOhPsM6k,164
|
4
4
|
py_near/dapps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
py_near/dapps/core.py,sha256=LtN9aW2gw2mvEdhzQcQJIidtjv-XL1xjb0LK8DzqtqE,231
|
@@ -19,11 +19,20 @@ py_near/dapps/staking/models.py,sha256=zC5M_pc1oMqHq4GaYif1uwFbW6acD2BsiA9rbyiaU
|
|
19
19
|
py_near/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
20
|
py_near/exceptions/exceptions.py,sha256=DEFipaAHm0y7oCuN2QKzHsiQvUTUQVl-Ce36Ag7n7hs,5509
|
21
21
|
py_near/exceptions/provider.py,sha256=K-wexgjPJ8sw42JePwaP7R5dJEIn9DoFJRvVcURsx6s,7718
|
22
|
-
py_near/models.py,sha256=
|
23
|
-
py_near/
|
24
|
-
py_near/
|
22
|
+
py_near/models.py,sha256=85ynmLVJnzWvFKq-Z8iIO0waU-2iP-CSkaOGX4mYoL0,11590
|
23
|
+
py_near/mpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
|
+
py_near/mpc/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
|
+
py_near/mpc/auth/auth_2fa.py,sha256=ZxO8mGO95ixb0qXnKrfmVxP7ivpDKerH4-udV_vUmbs,3124
|
26
|
+
py_near/mpc/auth/base.py,sha256=IoFdRWUQcerYZq8TCkpjXJQRcENnzA9WwFbVPgmHgcs,629
|
27
|
+
py_near/mpc/auth/core.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
|
+
py_near/mpc/auth/default_auth.py,sha256=i70412yXYx3MlFSsPxHLKbF0CXTjC39Qtl34et8kAEw,751
|
29
|
+
py_near/mpc/auth/keys_auth.py,sha256=DJPi37DnV6r6cSskHEhHBiR_QiaAQJkg0AEsYp6cePA,2359
|
30
|
+
py_near/mpc/models.py,sha256=9MSfBDqfqkaJu6eJxnwyjgihsexZboehbJ6vAGa4BTU,663
|
31
|
+
py_near/mpc/wallet.py,sha256=Nihr4TR7MQURyI2wvoNIQ77Dx2_mR797RgyN91UHB8Q,8906
|
32
|
+
py_near/providers.py,sha256=X_GfddFzmafBatqFJH41Dp_lYvTNcz1Q3Ko2tEnN0VU,16193
|
33
|
+
py_near/transactions.py,sha256=QwEP3qOGxb8OB2vZA_rU20jvkPA0FmJowUKDxbuzOuI,3251
|
25
34
|
py_near/utils.py,sha256=FirRH93ydH1cwjn0-sNrZeIn3BRD6QHedrP2VkAdJ6g,126
|
26
|
-
py_near-1.1.
|
27
|
-
py_near-1.1.
|
28
|
-
py_near-1.1.
|
29
|
-
py_near-1.1.
|
35
|
+
py_near-1.1.51.dist-info/LICENSE,sha256=I_GOA9xJ35FiL-KnYXZJdATkbO2KcV2dK2enRGVxzKM,1023
|
36
|
+
py_near-1.1.51.dist-info/METADATA,sha256=0Tcfm023KquhJIWBY5JWhcBb_cyHQsmmIpv3eqloP6A,4846
|
37
|
+
py_near-1.1.51.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
38
|
+
py_near-1.1.51.dist-info/RECORD,,
|
File without changes
|
File without changes
|