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(walletconnect): walletconnect integration #2223

Open
wants to merge 157 commits into
base: dev
Choose a base branch
from
Open

Conversation

borngraced
Copy link
Member

@borngraced borngraced commented Sep 16, 2024

#1543
This PR introduces the integration of WalletConnect into the Komodo DeFi Framework (KDF), enabling secure wallet connections for Cosmos and EVM-based chains. KDF acts as the DApp(in this PR), allowing users to initiate and manage transactions securely with external wallets e.g via Metamask.
Key changes include:

  • Implement multi-session handling for concurrent WalletConnect connections across accounts, integrates SQLite/IndexedDB for persistent session storage across devices, enhances relayer disconnection handling for smoother session management
  • Implemented WalletConnect coin activation for Tendermint and EVM.
  • Implemented Withdraw and Swap functionalities for Tendermint and EVM

https://specs.walletconnect.com/2.0/specs/clients/sign/
https://specs.walletconnect.com/2.0/specs/clients/core/pairing/
https://specs.walletconnect.com/2.0/specs/clients/core/crypto/
https://specs.walletconnect.com/2.0/specs/servers/relay/
https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc

Additional improvements include cleanup of unused dependencies, minor code refinements, and WASM compatibility

Updated deps:
Added deps:
Removed deps:

How to test using EVM coin e.g ETH (Native && WASM)

  1. Start kdf (cargo run)
  2. Create WalletConnect session connection
    • Use the RPC below
    • Copy the connection URL and either:
      • Paste directly in your wallet or
      • Generate QR code using QR Code Generator and scan with your wallet (e.g., MetaMask)
{
     "method": "wc_new_connection",
	"userpass": "{{ _.userpass }}",
	"mmrpc": "2.0",
	"params": {
		"required_namespaces": {
			"eip155": {
				"chains": ["eip155:1"],
				"methods": [
					"eth_sendTransaction",
					"eth_signTransaction",
					"personal_sign"
				],
				"events": [
					"accountsChanged",
					"chainChanged"
				]
			}
		}
	}
}
  1. Activate ETH coin (set "priv_key_policy": "WalletConnect", in activation params).
  2. Approve authentication request in your Wallet
  3. Withdraw or trade.

Note: To add more eip155 chains, modify the chains array like this: ["eip155:1", "eip155:250"]

@borngraced borngraced self-assigned this Sep 16, 2024
mm2src/kdf_walletconnect/src/session/rpc/settle.rs Outdated Show resolved Hide resolved
session.relay = settle.relay;
session.expiry = settle.expiry;

if let Some(value) = settle.session_properties {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why not let settle.session_properties be of type Option<SessionProperties> instead of Option<Value>?

Copy link
Member Author

@borngraced borngraced Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

session_properties field varies depending on the wallet and is currently used primarily by certain wallets like Keplr. Its fields can differ across wallets, as it isn’t standardized.

We can extend this using any enum once more wallets are discovered. For now, only Keplr sends this field during session initialization afaik.

pub session_properties: Option<SessionProperties>,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean we want to be able to discard this field if we were not able to deserialize it?
i think then the serde_json::from_str::<SessionProperties>(&value.to_string())? should be optional, i.e. we shouldn't fail (?) the whole function if we fail deserializing the arbitrary value.

p.s. we can also use serde_json::from_value instead of from_str and ditch .to_string()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why not let settle.session_properties be of type Option<SessionProperties> instead of Option<Value>?

settle.session_properties is generic I mean it can be any arbitrary data depending on the wallet of course.

Copy link
Collaborator

@mariocynicys mariocynicys Dec 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so,

  • if session_properties isn't sent, it's fine since it's optional,
  • if session_properties is sent along in the request, we MUST succeed in parsing it (that means we must have an enumration of all the possible shapes it could be)
    • if we fail to de-serialize session_properties we will fail the request (the MUST part).

why do we need it as Option<Value> (instead of Option<SessionProperties>) if we will fail the request if we fail to deserialize it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not all wallet will send this payload e.g metamask doesn't send but Kelpr does and maybe TrustWallet

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's not sent we will deserialize it as None (because session_properties: Option<SessionProps>) so it's fine and exactly the same as using Option<Value>. the only diff is we let serde deser the value instead of we desering it ourselves.

Copy link
Member Author

@borngraced borngraced Dec 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another thing, WalletConnect lib shouldn't handle this data since it can't know what type it is so that's why I delegate the data deserialization to the lib user, which I assume is the right way.

LIBRARY: doesn't have any idea what data it is, so session_properties: Option<Value>
USER: have an idea of what the data type is, so Option<SessionProps>

mm2src/kdf_walletconnect/src/session/rpc/settle.rs Outdated Show resolved Hide resolved
Comment on lines 73 to 74
// Delete other sessions with same controller
// TODO: we might not want to do this!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q to get more context: why do we delete sessions with the same controller?
and what does the controller mean/does?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say there's already a session with Metamask wallet, any new connection with Metamask will override the existing connection.

So, I'm unsure if we should do this!

mm2src/kdf_walletconnect/src/connection_handler.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/connection_handler.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/session/rpc/event.rs Outdated Show resolved Hide resolved
mm2src/crypto/Cargo.toml Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/Cargo.toml Outdated Show resolved Hide resolved
Some(value) => Some(serde_json::from_value(value)?),
None => None,
};
let (topic, url) = self.pairing.create(self.metadata.clone(), None)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think then we should clean up these old pairings.

/// Information about the controlling party (typically a wallet).
pub controller: Controller,
/// Information about the proposing party (typically a dApp).
pub proposer: Proposer,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean the other one is always Default::default() so an enum makes sense here then we can store only one of them.

mm2src/kdf_walletconnect/src/session/rpc/update.rs Outdated Show resolved Hide resolved
session.relay = settle.relay;
session.expiry = settle.expiry;

if let Some(value) = settle.session_properties {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean we want to be able to discard this field if we were not able to deserialize it?
i think then the serde_json::from_str::<SessionProperties>(&value.to_string())? should be optional, i.e. we shouldn't fail (?) the whole function if we fail deserializing the arbitrary value.

p.s. we can also use serde_json::from_value instead of from_str and ditch .to_string()

mm2src/kdf_walletconnect/src/storage/sqlite.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/connection_handler.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/lib.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/connection_handler.rs Outdated Show resolved Hide resolved
let topic = session.topic.clone();
let pairing_topic = session.pairing_topic.clone();
debug!("[{topic}] Session found! activating");
self.session_manager.add_session(session);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we add their pairings?

Copy link
Member Author

@borngraced borngraced Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no use for storing pairings as they're only needed to activate a session and then we'll move to using session for all communication. albeit I still consider extending pairing storage just like session storage but there's not any use for it right now.

Comment on lines 30 to 32
{
ctx.pairing.activate(topic)?;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the extra scope not needed.
also this always returns error (check KomodoPlatform/WalletConnectRust#3 (comment))

mm2src/kdf_walletconnect/src/pairing.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/session/rpc/propose.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/lib.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/lib.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/lib.rs Outdated Show resolved Hide resolved
let irn_metadata = param.irn_metadata();
let ttl = irn_metadata.ttl;
Copy link
Collaborator

@mariocynicys mariocynicys Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use a timed/expirable map for this case. we do that for electrum and eth also i think. this is to avoid network-induced memory leaks.

mm2src/kdf_walletconnect/src/inbound_message.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/session/rpc/delete.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@mariocynicys mariocynicys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i mainly went over some of old comments and resolved the ones which i can see have been updated (could use a helping hand in the remaining ones by pointing where the updates are).

my estimation right now is i would need one more all-over iteration for approval (+ old comments), but i don't wanna promise since break my promises :D

Thanks for that huge work!

mm2src/kdf_walletconnect/src/lib.rs Outdated Show resolved Hide resolved
mm2src/kdf_walletconnect/src/lib.rs Outdated Show resolved Hide resolved
session.relay = settle.relay;
session.expiry = settle.expiry;

if let Some(value) = settle.session_properties {
Copy link
Collaborator

@mariocynicys mariocynicys Dec 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so,

  • if session_properties isn't sent, it's fine since it's optional,
  • if session_properties is sent along in the request, we MUST succeed in parsing it (that means we must have an enumration of all the possible shapes it could be)
    • if we fail to de-serialize session_properties we will fail the request (the MUST part).

why do we need it as Option<Value> (instead of Option<SessionProperties>) if we will fail the request if we fail to deserialize it?


self.validate_chain_id(&session, chain_id)?;

// TODO: uncomment when WalletConnect wallets start listening to chainChanged event
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So nothing happens(I mean wallets don't listen to this event) if we send such request

so what about just sending it for now and when wallets listen to it later we get this feat for free

@borngraced
Copy link
Member Author

so what about just sending it for now and when wallets listen to it later we get this feat for free

Two reasons I didn't want to, this will add extra latency and corresponding wallet will return error as they don't recognize such session request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants