Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Permissioned keys #322

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion v4-client-rs/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ anyhow.workspace = true
async-trait.workspace = true
bigdecimal.workspace = true
bip32 = { version = "0.5", default-features = false, features = ["bip39", "alloc", "secp256k1"] }
cosmrs = "0.21"
cosmrs = "0.21.1"
chrono = { version = "0.4", features = ["serde"] }
delegate = "0.13"
derive_more.workspace = true
futures-util = "0.3"
governor = { version = "0.8", default-features = false, features = ["std"] }
Expand Down
153 changes: 153 additions & 0 deletions v4-client-rs/client/examples/authenticator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Permissioned keys/authenticators example.
//! For more information see [the docs](https://docs.dydx.exchange/api_integration-guides/how_to_permissioned_keys).

mod support;
use anyhow::{Error, Result};
use bigdecimal::BigDecimal;
use dydx::config::ClientConfig;
use dydx::indexer::{IndexerClient, Subaccount};
use dydx::node::{
Account, AuthenticatorBuilder, NodeClient, OrderBuilder, OrderSide, PublicAccount, Wallet,
};
use dydx_proto::dydxprotocol::clob::order::TimeInForce;
use std::str::FromStr;
use support::constants::TEST_MNEMONIC;
use tokio::time::{sleep, Duration};

const ETH_USD_TICKER: &str = "ETH-USD";

pub struct Trader {
client: NodeClient,
indexer: IndexerClient,
account: Account,
}

impl Trader {
pub async fn connect(index: u32) -> Result<Self> {
let config = ClientConfig::from_file("client/tests/testnet.toml").await?;
let mut client = NodeClient::connect(config.node).await?;
let indexer = IndexerClient::new(config.indexer);
let wallet = Wallet::from_mnemonic(TEST_MNEMONIC)?;
let account = wallet.account(index, &mut client).await?;
Ok(Self {
client,
indexer,
account,
})
}
}

#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt().try_init().map_err(Error::msg)?;
#[cfg(feature = "telemetry")]
support::telemetry::metrics_dashboard().await?;

// We will just create two (isolated) accounts using the same mnemonic.
// In a more realistic setting each user would have its own mnemonic/wallet.
let mut master = Trader::connect(0).await?;
let master_address = master.account.address().clone();
let mut permissioned = Trader::connect(1).await?;

// -- Permissioning account actions --

log::info!("[master] Creating the authenticator.");

// For permissioned trading, the permissioned account needs an associated authenticator ID,
// created by the permissioning account.
// An authenticator declares the conditions/permissions that allow the permissioned account to
// trade under.
let authenticator = AuthenticatorBuilder::empty()
// The permissioned account needs to share its public key with the permissioning account.
// Through other channels, users can share their public keys using hex strings, e.g.,
// let keystring = hex::encode(&account.public_key().to_bytes())
// let bytes = hex::decode(&keystring);
.signature_verification(permissioned.account.public_key().to_bytes())
// The allowed actions
.filter_message(&["/dydxprotocol.clob.MsgPlaceOrder"])
// The allowed markets
.filter_clob_pair([0, 1])
// The allowed subaccounts
.filter_subaccount([0])?
// A transaction will only be accepted if all conditions above are satisfied.
// Alternatively, `.any_of()` can be used.
// If only one condition was declared, `.all_of()` or `.any_of()` should not be used.
.all_of()
.build()?;

// Broadcast the built authenticator.
master
.client
.authenticators()
.add(&mut master.account, master_address.clone(), authenticator)
.await?;

sleep(Duration::from_secs(3)).await;

// -- Permissioned account actions --

log::info!("[trader] Fetching the authenticator.");

// The permissioned account needs then to acquire the ID associated with the authenticator.
// Here, we will just grab the last authenticator ID pushed under the permissioning account.
let id = permissioned
.client
.authenticators()
.list(master_address.clone())
.await?
.last()
.unwrap()
.id;

// The permissioned account then adds that ID.
// An updated `PublicAccount` account, representing the permissioner, needs to be created.
let external_account =
PublicAccount::updated(master_address.clone(), &mut permissioned.client).await?;
permissioned
.account
.authenticators_mut()
.add(external_account, id);

let master_subaccount = Subaccount {
address: master_address.clone(),
number: 0.try_into()?,
};

log::info!("[trader] Creating the order. Using authenticator ID {id}.");

// Create an order as usual, however for the permissioning account's subaccount.
let market = permissioned
.indexer
.markets()
.get_perpetual_market(&ETH_USD_TICKER.into())
.await?;
let current_block_height = permissioned.client.get_latest_block_height().await?;

let size = BigDecimal::from_str("0.02")?;
let (_id, order) = OrderBuilder::new(market, master_subaccount)
.market(OrderSide::Buy, size)
.reduce_only(false)
.price(100) // market-order slippage protection price
.time_in_force(TimeInForce::Unspecified)
.until(current_block_height.ahead(10))
.build(123456)?;

let tx_hash = permissioned
.client
.place_order(&mut permissioned.account, order)
.await?;
tracing::info!("Broadcast transaction hash: {:?}", tx_hash);

// -- Permissioning account actions --

log::info!("[master] Removing the authenticator.");

// Authenticators can also be removed when not needed anymore
master
.client
.authenticators()
.remove(&mut master.account, master_address, id)
.await?;

Ok(())
}
4 changes: 2 additions & 2 deletions v4-client-rs/client/examples/withdraw_other.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ async fn main() -> Result<()> {
.to_any();
let simulated_tx = client
.builder
.build_transaction(&account, once(msg), None)?;
.build_transaction(&account, once(msg), None, None)?;
let simulation = client.simulate(&simulated_tx).await?;
tracing::info!("Simulation: {:?}", simulation);

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

Expand Down
13 changes: 13 additions & 0 deletions v4-client-rs/client/src/indexer/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ impl From<u32> for ClobPairId {
}
}

impl From<&u32> for ClobPairId {
fn from(value: &u32) -> Self {
ClobPairId::from(*value)
}
}

/// Client metadata.
#[serde_as]
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down Expand Up @@ -455,6 +461,13 @@ impl TryFrom<u32> for SubaccountNumber {
}
}

impl TryFrom<&u32> for SubaccountNumber {
type Error = Error;
fn try_from(number: &u32) -> Result<Self, Error> {
Self::try_from(*number)
}
}

impl From<ParentSubaccountNumber> for SubaccountNumber {
fn from(parent: ParentSubaccountNumber) -> Self {
Self(parent.value())
Expand Down
4 changes: 2 additions & 2 deletions v4-client-rs/client/src/noble/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ impl NobleClient {

let tx_raw =
self.builder
.build_transaction(account, std::iter::once(msg.to_any()), None)?;
.build_transaction(account, std::iter::once(msg.to_any()), None, None)?;

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

let request = BroadcastTxRequest {
tx_bytes: tx_raw
Expand Down
40 changes: 28 additions & 12 deletions v4-client-rs/client/src/node/builder.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use super::sequencer::Nonce;
use super::{fee, Account};
use crate::indexer::Denom;
use super::{fee, sequencer::Nonce, Account};
use crate::indexer::{Address, Denom};
use anyhow::{anyhow as err, Error, Result};
pub use cosmrs::tendermint::chain::Id;
use cosmrs::{
tx::{self, Fee, SignDoc, SignerInfo},
Any,
};
use dydx_proto::{dydxprotocol::accountplus::TxExtension, ToAny};

/// Transaction builder.
pub struct TxBuilder {
Expand Down Expand Up @@ -44,24 +44,40 @@ impl TxBuilder {
account: &Account,
msgs: impl IntoIterator<Item = Any>,
fee: Option<Fee>,
auth: Option<&Address>,
) -> Result<tx::Raw, Error> {
let tx_body = tx::BodyBuilder::new().msgs(msgs).memo("").finish();
let mut builder = tx::BodyBuilder::new();
builder.msgs(msgs).memo("");
// Add authenticators, if present, as a Tx extension
let mut authing = None;
if let Some(address) = auth {
if let Some((acc, ids)) = account.authenticators().get(address) {
let ext = TxExtension {
selected_authenticators: ids.clone(),
};
builder.non_critical_extension_option(ext.to_any());
authing = Some(acc);
}
}
let tx_body = builder.finish();

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

let nonce = match account.next_nonce() {
// If an authenticator is used, use its parameters instead
let (next_nonce, account_number) = if let Some(authing) = authing {
(authing.next_nonce(), authing.account_number())
} else {
(account.next_nonce(), account.account_number())
};

let nonce = match next_nonce {
Some(Nonce::Sequence(number) | Nonce::Timestamp(number)) => *number,
None => return Err(err!("Account's next nonce not set")),
};
let auth_info = SignerInfo::single_direct(Some(account.public_key()), nonce).auth_info(fee);

let sign_doc = SignDoc::new(
&tx_body,
&auth_info,
&self.chain_id,
account.account_number(),
)
.map_err(|e| err!("cannot create sign doc: {e}"))?;
let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_number)
.map_err(|e| err!("cannot create sign doc: {e}"))?;

account.sign(sign_doc)
}
Expand Down
Loading
Loading