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: keyloader interopable with near-cli #310

Closed
wants to merge 9 commits into from
Closed
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
7 changes: 7 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,12 @@ jobs:
run: rustup target add wasm32-unknown-unknown
- name: Check with stable features
run: cargo check --verbose
- name: Linux required keychain dependencies
run: |
sudo apt update -y
sudo apt install -y build-essential gnome-keyring
rm -f $HOME/.local/share/keyrings/*
echo -n "test" | gnome-keyring-daemon --unlock
if: matrix.platform == 'ubuntu-latest'
- name: Run tests with unstable features
run: NEAR_RPC_TIMEOUT_SECS=100 cargo test --verbose --features unstable
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,35 @@ async fn test_contract() -> anyhow::Result<()> {

For a full example, take a look at [workspaces/tests/deploy_project.rs](https://github.com/near/workspaces-rs/blob/main/workspaces/tests/deploy_project.rs).

### Accessing Account Credentials from System Keychain

Note, this feature is under the unstable flag as `near-cli` has not hit v1.0 yet. To enable it, add the `unstable` feature flag to `workspaces` dependency in `Cargo.toml`:

```toml
[dependencies]
workspaces = { version = "...", features = ["unstable"] }
```

This is interopable with the `near-cli` tool. If we have a `near-cli` account already setup, we can use the same account credentials to interact with our sandbox/testnet environment.

We can also just use it to set and get account credentials from our system keychain.

```rust
async fn access_account_credentials(account_id: AccountId) -> anyhow::Result<()> {
let worker = workspaces::testnet().await?;

// retrieve from keychain, view account
let account = KeyLoader::from_keychain(&worker, "testnet", &account_id)).await?;
let res = Account::from_secret_key(account_id, account.private_key.into(), &worker)
.view_account()
.await?;

assert!(res.balance > 0);

Ok(())
}
```

### Other Features

Other features can be directly found in the `examples/` folder, with some documentation outlining how they can be used.
Expand Down
5 changes: 3 additions & 2 deletions workspaces/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ tokio = { version = "1", features = ["full"] }
tokio-retry = "0.3"
tracing = "0.1"
url = { version = "2.2.2", features = ["serde"] }
keyring = { version = "2.0.5", optional = true }

near-gas = { version = "0.2.3", features = ["serde", "borsh", "schemars"] }
near-sdk = { version = "4.1", optional = true }
Expand Down Expand Up @@ -58,9 +59,9 @@ tracing-subscriber = { version = "0.3.5", features = ["env-filter"] }

[features]
default = ["install", "interop_sdk"]
install = [] # Install the sandbox binary during compile time
install = [] # Install the sandbox binary during compile time
interop_sdk = ["near-sdk"]
unstable = ["cargo_metadata"]
unstable = ["cargo_metadata", "keyring"]
experimental = ["near-chain-configs"]

[package.metadata.docs.rs]
Expand Down
2 changes: 2 additions & 0 deletions workspaces/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
mod cargo;
#[cfg(feature = "unstable")]
pub use cargo::compile_project;
#[cfg(feature = "unstable")]
pub use types::keyloader::KeyLoader;

mod worker;

Expand Down
110 changes: 110 additions & 0 deletions workspaces/src/types/keyloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#![cfg(feature = "unstable")]

use near_account_id::AccountId;

use crate::{
error::{Error, ErrorKind},
network::NetworkClient,
rpc::query::{Query, ViewAccessKeyList},
types::AccessKeyPermission,
Worker,
};

use super::{PublicKey, SecretKey};

#[derive(Debug, serde::Serialize)]
struct KeyPairProperties {
public_key: near_crypto::PublicKey,
private_key: near_crypto::SecretKey,
}

#[derive(Debug, serde::Deserialize)]
pub struct AccountKeyPair {
pub public_key: near_crypto::PublicKey,
pub private_key: near_crypto::SecretKey,
}

pub type KeyLoader = AccountKeyPair;

impl KeyLoader {
pub fn new(secret_key: SecretKey, public_key: PublicKey) -> Self {
Self {
public_key: public_key.0,
private_key: secret_key.0,
}
}

/// This loads the account information from the keychain. This is interoperable with credentials saved using
/// `near-cli-rs` using the "save-to-keychain" option.
///
/// Note: Other tools may use different paths/formats.
pub async fn from_keychain(
worker: &Worker<impl NetworkClient>,
network: &str,
account_id: &AccountId,
) -> Result<AccountKeyPair, Error> {
let service_name: std::borrow::Cow<'_, str> =
std::borrow::Cow::Owned(format!("near-{}-{}", network, account_id.as_str()));

let access_key_list = Query::new(
worker.client(),
ViewAccessKeyList {
account_id: account_id.clone(),
},
)
.await?;

let credentials = access_key_list
.into_iter()
.filter(|key| matches!(key.access_key.permission, AccessKeyPermission::FullAccess,))
.map(|key| key.public_key)
.find_map(|public_key| {
let keyring =
keyring::Entry::new(&service_name, &format!("{}:{}", account_id, public_key))
.ok()?;
keyring.get_password().ok()
});

match credentials {
Some(cred) => serde_json::from_str::<AccountKeyPair>(&cred)
.map_err(|e| Error::custom(ErrorKind::DataConversion, e)),

None => Err(Error::custom(
ErrorKind::Other,
"No access keys found in keychain",
)),
}
}

/// This saves the account information to the keychain. This is interoperable with credentials saved using
/// `near-cli-rs` using the "save-to-keychain" option.
pub async fn to_keychain(&self, network: &str, account_id: &str) -> Result<(), Error> {
let service_name = std::borrow::Cow::Owned(format!("near-{}-{}", network, account_id));

keyring::Entry::new(
&service_name,
&format!("{}:{}", account_id, self.public_key),
)
.map_err(|e| {
Error::custom(
ErrorKind::Io,
format!("Failed to create keyring entry: {}", e),
)
})?
.set_password(
&serde_json::to_string(&KeyPairProperties {
public_key: self.public_key.clone(),
private_key: self.private_key.clone(),
})
.expect("KeyPairProperties is serializable"),
)
.map_err(|e| {
Error::custom(
ErrorKind::Io,
format!("Failed to set keyring credentials: {}", e),
)
})?;

Ok(())
}
}
13 changes: 13 additions & 0 deletions workspaces/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pub(crate) mod account;
pub(crate) mod block;
pub(crate) mod chunk;
pub mod keyloader;

#[cfg(feature = "interop_sdk")]
mod sdk;
Expand Down Expand Up @@ -189,6 +190,12 @@ impl FromStr for PublicKey {
}
}

impl From<near_crypto::PublicKey> for PublicKey {
fn from(pk: near_crypto::PublicKey) -> Self {
Self(pk)
}
}

impl BorshSerialize for PublicKey {
fn serialize<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
// NOTE: sdk::PublicKey requires that we serialize the length of the key first, then the key itself.
Expand Down Expand Up @@ -267,6 +274,12 @@ impl FromStr for SecretKey {
}
}

impl From<near_crypto::SecretKey> for SecretKey {
fn from(sk: near_crypto::SecretKey) -> Self {
Self(sk)
}
}

#[derive(Clone)]
pub struct InMemorySigner {
pub(crate) account_id: AccountId,
Expand Down
26 changes: 26 additions & 0 deletions workspaces/tests/keyloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#![cfg(feature = "unstable")]
use workspaces::{types::keyloader::KeyLoader, Account};

const NODE_NET: &str = "sandbox";

#[tokio::test]
async fn test_keyloader() -> anyhow::Result<()> {
// creating an account and saving credentials to keychain
let worker = workspaces::sandbox().await?;
let (id, sk) = worker.dev_generate().await;
let res = worker.create_tla(id.clone(), sk.clone()).await?;
assert!(res.is_success());

let credentials = KeyLoader::new(sk.clone(), sk.public_key());
credentials.to_keychain(NODE_NET, &id).await?;

// retrieve from keychain, view account
let account = KeyLoader::from_keychain(&worker, NODE_NET, &id).await?;
let res = Account::from_secret_key(id, account.private_key.into(), &worker)
.view_account()
.await?;

assert!(res.balance > 0);

Ok(())
}
Loading