Skip to content

Commit 1677a41

Browse files
authored
feat: Permissioned keys (#322)
Permissionless key implementation. Interface for the permissioning account is through `NodeClient::authenticators()` for gRPC requests. Interface for the permissioned account is through `Account::authenticators()`. Closes #318. Had to increase fee adjustment multiplier a bit (from `1.4` to `1.8`). While the TS client has it at `1.6`, some transactions were being rejected at this value here. Issue #320 may be related. (edit: ready) Awaiting on a `cosmrs` release which fixes a [small issue](cosmos/cosmos-rust#516) related with non-critical Tx extensions.
1 parent 09706e8 commit 1677a41

File tree

15 files changed

+836
-67
lines changed

15 files changed

+836
-67
lines changed

v4-client-rs/client/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ anyhow.workspace = true
2626
async-trait.workspace = true
2727
bigdecimal.workspace = true
2828
bip32 = { version = "0.5", default-features = false, features = ["bip39", "alloc", "secp256k1"] }
29-
cosmrs = "0.21"
29+
cosmrs = "0.21.1"
3030
chrono = { version = "0.4", features = ["serde"] }
31+
delegate = "0.13"
3132
derive_more.workspace = true
3233
futures-util = "0.3"
3334
governor = { version = "0.8", default-features = false, features = ["std"] }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! Permissioned keys/authenticators example.
2+
//! For more information see [the docs](https://docs.dydx.exchange/api_integration-guides/how_to_permissioned_keys).
3+
4+
mod support;
5+
use anyhow::{Error, Result};
6+
use bigdecimal::BigDecimal;
7+
use dydx::config::ClientConfig;
8+
use dydx::indexer::{IndexerClient, Subaccount};
9+
use dydx::node::{
10+
Account, Authenticator, NodeClient, OrderBuilder, OrderSide, PublicAccount, Wallet,
11+
};
12+
use dydx_proto::dydxprotocol::clob::order::TimeInForce;
13+
use std::str::FromStr;
14+
use support::constants::TEST_MNEMONIC;
15+
use tokio::time::{sleep, Duration};
16+
17+
const ETH_USD_TICKER: &str = "ETH-USD";
18+
19+
pub struct Trader {
20+
client: NodeClient,
21+
indexer: IndexerClient,
22+
account: Account,
23+
}
24+
25+
impl Trader {
26+
pub async fn connect(index: u32) -> Result<Self> {
27+
let config = ClientConfig::from_file("client/tests/testnet.toml").await?;
28+
let mut client = NodeClient::connect(config.node).await?;
29+
let indexer = IndexerClient::new(config.indexer);
30+
let wallet = Wallet::from_mnemonic(TEST_MNEMONIC)?;
31+
let account = wallet.account(index, &mut client).await?;
32+
Ok(Self {
33+
client,
34+
indexer,
35+
account,
36+
})
37+
}
38+
}
39+
40+
#[tokio::main]
41+
async fn main() -> Result<()> {
42+
tracing_subscriber::fmt().try_init().map_err(Error::msg)?;
43+
#[cfg(feature = "telemetry")]
44+
support::telemetry::metrics_dashboard().await?;
45+
46+
// We will just create two (isolated) accounts using the same mnemonic.
47+
// In a more realistic setting each user would have its own mnemonic/wallet.
48+
let mut master = Trader::connect(0).await?;
49+
let master_address = master.account.address().clone();
50+
let mut permissioned = Trader::connect(1).await?;
51+
52+
// -- Permissioning account actions --
53+
54+
log::info!("[master] Creating the authenticator.");
55+
56+
// For permissioned trading, the permissioned account needs an associated authenticator ID,
57+
// created by the permissioning account.
58+
// An authenticator declares the conditions/permissions that allow the permissioned account to
59+
// trade under.
60+
let authenticator = Authenticator::AllOf(vec![
61+
// The permissioned account needs to share its public key with the permissioning account.
62+
// Through other channels, users can share their public keys using hex strings, e.g.,
63+
// let keystring = hex::encode(&account.public_key().to_bytes())
64+
// let bytes = hex::decode(&keystring);
65+
Authenticator::SignatureVerification(permissioned.account.public_key().to_bytes()),
66+
// The allowed actions. Several message types are allowed to be defined, separated by commas.
67+
Authenticator::MessageFilter("/dydxprotocol.clob.MsgPlaceOrder".into()),
68+
// The allowed markets. Several IDs allowed to be defined.
69+
Authenticator::ClobPairIdFilter("0,1".into()),
70+
// The allowed subaccounts. Several subaccounts allowed to be defined.
71+
Authenticator::SubaccountFilter("0".into()),
72+
// A transaction will only be accepted if all conditions above are satisfied.
73+
// Alternatively, `Authenticator::AnyOf` can be used.
74+
// If only one condition was declared (if so, it must be a `Authenticator::SignatureVerification`),
75+
// `AllOf` or `AnyOf` should not be used.
76+
]);
77+
78+
// Broadcast the built authenticator.
79+
master
80+
.client
81+
.authenticators()
82+
.add(&mut master.account, master_address.clone(), authenticator)
83+
.await?;
84+
85+
sleep(Duration::from_secs(3)).await;
86+
87+
// -- Permissioned account actions --
88+
89+
log::info!("[trader] Fetching the authenticator.");
90+
91+
// The permissioned account needs then to acquire the ID associated with the authenticator.
92+
// Here, we will just grab the last authenticator ID pushed under the permissioning account.
93+
let id = permissioned
94+
.client
95+
.authenticators()
96+
.list(master_address.clone())
97+
.await?
98+
.last()
99+
.unwrap()
100+
.id;
101+
102+
// The permissioned account then adds that ID.
103+
// An updated `PublicAccount` account, representing the permissioner, needs to be created.
104+
let external_account =
105+
PublicAccount::updated(master_address.clone(), &mut permissioned.client).await?;
106+
permissioned
107+
.account
108+
.authenticators_mut()
109+
.add(external_account, id);
110+
111+
let master_subaccount = Subaccount {
112+
address: master_address.clone(),
113+
number: 0.try_into()?,
114+
};
115+
116+
log::info!("[trader] Creating the order. Using authenticator ID {id}.");
117+
118+
// Create an order as usual, however for the permissioning account's subaccount.
119+
let market = permissioned
120+
.indexer
121+
.markets()
122+
.get_perpetual_market(&ETH_USD_TICKER.into())
123+
.await?;
124+
let current_block_height = permissioned.client.get_latest_block_height().await?;
125+
126+
let size = BigDecimal::from_str("0.02")?;
127+
let (_id, order) = OrderBuilder::new(market, master_subaccount)
128+
.market(OrderSide::Buy, size)
129+
.reduce_only(false)
130+
.price(100) // market-order slippage protection price
131+
.time_in_force(TimeInForce::Unspecified)
132+
.until(current_block_height.ahead(10))
133+
.build(123456)?;
134+
135+
let tx_hash = permissioned
136+
.client
137+
.place_order(&mut permissioned.account, order)
138+
.await?;
139+
tracing::info!("Broadcast transaction hash: {:?}", tx_hash);
140+
141+
// -- Permissioning account actions --
142+
143+
log::info!("[master] Removing the authenticator.");
144+
145+
// Authenticators can also be removed when not needed anymore
146+
master
147+
.client
148+
.authenticators()
149+
.remove(&mut master.account, master_address, id)
150+
.await?;
151+
152+
Ok(())
153+
}

v4-client-rs/client/examples/withdraw_other.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async fn main() -> Result<()> {
4848
.to_any();
4949
let simulated_tx = client
5050
.builder
51-
.build_transaction(&account, once(msg), None)?;
51+
.build_transaction(&account, once(msg), None, None)?;
5252
let simulation = client.simulate(&simulated_tx).await?;
5353
tracing::info!("Simulation: {:?}", simulation);
5454

@@ -67,7 +67,7 @@ async fn main() -> Result<()> {
6767
.to_any();
6868
let final_tx = client
6969
.builder
70-
.build_transaction(&account, once(final_msg), Some(fee))?;
70+
.build_transaction(&account, once(final_msg), Some(fee), None)?;
7171
let tx_hash = client.broadcast_transaction(final_tx).await?;
7272
tracing::info!("Withdraw transaction hash: {:?}", tx_hash);
7373

v4-client-rs/client/src/indexer/types.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ impl From<AnyId> for ClientId {
161161

162162
/// Clob pair id.
163163
#[serde_as]
164-
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
164+
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
165165
pub struct ClobPairId(#[serde_as(as = "DisplayFromStr")] pub u32);
166166

167167
impl From<u32> for ClobPairId {
@@ -170,6 +170,12 @@ impl From<u32> for ClobPairId {
170170
}
171171
}
172172

173+
impl From<&u32> for ClobPairId {
174+
fn from(value: &u32) -> Self {
175+
ClobPairId::from(*value)
176+
}
177+
}
178+
173179
/// Client metadata.
174180
#[serde_as]
175181
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -455,6 +461,13 @@ impl TryFrom<u32> for SubaccountNumber {
455461
}
456462
}
457463

464+
impl TryFrom<&u32> for SubaccountNumber {
465+
type Error = Error;
466+
fn try_from(number: &u32) -> Result<Self, Error> {
467+
Self::try_from(*number)
468+
}
469+
}
470+
458471
impl From<ParentSubaccountNumber> for SubaccountNumber {
459472
fn from(parent: ParentSubaccountNumber) -> Self {
460473
Self(parent.value())

v4-client-rs/client/src/noble/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ impl NobleClient {
201201

202202
let tx_raw =
203203
self.builder
204-
.build_transaction(account, std::iter::once(msg.to_any()), None)?;
204+
.build_transaction(account, std::iter::once(msg.to_any()), None, None)?;
205205

206206
let simulated = self.simulate(&tx_raw).await?;
207207
let gas = simulated.gas_used;
@@ -212,7 +212,7 @@ impl NobleClient {
212212
.map_err(|e| err!("Raw Tx to bytes failed: {e}"))?;
213213
let tx = Tx::from_bytes(&tx_bytes).map_err(|e| err!("Failed to decode Tx bytes: {e}"))?;
214214
self.builder
215-
.build_transaction(account, tx.body.messages, Some(fee))?;
215+
.build_transaction(account, tx.body.messages, Some(fee), None)?;
216216

217217
let request = BroadcastTxRequest {
218218
tx_bytes: tx_raw

v4-client-rs/client/src/node/builder.rs

+28-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
use super::sequencer::Nonce;
2-
use super::{fee, Account};
3-
use crate::indexer::Denom;
1+
use super::{fee, sequencer::Nonce, Account};
2+
use crate::indexer::{Address, Denom};
43
use anyhow::{anyhow as err, Error, Result};
54
pub use cosmrs::tendermint::chain::Id;
65
use cosmrs::{
76
tx::{self, Fee, SignDoc, SignerInfo},
87
Any,
98
};
9+
use dydx_proto::{dydxprotocol::accountplus::TxExtension, ToAny};
1010

1111
/// Transaction builder.
1212
pub struct TxBuilder {
@@ -44,24 +44,40 @@ impl TxBuilder {
4444
account: &Account,
4545
msgs: impl IntoIterator<Item = Any>,
4646
fee: Option<Fee>,
47+
auth: Option<&Address>,
4748
) -> Result<tx::Raw, Error> {
48-
let tx_body = tx::BodyBuilder::new().msgs(msgs).memo("").finish();
49+
let mut builder = tx::BodyBuilder::new();
50+
builder.msgs(msgs).memo("");
51+
// Add authenticators, if present, as a Tx extension
52+
let mut authing = None;
53+
if let Some(address) = auth {
54+
if let Some((acc, ids)) = account.authenticators().get(address) {
55+
let ext = TxExtension {
56+
selected_authenticators: ids.clone(),
57+
};
58+
builder.non_critical_extension_option(ext.to_any());
59+
authing = Some(acc);
60+
}
61+
}
62+
let tx_body = builder.finish();
4963

5064
let fee = fee.unwrap_or(self.calculate_fee(None)?);
5165

52-
let nonce = match account.next_nonce() {
66+
// If an authenticator is used, use its parameters instead
67+
let (next_nonce, account_number) = if let Some(authing) = authing {
68+
(authing.next_nonce(), authing.account_number())
69+
} else {
70+
(account.next_nonce(), account.account_number())
71+
};
72+
73+
let nonce = match next_nonce {
5374
Some(Nonce::Sequence(number) | Nonce::Timestamp(number)) => *number,
5475
None => return Err(err!("Account's next nonce not set")),
5576
};
5677
let auth_info = SignerInfo::single_direct(Some(account.public_key()), nonce).auth_info(fee);
5778

58-
let sign_doc = SignDoc::new(
59-
&tx_body,
60-
&auth_info,
61-
&self.chain_id,
62-
account.account_number(),
63-
)
64-
.map_err(|e| err!("cannot create sign doc: {e}"))?;
79+
let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_number)
80+
.map_err(|e| err!("cannot create sign doc: {e}"))?;
6581

6682
account.sign(sign_doc)
6783
}

0 commit comments

Comments
 (0)