From 32c0d796299c95ec9d6e5f272fb60ef86add6f45 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Mon, 25 Sep 2023 18:09:55 +0300 Subject: [PATCH 1/9] feat: keyloader interopable with near-cli --- workspaces/Cargo.toml | 1 + workspaces/src/lib.rs | 1 + workspaces/src/types/keyloader.rs | 108 ++++++++++++++++++++++++++++++ workspaces/src/types/mod.rs | 13 ++++ workspaces/tests/keyloader.rs | 23 +++++++ 5 files changed, 146 insertions(+) create mode 100644 workspaces/src/types/keyloader.rs create mode 100644 workspaces/tests/keyloader.rs diff --git a/workspaces/Cargo.toml b/workspaces/Cargo.toml index 93b29fc7..1d0d8d2c 100644 --- a/workspaces/Cargo.toml +++ b/workspaces/Cargo.toml @@ -31,6 +31,7 @@ tokio = { version = "1", features = ["full"] } tokio-retry = "0.3" tracing = "0.1" url = { version = "2.2.2", features = ["serde"] } +keyring = "2.0.5" near-gas = { version = "0.2.3", features = ["serde", "borsh", "schemars"] } near-sdk = { version = "4.1", optional = true } diff --git a/workspaces/src/lib.rs b/workspaces/src/lib.rs index bff3b9fd..c82054e3 100644 --- a/workspaces/src/lib.rs +++ b/workspaces/src/lib.rs @@ -24,6 +24,7 @@ pub use result::Result; pub use types::account::{Account, AccountDetailsPatch, Contract}; pub use types::block::Block; pub use types::chunk::Chunk; +pub use types::keyloader::KeyLoader; pub use types::{AccessKey, AccountId, BlockHeight, CryptoHash, InMemorySigner}; pub use worker::{ betanet, mainnet, mainnet_archival, sandbox, testnet, testnet_archival, with_betanet, diff --git a/workspaces/src/types/keyloader.rs b/workspaces/src/types/keyloader.rs new file mode 100644 index 00000000..3116f1f2 --- /dev/null +++ b/workspaces/src/types/keyloader.rs @@ -0,0 +1,108 @@ +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, + network: &str, + account_id: AccountId, + ) -> Result { + 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() + }); + + if let Some(cred) = credentials { + return Ok(serde_json::from_str::(&cred) + .map_err(|e| Error::custom(ErrorKind::DataConversion, e))?); + } + + 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.to_string()), + ) + .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(()) + } +} diff --git a/workspaces/src/types/mod.rs b/workspaces/src/types/mod.rs index 56a26e43..6d60e99b 100644 --- a/workspaces/src/types/mod.rs +++ b/workspaces/src/types/mod.rs @@ -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; @@ -189,6 +190,12 @@ impl FromStr for PublicKey { } } +impl From for PublicKey { + fn from(pk: near_crypto::PublicKey) -> Self { + Self(pk) + } +} + impl BorshSerialize for PublicKey { fn serialize(&self, writer: &mut W) -> io::Result<()> { // NOTE: sdk::PublicKey requires that we serialize the length of the key first, then the key itself. @@ -267,6 +274,12 @@ impl FromStr for SecretKey { } } +impl From for SecretKey { + fn from(sk: near_crypto::SecretKey) -> Self { + Self(sk) + } +} + #[derive(Clone)] pub struct InMemorySigner { pub(crate) account_id: AccountId, diff --git a/workspaces/tests/keyloader.rs b/workspaces/tests/keyloader.rs new file mode 100644 index 00000000..e1310497 --- /dev/null +++ b/workspaces/tests/keyloader.rs @@ -0,0 +1,23 @@ +use workspaces::{types::keyloader::KeyLoader, Account}; + +#[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("localnet", &id).await?; + + // retrieve from keychain, view account + let account = KeyLoader::from_keychain(&worker, "localnet", id.clone()).await?; + let res = Account::from_secret_key(id, account.private_key.into(), &worker) + .view_account() + .await?; + + assert!(res.balance > 0); + + Ok(()) +} From f88fb3cfd093e77f3e8fd17418ebf3226721852c Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Mon, 25 Sep 2023 18:21:03 +0300 Subject: [PATCH 2/9] fix: clippy & rebase main --- workspaces/src/types/keyloader.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workspaces/src/types/keyloader.rs b/workspaces/src/types/keyloader.rs index 3116f1f2..b5e62965 100644 --- a/workspaces/src/types/keyloader.rs +++ b/workspaces/src/types/keyloader.rs @@ -64,8 +64,8 @@ impl KeyLoader { }); if let Some(cred) = credentials { - return Ok(serde_json::from_str::(&cred) - .map_err(|e| Error::custom(ErrorKind::DataConversion, e))?); + return serde_json::from_str::(&cred) + .map_err(|e| Error::custom(ErrorKind::DataConversion, e)); } Err(Error::custom( @@ -81,7 +81,7 @@ impl KeyLoader { keyring::Entry::new( &service_name, - &format!("{}:{}", account_id, self.public_key.to_string()), + &format!("{}:{}", account_id, self.public_key), ) .map_err(|e| { Error::custom( From 5e2f84d5ffd8525abb181b884b38e2d8e1c78f38 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Mon, 25 Sep 2023 22:58:00 +0300 Subject: [PATCH 3/9] fix: linux sys err --- .github/workflows/test.yml | 7 ++++++- workspaces/src/types/keyloader.rs | 20 ++++++++++---------- workspaces/tests/keyloader.rs | 6 ++++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 308e83ff..b831a2ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,5 +47,10 @@ jobs: run: rustup target add wasm32-unknown-unknown - name: Check with stable features run: cargo check --verbose + - name: Install required dependencies (linux) + run: sudo apt install gnome-keyring + if: matrix.platform == 'ubuntu-latest' - name: Run tests with unstable features - run: NEAR_RPC_TIMEOUT_SECS=100 cargo test --verbose --features unstable + run: | + sudo apt install gnome-ring + NEAR_RPC_TIMEOUT_SECS=100 cargo test --verbose --features unstable diff --git a/workspaces/src/types/keyloader.rs b/workspaces/src/types/keyloader.rs index b5e62965..fc6ee920 100644 --- a/workspaces/src/types/keyloader.rs +++ b/workspaces/src/types/keyloader.rs @@ -36,8 +36,8 @@ impl KeyLoader { /// `near-cli-rs` using the "save-to-keychain" option. /// /// Note: Other tools may use different paths/formats. - pub async fn from_keychain( - worker: &Worker, + pub async fn from_keychain( + worker: &Worker, network: &str, account_id: AccountId, ) -> Result { @@ -63,15 +63,15 @@ impl KeyLoader { keyring.get_password().ok() }); - if let Some(cred) = credentials { - return serde_json::from_str::(&cred) - .map_err(|e| Error::custom(ErrorKind::DataConversion, e)); - } + match credentials { + Some(cred) => serde_json::from_str::(&cred) + .map_err(|e| Error::custom(ErrorKind::DataConversion, e)), - Err(Error::custom( - ErrorKind::Other, - "No access keys found in keychain", - )) + 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 diff --git a/workspaces/tests/keyloader.rs b/workspaces/tests/keyloader.rs index e1310497..8b4d11f5 100644 --- a/workspaces/tests/keyloader.rs +++ b/workspaces/tests/keyloader.rs @@ -1,5 +1,7 @@ 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 @@ -9,10 +11,10 @@ async fn test_keyloader() -> anyhow::Result<()> { assert!(res.is_success()); let credentials = KeyLoader::new(sk.clone(), sk.public_key()); - credentials.to_keychain("localnet", &id).await?; + credentials.to_keychain(NODE_NET, &id).await?; // retrieve from keychain, view account - let account = KeyLoader::from_keychain(&worker, "localnet", id.clone()).await?; + let account = KeyLoader::from_keychain(&worker, NODE_NET, id.clone()).await?; let res = Account::from_secret_key(id, account.private_key.into(), &worker) .view_account() .await?; From 439443bdec03d784853b98716083898950a04890 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Mon, 2 Oct 2023 19:44:12 +0300 Subject: [PATCH 4/9] add: required linux libs --- .github/workflows/test.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b831a2ac..cebf3fb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,11 +45,13 @@ jobs: - uses: Swatinem/rust-cache@v1 - name: Add wasm32 target run: rustup target add wasm32-unknown-unknown + - name: Linux required dependencies + run: | + sudo apt update -y + sudo apt install -y build-essential gnome-keyring + if: matrix.platform == 'ubuntu-latest' - name: Check with stable features run: cargo check --verbose - - name: Install required dependencies (linux) - run: sudo apt install gnome-keyring - if: matrix.platform == 'ubuntu-latest' - name: Run tests with unstable features run: | sudo apt install gnome-ring From 252e33635e699e0f328356cf069adfd45c379fa6 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Tue, 3 Oct 2023 10:56:13 +0300 Subject: [PATCH 5/9] fix: ci test run --- .github/workflows/test.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cebf3fb5..3a47f747 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,14 +45,12 @@ jobs: - uses: Swatinem/rust-cache@v1 - name: Add wasm32 target run: rustup target add wasm32-unknown-unknown - - name: Linux required dependencies + - 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: Check with stable features - run: cargo check --verbose - name: Run tests with unstable features - run: | - sudo apt install gnome-ring - NEAR_RPC_TIMEOUT_SECS=100 cargo test --verbose --features unstable + run: NEAR_RPC_TIMEOUT_SECS=100 cargo test --verbose --features unstable From 63f258093659cb2bd2ee0090a1db66fc7c330de8 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Tue, 3 Oct 2023 12:02:50 +0300 Subject: [PATCH 6/9] add: docs & under unstable --- README.md | 29 +++++++++++++++++++++++++++++ workspaces/src/lib.rs | 3 ++- workspaces/src/types/keyloader.rs | 2 +- workspaces/tests/keyloader.rs | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3bb8aab0..2bf3b4f8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/workspaces/src/lib.rs b/workspaces/src/lib.rs index c82054e3..afcd798c 100644 --- a/workspaces/src/lib.rs +++ b/workspaces/src/lib.rs @@ -7,6 +7,8 @@ mod cargo; #[cfg(feature = "unstable")] pub use cargo::compile_project; +#[cfg(feature = "unstable")] +pub use types::keyloader::KeyLoader; mod worker; @@ -24,7 +26,6 @@ pub use result::Result; pub use types::account::{Account, AccountDetailsPatch, Contract}; pub use types::block::Block; pub use types::chunk::Chunk; -pub use types::keyloader::KeyLoader; pub use types::{AccessKey, AccountId, BlockHeight, CryptoHash, InMemorySigner}; pub use worker::{ betanet, mainnet, mainnet_archival, sandbox, testnet, testnet_archival, with_betanet, diff --git a/workspaces/src/types/keyloader.rs b/workspaces/src/types/keyloader.rs index fc6ee920..179c370e 100644 --- a/workspaces/src/types/keyloader.rs +++ b/workspaces/src/types/keyloader.rs @@ -39,7 +39,7 @@ impl KeyLoader { pub async fn from_keychain( worker: &Worker, network: &str, - account_id: AccountId, + account_id: &AccountId, ) -> Result { let service_name: std::borrow::Cow<'_, str> = std::borrow::Cow::Owned(format!("near-{}-{}", network, account_id.as_str())); diff --git a/workspaces/tests/keyloader.rs b/workspaces/tests/keyloader.rs index 8b4d11f5..0dc22924 100644 --- a/workspaces/tests/keyloader.rs +++ b/workspaces/tests/keyloader.rs @@ -14,7 +14,7 @@ async fn test_keyloader() -> anyhow::Result<()> { credentials.to_keychain(NODE_NET, &id).await?; // retrieve from keychain, view account - let account = KeyLoader::from_keychain(&worker, NODE_NET, id.clone()).await?; + 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?; From 50a191ac6a6200440b480791f5a140029828c65a Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Tue, 3 Oct 2023 18:37:01 +0300 Subject: [PATCH 7/9] upd: cleanup --- workspaces/Cargo.toml | 6 +++--- workspaces/tests/keyloader.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/workspaces/Cargo.toml b/workspaces/Cargo.toml index 1d0d8d2c..80f5c78b 100644 --- a/workspaces/Cargo.toml +++ b/workspaces/Cargo.toml @@ -31,7 +31,7 @@ tokio = { version = "1", features = ["full"] } tokio-retry = "0.3" tracing = "0.1" url = { version = "2.2.2", features = ["serde"] } -keyring = "2.0.5" +keyring = { version = "2.0.5", optional = true } near-gas = { version = "0.2.3", features = ["serde", "borsh", "schemars"] } near-sdk = { version = "4.1", optional = true } @@ -59,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] diff --git a/workspaces/tests/keyloader.rs b/workspaces/tests/keyloader.rs index 0dc22924..d5f88769 100644 --- a/workspaces/tests/keyloader.rs +++ b/workspaces/tests/keyloader.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "unstable")] use workspaces::{types::keyloader::KeyLoader, Account}; const NODE_NET: &str = "sandbox"; From 9288c5d7d1ddddcbc507c795ee14964825e21be7 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Tue, 3 Oct 2023 18:38:09 +0300 Subject: [PATCH 8/9] fix: undo change --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a47f747..b8315db8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,8 @@ jobs: - uses: Swatinem/rust-cache@v1 - name: Add wasm32 target 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 From c4b2bb050a65b24741d825cf85034bd1c656d4e8 Mon Sep 17 00:00:00 2001 From: Yasir Shariff Date: Tue, 3 Oct 2023 18:41:08 +0300 Subject: [PATCH 9/9] fix: crate level feat cfg --- workspaces/src/types/keyloader.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workspaces/src/types/keyloader.rs b/workspaces/src/types/keyloader.rs index 179c370e..508e34ac 100644 --- a/workspaces/src/types/keyloader.rs +++ b/workspaces/src/types/keyloader.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "unstable")] + use near_account_id::AccountId; use crate::{