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 190 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)
  3. Activate ETH coin (set "priv_key_policy": "wallet_connect": { "session_topic": your_session_topic" } in activation params).
  4. Approve authentication request in your Wallet
  5. Withdraw or trade.

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

New RPC Methods

New connection

method: wc_new_connection

{
	"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"
				]
			},
			"cosmos": {
				"chains": [
					"cosmos:cosmoshub-4"
				],
				"methods": [
					"cosmos_signDirect",
					"cosmos_signAmino",
					"cosmos_getAccounts"
				],
				"events": []
			}
		}
	}
}

Get/Retrieve Walletconnect session(s)

method: wc_get_session | wc_get_sessions

{
	"method": "wc_get_session" | "wc_get_sessions",
	"userpass": "{{ _.userpass }}",
	"mmrpc": "2.0",
	"params": {
		"topic": "3a6327f78840427d3a428e8fbaaaaabc8c89c582bb2ff4464d3c73d8e2e5b253"
	}
}

Ping Walletconnect session

method: wc_ping_session

{
	"method": "wc_ping_session",
	"userpass": "{{ _.userpass }}",
	"mmrpc": "2.0",
	"params": {
		"topic": "3a6327f78840427d3a428e8fbaaaaabc8c89c582bb2ff4464d3c73d8e2e5b253"
	}
}

Delete Walletconnect session

method: wc_delete_session

{
	"method": "wc_delete_session",
	"userpass": "{{ _.userpass }}",
	"mmrpc": "2.0",
	"params": {
		"topic": "3a6327f78840427d3a428e8fbaaaaabc8c89c582bb2ff4464d3c73d8e2e5b253"
	}
}

Active ETH/ERC

method: enable_eth_with_tokens

{
    "priv_key_policy": {
        "wallet_connect": {
            "session_topic": "3a6327f78840427d3a428e8fbaaaaabc8c89c582bb2ff4464d3c73d8e2e5b253"
        }
    },
    ...
}

Active Tendermint/Cosmos

method: enable_tendermint_with_assets

{
    "activation_params": {
        "wallet_connect": {
            "session_topic": "'3a6327f78840427d3a428e8fbaaaaabc8c89c582bb2ff4464d3c73d8e2e5b253"
        }
    },
    ...
}

cc @smk762

@borngraced borngraced self-assigned this Sep 16, 2024
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.

Done soon :)
there is one pending request in the previous review iter.

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.27" }
mm2_db = { path = "../mm2_db" }
mm2_test_helpers = { path = "../mm2_test_helpers" }
Copy link
Collaborator

Choose a reason for hiding this comment

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

we shouldn't need this crate here? no?

@@ -358,10 +359,11 @@ impl WalletConnectCtxImpl {
.await?;

let (tx, rx) = oneshot::channel();
// insert request to map with a reasonable expiration time of 30 minutes
self.pending_requests
.lock()
.expect("pending request lock shouldn't fail!")
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: we can just unwrap here. no need for justification for mutex unwraps.

@@ -358,10 +359,11 @@ impl WalletConnectCtxImpl {
.await?;

let (tx, rx) = oneshot::channel();
// insert request to map with a reasonable expiration time of 30 minutes
Copy link
Collaborator

Choose a reason for hiding this comment

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

ummm... isn't 30 mins veeery long?

Copy link
Collaborator

Choose a reason for hiding this comment

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

as a user, i wouldn't accept any WC msg if it came so late. that's sus.

Copy link
Member Author

Choose a reason for hiding this comment

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

WalletConnect doesn't limit timeout it all depends on the timeout we want to set for each request.
I agree 30minsis a very long expiration time.

reduced to 5 mins

@shamardy shamardy requested a review from onur-ozkan March 5, 2025 08:35
Comment on lines +2821 to +2836
let _nonce_lock = address_lock.lock().await;
let pay_for_gas_option = if let Some(ref pay_for_gas) = args.pay_for_gas {
pay_for_gas.clone().try_into()?
} else {
// use legacy gas_price() if not set
info!(target: "sign-and-send", "get_gas_price…");
let gas_price = coin.get_gas_price().await?;
PayForGasOption::Legacy(LegacyGasPrice { gas_price })
};
let (nonce, _) = coin
.clone()
.get_addr_nonce(my_address)
.compat()
.await
.map_to_mm(RawTransactionError::InvalidParam)?;

Copy link
Member

Choose a reason for hiding this comment

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

I am curious about if we can handle nonces in a lockless way (like how it's done in TendermintCoin::seq_safe_send_raw_tx_bytes for tendermint sequences), so we can perform concurrent txs on for ETH.

Perhaps we should create a tracking issue for this topic?

Copy link
Member

@onur-ozkan onur-ozkan Mar 5, 2025

Choose a reason for hiding this comment

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

Perhaps we should create a tracking issue for this topic?

#2379

Comment on lines -5382 to +5461
wait_rpc_timeout_ms: u64,
wait_rpc_timeout_s: u64,
check_every: f64,
) -> Web3RpcResult<Option<SignedEthTx>> {
let wait_until = wait_until_ms(wait_rpc_timeout_ms);
while now_ms() < wait_until {
let wait_until = wait_until_sec(wait_rpc_timeout_s);
while now_sec() < wait_until {
Copy link
Member

Choose a reason for hiding this comment

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

Do we have any motivation for this change? Milliseconds can be more useful for such logic flows as it can be configurable more precisely.

Copy link
Member Author

Choose a reason for hiding this comment

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

Comment on lines 77 to 86
let to_addr = match self.action {
Action::Create => None,
Action::Call(addr) => Some(addr),
};
if let Some(to) = to_addr {
tx_json
.as_object_mut()
.unwrap()
.insert("to".to_string(), json!(format!("0x{}", hex::encode(to.as_bytes()))));
}
Copy link
Member

Choose a reason for hiding this comment

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

Should be simpler with if let Action::Call(addr) = self.action.

Comment on lines 62 to 74
let mut tx_json = json!({
"nonce": u256_to_hex(self.nonce),
"from": format!("{:x}", self.my_address),
"gas": u256_to_hex(self.gas),
"value": u256_to_hex(self.value),
"data": format!("0x{}", hex::encode(self.data))
});

if let Some(gas_price) = self.gas_price {
tx_json
.as_object_mut()
.unwrap()
.insert("gasPrice".to_string(), json!(u256_to_hex(gas_price)));
Copy link
Member

Choose a reason for hiding this comment

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

You can use serde_json::Map to avoid as_object_mut().unwrap() calls for easier mutation and convert it to serde_json::Value once you are done with mutating it.

Comment on lines 100 to 101
let chain_id = WcChainId::new_eip155(self.chain_id.to_string());
let session_topic = self.session_topic()?;
Copy link
Member

Choose a reason for hiding this comment

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

I would move session_topic above as it's fallible but the chain_id is not.

.send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSignTransaction, tx_json)
.await?;
// First 4 bytes from WalletConnect represents Protoc info
hex::decode(&tx_hex[4..])?
Copy link
Member

Choose a reason for hiding this comment

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

Could be more safe to check length of tx_hex first.

wc.validate_update_active_chain_id(session_topic, &chain_id).await?;

let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?;
let message = "Authenticate with Komodefi";
Copy link
Member

Choose a reason for hiding this comment

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

Is this going to show-up on the external wallet screen?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes

Copy link
Member

Choose a reason for hiding this comment

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

I think this needs approval from various people outside our team whether we want to present ourselves as Komodefi or we should prefer some other name.

cc @ca333

@smk762
Copy link

smk762 commented Mar 7, 2025

Current code state fails to build/run https://github.com/KomodoPlatform/komodo-defi-framework/actions/runs/13696493437/job/38300021140?pr=2223#step:10:1222 (same error seen locally with cargo run or cargo build -vv.

Commit 167734b (prior to merging in dev) builds ok, so likely error is result of merge conflict.

@smk762
Copy link

smk762 commented Mar 7, 2025

"chains": [
    "eip155:1",
    "eip155:137"
],

Would it be ok to use actual chain/network name as shown in coins file instead?

@borngraced
Copy link
Member Author

"chains": [
    "eip155:1",
    "eip155:137"
],

Would it be ok to use actual chain/network name as shown in coins file instead?

No, just the chain_id https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces

@smk762
Copy link

smk762 commented Mar 10, 2025

Unfortunately I'm still having some trouble with this. Notes below:

Test Workflow

  • run wc_new_connection and use the response.url to generate a QR code (e.g. wc:foo@2?symkey=bar7&relay-protocol=irn&expiryTimestamp=1741582989
  • Scan QR code from Trust Wallet in mobile. Confirm the permissions requested. (unsure if using scan icon on main page is ok, or if settings > wallet connect must be used. Use the latter just in case)
  • Extract the substring between wc: and @ to determine the pairing_topic (e.g. foo)
  • Run wc_get_sessions, then iterate over the returned session list to extract the session_topic (baz) from the item which matches the pairing_topic (foo) extracted from the newly created connection.
    e.g.
    [
	 {
                "topic": "baz",
                "pairing_topic": "foo",
                "metadata": {
                    ....
                },
                 ...
                "peer_pubkey": "adcd46fc3cf71128ef855c6a1828549ae6e380d2d56afe596c66b1c7820ecc70",
                "subscription_id": "ca5909a8645541a87ed2b45d2a926f0577b14a5e85b14f3b2967ffb51d6602f0",
                "properties": null,
                "expiry": 1742185747
            },
            ...
        ]
  • Attempt to connect ETH with the session_topic. Response includes ErrorData { code: 5300, message: \"Invalid Session Properties requested\", data: None }
    e.g. with params like
        "priv_key_policy": {
            "wallet_connect": {
                "session_topic": "baz"
            }
        },

If using the "pairing topic" (i.e. foo instead of baz) the response is Session Error: No active WalletConnect session found, so I'm fairly confident the pairing topic is not used here.

General notes:

wc_get_sessions:

  • Using topic param still returns whole list. Is this param only intended for wc_get_session?
  • Sessions from previous runs are still present in response. Unsure if still usable, but unable to delete.
  • new connections will not appear here until after approved in trust wallet mobile app.

wc_get_session:

  • Using topic param still returns data for specific session topic, but this value seems to only be able to determined from running wc_get_sessions first and finding the item that matches the pairing_topic from a substring of the wc_new_connection reponse. Is there another (better) way, because this is a little cumbersome.
  • new connections will not appear here until after approved in trust wallet mobile app.

wc_ping_session:

  • times out, no response returned.
  • in logs:
    10 05:24:59, kdf_walletconnect:422] INFO [3569914dd09a5cc4ac92dedab354f06ff5db17ef616233a8ba562cbea51269fd] Publishing message=Request(Request { id: MessageId(445845580595480), jsonrpc: "2.0", params: SessionPing(()) })
    10 05:24:59, kdf_walletconnect:444] INFO [3569914dd09a5cc4ac92dedab354f06ff5db17ef616233a8ba562cbea51269fd] Message published successfully
    10 05:25:29, mm2_main::rpc::dispatcher:127] ERROR RPC error response: sessions:78] ping:24] Request timeout error

wc_delete_session:

  • times out, no response returned.
  • In logs:
    10 04:05:06, kdf_walletconnect:422] INFO [feeabbad7ac1f1ba720d4441c299eab2b239f0deefb050fe50dbc7a350d55f75] Publishing message=Request(Request { id: MessageId(445844353599488), jsonrpc: "2.0", params: SessionDelete(SessionDeleteRequest { code: 6000, message: "User Disconnected" }) })
    10 04:05:06, kdf_walletconnect:444] INFO [feeabbad7ac1f1ba720d4441c299eab2b239f0deefb050fe50dbc7a350d55f75] Message published successfully
  • Session attempting to delete still appears in wc_get_sessions response
  • Session attempting to delete still appears in wc_get_session response for the topic.

@borngraced
Copy link
Member Author

  1. pairing_topic: only for connecting with external wallets, afterwards, it's basically useless.
  2. session_topic: communication with connected external wallet is moved to session_topic from pairing_topic after connecting with external wallet.
  3. for some unknown reason trustwallet is not replying to session_pings. You can skip doing this if you're using trustwallet

Komodefi WalletConnect Test Flow

1. External Wallet Connection Test Flow

I will be using ETH for this test flow.

  • run wc_new_connection and use the response.url to generate a QR code (e.g. wc:foo@2?symkey=bar7&relay-protocol=irn&expiryTimestamp=1741582989
  • Scan QR code from Trust Wallet in mobile. Confirm the permissions requested.
  • If session connection is successful, run wc_get_session e.g with response.topic and with_pairing_topic: true to enable us use pairing_topic to fetch session IF above step is successful, like:
{
	"method": "wc_get_session",
	"userpass": "{{ _.userpass }}",
	"mmrpc": "2.0",
	"params": {
		"with_pairing_topic": true,
		"topic": "efe5d00ff97750d40e96f0e794cc10106245da8b98091af69d2d42faa94efcd5"
	}
}
  • Let's call the result from this request session
  • Scan QR code from Trust Wallet in mobile. Confirm the permissions requested.

2. Coin Activation Test Flow(Needs 1 to be completed)

  • Make sure ETH or any coin you want to use with kdf is already activated in your wallet e.g trust, metamask

  • Activate ETH with enable_eth_with_tokens coin activation rpc, like:

    {
     "userpass": "{{ _.userpass }}",
     "method": "enable_eth_with_tokens",
     "mmrpc": "2.0",
     "params": {
     	"ticker": "ETH",
     	"mm2": 1,
     	"priv_key_policy": {
     		"wallet_connect": {
     			"session_topic": session.topic
     		}
     	},
     	....
     }

}

- Accept session/sign request for `ETH` coin in  your external wallet.


#### 3. Swap/Withdraw test
- If the everything above is done as mentioned, you can start a swap/withdrawal as you would normally do
- For `ETH` specifically, when using `metamask` wallet, you are only allowed to withdraw with `sign_and_send_tx` from `metamask` meaning you can't `withdraw` rpc will sign and send the tx. You won't need  to `sendrawtransaction` manually.
 

cc @smk762 

@smk762
Copy link

smk762 commented Mar 12, 2025

Thanks. I'm still having some issues with Trust Wallet failing to scan more often than not, so its been a bit difficult to progress with testing, tho I've added the docs I can so far,

Can you please confirm wc_delete_session and wc_ping_session also take the "with_pairing_topic": true?

Code at https://github.com/KomodoPlatform/komodo-defi-framework/blob/wc-integration/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs#L45 implies it as input, but when using it I'm getting error returned.

E.g Session is

            {
                "topic": "feeabbad7ac1f1ba720d4441c299eab2b239f0deefb050fe50dbc7a350d55f75",
                "metadata": {
                	...
                },
                "pairing_topic": "ad604cd186f5dc9498343fbd763f6d6963ea511de0e5d557a33a8e3790d6d4d5",
                "namespaces": {
                	...
                },
                "expiry": 1741952829
            }

Using topic feeabbad7ac1f1ba720d4441c299eab2b239f0deefb050fe50dbc7a350d55f75, the request times out before a response is received for both wc_delete_session and wc_ping_session.

When request is sent, logs show

12 03:14:26, kdf_walletconnect:422] INFO [feeabbad7ac1f1ba720d4441c299eab2b239f0deefb050fe50dbc7a350d55f75] Publishing message=Request(Request { id: MessageId(445887812284165), jsonrpc: "2.0", params: SessionDelete(SessionDeleteRequest { code: 6000, message: "User Disconnected" }) })
12 03:14:27, kdf_walletconnect:444] INFO [feeabbad7ac1f1ba720d4441c299eab2b239f0deefb050fe50dbc7a350d55f75] Message published successfully
12 03:14:27, mm2_main::rpc::dispatcher:127] ERROR RPC error response: sessions:69] delete:37] Internal Error: channel closed

but the request continues to wait. After timeout for wc_delete_session, the session is still shown in the response for wc_get_sessions.

If using the pairing topic with

	"params": {
		"with_pairing_topic": true,
		"topic": "ad604cd186f5dc9498343fbd763f6d6963ea511de0e5d557a33a8e3790d6d4d5"
	}

the response is error:

{
    "mmrpc": "2.0",
    "error": "Internal Error: topic sym_key not found: 31ad8ac1312e01ff7ff656ed5507eb9fd6f2f435668fd86331e00b33627bfc14",
    "error_path": "sessions.lib",
    "error_trace": "sessions:69] lib:281]",
    "error_type": "SessionRequestError",
    "error_data": "Internal Error: topic sym_key not found: 31ad8ac1312e01ff7ff656ed5507eb9fd6f2f435668fd86331e00b33627bfc14",
    "id": null
}

Incidentally, when set to "with_pairing_topic": true, but using the session topic regardless, it also times out - which seems to indicate the with_pairing_topic param is not applied for the wc_delete_session and wc_ping_session requests.

@smk762
Copy link

smk762 commented Mar 12, 2025

I'm also facing timeouts sometimes when attempting to activate ETH. In logs I see:

12 03:42:07, mm2_main::rpc::dispatcher:127] ERROR RPC error response: platform_coin_with_tokens:454] eth_with_token_activation:489] wallet_connect:178] lib:630] Request timeout error
12 03:42:52, kdf_walletconnect:422] INFO [7320725519c81f17ba098eb2b76463da4c556d08b22e22779005011610bc2a9a] Publishing message=Request(Request { id: MessageId(445888248882691), jsonrpc: "2.0", params: SessionRequest(SessionRequestRequest { request: Request { method: "personal_sign", params: Array [String("0x41757468656e7469636174652077697468204b6f6d6f64656669"), String("0xf5c6738ff36b62a032d11c4badeebf088db58e70")], expiry: None }, chain_id: "eip155:1" }) })
12 03:42:52, kdf_walletconnect:444] INFO [7320725519c81f17ba098eb2b76463da4c556d08b22e22779005011610bc2a9a] Message published successfully
12 03:42:52, mm2_main::rpc::dispatcher:127] ERROR RPC error response: sessions:69] delete:37] Internal Error: channel closed

This instance above was on a topic I attempted to delete, which might explain it a little. A fresh connection, attempting ETH activation post QR scan returned

{
    "mmrpc": "2.0",
    "error": "UnSuccessfulResponse Error: ErrorResponse { id: MessageId(445888342993158), jsonrpc: \"2.0\", error: ErrorData { code: 5300, message: \"Invalid Session Properties requested\", data: None } }",
    "error_path": "platform_coin_with_tokens.eth_with_token_activation.wallet_connect.inbound_message",
    "error_trace": "platform_coin_with_tokens:454] eth_with_token_activation:489] wallet_connect:178] inbound_message:86]",
    "error_type": "Internal",
    "error_data": "UnSuccessfulResponse Error: ErrorResponse { id: MessageId(445888342993158), jsonrpc: \"2.0\", error: ErrorData { code: 5300, message: \"Invalid Session Properties requested\", data: None } }",
    "id": null
}

Which blocks me from any further tests :(

When attempting to initiate another connection at this point, Trust wallet will scan the QR code, but then does nothing (no modal to confirm etc). Seems I need to uninstall, reinstall and use a fresh seed to wake it up again.

@borngraced
Copy link
Member Author

Can you please confirm wc_delete_session and wc_ping_session also take the "with_pairing_topic": true?

No, wc_delete_session and wc_ping_session should take only the session topic.

@smk762
Copy link

smk762 commented Mar 13, 2025

Can you please confirm wc_delete_session and wc_ping_session also take the "with_pairing_topic": true?

No, wc_delete_session and wc_ping_session should take only the session topic.

Both these methods are working as expected using https://react-wallet.walletconnect.com/ (instead of Trust Wallet android app), and I am able to activate ETH/AVAX/MATIC (with wc confirmation). Unfortunately unable to withdraw, details of browser console error has been shared with @borngraced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.5.0-beta priority: high Important tasks that need attention soon. status: pending review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants