archethic
Version:
Archethic Javascript SDK
408 lines (363 loc) • 11.5 kB
JavaScript
import { JSONRPCClient, JSONRPCServer, JSONRPCServerAndClient } from "json-rpc-2.0";
import { AccountIdentity } from "./model/account.js";
export class ConnectionState {
static Closed = 'WalletRPCConnection_closed'
static Closing = 'WalletRPCConnection_closing'
static Connecting = 'WalletRPCConnection_connecting'
static Open = 'WalletRPCConnection_open'
}
export class RpcErrorCode {
static UnsupportedMethod = -32601
static Timeout = 5001
static Connectivity = 4901
static ConsensusNotReached = 5002
static InvalidParams = -32602
static InvalidTransaction = 5003
static InvalidConfirmation = 5006
static InsufficientFunds = 5004
static ServiceNotFound = 5007
static UserRejected = 4001
static UnknownAccount = 5005
static Other = 5000
}
export class RpcSubscription {
/**
* @param {String} id
* @param {EventListenerOrEventListenerObject} eventListener
*/
constructor(id, eventListener) {
this.id = id
this.eventListener = eventListener
}
}
export class RpcNotification {
/**
* @param {String} subscriptionId
* @param {Object} data
*/
constructor(subscriptionId, data) {
this.subscriptionId = subscriptionId
this.data = data
}
}
export class RpcRequestOrigin {
/**
* @param {String} name
* @param {String | undefined} url
* @param {String | undefined} logo
*/
constructor(name, url, logo) {
this.name = name
this.url = url
this.logo = logo
}
}
export class RpcRequest {
/**
* @param {RpcRequestOrigin} origin Application emitting the request
* @param {Object} payload Request payload
* @param {number} version Wallet Rpc protocol version
*/
constructor(origin, payload = {}, version = 1) {
this.origin = origin
this.version = version
this.payload = payload
}
}
export class ArchethicRPCClient {
constructor() {
/** @type {RpcRequestOrigin} origin */
this.origin = new RpcRequestOrigin('')
/** @type {JSONRPCServerAndClient | undefined} client */
this.client;
/** @type {WebSocket | undefined} websocket */
this.websocket;
/** @type {EventTarget} _connectionStateEventTarget */
this._connectionStateEventTarget = new EventTarget()
/** @type {EventTarget} _rpcNotificationEventTarget */
this._rpcNotificationEventTarget = new EventTarget()
}
/** @type {ArchethicRPCClient} */
static _instance;
/**
* @return {ArchethicRPCClient}
*/
static get instance() {
if (this._instance === undefined) {
this._instance = new ArchethicRPCClient()
}
return this._instance
}
/**
* @param {RpcRequestOrigin} origin Identifying data about the client application.
*/
setOrigin(origin) {
this.origin = origin
return this
}
_dispatchConnectionState() {
this._connectionStateEventTarget.dispatchEvent(new Event(this.connectionState))
}
/**
*
* @param {string} host
* @param {number} port
* @returns {Promise<void>}
*/
async connect(host, port) {
return new Promise((resolve, reject) => {
if (this.connectionState != ConnectionState.Closed) {
return reject(new Error("Connection already established. Cancelling new connection request"))
}
this.websocket = new WebSocket(`ws://${host}:${port}`)
this._dispatchConnectionState()
this.client = new JSONRPCServerAndClient(
new JSONRPCServer(),
new JSONRPCClient((request) => {
try {
this.websocket?.send(JSON.stringify(request))
return Promise.resolve()
} catch (error) {
return Promise.reject(error)
}
})
)
this.client.addMethod(
'addSubscriptionNotification',
(request) => {
const notification = new RpcNotification(
request['subscriptionId'],
request['data']
)
this._rpcNotificationEventTarget.dispatchEvent(new CustomEvent(
notification.subscriptionId,
{
detail: notification.data
}
))
}
)
this.websocket.onmessage = (event) => {
this.client?.receiveAndSend(JSON.parse(event.data.toString()))
}
// On close, make sure to reject all the pending requests to prevent hanging.
this.websocket.onclose = (event) => {
this.client?.rejectAllPendingRequests(
`Connection is closed (${event.reason}).`
)
this.close()
}
this.websocket.onopen = (_) => {
resolve();
this._dispatchConnectionState()
}
})
}
/**
* @return {Promise<void>}
*/
async close() {
this.websocket?.close()
this.client = undefined
this.websocket = undefined
this._dispatchConnectionState()
}
_ensuresConnectionAlive() {
if (this.client == null || this.connectionState != ConnectionState.Open) throw new Error('RPC connection must be alive.')
}
/**
* @return {ConnectionState}
*/
get connectionState() {
const state = this.websocket?.readyState
switch (state) {
case WebSocket.CLOSING:
return ConnectionState.Closing
case WebSocket.CONNECTING:
return ConnectionState.Connecting
case WebSocket.OPEN:
return ConnectionState.Open
}
return ConnectionState.Closed
}
/**
* @param {function(String)} listener
* @return {ArchethicRPCClient}
*/
onconnectionstatechange(listener) {
this._connectionStateEventTarget.addEventListener(ConnectionState.Connecting, () => { listener(ConnectionState.Connecting) })
this._connectionStateEventTarget.addEventListener(ConnectionState.Open, () => { listener(ConnectionState.Open) })
this._connectionStateEventTarget.addEventListener(ConnectionState.Closed, () => { listener(ConnectionState.Closed) })
return this
}
/**
* @return {ArchethicRPCClient}
*/
unsubscribeconnectionstatechange() {
this._connectionStateEventTarget.removeEventListener(ConnectionState.Connecting)
this._connectionStateEventTarget.removeEventListener(ConnectionState.Open)
this._connectionStateEventTarget.removeEventListener(ConnectionState.Closed)
return this
}
/**
* @return {Promise<String>} Keychain endpoint URL.
*/
async getEndpoint() {
this._ensuresConnectionAlive();
return this.client?.request(
'getEndpoint',
new RpcRequest(this.origin),
)
.then(
(value) => {
return value;
}
)
}
/**
* Signs and sends a Transaction in one of the Keychain services.
*
*
* @param {Object} transaction Object is a raw transaction. For example, a NFT creation transaction would look like this :
* ```
* {
* "type": "token",
* "version": 1,
* "data": {
* "content": "{ \"name\": \"NFT 001\", \"supply\": 100000000, \"type\": \"non-fungible\", \"symbol\": \"NFT1\", \"aeip\": [2], \"properties\": {}}",
* "code": "",
* "ownerships":[],
* "ledger": {
* "uco": {
* "transfers": []
* },
* "token": {
* "transfers": []
* }
* },
* "recipients": []
* }
* }
* ```
*
* @returns {Promise<TransactionSuccess>}
*/
async sendTransaction(transaction) {
this._ensuresConnectionAlive();
return this.client?.request(
'sendTransaction',
new RpcRequest(this.origin, transaction),
)
.then(
(result) => {
return result;
}
)
}
/**
* Gets the accounts list.
*
* @returns {Promise<Array<AccountIdentity>>}
*/
async getAccounts() {
this._ensuresConnectionAlive();
return this.client?.request(
'getAccounts',
new RpcRequest(this.origin),
).then(
(result) => {
return result['accounts'].map((account) => {
return Object.setPrototypeOf(
account,
AccountIdentity.prototype
)
});
}
)
}
/**
* Gets the currently selected account.
*
* @returns {Promise<AccountIdentity>}
*/
async getCurrentAccount() {
this._ensuresConnectionAlive();
return this.client?.request(
'getCurrentAccount',
new RpcRequest(this.origin),
).then(
(result) => {
return Object.setPrototypeOf(
result,
AccountIdentity.prototype
)
}
)
}
/**
* Starts listening to current account updates.
*
* @param {function(AccountUpdate)} listener
* @return {Promise<RpcSubscription>} created subscription
*/
onCurrentAccountChange(listener) {
return this._subscribe(
'subscribeCurrentAccount',
{},
listener
)
}
/**
* Starts listening to account updates.
*
* @param {string} accountName
* @param {function(AccountUpdate)} listener
* @return {Promise<RpcSubscription>} created subscription
*/
onAccountUpdate(accountName, listener) {
return this._subscribe(
'subscribeAccount',
{
name: accountName
},
listener
)
}
/**
* Starts listening to account updates.
*
* @param {string} method
* @param {string} data
* @param {function(AccountUpdate)} listener
* @return {Promise<RpcSubscription>} created subscription
*/
_subscribe(method, data, listener) {
this._ensuresConnectionAlive();
return this.client?.request(
method,
new RpcRequest(
this.origin,
data,
),
).then(
(result) => {
const subscription = new RpcSubscription(
result['subscriptionId'],
(event) => listener(event.detail)
)
this._rpcNotificationEventTarget.addEventListener(
subscription.id,
subscription.eventListener
)
return subscription
}
)
}
/**
* Stops listening to a subscription
* @param {RpcSubscription} subscription
*/
unsubscribe(subscription) {
this._rpcNotificationEventTarget.removeEventListener(subscription.id, subscription.eventListener)
}
}