Add pubsub module for rpc info subscriptions (#1439)
This commit is contained in:
parent
24a993710d
commit
785c619198
|
@ -83,6 +83,8 @@ influx_db_client = "0.3.4"
|
|||
solana-jsonrpc-core = "0.1"
|
||||
solana-jsonrpc-http-server = "0.1"
|
||||
solana-jsonrpc-macros = "0.1"
|
||||
solana-jsonrpc-pubsub = "0.1"
|
||||
solana-jsonrpc-ws-server = "0.1"
|
||||
ipnetwork = "0.12.7"
|
||||
itertools = "0.7.8"
|
||||
libc = "0.2.43"
|
||||
|
|
126
doc/json-rpc.md
126
doc/json-rpc.md
|
@ -23,6 +23,13 @@ Methods
|
|||
* [getTransactionCount](#gettransactioncount)
|
||||
* [requestAirdrop](#requestairdrop)
|
||||
* [sendTransaction](#sendtransaction)
|
||||
* [subscriptionChannel](#subscriptionChannel)
|
||||
|
||||
* [Subscription Websocket](#subscription-websocket)
|
||||
* [accountSubscribe](#accountsubscribe)
|
||||
* [accountUnsubscribe](#accountunsubscribe)
|
||||
* [signatureSubscribe](#signaturesubscribe)
|
||||
* [signatureUnsubscribe](#signatureunsubscribe)
|
||||
|
||||
Request Formatting
|
||||
---
|
||||
|
@ -227,3 +234,122 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m
|
|||
```
|
||||
|
||||
---
|
||||
|
||||
### startSubscriptionChannel
|
||||
Open a socket on the node for JSON-RPC subscription requests
|
||||
|
||||
##### Parameters:
|
||||
None
|
||||
|
||||
##### Results:
|
||||
* `string` - "port", open websocket port
|
||||
* `string` - "path", unique key to use as websocket path
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"startSubscriptionChannel"}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":{"port":9876,"path":"BRbmMXn71cKfzXjFsmrTsWsXuQwbjXbwKdoRwVw1FRA3"},"id":1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Subscription Websocket
|
||||
After opening a subscription socket with the `subscriptionChannel` JSON-RPC request method, submit subscription requests via websocket protocol
|
||||
Connect to the websocket at `ws://<ADDRESS>/<PATH>` returned from the request
|
||||
- Submit subscription requests to the websocket using the methods below
|
||||
- Multiple subscriptions may be active at once
|
||||
- The subscription-channel socket will close when client closes websocket. To create new subscriptions, send a new `subscriptionChannel` JSON-RPC request.
|
||||
|
||||
---
|
||||
|
||||
### accountSubscribe
|
||||
Subscribe to an account to receive notifications when the userdata for a given account public key changes
|
||||
|
||||
##### Parameters:
|
||||
* `string` - account Pubkey, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `integer` - Subscription id (needed to unsubscribe)
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"accountSubscribe", "params":["CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12"]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||
```
|
||||
|
||||
##### Notification Format:
|
||||
```bash
|
||||
{"jsonrpc": "2.0","method": "accountNotification", "params": {"result": {"program_id":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"tokens":1,"userdata":[3,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,20,0,0,0,0,0,0,0,50,48,53,48,45,48,49,45,48,49,84,48,48,58,48,48,58,48,48,90,252,10,7,28,246,140,88,177,98,82,10,227,89,81,18,30,194,101,199,16,11,73,133,20,246,62,114,39,20,113,189,32,50,0,0,0,0,0,0,0,247,15,36,102,167,83,225,42,133,127,82,34,36,224,207,130,109,230,224,188,163,33,213,13,5,117,211,251,65,159,197,51,0,0,0,0,0,0]},"subscription":0}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### accountUnsubscribe
|
||||
Unsubscribe from account userdata change notifications
|
||||
|
||||
##### Parameters:
|
||||
* `integer` - id of account Subscription to cancel
|
||||
|
||||
##### Results:
|
||||
* `bool` - unsubscribe success message
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"accountUnsubscribe", "params":[0]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": true,"id": 1}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### signatureSubscribe
|
||||
Subscribe to a transaction signature to receive notification when the transaction is confirmed
|
||||
On `signatureNotification`, the subscription is automatically cancelled
|
||||
|
||||
##### Parameters:
|
||||
* `string` - Transaction Signature, as base-58 encoded string
|
||||
|
||||
##### Results:
|
||||
* `integer` - subscription id (needed to unsubscribe)
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"signatureSubscribe", "params":["2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b"]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": 0,"id": 1}
|
||||
```
|
||||
|
||||
##### Notification Format:
|
||||
```bash
|
||||
{"jsonrpc": "2.0","method": "signatureNotification", "params": {"result": "Confirmed","subscription":0}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### signatureUnsubscribe
|
||||
Unsubscribe from account userdata change notifications
|
||||
|
||||
##### Parameters:
|
||||
* `integer` - id of account subscription to cancel
|
||||
|
||||
##### Results:
|
||||
* `bool` - unsubscribe success message
|
||||
|
||||
##### Example:
|
||||
```bash
|
||||
// Request
|
||||
{"jsonrpc":"2.0", "id":1, "method":"signatureUnsubscribe", "params":[0]}
|
||||
|
||||
// Result
|
||||
{"jsonrpc": "2.0","result": true,"id": 1}
|
||||
```
|
||||
|
|
217
src/bank.rs
217
src/bank.rs
|
@ -11,11 +11,13 @@ use dynamic_program::DynamicProgram;
|
|||
use entry::Entry;
|
||||
use hash::{hash, Hash};
|
||||
use itertools::Itertools;
|
||||
use jsonrpc_macros::pubsub::Sink;
|
||||
use ledger::Block;
|
||||
use log::Level;
|
||||
use mint::Mint;
|
||||
use payment_plan::Payment;
|
||||
use poh_recorder::PohRecorder;
|
||||
use rpc::RpcSignatureStatus;
|
||||
use signature::Keypair;
|
||||
use signature::Signature;
|
||||
use solana_program_interface::account::{Account, KeyedAccount};
|
||||
|
@ -33,6 +35,7 @@ use tictactoe_dashboard_program::TicTacToeDashboardProgram;
|
|||
use tictactoe_program::TicTacToeProgram;
|
||||
use timing::{duration_as_us, timestamp};
|
||||
use token_program::TokenProgram;
|
||||
use tokio::prelude::Future;
|
||||
use transaction::Transaction;
|
||||
use window::WINDOW_SIZE;
|
||||
|
||||
|
@ -135,6 +138,12 @@ pub struct Bank {
|
|||
|
||||
// loaded contracts hashed by program_id
|
||||
loaded_contracts: RwLock<HashMap<Pubkey, DynamicProgram>>,
|
||||
|
||||
// Mapping of account ids to Subscriber ids and sinks to notify on userdata update
|
||||
account_subscriptions: RwLock<HashMap<Pubkey, HashMap<Pubkey, Sink<Account>>>>,
|
||||
|
||||
// Mapping of signatures to Subscriber ids and sinks to notify on confirmation
|
||||
signature_subscriptions: RwLock<HashMap<Signature, HashMap<Pubkey, Sink<RpcSignatureStatus>>>>,
|
||||
}
|
||||
|
||||
impl Default for Bank {
|
||||
|
@ -148,6 +157,8 @@ impl Default for Bank {
|
|||
is_leader: true,
|
||||
finality_time: AtomicUsize::new(std::usize::MAX),
|
||||
loaded_contracts: RwLock::new(HashMap::new()),
|
||||
account_subscriptions: RwLock::new(HashMap::new()),
|
||||
signature_subscriptions: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -259,6 +270,18 @@ impl Bank {
|
|||
&res[i],
|
||||
&tx.last_id,
|
||||
);
|
||||
if res[i] != Err(BankError::SignatureNotFound) {
|
||||
let status = match res[i] {
|
||||
Ok(_) => RpcSignatureStatus::Confirmed,
|
||||
Err(BankError::ProgramRuntimeError(_)) => {
|
||||
RpcSignatureStatus::ProgramRuntimeError
|
||||
}
|
||||
Err(_) => RpcSignatureStatus::GenericFailure,
|
||||
};
|
||||
if status != RpcSignatureStatus::SignatureNotFound {
|
||||
self.check_signature_subscriptions(&tx.signature, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -499,6 +522,17 @@ impl Bank {
|
|||
.map(|a| (a.program_id, a.tokens))
|
||||
.collect();
|
||||
|
||||
// Check account subscriptions before storing data for notifications
|
||||
let subscriptions = self.account_subscriptions.read().unwrap();
|
||||
let pre_userdata: Vec<_> = tx
|
||||
.account_keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.zip(program_accounts.iter_mut())
|
||||
.filter(|((_, pubkey), _)| subscriptions.get(&pubkey).is_some())
|
||||
.map(|((i, pubkey), a)| ((i, pubkey), a.userdata.clone()))
|
||||
.collect();
|
||||
|
||||
// Call the contract method
|
||||
// It's up to the contract to implement its own rules on moving funds
|
||||
if SystemProgram::check_id(&tx_program_id) {
|
||||
|
@ -554,6 +588,13 @@ impl Bank {
|
|||
post_account,
|
||||
)?;
|
||||
}
|
||||
// Send notifications
|
||||
for ((i, pubkey), userdata) in &pre_userdata {
|
||||
let account = &program_accounts[*i];
|
||||
if userdata != &account.userdata {
|
||||
self.check_account_subscriptions(&pubkey, &account);
|
||||
}
|
||||
}
|
||||
// The total sum of all the tokens in all the pages cannot change.
|
||||
let post_total: i64 = program_accounts.iter().map(|a| a.tokens).sum();
|
||||
if pre_total != post_total {
|
||||
|
@ -942,16 +983,101 @@ impl Bank {
|
|||
pub fn set_finality(&self, finality: usize) {
|
||||
self.finality_time.store(finality, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn add_account_subscription(
|
||||
&self,
|
||||
bank_sub_id: Pubkey,
|
||||
pubkey: Pubkey,
|
||||
sink: Sink<Account>,
|
||||
) {
|
||||
let mut subscriptions = self.account_subscriptions.write().unwrap();
|
||||
if let Some(current_hashmap) = subscriptions.get_mut(&pubkey) {
|
||||
current_hashmap.insert(bank_sub_id, sink);
|
||||
return;
|
||||
}
|
||||
let mut hashmap = HashMap::new();
|
||||
hashmap.insert(bank_sub_id, sink);
|
||||
subscriptions.insert(pubkey, hashmap);
|
||||
}
|
||||
|
||||
pub fn remove_account_subscription(&self, bank_sub_id: &Pubkey, pubkey: &Pubkey) -> bool {
|
||||
let mut subscriptions = self.account_subscriptions.write().unwrap();
|
||||
match subscriptions.get_mut(pubkey) {
|
||||
Some(ref current_hashmap) if current_hashmap.len() == 1 => {}
|
||||
Some(current_hashmap) => {
|
||||
return current_hashmap.remove(bank_sub_id).is_some();
|
||||
}
|
||||
None => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
subscriptions.remove(pubkey).is_some()
|
||||
}
|
||||
|
||||
fn check_account_subscriptions(&self, pubkey: &Pubkey, account: &Account) {
|
||||
let subscriptions = self.account_subscriptions.read().unwrap();
|
||||
if let Some(hashmap) = subscriptions.get(pubkey) {
|
||||
for (_bank_sub_id, sink) in hashmap.iter() {
|
||||
sink.notify(Ok(account.clone())).wait().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_signature_subscription(
|
||||
&self,
|
||||
bank_sub_id: Pubkey,
|
||||
signature: Signature,
|
||||
sink: Sink<RpcSignatureStatus>,
|
||||
) {
|
||||
let mut subscriptions = self.signature_subscriptions.write().unwrap();
|
||||
if let Some(current_hashmap) = subscriptions.get_mut(&signature) {
|
||||
current_hashmap.insert(bank_sub_id, sink);
|
||||
return;
|
||||
}
|
||||
let mut hashmap = HashMap::new();
|
||||
hashmap.insert(bank_sub_id, sink);
|
||||
subscriptions.insert(signature, hashmap);
|
||||
}
|
||||
|
||||
pub fn remove_signature_subscription(
|
||||
&self,
|
||||
bank_sub_id: &Pubkey,
|
||||
signature: &Signature,
|
||||
) -> bool {
|
||||
let mut subscriptions = self.signature_subscriptions.write().unwrap();
|
||||
match subscriptions.get_mut(signature) {
|
||||
Some(ref current_hashmap) if current_hashmap.len() == 1 => {}
|
||||
Some(current_hashmap) => {
|
||||
return current_hashmap.remove(bank_sub_id).is_some();
|
||||
}
|
||||
None => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
subscriptions.remove(signature).is_some()
|
||||
}
|
||||
|
||||
fn check_signature_subscriptions(&self, signature: &Signature, status: RpcSignatureStatus) {
|
||||
let mut subscriptions = self.signature_subscriptions.write().unwrap();
|
||||
if let Some(hashmap) = subscriptions.get(signature) {
|
||||
for (_bank_sub_id, sink) in hashmap.iter() {
|
||||
sink.notify(Ok(status)).wait().unwrap();
|
||||
}
|
||||
}
|
||||
subscriptions.remove(&signature);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bincode::serialize;
|
||||
use budget_program::BudgetState;
|
||||
use entry::next_entry;
|
||||
use entry::Entry;
|
||||
use entry_writer::{self, EntryWriter};
|
||||
use hash::hash;
|
||||
use jsonrpc_macros::pubsub::{Subscriber, SubscriptionId};
|
||||
use ledger;
|
||||
use logger;
|
||||
use signature::Keypair;
|
||||
|
@ -959,6 +1085,7 @@ mod tests {
|
|||
use std;
|
||||
use std::io::{BufReader, Cursor, Seek, SeekFrom};
|
||||
use system_transaction::SystemTransaction;
|
||||
use tokio::prelude::{Async, Stream};
|
||||
use transaction::Instruction;
|
||||
|
||||
#[test]
|
||||
|
@ -1496,4 +1623,94 @@ mod tests {
|
|||
Ok(_)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_bank_account_subscribe() {
|
||||
let mint = Mint::new(100);
|
||||
let bank = Bank::new(&mint);
|
||||
let alice = Keypair::new();
|
||||
let bank_sub_id = Keypair::new().pubkey();
|
||||
let last_id = bank.last_id();
|
||||
let tx = Transaction::system_create(
|
||||
&mint.keypair(),
|
||||
alice.pubkey(),
|
||||
last_id,
|
||||
1,
|
||||
16,
|
||||
BudgetState::id(),
|
||||
0,
|
||||
);
|
||||
bank.process_transaction(&tx).unwrap();
|
||||
|
||||
let (subscriber, _id_receiver, mut transport_receiver) =
|
||||
Subscriber::new_test("accountNotification");
|
||||
let sub_id = SubscriptionId::Number(0 as u64);
|
||||
let sink = subscriber.assign_id(sub_id.clone()).unwrap();
|
||||
bank.add_account_subscription(bank_sub_id, alice.pubkey(), sink);
|
||||
|
||||
assert!(
|
||||
bank.account_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.contains_key(&alice.pubkey())
|
||||
);
|
||||
|
||||
let account = bank.get_account(&alice.pubkey()).unwrap();
|
||||
bank.check_account_subscriptions(&alice.pubkey(), &account);
|
||||
let string = transport_receiver.poll();
|
||||
assert!(string.is_ok());
|
||||
if let Async::Ready(Some(response)) = string.unwrap() {
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","method":"accountNotification","params":{{"result":{{"program_id":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"tokens":1,"userdata":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},"subscription":0}}}}"#);
|
||||
assert_eq!(expected, response);
|
||||
}
|
||||
|
||||
bank.remove_account_subscription(&bank_sub_id, &alice.pubkey());
|
||||
assert!(
|
||||
!bank
|
||||
.account_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.contains_key(&alice.pubkey())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_bank_signature_subscribe() {
|
||||
let mint = Mint::new(100);
|
||||
let bank = Bank::new(&mint);
|
||||
let alice = Keypair::new();
|
||||
let bank_sub_id = Keypair::new().pubkey();
|
||||
let last_id = bank.last_id();
|
||||
let tx = Transaction::system_move(&mint.keypair(), alice.pubkey(), 20, last_id, 0);
|
||||
let signature = tx.signature;
|
||||
bank.process_transaction(&tx).unwrap();
|
||||
|
||||
let (subscriber, _id_receiver, mut transport_receiver) =
|
||||
Subscriber::new_test("signatureNotification");
|
||||
let sub_id = SubscriptionId::Number(0 as u64);
|
||||
let sink = subscriber.assign_id(sub_id.clone()).unwrap();
|
||||
bank.add_signature_subscription(bank_sub_id, signature, sink);
|
||||
|
||||
assert!(
|
||||
bank.signature_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.contains_key(&signature)
|
||||
);
|
||||
|
||||
bank.check_signature_subscriptions(&signature, RpcSignatureStatus::Confirmed);
|
||||
let string = transport_receiver.poll();
|
||||
assert!(string.is_ok());
|
||||
if let Async::Ready(Some(response)) = string.unwrap() {
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","method":"signatureNotification","params":{{"result":"Confirmed","subscription":0}}}}"#);
|
||||
assert_eq!(expected, response);
|
||||
}
|
||||
|
||||
bank.remove_signature_subscription(&bank_sub_id, &signature);
|
||||
assert!(
|
||||
!bank
|
||||
.signature_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.contains_key(&signature)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ pub mod packet;
|
|||
pub mod payment_plan;
|
||||
pub mod poh;
|
||||
pub mod poh_recorder;
|
||||
pub mod pubsub;
|
||||
pub mod recvmmsg;
|
||||
pub mod replicate_stage;
|
||||
pub mod replicator;
|
||||
|
@ -109,6 +110,8 @@ extern crate solana_jsonrpc_core as jsonrpc_core;
|
|||
extern crate solana_jsonrpc_http_server as jsonrpc_http_server;
|
||||
#[macro_use]
|
||||
extern crate solana_jsonrpc_macros as jsonrpc_macros;
|
||||
extern crate solana_jsonrpc_pubsub as jsonrpc_pubsub;
|
||||
extern crate solana_jsonrpc_ws_server as jsonrpc_ws_server;
|
||||
extern crate solana_program_interface;
|
||||
extern crate sys_info;
|
||||
extern crate tokio;
|
||||
|
|
|
@ -7,7 +7,7 @@ use rand::{thread_rng, Rng};
|
|||
use reqwest;
|
||||
use socket2::{Domain, SockAddr, Socket, Type};
|
||||
use std::io;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, UdpSocket};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
/// A data type representing a public Udp socket
|
||||
|
@ -159,6 +159,26 @@ pub fn bind_to(port: u16, reuseaddr: bool) -> io::Result<UdpSocket> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn find_available_port_in_range(range: (u16, u16)) -> io::Result<u16> {
|
||||
let (start, end) = range;
|
||||
let mut tries_left = end - start;
|
||||
loop {
|
||||
let rand_port = thread_rng().gen_range(start, end);
|
||||
match TcpListener::bind(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
||||
rand_port,
|
||||
)) {
|
||||
Ok(_) => {
|
||||
break Ok(rand_port);
|
||||
}
|
||||
Err(err) => if err.kind() != io::ErrorKind::AddrInUse || tries_left == 0 {
|
||||
return Err(err);
|
||||
},
|
||||
}
|
||||
tries_left -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ipnetwork::IpNetwork;
|
||||
|
|
|
@ -0,0 +1,638 @@
|
|||
//! The `pubsub` module implements a threaded subscription service on client RPC request
|
||||
|
||||
use bank::Bank;
|
||||
use bs58;
|
||||
use jsonrpc_core::futures::Future;
|
||||
use jsonrpc_core::*;
|
||||
use jsonrpc_macros::pubsub;
|
||||
use jsonrpc_pubsub::{PubSubHandler, PubSubMetadata, Session, SubscriptionId};
|
||||
use jsonrpc_ws_server::ws;
|
||||
use jsonrpc_ws_server::{RequestContext, Sender, ServerBuilder};
|
||||
use rpc::{JsonRpcRequestProcessor, RpcSignatureStatus};
|
||||
use signature::{Keypair, KeypairUtil, Signature};
|
||||
use solana_program_interface::account::Account;
|
||||
use solana_program_interface::pubkey::Pubkey;
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{atomic, Arc, Mutex, RwLock};
|
||||
use std::thread::{sleep, Builder, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
pub enum ClientState {
|
||||
Uninitialized,
|
||||
Init(Sender),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SubscriptionResponse {
|
||||
pub port: u16,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub struct PubSubService {
|
||||
_thread_hdl: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl PubSubService {
|
||||
pub fn new(
|
||||
bank: &Arc<Bank>,
|
||||
pubsub_addr: SocketAddr,
|
||||
path: Pubkey,
|
||||
exit: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
let request_processor = JsonRpcRequestProcessor::new(bank.clone());
|
||||
let status = Arc::new(Mutex::new(ClientState::Uninitialized));
|
||||
let client_status = status.clone();
|
||||
let server_bank = bank.clone();
|
||||
let _thread_hdl = Builder::new()
|
||||
.name("solana-pubsub".to_string())
|
||||
.spawn(move || {
|
||||
let mut io = PubSubHandler::default();
|
||||
let rpc = RpcSolPubSubImpl::default();
|
||||
let account_subs = rpc.account_subscriptions.clone();
|
||||
let signature_subs = rpc.signature_subscriptions.clone();
|
||||
io.extend_with(rpc.to_delegate());
|
||||
|
||||
let server = ServerBuilder::with_meta_extractor(io, move |context: &RequestContext|
|
||||
{
|
||||
*client_status.lock().unwrap() = ClientState::Init(context.out.clone());
|
||||
Meta {
|
||||
request_processor: request_processor.clone(),
|
||||
session: Arc::new(Session::new(context.sender().clone())),
|
||||
}
|
||||
})
|
||||
.request_middleware(move |req: &ws::Request|
|
||||
if req.resource() != format!("/{}", path.to_string()) {
|
||||
Some(ws::Response::new(403, "Client path incorrect or not provided"))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.start(&pubsub_addr);
|
||||
|
||||
if server.is_err() {
|
||||
warn!("Pubsub service unavailable: unable to bind to port {}. \nMake sure this port is not already in use by another application", pubsub_addr.port());
|
||||
return;
|
||||
}
|
||||
while !exit.load(Ordering::Relaxed) {
|
||||
if let ClientState::Init(ref mut sender) = *status.lock().unwrap() {
|
||||
if sender.check_active().is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
for (_, (bank_sub_id, pubkey)) in account_subs.read().unwrap().iter() {
|
||||
server_bank.remove_account_subscription(bank_sub_id, pubkey);
|
||||
}
|
||||
for (_, (bank_sub_id, signature)) in signature_subs.read().unwrap().iter() {
|
||||
server_bank.remove_signature_subscription(bank_sub_id, signature);
|
||||
}
|
||||
server.unwrap().close();
|
||||
()
|
||||
})
|
||||
.unwrap();
|
||||
PubSubService { _thread_hdl }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Meta {
|
||||
pub request_processor: JsonRpcRequestProcessor,
|
||||
pub session: Arc<Session>,
|
||||
}
|
||||
impl Metadata for Meta {}
|
||||
impl PubSubMetadata for Meta {
|
||||
fn session(&self) -> Option<Arc<Session>> {
|
||||
Some(self.session.clone())
|
||||
}
|
||||
}
|
||||
|
||||
build_rpc_trait! {
|
||||
pub trait RpcSolPubSub {
|
||||
type Metadata;
|
||||
|
||||
#[pubsub(name = "accountNotification")] {
|
||||
// Get notification every time account userdata is changed
|
||||
// Accepts pubkey parameter as base-58 encoded string
|
||||
#[rpc(name = "accountSubscribe")]
|
||||
fn account_subscribe(&self, Self::Metadata, pubsub::Subscriber<Account>, String);
|
||||
|
||||
// Unsubscribe from account notification subscription.
|
||||
#[rpc(name = "accountUnsubscribe")]
|
||||
fn account_unsubscribe(&self, Self::Metadata, SubscriptionId) -> Result<bool>;
|
||||
}
|
||||
#[pubsub(name = "signatureNotification")] {
|
||||
// Get notification when signature is verified
|
||||
// Accepts signature parameter as base-58 encoded string
|
||||
#[rpc(name = "signatureSubscribe")]
|
||||
fn signature_subscribe(&self, Self::Metadata, pubsub::Subscriber<RpcSignatureStatus>, String);
|
||||
|
||||
// Unsubscribe from signature notification subscription.
|
||||
#[rpc(name = "signatureUnsubscribe")]
|
||||
fn signature_unsubscribe(&self, Self::Metadata, SubscriptionId) -> Result<bool>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RpcSolPubSubImpl {
|
||||
uid: atomic::AtomicUsize,
|
||||
account_subscriptions: Arc<RwLock<HashMap<SubscriptionId, (Pubkey, Pubkey)>>>,
|
||||
signature_subscriptions: Arc<RwLock<HashMap<SubscriptionId, (Pubkey, Signature)>>>,
|
||||
}
|
||||
impl RpcSolPubSub for RpcSolPubSubImpl {
|
||||
type Metadata = Meta;
|
||||
|
||||
fn account_subscribe(
|
||||
&self,
|
||||
meta: Self::Metadata,
|
||||
subscriber: pubsub::Subscriber<Account>,
|
||||
pubkey_str: String,
|
||||
) {
|
||||
let pubkey_vec = bs58::decode(pubkey_str).into_vec().unwrap();
|
||||
if pubkey_vec.len() != mem::size_of::<Pubkey>() {
|
||||
subscriber
|
||||
.reject(Error {
|
||||
code: ErrorCode::InvalidParams,
|
||||
message: "Invalid Request: Invalid pubkey provided".into(),
|
||||
data: None,
|
||||
}).unwrap();
|
||||
return;
|
||||
}
|
||||
let pubkey = Pubkey::new(&pubkey_vec);
|
||||
|
||||
let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
|
||||
let sub_id = SubscriptionId::Number(id as u64);
|
||||
let sink = subscriber.assign_id(sub_id.clone()).unwrap();
|
||||
let bank_sub_id = Keypair::new().pubkey();
|
||||
self.account_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(sub_id.clone(), (bank_sub_id, pubkey));
|
||||
|
||||
meta.request_processor
|
||||
.add_account_subscription(bank_sub_id, pubkey, sink);
|
||||
}
|
||||
|
||||
fn account_unsubscribe(&self, meta: Self::Metadata, id: SubscriptionId) -> Result<bool> {
|
||||
if let Some((bank_sub_id, pubkey)) = self.account_subscriptions.write().unwrap().remove(&id)
|
||||
{
|
||||
meta.request_processor
|
||||
.remove_account_subscription(&bank_sub_id, &pubkey);
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(Error {
|
||||
code: ErrorCode::InvalidParams,
|
||||
message: "Invalid Request: Subscription id does not exist".into(),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn signature_subscribe(
|
||||
&self,
|
||||
meta: Self::Metadata,
|
||||
subscriber: pubsub::Subscriber<RpcSignatureStatus>,
|
||||
signature_str: String,
|
||||
) {
|
||||
let signature_vec = bs58::decode(signature_str).into_vec().unwrap();
|
||||
if signature_vec.len() != mem::size_of::<Signature>() {
|
||||
subscriber
|
||||
.reject(Error {
|
||||
code: ErrorCode::InvalidParams,
|
||||
message: "Invalid Request: Invalid signature provided".into(),
|
||||
data: None,
|
||||
}).unwrap();
|
||||
return;
|
||||
}
|
||||
let signature = Signature::new(&signature_vec);
|
||||
|
||||
let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
|
||||
let sub_id = SubscriptionId::Number(id as u64);
|
||||
let sink = subscriber.assign_id(sub_id.clone()).unwrap();
|
||||
let bank_sub_id = Keypair::new().pubkey();
|
||||
self.signature_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(sub_id.clone(), (bank_sub_id, signature));
|
||||
|
||||
match meta.request_processor.get_signature_status(signature) {
|
||||
Ok(_) => {
|
||||
sink.notify(Ok(RpcSignatureStatus::Confirmed))
|
||||
.wait()
|
||||
.unwrap();
|
||||
self.signature_subscriptions
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&sub_id);
|
||||
}
|
||||
Err(_) => {
|
||||
meta.request_processor
|
||||
.add_signature_subscription(bank_sub_id, signature, sink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn signature_unsubscribe(&self, meta: Self::Metadata, id: SubscriptionId) -> Result<bool> {
|
||||
if let Some((bank_sub_id, signature)) =
|
||||
self.signature_subscriptions.write().unwrap().remove(&id)
|
||||
{
|
||||
meta.request_processor
|
||||
.remove_signature_subscription(&bank_sub_id, &signature);
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(Error {
|
||||
code: ErrorCode::InvalidParams,
|
||||
message: "Invalid Request: Subscription id does not exist".into(),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use budget_program::BudgetState;
|
||||
use budget_transaction::BudgetTransaction;
|
||||
use jsonrpc_core::futures::sync::mpsc;
|
||||
use mint::Mint;
|
||||
use signature::{Keypair, KeypairUtil};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use system_transaction::SystemTransaction;
|
||||
use tokio::prelude::{Async, Stream};
|
||||
use transaction::Transaction;
|
||||
|
||||
#[test]
|
||||
fn test_pubsub_new() {
|
||||
let alice = Mint::new(10_000);
|
||||
let bank = Bank::new(&alice);
|
||||
let pubsub_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
|
||||
let pubkey = Keypair::new().pubkey();
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
let pubsub_service = PubSubService::new(&Arc::new(bank), pubsub_addr, pubkey, exit);
|
||||
let thread = pubsub_service._thread_hdl.thread();
|
||||
assert_eq!(thread.name().unwrap(), "solana-pubsub");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_subscribe() {
|
||||
let alice = Mint::new(10_000);
|
||||
let bob = Keypair::new();
|
||||
let bob_pubkey = bob.pubkey();
|
||||
let bank = Bank::new(&alice);
|
||||
let arc_bank = Arc::new(bank);
|
||||
let last_id = arc_bank.last_id();
|
||||
|
||||
let request_processor = JsonRpcRequestProcessor::new(arc_bank.clone());
|
||||
let (sender, mut receiver) = mpsc::channel(1);
|
||||
let session = Arc::new(Session::new(sender));
|
||||
|
||||
let mut io = PubSubHandler::default();
|
||||
let rpc = RpcSolPubSubImpl::default();
|
||||
io.extend_with(rpc.to_delegate());
|
||||
let meta = Meta {
|
||||
request_processor,
|
||||
session,
|
||||
};
|
||||
|
||||
// Test signature subscription
|
||||
let tx = Transaction::system_move(&alice.keypair(), bob_pubkey, 20, last_id, 0);
|
||||
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"signatureSubscribe","params":["{}"]}}"#,
|
||||
tx.signature.to_string()
|
||||
);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","result":0,"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// Test bad parameter
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"signatureSubscribe","params":["a1b2c3"]}}"#
|
||||
);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","error":{{"code":-32602,"message":"Invalid Request: Invalid signature provided"}},"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
arc_bank
|
||||
.process_transaction(&tx)
|
||||
.expect("process transaction");
|
||||
sleep(Duration::from_millis(200));
|
||||
|
||||
// Test signature confirmation notification
|
||||
let string = receiver.poll();
|
||||
assert!(string.is_ok());
|
||||
if let Async::Ready(Some(response)) = string.unwrap() {
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","method":"signatureNotification","params":{{"result":"Confirmed","subscription":0}}}}"#);
|
||||
assert_eq!(expected, response);
|
||||
}
|
||||
|
||||
// Test subscription id increment
|
||||
let tx = Transaction::system_move(&alice.keypair(), bob_pubkey, 10, last_id, 0);
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"signatureSubscribe","params":["{}"]}}"#,
|
||||
tx.signature.to_string()
|
||||
);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","result":1,"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_unsubscribe() {
|
||||
let alice = Mint::new(10_000);
|
||||
let bob_pubkey = Keypair::new().pubkey();
|
||||
let bank = Bank::new(&alice);
|
||||
let arc_bank = Arc::new(bank);
|
||||
let last_id = arc_bank.last_id();
|
||||
|
||||
let request_processor = JsonRpcRequestProcessor::new(arc_bank);
|
||||
let (sender, _receiver) = mpsc::channel(1);
|
||||
let session = Arc::new(Session::new(sender));
|
||||
|
||||
let mut io = PubSubHandler::default();
|
||||
let rpc = RpcSolPubSubImpl::default();
|
||||
io.extend_with(rpc.to_delegate());
|
||||
let meta = Meta {
|
||||
request_processor,
|
||||
session: session.clone(),
|
||||
};
|
||||
|
||||
let tx = Transaction::system_move(&alice.keypair(), bob_pubkey, 20, last_id, 0);
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"signatureSubscribe","params":["{}"]}}"#,
|
||||
tx.signature.to_string()
|
||||
);
|
||||
let _res = io.handle_request_sync(&req, meta.clone());
|
||||
|
||||
let req =
|
||||
format!(r#"{{"jsonrpc":"2.0","id":1,"method":"signatureUnsubscribe","params":[0]}}"#);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","result":true,"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// Test bad parameter
|
||||
let req =
|
||||
format!(r#"{{"jsonrpc":"2.0","id":1,"method":"signatureUnsubscribe","params":[1]}}"#);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","error":{{"code":-32602,"message":"Invalid Request: Subscription id does not exist"}},"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_subscribe() {
|
||||
let alice = Mint::new(10_000);
|
||||
let bob_pubkey = Keypair::new().pubkey();
|
||||
let witness = Keypair::new();
|
||||
let contract_funds = Keypair::new();
|
||||
let contract_state = Keypair::new();
|
||||
let budget_program_id = BudgetState::id();
|
||||
let bank = Bank::new(&alice);
|
||||
let arc_bank = Arc::new(bank);
|
||||
let last_id = arc_bank.last_id();
|
||||
|
||||
let request_processor = JsonRpcRequestProcessor::new(arc_bank.clone());
|
||||
let (sender, mut receiver) = mpsc::channel(1);
|
||||
let session = Arc::new(Session::new(sender));
|
||||
|
||||
let mut io = PubSubHandler::default();
|
||||
let rpc = RpcSolPubSubImpl::default();
|
||||
io.extend_with(rpc.to_delegate());
|
||||
let meta = Meta {
|
||||
request_processor,
|
||||
session,
|
||||
};
|
||||
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["{}"]}}"#,
|
||||
contract_state.pubkey().to_string()
|
||||
);
|
||||
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","result":0,"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// Test bad parameter
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["a1b2c3"]}}"#
|
||||
);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","error":{{"code":-32602,"message":"Invalid Request: Invalid pubkey provided"}},"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let tx = Transaction::system_create(
|
||||
&alice.keypair(),
|
||||
contract_funds.pubkey(),
|
||||
last_id,
|
||||
50,
|
||||
0,
|
||||
budget_program_id,
|
||||
0,
|
||||
);
|
||||
arc_bank
|
||||
.process_transaction(&tx)
|
||||
.expect("process transaction");
|
||||
|
||||
let tx = Transaction::system_create(
|
||||
&alice.keypair(),
|
||||
contract_state.pubkey(),
|
||||
last_id,
|
||||
1,
|
||||
196,
|
||||
budget_program_id,
|
||||
0,
|
||||
);
|
||||
arc_bank
|
||||
.process_transaction(&tx)
|
||||
.expect("process transaction");
|
||||
|
||||
// Test signature confirmation notification #1
|
||||
let string = receiver.poll();
|
||||
assert!(string.is_ok());
|
||||
let expected_userdata = arc_bank
|
||||
.get_account(&contract_state.pubkey())
|
||||
.unwrap()
|
||||
.userdata;
|
||||
let expected = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "accountNotification",
|
||||
"params": {
|
||||
"result": {
|
||||
"program_id": budget_program_id,
|
||||
"tokens": 1,
|
||||
"userdata": expected_userdata
|
||||
},
|
||||
"subscription": 0,
|
||||
}
|
||||
});
|
||||
|
||||
if let Async::Ready(Some(response)) = string.unwrap() {
|
||||
assert_eq!(serde_json::to_string(&expected).unwrap(), response);
|
||||
}
|
||||
|
||||
let tx = Transaction::budget_new_when_signed(
|
||||
&contract_funds,
|
||||
bob_pubkey,
|
||||
contract_state.pubkey(),
|
||||
witness.pubkey(),
|
||||
None,
|
||||
50,
|
||||
last_id,
|
||||
);
|
||||
arc_bank
|
||||
.process_transaction(&tx)
|
||||
.expect("process transaction");
|
||||
sleep(Duration::from_millis(200));
|
||||
|
||||
// Test signature confirmation notification #2
|
||||
let string = receiver.poll();
|
||||
assert!(string.is_ok());
|
||||
let expected_userdata = arc_bank
|
||||
.get_account(&contract_state.pubkey())
|
||||
.unwrap()
|
||||
.userdata;
|
||||
let expected = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "accountNotification",
|
||||
"params": {
|
||||
"result": {
|
||||
"program_id": budget_program_id,
|
||||
"tokens": 51,
|
||||
"userdata": expected_userdata
|
||||
},
|
||||
"subscription": 0,
|
||||
}
|
||||
});
|
||||
|
||||
if let Async::Ready(Some(response)) = string.unwrap() {
|
||||
assert_eq!(serde_json::to_string(&expected).unwrap(), response);
|
||||
}
|
||||
|
||||
let tx = Transaction::system_new(&alice.keypair(), witness.pubkey(), 1, last_id);
|
||||
arc_bank
|
||||
.process_transaction(&tx)
|
||||
.expect("process transaction");
|
||||
sleep(Duration::from_millis(200));
|
||||
let tx = Transaction::budget_new_signature(
|
||||
&witness,
|
||||
contract_state.pubkey(),
|
||||
bob_pubkey,
|
||||
last_id,
|
||||
);
|
||||
arc_bank
|
||||
.process_transaction(&tx)
|
||||
.expect("process transaction");
|
||||
sleep(Duration::from_millis(200));
|
||||
|
||||
let expected_userdata = arc_bank
|
||||
.get_account(&contract_state.pubkey())
|
||||
.unwrap()
|
||||
.userdata;
|
||||
let expected = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "accountNotification",
|
||||
"params": {
|
||||
"result": {
|
||||
"program_id": budget_program_id,
|
||||
"tokens": 1,
|
||||
"userdata": expected_userdata
|
||||
},
|
||||
"subscription": 0,
|
||||
}
|
||||
});
|
||||
let string = receiver.poll();
|
||||
assert!(string.is_ok());
|
||||
if let Async::Ready(Some(response)) = string.unwrap() {
|
||||
assert_eq!(serde_json::to_string(&expected).unwrap(), response);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_unsubscribe() {
|
||||
let alice = Mint::new(10_000);
|
||||
let bob_pubkey = Keypair::new().pubkey();
|
||||
let bank = Bank::new(&alice);
|
||||
let arc_bank = Arc::new(bank);
|
||||
|
||||
let request_processor = JsonRpcRequestProcessor::new(arc_bank);
|
||||
let (sender, _receiver) = mpsc::channel(1);
|
||||
let session = Arc::new(Session::new(sender));
|
||||
|
||||
let mut io = PubSubHandler::default();
|
||||
let rpc = RpcSolPubSubImpl::default();
|
||||
io.extend_with(rpc.to_delegate());
|
||||
let meta = Meta {
|
||||
request_processor,
|
||||
session: session.clone(),
|
||||
};
|
||||
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["{}"]}}"#,
|
||||
bob_pubkey.to_string()
|
||||
);
|
||||
let _res = io.handle_request_sync(&req, meta.clone());
|
||||
|
||||
let req =
|
||||
format!(r#"{{"jsonrpc":"2.0","id":1,"method":"accountUnsubscribe","params":[0]}}"#);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","result":true,"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// Test bad parameter
|
||||
let req =
|
||||
format!(r#"{{"jsonrpc":"2.0","id":1,"method":"accountUnsubscribe","params":[1]}}"#);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = format!(r#"{{"jsonrpc":"2.0","error":{{"code":-32602,"message":"Invalid Request: Subscription id does not exist"}},"id":1}}"#);
|
||||
let expected: Response =
|
||||
serde_json::from_str(&expected).expect("expected response deserialization");
|
||||
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
}
|
69
src/rpc.rs
69
src/rpc.rs
|
@ -3,10 +3,14 @@
|
|||
use bank::{Bank, BankError};
|
||||
use bincode::deserialize;
|
||||
use bs58;
|
||||
use cluster_info::FULLNODE_PORT_RANGE;
|
||||
use jsonrpc_core::*;
|
||||
use jsonrpc_http_server::*;
|
||||
use jsonrpc_macros::pubsub::Sink;
|
||||
use netutil::find_available_port_in_range;
|
||||
use pubsub::{PubSubService, SubscriptionResponse};
|
||||
use service::Service;
|
||||
use signature::Signature;
|
||||
use signature::{Keypair, KeypairUtil, Signature};
|
||||
use solana_program_interface::account::Account;
|
||||
use solana_program_interface::pubkey::Pubkey;
|
||||
use std::mem;
|
||||
|
@ -35,6 +39,7 @@ impl JsonRpcService {
|
|||
exit: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
let request_processor = JsonRpcRequestProcessor::new(bank.clone());
|
||||
let exit_pubsub = exit.clone();
|
||||
let thread_hdl = Builder::new()
|
||||
.name("solana-jsonrpc".to_string())
|
||||
.spawn(move || {
|
||||
|
@ -47,6 +52,8 @@ impl JsonRpcService {
|
|||
request_processor: request_processor.clone(),
|
||||
transactions_addr,
|
||||
drone_addr,
|
||||
rpc_addr,
|
||||
exit: exit_pubsub.clone(),
|
||||
}).threads(4)
|
||||
.cors(DomainsValidation::AllowOnly(vec![
|
||||
AccessControlAllowOrigin::Any,
|
||||
|
@ -83,10 +90,12 @@ pub struct Meta {
|
|||
pub request_processor: JsonRpcRequestProcessor,
|
||||
pub transactions_addr: SocketAddr,
|
||||
pub drone_addr: SocketAddr,
|
||||
pub rpc_addr: SocketAddr,
|
||||
pub exit: Arc<AtomicBool>,
|
||||
}
|
||||
impl Metadata for Meta {}
|
||||
|
||||
#[derive(PartialEq, Serialize)]
|
||||
#[derive(Copy, Clone, PartialEq, Serialize, Debug)]
|
||||
pub enum RpcSignatureStatus {
|
||||
Confirmed,
|
||||
SignatureNotFound,
|
||||
|
@ -124,6 +133,9 @@ build_rpc_trait! {
|
|||
|
||||
#[rpc(meta, name = "sendTransaction")]
|
||||
fn send_transaction(&self, Self::Metadata, Vec<u8>) -> Result<String>;
|
||||
|
||||
#[rpc(meta, name = "startSubscriptionChannel")]
|
||||
fn start_subscription_channel(&self, Self::Metadata) -> Result<SubscriptionResponse>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,6 +234,22 @@ impl RpcSol for RpcSolImpl {
|
|||
})?;
|
||||
Ok(bs58::encode(tx.signature).into_string())
|
||||
}
|
||||
fn start_subscription_channel(&self, meta: Self::Metadata) -> Result<SubscriptionResponse> {
|
||||
let port: u16 = find_available_port_in_range(FULLNODE_PORT_RANGE).map_err(|_| Error {
|
||||
code: ErrorCode::InternalError,
|
||||
message: "No available port in range".into(),
|
||||
data: None,
|
||||
})?;
|
||||
let mut pubsub_addr = meta.rpc_addr;
|
||||
pubsub_addr.set_port(port);
|
||||
let pubkey = Keypair::new().pubkey();
|
||||
let _pubsub_service =
|
||||
PubSubService::new(&meta.request_processor.bank, pubsub_addr, pubkey, meta.exit);
|
||||
Ok(SubscriptionResponse {
|
||||
port,
|
||||
path: pubkey.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct JsonRpcRequestProcessor {
|
||||
|
@ -234,7 +262,7 @@ impl JsonRpcRequestProcessor {
|
|||
}
|
||||
|
||||
/// Process JSON-RPC request items sent via JSON-RPC.
|
||||
fn get_account_info(&self, pubkey: Pubkey) -> Result<Account> {
|
||||
pub fn get_account_info(&self, pubkey: Pubkey) -> Result<Account> {
|
||||
self.bank
|
||||
.get_account(&pubkey)
|
||||
.ok_or_else(Error::invalid_request)
|
||||
|
@ -250,12 +278,37 @@ impl JsonRpcRequestProcessor {
|
|||
let id = self.bank.last_id();
|
||||
Ok(bs58::encode(id).into_string())
|
||||
}
|
||||
fn get_signature_status(&self, signature: Signature) -> result::Result<(), BankError> {
|
||||
pub fn get_signature_status(&self, signature: Signature) -> result::Result<(), BankError> {
|
||||
self.bank.get_signature_status(&signature)
|
||||
}
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
Ok(self.bank.transaction_count() as u64)
|
||||
}
|
||||
pub fn add_account_subscription(
|
||||
&self,
|
||||
bank_sub_id: Pubkey,
|
||||
pubkey: Pubkey,
|
||||
sink: Sink<Account>,
|
||||
) {
|
||||
self.bank
|
||||
.add_account_subscription(bank_sub_id, pubkey, sink);
|
||||
}
|
||||
pub fn remove_account_subscription(&self, bank_sub_id: &Pubkey, pubkey: &Pubkey) {
|
||||
self.bank.remove_account_subscription(bank_sub_id, pubkey);
|
||||
}
|
||||
pub fn add_signature_subscription(
|
||||
&self,
|
||||
bank_sub_id: Pubkey,
|
||||
signature: Signature,
|
||||
sink: Sink<RpcSignatureStatus>,
|
||||
) {
|
||||
self.bank
|
||||
.add_signature_subscription(bank_sub_id, signature, sink);
|
||||
}
|
||||
pub fn remove_signature_subscription(&self, bank_sub_id: &Pubkey, signature: &Signature) {
|
||||
self.bank
|
||||
.remove_signature_subscription(bank_sub_id, signature);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -283,6 +336,8 @@ mod tests {
|
|||
let request_processor = JsonRpcRequestProcessor::new(Arc::new(bank));
|
||||
let transactions_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
|
||||
let drone_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
|
||||
let rpc_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
|
||||
let exit = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut io = MetaIoHandler::default();
|
||||
let rpc = RpcSolImpl;
|
||||
|
@ -291,6 +346,8 @@ mod tests {
|
|||
request_processor,
|
||||
transactions_addr,
|
||||
drone_addr,
|
||||
rpc_addr,
|
||||
exit,
|
||||
};
|
||||
|
||||
let req = format!(
|
||||
|
@ -351,6 +408,8 @@ mod tests {
|
|||
request_processor: JsonRpcRequestProcessor::new(Arc::new(bank)),
|
||||
transactions_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
|
||||
drone_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
|
||||
rpc_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
|
||||
exit: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
|
||||
let res = io.handle_request_sync(req, meta);
|
||||
|
@ -376,6 +435,8 @@ mod tests {
|
|||
request_processor: JsonRpcRequestProcessor::new(Arc::new(bank)),
|
||||
transactions_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
|
||||
drone_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
|
||||
rpc_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
|
||||
exit: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
|
||||
let res = io.handle_request_sync(req, meta);
|
||||
|
|
|
@ -455,6 +455,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_thin_client() {
|
||||
logger::setup();
|
||||
let leader_keypair = Keypair::new();
|
||||
|
|
Loading…
Reference in New Issue