Skip to content

Commit 97aaf6c

Browse files
konichuvakpnowosie
authored andcommitted
Add Authenticator flow for Permissioned Keys
I only fixed and cleaned previous PR #328 by konichuvak Author: pnowosie <pawel@nethermind.io>, konichuvak <konichuvak@proton.me>
1 parent 1e2842f commit 97aaf6c

File tree

14 files changed

+1275
-859
lines changed

14 files changed

+1275
-859
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
GAS_MULTIPLIER = 1.4
1+
GAS_MULTIPLIER = 1.7

v4-client-py-v2/dydx_v4_client/network.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ def make_config(
4646
make_secure = partial(make_config, secure_channel)
4747
make_insecure = partial(make_config, insecure_channel)
4848

49-
5049
mainnet_node = partial(
5150
NodeConfig,
5251
"dydx-mainnet-1",
@@ -73,7 +72,6 @@ def make_config(
7372
TESTNET_FAUCET = "https://faucet.v4testnet.dydx.exchange"
7473
TESTNET_NOBLE = "https://rpc.testnet.noble.strange.love"
7574

76-
7775
local_node = partial(
7876
NodeConfig,
7977
"localdydxprotocol",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from enum import Enum
2+
import json
3+
from dataclasses import asdict, dataclass
4+
from typing import List
5+
6+
7+
class AuthenticatorType(str, Enum):
8+
AllOf = "AllOf"
9+
AnyOf = "AnyOf"
10+
SignatureVerification = "SignatureVerification"
11+
MessageFilter = "MessageFilter"
12+
SubaccountFilter = "SubaccountFilter"
13+
ClobPairIdFilter = "ClobPairIdFilter"
14+
15+
16+
@dataclass
17+
class Authenticator:
18+
type: AuthenticatorType
19+
config: bytes
20+
21+
# helpers to create Authenticator instances
22+
@classmethod
23+
def signature_verification(cls, pub_key: bytes) -> "Authenticator":
24+
"""Enables authentication via a specific key."""
25+
return Authenticator(
26+
AuthenticatorType.SignatureVerification,
27+
pub_key,
28+
)
29+
30+
@classmethod
31+
def message_filter(cls, msg_type: str) -> "Authenticator":
32+
"""Restricts authentication to certain message types."""
33+
return Authenticator(
34+
AuthenticatorType.MessageFilter,
35+
msg_type.encode(),
36+
)
37+
38+
@classmethod
39+
def subaccount_filter(cls, subaccounts: List[int]) -> "Authenticator":
40+
"""Restricts authentication to a specific subaccount."""
41+
config = ",".join(map(str, subaccounts))
42+
return Authenticator(
43+
AuthenticatorType.SubaccountFilter,
44+
config.encode(),
45+
)
46+
47+
@classmethod
48+
def clob_pair_id_filter(cls, clob_pair_ids: List[int]) -> "Authenticator":
49+
"""Restricts authentication to a specific clob pair id."""
50+
config = ",".join(map(str, clob_pair_ids))
51+
return Authenticator(
52+
AuthenticatorType.ClobPairIdFilter,
53+
config.encode(),
54+
)
55+
56+
@classmethod
57+
def compose(
58+
cls, auth_type: AuthenticatorType, sub_authenticators: list["Authenticator"]
59+
) -> "Authenticator":
60+
"""Combines multiple sub-authenticators into a single one."""
61+
composed_config = json.dumps(
62+
[sa.encode() for sa in sub_authenticators],
63+
separators=(",", ":"),
64+
)
65+
66+
return Authenticator(
67+
auth_type,
68+
composed_config.encode(),
69+
)
70+
71+
def encode(self):
72+
"""Prepare object for composition."""
73+
dicls = asdict(self)
74+
dicls["config"] = list(dicls["config"])
75+
return dicls

v4-client-py-v2/dydx_v4_client/node/builder.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import List
2+
from typing import List, Optional
33

44
import google
55
from google.protobuf.message import Message
@@ -17,6 +17,7 @@
1717

1818
from dydx_v4_client.node.fee import calculate_fee, Denom
1919
from dydx_v4_client.wallet import Wallet
20+
from v4_proto.dydxprotocol.accountplus.tx_pb2 import TxExtension
2021

2122

2223
def as_any(message: Message):
@@ -50,6 +51,13 @@ def get_signature(key_pair, body, auth_info, account_number, chain_id):
5051
)
5152

5253

54+
@dataclass
55+
class TxOptions:
56+
authenticators: List[int]
57+
sequence: int
58+
account_number: int
59+
60+
5361
@dataclass
5462
class Builder:
5563
chain_id: str
@@ -69,17 +77,48 @@ def fee(self, gas_limit: int, *amount: List[Coin]) -> Fee:
6977
gas_limit=gas_limit,
7078
)
7179

72-
def build_transaction(self, wallet: Wallet, messages: List[Message], fee: Fee):
73-
body = TxBody(messages=messages, memo=self.memo)
80+
def build_transaction(
81+
self,
82+
wallet: Wallet,
83+
messages: List[Message],
84+
fee: Fee,
85+
tx_options: Optional[TxOptions] = None,
86+
):
87+
non_critical_extension_options = []
88+
if tx_options is not None:
89+
tx_extension = TxExtension(
90+
selected_authenticators=tx_options.authenticators,
91+
)
92+
non_critical_extension_options.append(as_any(tx_extension))
93+
body = TxBody(
94+
messages=messages,
95+
memo=self.memo,
96+
non_critical_extension_options=non_critical_extension_options,
97+
)
7498
auth_info = AuthInfo(
75-
signer_infos=[get_signer_info(wallet.public_key, wallet.sequence)],
99+
signer_infos=[
100+
get_signer_info(
101+
wallet.public_key,
102+
tx_options.sequence if tx_options else wallet.sequence,
103+
)
104+
],
76105
fee=fee,
77106
)
78107
signature = get_signature(
79-
wallet.key, body, auth_info, wallet.account_number, self.chain_id
108+
wallet.key,
109+
body,
110+
auth_info,
111+
tx_options.account_number if tx_options else wallet.account_number,
112+
self.chain_id,
80113
)
81114

82115
return Tx(body=body, auth_info=auth_info, signatures=[signature])
83116

84-
def build(self, wallet: Wallet, message: Message, fee: Fee = DEFAULT_FEE):
85-
return self.build_transaction(wallet, [as_any(message)], fee)
117+
def build(
118+
self,
119+
wallet: Wallet,
120+
message: Message,
121+
fee: Fee = DEFAULT_FEE,
122+
tx_options: Optional[dict] = None,
123+
):
124+
return self.build_transaction(wallet, [as_any(message)], fee, tx_options)

v4-client-py-v2/dydx_v4_client/node/client.py

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
SimulateRequest,
2626
)
2727
from v4_proto.cosmos.tx.v1beta1.tx_pb2 import Tx
28+
from v4_proto.dydxprotocol.accountplus import query_pb2 as accountplus_query
29+
from v4_proto.dydxprotocol.accountplus import query_pb2_grpc as accountplus_query_grpc
30+
from v4_proto.dydxprotocol.accountplus import tx_pb2 as accountplus_tx
2831
from v4_proto.dydxprotocol.bridge import query_pb2 as bridge_query
2932
from v4_proto.dydxprotocol.bridge import query_pb2_grpc as bridge_query_grpc
3033
from v4_proto.dydxprotocol.clob import clob_pair_pb2 as clob_pair_type
@@ -69,7 +72,8 @@
6972
from v4_proto.dydxprotocol.clob.tx_pb2 import OrderBatch
7073

7174
from dydx_v4_client.network import NodeConfig
72-
from dydx_v4_client.node.builder import Builder
75+
from dydx_v4_client.node.authenticators import *
76+
from dydx_v4_client.node.builder import Builder, TxOptions
7377
from dydx_v4_client.node.fee import Coin, Fee, calculate_fee, Denom
7478
from dydx_v4_client.node.message import (
7579
cancel_order,
@@ -79,6 +83,8 @@
7983
transfer,
8084
withdraw,
8185
batch_cancel,
86+
add_authenticator,
87+
remove_authenticator,
8288
)
8389
from dydx_v4_client.wallet import Wallet
8490

@@ -429,6 +435,14 @@ async def get_rewards_params(self) -> rewards_query.QueryParamsResponse:
429435
stub = rewards_query_grpc.QueryStub(self.channel)
430436
return stub.Params(rewards_query.QueryParamsRequest())
431437

438+
async def get_authenticators(
439+
self, address: str
440+
) -> accountplus_query.GetAuthenticatorsResponse:
441+
stub = accountplus_query_grpc.QueryStub(self.channel)
442+
return stub.GetAuthenticators(
443+
accountplus_query.GetAuthenticatorsRequest(account=address)
444+
)
445+
432446

433447
class SequenceManager:
434448
def __init__(self, query_node_client: QueryNodeClient):
@@ -528,7 +542,11 @@ async def send_message(
528542
return response
529543

530544
async def broadcast_message(
531-
self, wallet: Wallet, message: Message, mode=BroadcastMode.BROADCAST_MODE_SYNC
545+
self,
546+
wallet: Wallet,
547+
message: Message,
548+
mode=BroadcastMode.BROADCAST_MODE_SYNC,
549+
tx_options: Optional[TxOptions] = None,
532550
):
533551
"""
534552
Broadcasts a message.
@@ -537,16 +555,20 @@ async def broadcast_message(
537555
wallet (Wallet): The wallet to use for signing the transaction.
538556
message (Message): The message to broadcast.
539557
mode (BroadcastMode, optional): The broadcast mode. Defaults to BroadcastMode.BROADCAST_MODE_SYNC.
558+
tx_options (TxOptions, optional): Options for transaction to support authenticators.
540559
541560
Returns:
542561
The response from the broadcast.
543562
"""
544-
if self.sequence_manager:
563+
if not tx_options and self.sequence_manager:
545564
await self.sequence_manager.before_send(wallet)
546565

547-
response = await self.broadcast(self.builder.build(wallet, message), mode)
566+
response = await self.broadcast(
567+
self.builder.build(wallet, message, tx_options=tx_options),
568+
mode,
569+
)
548570

549-
if self.sequence_manager:
571+
if not tx_options and self.sequence_manager:
550572
await self.sequence_manager.after_send(wallet)
551573

552574
return response
@@ -710,25 +732,36 @@ async def transfer(
710732
),
711733
)
712734

713-
async def place_order(self, wallet: Wallet, order: Order):
735+
async def place_order(
736+
self,
737+
wallet: Wallet,
738+
order: Order,
739+
tx_options: Optional[TxOptions] = None,
740+
):
714741
"""
715742
Places an order.
716743
717744
Args:
718745
wallet (Wallet): The wallet to use for signing the transaction.
719746
order (Order): The order to place.
747+
tx_options (TxOptions, optional): Options for transaction to support authenticators.
720748
721749
Returns:
722750
The response from the transaction broadcast.
723751
"""
724-
return await self.broadcast_message(wallet, place_order(order))
752+
return await self.broadcast_message(
753+
wallet,
754+
place_order(order),
755+
tx_options=tx_options,
756+
)
725757

726758
async def cancel_order(
727759
self,
728760
wallet: Wallet,
729761
order_id: OrderId,
730762
good_til_block: int = None,
731763
good_til_block_time: int = None,
764+
tx_options: Optional[TxOptions] = None,
732765
):
733766
"""
734767
Cancels an order.
@@ -738,12 +771,15 @@ async def cancel_order(
738771
order_id (OrderId): The ID of the order to cancel.
739772
good_til_block (int, optional): The block number until which the order is valid. Defaults to None.
740773
good_til_block_time (int, optional): The block time until which the order is valid. Defaults to None.
774+
tx_options (TxOptions, optional): Options for transaction to support authenticators.
741775
742776
Returns:
743777
The response from the transaction broadcast.
744778
"""
745779
return await self.broadcast_message(
746-
wallet, cancel_order(order_id, good_til_block, good_til_block_time)
780+
wallet,
781+
cancel_order(order_id, good_til_block, good_til_block_time),
782+
tx_options=tx_options,
747783
)
748784

749785
async def batch_cancel_orders(
@@ -752,6 +788,7 @@ async def batch_cancel_orders(
752788
subaccount_id: SubaccountId,
753789
short_term_cancels: List[OrderBatch],
754790
good_til_block: int,
791+
tx_options: Optional[TxOptions] = None,
755792
):
756793
"""
757794
Batch cancels orders for a subaccount.
@@ -761,6 +798,7 @@ async def batch_cancel_orders(
761798
subaccount_id (SubaccountId): The subaccount ID for which to cancel orders.
762799
short_term_cancels (List[OrderBatch]): List of OrderBatch objects containing the orders to cancel.
763800
good_til_block (int): The last block the short term order cancellations can be executed at.
801+
tx_options (TxOptions, optional): Options for transaction to support authenticators.
764802
765803
Returns:
766804
The response from the transaction broadcast.
@@ -770,4 +808,56 @@ async def batch_cancel_orders(
770808
short_term_cancels=short_term_cancels,
771809
good_til_block=good_til_block,
772810
)
773-
return await self.broadcast_message(wallet, batch_cancel_msg)
811+
return await self.broadcast_message(
812+
wallet,
813+
batch_cancel_msg,
814+
tx_options=tx_options,
815+
)
816+
817+
async def add_authenticator(
818+
self,
819+
wallet: Wallet,
820+
authenticator: Authenticator,
821+
):
822+
"""
823+
Adds authenticator to a subaccount.
824+
825+
Args:
826+
wallet (Wallet): The wallet to use for signing the transaction or authenticating the request.
827+
authenticator Authenticator: The authenticator to be added.
828+
829+
Returns:
830+
The response from the transaction broadcast.
831+
"""
832+
add_authenticator_msg = add_authenticator(
833+
wallet.address,
834+
authenticator.type,
835+
authenticator.config,
836+
)
837+
838+
return await self.send_message(
839+
wallet,
840+
add_authenticator_msg,
841+
)
842+
843+
async def remove_authenticator(self, wallet: Wallet, authenticator_id: int):
844+
"""
845+
Removes authenticator from a subaccount.
846+
847+
Args:
848+
wallet (Wallet): The wallet to use for signing the transaction or authenticating the request.
849+
authenticator_id (int): The authenticator identifier.
850+
851+
Returns:
852+
The response from the transaction broadcast.
853+
"""
854+
855+
remove_authenticator_msg = remove_authenticator(
856+
wallet.address,
857+
authenticator_id,
858+
)
859+
860+
return await self.send_message(
861+
wallet,
862+
remove_authenticator_msg,
863+
)

0 commit comments

Comments
 (0)