Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.
Merged
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: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/gem_near/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@ chrono = { workspace = true, features = ["serde"], optional = true }
bs58 = { workspace = true, optional = true }
hex = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
base64 = { workspace = true, optional = true }
gem_hash = { path = "../gem_hash", optional = true }
signer = { path = "../signer", optional = true }

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
reqwest = { workspace = true }
settings = { path = "../settings", features = ["testkit"] }
hex = { workspace = true }

[features]
default = []
rpc = ["dep:gem_jsonrpc", "dep:gem_client", "dep:chain_traits", "dep:async-trait", "dep:chrono", "dep:bs58", "dep:hex", "dep:futures"]
signer = ["dep:base64", "dep:bs58", "dep:gem_hash", "dep:signer"]
reqwest = ["gem_client/reqwest", "gem_jsonrpc/reqwest"]
chain_integration_tests = ["rpc", "reqwest", "settings/testkit"]
4 changes: 4 additions & 0 deletions crates/gem_near/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ pub mod provider;

pub mod constants;
pub mod models;
#[cfg(feature = "signer")]
pub mod signer;

#[cfg(feature = "rpc")]
pub use rpc::*;
#[cfg(feature = "signer")]
pub use signer::*;
38 changes: 38 additions & 0 deletions crates/gem_near/src/signer/chain_signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::signer::models::NearTransfer;
use crate::signer::signing;
use primitives::{ChainSigner, SignerError, SignerInput};

#[derive(Default)]
pub struct NearChainSigner;

impl ChainSigner for NearChainSigner {
fn sign_transfer(&self, input: &SignerInput, private_key: &[u8]) -> Result<String, SignerError> {
let transaction = NearTransfer::from_input(input)?;
signing::sign_transfer(&transaction, private_key)
}
}

#[cfg(test)]
mod tests {
use super::*;
use primitives::{TransactionFee, TransactionLoadInput};

#[test]
fn test_sign_near_transfer() {
let private_key = bs58::decode("3hoMW1HvnRLSFCLZnvPzWeoGwtdHzke34B2cTHM8rhcbG3TbuLKtShTv3DvyejnXKXKBiV7YPkLeqUHN1ghnqpFv")
.into_vec()
.unwrap();

let input = SignerInput::new(
TransactionLoadInput::mock_near("test.near", "whatever.near", "1", 1, "244ZQ9cgj3CQ6bWBdytfrJMuMQ1jdXLFGnr4HhvtCTnM"),
TransactionFee::new_from_fee(0.into()),
);

let signed = NearChainSigner.sign_transfer(&input, &private_key[..32]).unwrap();

assert_eq!(
signed,
"CQAAAHRlc3QubmVhcgCRez0mjUtY9/7BsVC9aNab4+5dTMOYVeNBU4Rlu3eGDQEAAAAAAAAADQAAAHdoYXRldmVyLm5lYXIPpHP9JpAd8pa+atxMxN800EDvokNSJLaYaRDmMML+9gEAAAADAQAAAAAAAAAAAAAAAAAAAACWmoMzIYbul1Xkg5MlUlgG4Ymj0tK7S0dg6URD6X4cTyLe7vAFmo6XExAO2m4ZFE2n6KDvflObIHCLodjQIb0B"
);
}
}
6 changes: 6 additions & 0 deletions crates/gem_near/src/signer/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod chain_signer;
mod models;
mod serialization;
mod signing;

pub use chain_signer::NearChainSigner;
27 changes: 27 additions & 0 deletions crates/gem_near/src/signer/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use primitives::{SignerError, SignerInput};

pub struct NearTransfer {
pub signer_id: String,
pub receiver_id: String,
pub nonce: u64,
pub block_hash: [u8; 32],
pub deposit: [u8; 16],
}

impl NearTransfer {
pub fn from_input(input: &SignerInput) -> Result<Self, SignerError> {
let block_hash: [u8; 32] = bs58::decode(input.metadata.get_block_hash().map_err(SignerError::from_display)?)
.into_vec()
.map_err(|e| SignerError::invalid_input(format!("invalid NEAR block hash: {e}")))?
.try_into()
.map_err(|_| SignerError::invalid_input("NEAR block hash must be 32 bytes"))?;

Ok(Self {
signer_id: input.sender_address.clone(),
receiver_id: input.destination_address.clone(),
nonce: input.metadata.get_sequence().map_err(SignerError::from_display)?,
block_hash,
deposit: input.value.parse::<u128>().map_err(|_| SignerError::invalid_input("invalid NEAR amount"))?.to_le_bytes(),
})
}
}
24 changes: 24 additions & 0 deletions crates/gem_near/src/signer/serialization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use super::models::NearTransfer;

pub const ED25519_KEY_TYPE: u8 = 0;
const TRANSFER_ACTION: u8 = 3;

pub fn encode_transfer(transfer: &NearTransfer, public_key: &[u8; 32]) -> Vec<u8> {
let mut buf = Vec::with_capacity(128);
write_string(&mut buf, &transfer.signer_id);
buf.push(ED25519_KEY_TYPE);
buf.extend_from_slice(public_key);
buf.extend_from_slice(&transfer.nonce.to_le_bytes());
write_string(&mut buf, &transfer.receiver_id);
buf.extend_from_slice(&transfer.block_hash);
// 1 action
buf.extend_from_slice(&1u32.to_le_bytes());
buf.push(TRANSFER_ACTION);
buf.extend_from_slice(&transfer.deposit);
buf
}

fn write_string(buf: &mut Vec<u8>, value: &str) {
buf.extend_from_slice(&(value.len() as u32).to_le_bytes());
buf.extend_from_slice(value.as_bytes());
}
18 changes: 18 additions & 0 deletions crates/gem_near/src/signer/signing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use super::models::NearTransfer;
use super::serialization::{ED25519_KEY_TYPE, encode_transfer};
use base64::{Engine, engine::general_purpose::STANDARD};
use gem_hash::sha2::sha256;
use primitives::SignerError;
use signer::Ed25519KeyPair;

pub fn sign_transfer(transfer: &NearTransfer, private_key: &[u8]) -> Result<String, SignerError> {
let key_pair = Ed25519KeyPair::from_private_key(private_key)?;
Comment thread
0xh3rman marked this conversation as resolved.
let encoded = encode_transfer(transfer, &key_pair.public_key_bytes);
let digest = sha256(&encoded);
let signature = key_pair.sign(&digest);

let mut signed = encoded;
signed.push(ED25519_KEY_TYPE);
signed.extend_from_slice(&signature);
Ok(STANDARD.encode(&signed))
}
18 changes: 17 additions & 1 deletion crates/primitives/src/testkit/transaction_load_input_mock.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::signer_mock::{TEST_EVM_RECIPIENT, TEST_EVM_SENDER};
use crate::{
Asset, Chain, GasPriceType, SignerInput, TransactionFee, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransferDataExtra, TransferDataOutputAction,
TransferDataOutputType, WalletConnectionSessionAppMetadata,
Expand Down Expand Up @@ -56,7 +57,6 @@ impl TransactionLoadInput {
}

pub fn mock_evm_with_metadata(input_type: TransactionInputType, value: &str, metadata: TransactionLoadMetadata) -> Self {
use super::signer_mock::{TEST_EVM_RECIPIENT, TEST_EVM_SENDER};
TransactionLoadInput {
input_type,
sender_address: TEST_EVM_SENDER.to_string(),
Expand Down Expand Up @@ -84,6 +84,22 @@ impl SignerInput {
}

impl TransactionLoadInput {
pub fn mock_near(sender: &str, destination: &str, value: &str, sequence: u64, block_hash: &str) -> Self {
TransactionLoadInput {
input_type: TransactionInputType::Transfer(Asset::from_chain(Chain::Near)),
sender_address: sender.into(),
destination_address: destination.into(),
value: value.into(),
gas_price: GasPriceType::regular(0),
memo: None,
is_max_value: false,
metadata: TransactionLoadMetadata::Near {
sequence,
block_hash: block_hash.into(),
},
}
}

pub fn mock_sign_data(chain: Chain, data: &str, output_type: TransferDataOutputType) -> Self {
TransactionLoadInput {
input_type: TransactionInputType::Generic(
Expand Down
2 changes: 1 addition & 1 deletion gemstone/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ gem_cardano = { path = "../crates/gem_cardano", features = ["rpc"] }
gem_algorand = { path = "../crates/gem_algorand", features = ["rpc"] }
gem_stellar = { path = "../crates/gem_stellar", features = ["rpc"] }
gem_xrp = { path = "../crates/gem_xrp", features = ["rpc"] }
gem_near = { path = "../crates/gem_near", features = ["rpc"] }
gem_near = { path = "../crates/gem_near", features = ["rpc", "signer"] }
gem_polkadot = { path = "../crates/gem_polkadot", features = ["rpc"] }
gem_wallet_connect = { path = "../crates/gem_wallet_connect" }
chain_traits = { path = "../crates/chain_traits" }
Expand Down
2 changes: 2 additions & 0 deletions gemstone/src/signer/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use gem_aptos::AptosChainSigner;
use gem_cosmos::signer::CosmosChainSigner;
use gem_evm::signer::EvmChainSigner;
use gem_hypercore::signer::HyperCoreSigner;
use gem_near::NearChainSigner;
use gem_solana::signer::SolanaChainSigner;
use gem_sui::signer::SuiChainSigner;
use gem_tron::TronChainSigner;
Expand All @@ -26,6 +27,7 @@ impl GemChainSigner {
ChainType::Solana => Box::new(SolanaChainSigner),
ChainType::Tron => Box::new(TronChainSigner),
ChainType::Cosmos => Box::new(CosmosChainSigner),
ChainType::Near => Box::new(NearChainSigner),
_ => todo!("Signer not implemented for chain {:?}", chain),
};

Expand Down
Loading