All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.polkadot.rpc.provider.ws.WsProvider Maven / Gradle / Ivy

The newest version!
package org.polkadot.rpc.provider.ws;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.onehilltech.promises.Promise;
import org.apache.commons.lang3.StringUtils;
import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.polkadot.common.EventEmitter;
import org.polkadot.common.ExecutorsManager;
import org.polkadot.rpc.provider.Constants;
import org.polkadot.rpc.provider.IProvider;
import org.polkadot.rpc.provider.IWsProvider;
import org.polkadot.rpc.provider.Types;
import org.polkadot.rpc.provider.Types.JsonRpcResponse;
import org.polkadot.rpc.provider.coder.RpcCoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;


/**
 * # @polkadot/rpc-provider/ws
 *
 * WsProviderDir
 *
 * The WebSocket Provider allows sending requests using WebSocket to a WebSocket RPC server TCP port. Unlike the HttpProvider, it does support subscriptions and allows listening to events such as new blocks or balance changes.
 * **Example**
 * ```java
 * import org.polkadot.rpc.provider.ws.WsProviderDir;
 * WsProviderDir provider = new WsProviderDir('ws://127.0.0.1:9944');
 * ```
 * @see org.polkadot.rpc.provider.http.HttpProvider
 */
public class WsProvider implements IWsProvider {

    private static final Map ALIASSES = new HashMap<>();

    static {
        ALIASSES.put("chain_finalisedHead", "chain_finalizedHead");
    }


    public static class WsStateAwaiting {
        public CallbackHandler callBack;
        public String method;
        public List params;
        public SubscriptionHandler subscription;

        public WsStateAwaiting(CallbackHandler callBack, String method, List params, SubscriptionHandler subscription) {
            this.callBack = callBack;
            this.method = method;
            this.params = params;
            this.subscription = subscription;
        }
    }

    static class WsStateSubscription extends SubscriptionHandler {
        String method;
        List params;

        public WsStateSubscription(String method, List params) {
            this.method = method;
            this.params = params;
        }

        public WsStateSubscription(CallbackHandler callBack, String type, String method, List params) {
            super(callBack, type);
            this.method = method;
            this.params = params;
        }
    }

    private static final Logger logger = LoggerFactory.getLogger(WsProvider.class);

    private boolean isConnected;
    private boolean autoConnect;

    private EventEmitter eventemitter = new EventEmitter();

    private RpcCoder coder = new RpcCoder();
    private String endpoint;

    private Map handlers = new ConcurrentHashMap<>();

    private LinkedList queued = new LinkedList<>();


    private Map subscriptions = new ConcurrentHashMap<>();

    private Map waitingForId = new ConcurrentHashMap<>();

    private WebSocketClient webSocket;

    public WsProvider() {
        this(Constants.WS_URL, true);
    }

	/**
	* @param endpoint    The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`
	*/
    public WsProvider(String endpoint) {
        this(endpoint, true);
    }

	/**
	* @param endpoint    The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`
	* @param autoConnect Whether to connect automatically or not.
	*/
    public WsProvider(String endpoint, boolean autoConnect) {

        //assert(/^(wss|ws):\/\//.test(endpoint), `Endpoint should start with 'ws://', received '${endpoint}'`);
        if (Pattern.matches("^(wss|ws):\\/\\/", endpoint)) {
            throw new RuntimeException("Endpoint should start with 'ws://', received " + endpoint);
        }
        this.endpoint = endpoint;
        this.autoConnect = autoConnect;
        //this.coder = new RpcCoder();


        if (autoConnect) {
            this.connect();
        }
    }

    /**
     * Manually connect
     * The WsProviderDir connects automatically by default, however if you decided otherwise, you may
     * connect manually using this method.
     */
    @Override
    public void connect() {

        try {
            this.webSocket = new WebSocketClient(new URI(this.endpoint)) {
                WsProvider wsProvider = WsProvider.this;

                @Override
                public void onOpen(ServerHandshake handshakedata) {
                    logger.info("WebSocket onOpen: {}", getURI());

                    wsProvider.isConnected = true;
                    wsProvider.emit(ProviderInterfaceEmitted.connected);
                    wsProvider.sendQueue();
                    wsProvider.resubscribe();
                }

                @Override
                public void onMessage(String message) {
                    logger.debug("WebSocket onMessage:{}", message);

                    JsonRpcResponse response = JSONObject.parseObject(message, JsonRpcResponse.class);
                    if (StringUtils.isEmpty(response.getMethod())) {
                        wsProvider.onSocketMessageResult(response);
                    } else {
                        wsProvider.onSocketMessageSubscribe(response);
                    }
                }

                @Override
                public void onClose(int code, String reason, boolean remote) {

                    if (wsProvider.autoConnect) {
                        logger.error("disconnected from ${} code: '${}' reason: '${}'",
                                this.getURI(), code, reason);
                    }

                    wsProvider.isConnected = false;
                    wsProvider.emit(ProviderInterfaceEmitted.disconnected);

                    if (wsProvider.autoConnect) {
                        ExecutorsManager.schedule(() -> wsProvider.connect(), 1000, TimeUnit.MILLISECONDS);
                    }
                }

                @Override
                public void onError(Exception ex) {
                    logger.error(" socket error ", ex);
                    wsProvider.emit(ProviderInterfaceEmitted.error, ex);
                }
            };
            this.webSocket.connect();
        } catch (
                Exception e) {
            logger.error("connect error", e);
        }

    }

    private void emit(EventEmitter.EventType type, Object... args) {
        this.eventemitter.emit(type, args);
    }

    private void onSocketMessageSubscribe(JsonRpcResponse response) {
        String method = ALIASSES.get(response.getMethod());
        if (method == null) {
            method = response.getMethod();
        }

        String subId = method + "::" + response.getParams().getSubscription();

        logger.debug("handling: response =', {}, 'subscription =', {}", response, subId);

        WsStateSubscription handler = this.subscriptions.get(subId);

        if (handler == null) {
            // store the JSON, we could have out-of-order subid coming in
            this.waitingForId.put(subId, response);
            logger.info("Unable to find handler for subscription=${}", subId);
            return;
        }

        // housekeeping
        this.waitingForId.remove(subId);

        try {
            Object result = this.coder.decodeResponse(response);
            handler.getCallBack().callback(null, result);
        } catch (Exception e) {
            e.printStackTrace();
            handler.getCallBack().callback(e, null);
        }
    }

    private void onSocketMessageResult(JsonRpcResponse response) {
        logger.debug("handling response {}, {}", response, response.getId());

        WsStateAwaiting handler = this.handlers.get(response.getId());
        if (handler == null) {
            logger.error("Unable to find handler for id={}", response.getId());
            return;
        }

        try {
            Object result = this.coder.decodeResponse(response);

            // first send the result - in case of subs, we may have an update
            // immediately if we have some queued results already
            handler.callBack.callback(null, result);

            SubscriptionHandler subscription = handler.subscription;
            if (subscription != null) {
                String subId = subscription.getType() + "::" + result;
                this.subscriptions.put(subId, new WsStateSubscription(subscription.getCallBack(), subscription.getType(), handler.method, handler.params));

                // if we have a result waiting for this subscription already
                if (this.waitingForId.containsKey(subId)) {
                    this.onSocketMessageSubscribe(this.waitingForId.get(subId));
                }
            }

        } catch (Exception e) {
            handler.callBack.callback(e, null);
        }
        this.handlers.remove(response.getId());
    }

    private void sendQueue() {
        while (queued.peek() != null) {
            String head = queued.poll();
            try {
                this.webSocket.send(head);
            } catch (Exception e) {
                logger.error(" sendQueue error {}", head, e);
            }
        }
    }

    /**
     * @param method       The RPC methods to execute
     * @param params       Encoded paramaters as appliucable for the method
     * @param subscription Subscription details (internally used)
     * Send JSON data using WebSockets to configured HTTP Endpoint or queue.
     */
    @Override
    public Promise send(String method, List params, SubscriptionHandler subscription) {

        return new Promise((handler) -> {
            try {

                Types.JsonRpcRequest jsonRpcRequest = this.coder.encodeObject(method, params);
                String json = JSON.toJSONString(jsonRpcRequest);

                int id = jsonRpcRequest.getId();
                CallbackHandler callback = (err, result) -> {
                    if (err != null) {
                        handler.reject(err);
                    } else {
                        handler.resolve(result);
                    }
                };

                logger.debug("call {} {}, {}, {}, {}", id, method, params, json, subscription);

                this.handlers.put(id, new WsStateAwaiting(callback, method, params, subscription));
                if (this.isConnected() && this.webSocket != null) {
                    this.webSocket.send(json);
                } else {
                    //this.queued.set(id, json);
                    this.queued.add(json);
                }
            } catch (Exception e1) {
                handler.reject(e1);
            }

        });
    }

    private void resubscribe() {
        Map subscriptions = new HashMap<>(this.subscriptions);
        this.subscriptions.clear();

        for (WsStateSubscription subscription : subscriptions.values()) {

            // only re-create subscriptions which are not in author (only area where
            // transactions are created, i.e. submissions such as 'author_submitAndWatchExtrinsic'
            // are not included (and will not be re-broadcast)
            if (subscription.getType().startsWith("author_")) {
                continue;
            }

            try {
                Promise subscribe = this.subscribe(subscription.getType(), subscription.method, subscription.params, subscription.getCallBack());
                subscribe.then((String subscribeId) -> {
                    logger.info(" resubscribe {}", subscribeId);
                    return null;
                });
            } catch (Exception e) {
                logger.error("resubscribe error {}", subscription, e);
            }
        }

    }


    /**
     * `true` when this provider supports subscriptions
     */
    @Override
    public boolean isHasSubscriptions() {
        return true;
    }

    /**
     * Returns a clone of the object
     */
    @Override
    public IProvider clone() {
        return new WsProvider(this.endpoint);
    }

    /**
     * Manually disconnect from the connection, clearing autoconnect logic
     */
    @Override
    public void disconnect() {
        if (this.webSocket == null) {
            throw new RuntimeException("Cannot disconnect on a non-open websocket");
        }
        // switch off autoConnect, we are in manual mode now
        this.autoConnect = false;
        // 1000 - Normal closure; the connection successfully completed
        this.webSocket.close(1000);
        this.webSocket = null;
    }


    /**
     * @return true if connected
     * Whether the node is connected or not.
     */
    @Override
    public boolean isConnected() {
        return this.isConnected;
    }

    /**
     * @param {ProviderInterface$Emitted} type Event
     * @param {ProviderInterface$EmitCb}  sub  Callback
     * Listens on events after having subscribed using the subscribe function.
     */
    @Override
    public void on(ProviderInterfaceEmitted emitted, EventEmitter.EventListener cb) {
        this.eventemitter.on(emitted, cb);
    }

    /**
     * @param {string}                     type     Subscription type
     * @param {string}                     method   Subscription method
     * @param {Array}                 params   Parameters
     * @param {ProviderInterface$Callback} callback Callback
     * @return {Promise}                     Promise resolving to the dd of the subscription you can use with unsubscribe.
     * subscribe
     * Allows subscribing to a specific event.
     * **Example**
     * ```java
     * WsProviderDir provider = new WsProviderDir("ws://127.0.0.1:9944");
     * Rpc rpc = new Rpc(provider);
     * rpc.state.subscribeStorage(storage.balances.freeBalance, 
, (_, values) => { * System.out.println(values) * }).then((subscriptionId) => { * System.out.print("balance changes subscription id: ") * System.out.println(subscriptionId) * }) * ``` */ @Override public Promise subscribe(String type, String method, List params, CallbackHandler cb) { return this.send(method, params, new SubscriptionHandler(cb, type)); } /** * Allows unsubscribing to subscriptions made with subscribe. */ @Override public Promise unsubscribe(String type, String method, int id) { String subscription = type + "::" + id; // FIXME This now could happen with re-subscriptions. The issue is that with a re-sub // the assigned id now does not match what the API user originally received. It has // a slight complication in solving - since we cannot rely on the send id, but rather // need to find the actual subscription id to map it if (this.subscriptions.get(subscription) == null) { logger.info("Unable to find active subscription={}", subscription); return Promise.reject(new RuntimeException("Unable to find active subscription=" + subscription)); } this.subscriptions.remove(subscription); //TODO 2019-05-09 21:37 return this.send(method, Lists.newArrayList(id), null); } public void setConnected(boolean connected) { isConnected = connected; } public boolean isAutoConnect() { return autoConnect; } public void setAutoConnect(boolean autoConnect) { this.autoConnect = autoConnect; } public RpcCoder getCoder() { return coder; } public void setCoder(RpcCoder coder) { this.coder = coder; } public String getEndpoint() { return endpoint; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } public Queue getQueued() { return queued; } public void setQueued(LinkedList queued) { this.queued = queued; } public Map getWaitingForId() { return waitingForId; } public void setWaitingForId(Map waitingForId) { this.waitingForId = waitingForId; } public WebSocket getWebSocket() { return webSocket; } public void setWebSocket(WebSocketClient webSocket) { this.webSocket = webSocket; } }