Skip to content

Commit 77f5fef

Browse files
authored
feat: webhook example (#34)
1 parent b166f9d commit 77f5fef

File tree

6 files changed

+256
-3
lines changed

6 files changed

+256
-3
lines changed

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ anyhow = "1"
2727
structopt = { version = "0.3", default-features = false }
2828
tokio = { version = "1.22", features = ["full"] }
2929
url = "2.3"
30+
warp = { version = "0.3", default-features = false }
31+
serde_json = "1.0"
3032

3133
[[example]]
3234
name = "websocket_client"
@@ -35,3 +37,7 @@ required-features = ["client","rpc"]
3537
[[example]]
3638
name = "http_client"
3739
required-features = ["client","rpc"]
40+
41+
[[example]]
42+
name = "webhook"
43+
required-features = ["client","rpc"]

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
This is the foundation for the WalletConnect Rust SDK. Currently, there's only the core client and the RPC types required to communicate with the Relay.
44

5-
See the [basic example](examples/basic_client.rs).
5+
Examples:
6+
- [HTTP client](examples/http_client.rs)
7+
- [WebSocket client](examples/websocket_client.rs)
8+
- [Webhook dispatch](examples/webhook.rs)
69

710
## `relay_client`
811

examples/webhook.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
use {
2+
relay_client::{
3+
http::{Client, WatchRegisterRequest},
4+
ConnectionOptions,
5+
},
6+
relay_rpc::{
7+
auth::{ed25519_dalek::Keypair, rand, AuthToken},
8+
domain::{DecodedClientId, Topic},
9+
jwt::VerifyableClaims,
10+
rpc,
11+
},
12+
std::{
13+
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
14+
sync::Arc,
15+
time::Duration,
16+
},
17+
structopt::StructOpt,
18+
tokio::{sync::mpsc, task::JoinHandle},
19+
url::Url,
20+
warp::Filter,
21+
};
22+
23+
#[derive(StructOpt)]
24+
struct Args {
25+
/// Specify HTTP address.
26+
#[structopt(short, long, default_value = "https://relay.walletconnect.com/rpc")]
27+
address: String,
28+
29+
/// Specify WalletConnect project ID.
30+
#[structopt(short, long, default_value = "3cbaa32f8fbf3cdcc87d27ca1fa68069")]
31+
project_id: String,
32+
33+
/// Webhook server port.
34+
#[structopt(short, long, default_value = "10100")]
35+
webhook_server_port: u16,
36+
}
37+
38+
fn create_conn_opts(key: &Keypair, address: &str, project_id: &str) -> ConnectionOptions {
39+
let aud = Url::parse(address)
40+
.unwrap()
41+
.origin()
42+
.unicode_serialization();
43+
44+
let auth = AuthToken::new("http://example.com")
45+
.aud(aud)
46+
.ttl(Duration::from_secs(60 * 60))
47+
.as_jwt(key)
48+
.unwrap();
49+
50+
ConnectionOptions::new(project_id, auth).with_address(address)
51+
}
52+
53+
#[derive(Debug)]
54+
pub struct WebhookData {
55+
pub url: String,
56+
pub payload: rpc::WatchWebhookPayload,
57+
}
58+
59+
pub struct WebhookServer {
60+
addr: SocketAddr,
61+
handle: JoinHandle<()>,
62+
payload_rx: mpsc::UnboundedReceiver<WebhookData>,
63+
}
64+
65+
impl WebhookServer {
66+
pub fn new(port: u16) -> Self {
67+
let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, port).into();
68+
let (payload_tx, payload_rx) = mpsc::unbounded_channel();
69+
let handle = tokio::spawn(mock_webhook_server(addr, payload_tx));
70+
71+
Self {
72+
addr,
73+
handle,
74+
payload_rx,
75+
}
76+
}
77+
78+
pub fn url(&self) -> String {
79+
format!("http://{}", self.addr)
80+
}
81+
82+
pub async fn recv(&mut self) -> WebhookData {
83+
self.payload_rx.recv().await.unwrap()
84+
}
85+
}
86+
87+
impl Drop for WebhookServer {
88+
fn drop(&mut self) {
89+
self.handle.abort();
90+
}
91+
}
92+
93+
async fn mock_webhook_server(addr: SocketAddr, payload_tx: mpsc::UnboundedSender<WebhookData>) {
94+
let routes = warp::post()
95+
.and(warp::path::tail())
96+
.and(warp::body::json())
97+
.and(warp::any().map(move || payload_tx.clone()))
98+
.then(
99+
move |path: warp::path::Tail,
100+
payload: rpc::WatchWebhookPayload,
101+
payload_tx: mpsc::UnboundedSender<WebhookData>| async move {
102+
let url = format!("http://{addr}/{}", path.as_str());
103+
payload_tx.send(WebhookData { url, payload }).unwrap();
104+
warp::reply()
105+
},
106+
);
107+
108+
warp::serve(routes).run(addr).await;
109+
}
110+
111+
/// Note: This example will only work with a locally running relay, since it
112+
/// requires access to the local HTTP server.
113+
#[tokio::main]
114+
async fn main() -> anyhow::Result<()> {
115+
const PUB_WH_PATH: &str = "/publisher_webhook";
116+
const SUB_WH_PATH: &str = "/subscriber_webhook";
117+
118+
let args = Args::from_args();
119+
let mut server = WebhookServer::new(args.webhook_server_port);
120+
let server_url = server.url();
121+
122+
// Give time for the server to start up.
123+
tokio::time::sleep(Duration::from_millis(500)).await;
124+
125+
let publisher_key = Keypair::generate(&mut rand::thread_rng());
126+
let publisher = Client::new(&create_conn_opts(
127+
&publisher_key,
128+
&args.address,
129+
&args.project_id,
130+
))?;
131+
println!(
132+
"[publisher] client id: {}",
133+
DecodedClientId::from(publisher_key.public_key()).to_did_key()
134+
);
135+
136+
let subscriber_key = Keypair::generate(&mut rand::thread_rng());
137+
let subscriber = Client::new(&create_conn_opts(
138+
&subscriber_key,
139+
&args.address,
140+
&args.project_id,
141+
))?;
142+
println!(
143+
"[subscriber] client id: {}",
144+
DecodedClientId::from(subscriber_key.public_key()).to_did_key()
145+
);
146+
147+
let topic = Topic::generate();
148+
let message: Arc<str> = Arc::from("Hello WalletConnect!");
149+
150+
let sub_relay_id: DecodedClientId = subscriber
151+
.watch_register(
152+
WatchRegisterRequest {
153+
service_url: server_url.clone(),
154+
webhook_url: format!("{}{}", server_url, SUB_WH_PATH),
155+
watch_type: rpc::WatchType::Subscriber,
156+
tags: vec![1100],
157+
statuses: vec![rpc::WatchStatus::Queued],
158+
ttl: Duration::from_secs(600),
159+
},
160+
&subscriber_key,
161+
)
162+
.await
163+
.unwrap()
164+
.relay_id
165+
.into();
166+
subscriber.subscribe(topic.clone()).await.unwrap();
167+
println!(
168+
"[subscriber] watch registered: relay_id={}",
169+
sub_relay_id.to_did_key()
170+
);
171+
172+
let pub_relay_id: DecodedClientId = publisher
173+
.watch_register(
174+
WatchRegisterRequest {
175+
service_url: server_url.clone(),
176+
webhook_url: format!("{}{}", server_url, PUB_WH_PATH),
177+
watch_type: rpc::WatchType::Publisher,
178+
tags: vec![1100],
179+
statuses: vec![rpc::WatchStatus::Accepted],
180+
ttl: Duration::from_secs(600),
181+
},
182+
&publisher_key,
183+
)
184+
.await
185+
.unwrap()
186+
.relay_id
187+
.into();
188+
println!(
189+
"[publisher] watch registered: relay_id={}",
190+
pub_relay_id.to_did_key()
191+
);
192+
193+
publisher
194+
.publish(
195+
topic.clone(),
196+
message.clone(),
197+
1100,
198+
Duration::from_secs(30),
199+
false,
200+
)
201+
.await
202+
.unwrap();
203+
println!("[publisher] message published: topic={topic} message={message}");
204+
205+
tokio::time::sleep(Duration::from_secs(1)).await;
206+
207+
let messages = subscriber.fetch(topic).await?.messages;
208+
let message = messages
209+
.get(0)
210+
.ok_or(anyhow::anyhow!("fetch did not return any messages"))?;
211+
println!("[subscriber] received message: {}", message.message);
212+
213+
let pub_data = server.recv().await;
214+
let decoded = rpc::WatchEventClaims::try_from_str(&pub_data.payload.event_auth).unwrap();
215+
let decoded_json = serde_json::to_string_pretty(&decoded).unwrap();
216+
println!(
217+
"[webhook] publisher: url={} data={}",
218+
pub_data.url, decoded_json
219+
);
220+
221+
let sub_data = server.recv().await;
222+
let decoded = rpc::WatchEventClaims::try_from_str(&sub_data.payload.event_auth).unwrap();
223+
let decoded_json = serde_json::to_string_pretty(&decoded).unwrap();
224+
println!(
225+
"[webhook] subscriber: url={} data={}",
226+
sub_data.url, decoded_json
227+
);
228+
229+
Ok(())
230+
}

relay_rpc/src/jwt.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub struct JwtBasicClaims {
7676
/// Issued at, timestamp.
7777
pub iat: i64,
7878
/// Expiration, timestamp.
79+
#[serde(skip_serializing_if = "Option::is_none")]
7980
pub exp: Option<i64>,
8081
}
8182

relay_rpc/src/rpc.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,13 @@ impl From<WatchError> for GenericError {
681681
}
682682
}
683683

684+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
685+
#[serde(rename_all = "camelCase")]
686+
pub struct WatchRegisterResponse {
687+
/// The Relay's public key (did:key).
688+
pub relay_id: DidKey,
689+
}
690+
684691
/// Data structure representing watch registration request params.
685692
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
686693
#[serde(rename_all = "camelCase")]
@@ -691,8 +698,7 @@ pub struct WatchRegister {
691698

692699
impl RequestPayload for WatchRegister {
693700
type Error = WatchError;
694-
/// The Relay's public key.
695-
type Response = DidKey;
701+
type Response = WatchRegisterResponse;
696702

697703
fn validate(&self) -> Result<(), ValidationError> {
698704
Ok(())

relay_rpc/src/rpc/watch.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ impl VerifyableClaims for WatchEventClaims {
110110
}
111111
}
112112

113+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
114+
#[serde(rename_all = "camelCase")]
115+
pub struct WatchWebhookPayload {
116+
/// JWT with [`WatchEventClaims`] payload.
117+
pub event_auth: String,
118+
}
119+
113120
#[cfg(test)]
114121
mod test {
115122
use {

0 commit comments

Comments
 (0)