Skip to content
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
1 change: 1 addition & 0 deletions android/features/recipient/viewmodels/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ dependencies {
ksp(libs.hilt.compiler)

testImplementation(libs.junit)
testImplementation(testFixtures(project(":gemcore")))
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.gemwallet.android.features.recipient.viewmodel

import com.gemwallet.android.blockchain.operators.ValidateAddressOperator
import com.gemwallet.android.ext.matchesRecipient
import com.gemwallet.android.model.DestinationAddress
import com.wallet.core.primitives.Chain
import com.wallet.core.primitives.NameRecord

internal fun DestinationAddress.isValidRecipient(
inputAddress: String,
chain: Chain,
nameRecord: NameRecord?,
validateAddress: ValidateAddressOperator,
): Boolean = validateAddress(address, chain).getOrNull() == true &&
(nameRecord == null || nameRecord.matchesRecipient(inputAddress, address, chain))
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,16 @@ class RecipientViewModel @Inject constructor(
amountAction: AmountTransactionAction,
confirmAction: ConfirmTransactionAction,
) {
val resolvedNameRecord = nameRecord.value
submit(
type = type,
destination = DestinationAddress(
address = nameRecord.value?.address ?: address.value,
name = nameRecord.value?.name,
address = resolvedNameRecord?.address ?: address.value,
name = resolvedNameRecord?.name,
),
amountAction = amountAction,
confirmAction = confirmAction,
nameRecord = resolvedNameRecord,
)
}

Expand All @@ -194,10 +196,11 @@ class RecipientViewModel @Inject constructor(
destination: DestinationAddress,
amountAction: AmountTransactionAction,
confirmAction: ConfirmTransactionAction,
nameRecord: NameRecord? = null,
) {
val asset = type.assetInfo.asset
destination.copy(address = asset.chain.checksumAddress(destination.address)).let { destination ->
val validation = validateDestination(asset, destination)
val validation = validateDestination(asset, destination, nameRecord)
if (validation != RecipientError.None) {
if (!resolveName.canResolveName(destination.address)) {
addressError.update { validation }
Expand Down Expand Up @@ -268,8 +271,8 @@ class RecipientViewModel @Inject constructor(
confirmAction(params)
}

private fun validateDestination(asset: Asset, destination: DestinationAddress): RecipientError =
if (validateAddressOperator(destination.address, asset.chain).getOrNull() == true) {
private fun validateDestination(asset: Asset, destination: DestinationAddress, nameRecord: NameRecord? = null): RecipientError =
if (destination.isValidRecipient(address.value, asset.chain, nameRecord, validateAddressOperator)) {
RecipientError.None
} else {
RecipientError.IncorrectAddress(asset.name)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.gemwallet.android.features.recipient.viewmodel

import com.gemwallet.android.blockchain.operators.ValidateAddressOperator
import com.gemwallet.android.model.DestinationAddress
import com.gemwallet.android.testkit.mockNameRecord
import com.wallet.core.primitives.Chain
import com.wallet.core.primitives.NameProvider
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class RecipientValidationTest {

@Test
fun validRecipientRequiresValidAddressAndMatchingRecord() {
val accountId = "wrap.near"
val otherAccountId = "other.near"
val chain = Chain.Near
val destination = DestinationAddress(address = accountId, name = accountId)
val record = mockNameRecord(name = accountId, chain = chain, address = accountId, provider = NameProvider.Near)
val validAddress = addressValidator(true)
val invalidAddress = addressValidator(false)

assertTrue(destination.isValidRecipient(accountId, chain, record, validAddress))
assertFalse(destination.isValidRecipient(otherAccountId, chain, record, validAddress))
assertFalse(destination.isValidRecipient(accountId, Chain.Ethereum, record, validAddress))
assertFalse(destination.isValidRecipient(accountId, chain, record, invalidAddress))
assertFalse(destination.isValidRecipient(accountId, chain, null, invalidAddress))
assertTrue(destination.isValidRecipient(accountId, chain, null, validAddress))

val ethereumName = "example.eth"
val ethereumAddress = "0x5615E8AB93b9d695b6d4d6545f7792aA59e1069a"
val ethereumDestination = DestinationAddress(address = ethereumAddress, name = ethereumName)
val ethereumRecord = mockNameRecord(
name = ethereumName,
address = ethereumAddress,
)
val unresolvedEthereumRecord = ethereumRecord.copy(address = ethereumName)
val unresolvedEthereumDestination = DestinationAddress(address = ethereumName, name = ethereumName)

assertTrue(ethereumDestination.isValidRecipient(ethereumName, Chain.Ethereum, ethereumRecord, validAddress))
assertFalse(
unresolvedEthereumDestination.isValidRecipient(
ethereumName,
Chain.Ethereum,
unresolvedEthereumRecord,
invalidAddress,
),
)
}

private fun addressValidator(result: Boolean) = object : ValidateAddressOperator {
override fun invoke(address: String, chain: Chain): Result<Boolean> = Result.success(result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gemwallet.android.ext

import com.wallet.core.primitives.Chain
import com.wallet.core.primitives.NameRecord

fun NameRecord.matchesRecipient(name: String, address: String, chain: Chain): Boolean =
this.name == name && this.address == address && this.chain == chain
Comment thread
0xh3rman marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ enum class NameProvider(val string: String) {
Hyperliquid("hyperliquid"),
@SerialName("alldomains")
AllDomains("alldomains"),
@SerialName("near")
Near("near"),
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gemwallet.android.testkit

import com.wallet.core.primitives.Chain
import com.wallet.core.primitives.NameProvider
import com.wallet.core.primitives.NameRecord

fun mockNameRecord(
name: String = "example.eth",
chain: Chain = Chain.Ethereum,
address: String = "0x5615E8AB93b9d695b6d4d6545f7792aA59e1069a",
provider: NameProvider = NameProvider.Ens,
) = NameRecord(
name = name,
chain = chain,
address = address,
provider = provider,
)
1 change: 1 addition & 0 deletions core/Cargo.lock

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

47 changes: 39 additions & 8 deletions core/crates/gem_near/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,24 @@ impl AddressTrait for NearAddress {
}
}

pub fn validate_address(address: &str) -> bool {
is_implicit_address(address)
pub fn is_valid_address(address: &str) -> bool {
is_implicit_address(address) || is_valid_account_id(address)
}

pub fn is_valid_account_id(account_id: &str) -> bool {
if !(2..=64).contains(&account_id.len()) {
return false;
}

let mut previous_is_separator = true;
for character in account_id.bytes() {
match character {
b'a'..=b'z' | b'0'..=b'9' => previous_is_separator = false,
b'-' | b'_' | b'.' if !previous_is_separator => previous_is_separator = true,
_ => return false,
}
}
!previous_is_separator
}

pub(crate) fn is_implicit_address(address: &str) -> bool {
Expand All @@ -28,15 +44,30 @@ pub(crate) fn is_implicit_address(address: &str) -> bool {
mod tests {
use super::*;

#[test]
fn test_near_account() {
let implicit_address = "e3ac115fd911eb985ffd884ee60302c84dc94df52127ccde8d6fb97ad6d22945";
let eth_implicit_address = "0x85f17cf997934a597031b2e18a9ab6ebd4b9f6a4";
let deterministic_address = "0s85f17cf997934a597031b2e18a9ab6ebd4b9f6a4";

for address in ["aa", "alice-near_1.testnet", "h3rman.near", implicit_address, eth_implicit_address, deterministic_address] {
assert!(is_valid_account_id(address));
}
for address in ["a", "Alice.near", "ƒelicia.near", ".near", "alice..near", "alice.near-"] {
assert!(!is_valid_account_id(address));
}
assert!(!is_valid_account_id(&"a".repeat(65)));
}

#[test]
fn test_near_address() {
let address = "e3ac115fd911eb985ffd884ee60302c84dc94df52127ccde8d6fb97ad6d22945";
let parsed = NearAddress::try_parse(address).unwrap();
let implicit_address = "e3ac115fd911eb985ffd884ee60302c84dc94df52127ccde8d6fb97ad6d22945";
let parsed = NearAddress::try_parse(implicit_address).unwrap();

assert!(validate_address(address));
assert!(is_implicit_address(implicit_address));
assert!(!is_implicit_address("h3rman.near"));
assert!(!is_implicit_address("0x85f17cf997934a597031b2e18a9ab6ebd4b9f6a4"));
assert_eq!(parsed.as_bytes().len(), 32);
assert_eq!(parsed.encode(), address);
assert!(!validate_address("invalid"));
assert!(!validate_address("e3ac115fd911eb985ffd884ee60302c84dc94df52127ccde8d6fb97ad6d229"));
assert_eq!(parsed.encode(), implicit_address);
}
}
2 changes: 1 addition & 1 deletion core/crates/gem_near/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub mod address;
pub use address::validate_address;
pub use address::is_valid_address;
#[cfg(feature = "rpc")]
pub mod rpc;

Expand Down
1 change: 1 addition & 0 deletions core/crates/name_resolver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ primitives = { path = "../primitives" }
gem_evm = { path = "../gem_evm" }
gem_ton = { path = "../gem_ton" }
gem_solana = { path = "../gem_solana" }
gem_near = { path = "../gem_near", features = ["rpc", "reqwest"] }
gem_hash = { path = "../gem_hash" }

[dev-dependencies]
Expand Down
33 changes: 33 additions & 0 deletions core/crates/name_resolver/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::error::Error;
use std::sync::Arc;

use async_trait::async_trait;
use gem_evm::ethereum_address_checksum;
use primitives::EVMChain;
use primitives::chain::Chain;
use primitives::name::{NameProvider, NameRecord};

Expand Down Expand Up @@ -59,6 +61,10 @@ impl Client {

let provider = self.matched_provider(name, chain)?;
let address = provider.resolve(&query, chain).await?;
let address = match EVMChain::from_chain(chain) {
Some(_) => ethereum_address_checksum(&address)?,
None => address,
};

Ok(NameRecord {
provider: provider.provider(),
Expand Down Expand Up @@ -113,6 +119,33 @@ mod tests {
use super::{Client, NameConfig};
use primitives::chain::Chain;

#[tokio::test]
async fn test_resolve_checksums_evm_address() {
let client = Client::new(
vec![
Box::new(TestProvider::new(
NameProvider::Ud,
vec!["crypto"],
vec![Chain::Ethereum],
Ok("0x5615e8ab93b9d695b6d4d6545f7792aa59e1069a"),
)),
Box::new(TestProvider::new(
NameProvider::Sns,
vec!["sol"],
vec![Chain::Solana],
Ok("GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2"),
)),
],
NameConfig { max_name_length: 20 },
);

let ethereum = client.resolve("example.crypto", Chain::Ethereum).await.unwrap();
let solana = client.resolve("example.sol", Chain::Solana).await.unwrap();

assert_eq!(ethereum.address, "0x5615E8AB93b9d695b6d4d6545f7792aA59e1069a");
assert_eq!(solana.address, "GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2");
}

#[tokio::test]
async fn test_resolve_prefers_longer_domain_match() {
let client = Client::new(
Expand Down
2 changes: 2 additions & 0 deletions core/crates/name_resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod icns;
pub mod injective;
pub mod lens;
pub mod model;
pub mod near;
pub mod sns;
pub mod spaceid;
pub mod suins;
Expand Down Expand Up @@ -44,6 +45,7 @@ impl NameProviderFactory {
Box::new(base::Basenames::new(settings.name.base.url)),
Box::new(hyperliquid::Hyperliquid::new(settings.name.hyperliquid.url)),
Box::new(alldomains::AllDomainsClient::new(settings.name.alldomains.url)),
Box::new(near::NearNameClient::new(settings.chains.near.url)),
]
}
}
42 changes: 42 additions & 0 deletions core/crates/name_resolver/src/near.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use std::error::Error;

use async_trait::async_trait;
use gem_client::ReqwestClient;
use gem_jsonrpc::client::JsonRpcClient;
use gem_near::rpc::NearClient;
use primitives::{Chain, NameProvider};

use crate::client::NameClient;
use crate::model::NameQuery;

pub struct NearNameClient {
client: NearClient<ReqwestClient>,
}

impl NearNameClient {
pub fn new(url: String) -> Self {
Self {
client: NearClient::new(JsonRpcClient::new_reqwest(url)),
}
}
}

#[async_trait]
impl NameClient for NearNameClient {
async fn resolve(&self, query: &NameQuery, _chain: Chain) -> Result<String, Box<dyn Error + Send + Sync>> {
self.client.get_account(&query.domain).await?;
Ok(query.domain.clone())
}

fn provider(&self) -> NameProvider {
NameProvider::Near
}

fn domains(&self) -> Vec<&'static str> {
vec!["near"]
}

fn chains(&self) -> Vec<Chain> {
vec![Chain::Near]
}
}
9 changes: 9 additions & 0 deletions core/crates/name_resolver/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod tests {
hyperliquid::Hyperliquid,
injective::InjectiveNameClient,
model::NameQuery,
near::NearNameClient,
suins::SuinsClient,
};
use primitives::{Chain, node_config::get_nodes_for_chain};
Expand Down Expand Up @@ -80,4 +81,12 @@ mod tests {
let address = client.resolve(&NameQuery::new("miester.poor"), Chain::Solana).await.unwrap();
assert_eq!(address.trim(), "2EGGxj2qbNAJNgLCPKca8sxZYetyTjnoRspTPjzN2D67");
}

#[tokio::test]
async fn test_resolve_near_account() {
let nodes = get_nodes_for_chain(Chain::Near);
let client = NearNameClient::new(nodes[0].url.clone());
let address = client.resolve(&NameQuery::new("wrap.near"), Chain::Near).await.unwrap();
assert_eq!(address, "wrap.near");
}
}
1 change: 1 addition & 0 deletions core/crates/primitives/src/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ pub enum NameProvider {
Basenames,
Hyperliquid,
AllDomains,
Near,
}
Loading
Loading