soroban-client-nodejs
Version:
A Soroban RPC client for nodejs
165 lines (138 loc) • 4.35 kB
JavaScript
import {fetch} from 'undici'
import {socksDispatcher} from './fetch-socks.js'
import {createHash} from 'crypto'
export class RPCTimeoutError extends Error {
constructor(message) {
super(message)
this.name = "RPCTimeoutError"
}
}
export class RPCCallError extends Error {
constructor(message) {
super(message)
this.name = "RPCCallError"
}
}
export function encodeDirectory(name) {
if (name == undefined || name == null || name == '')
throw Error('encodeDirectory Invalid name')
const utf8Name = Buffer.from(name, 'utf-8').toString()
const encoded = createHash('sha256').update(utf8Name).digest('hex')
return encoded
}
function delay(milliseconds){
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
})
}
export class RPC {
/**
* Constructor
* @constructor
* @param {string | null} url
* @param {string | null | undefined} socks5Proxy
* @param {int | null | undefined} timeout
*/
constructor(url, socks5Proxy, timeout=15000) {
this.url = url
this.socksProxyAgent = null
this.timeout = timeout
if (socks5Proxy != null) {
const urlObj = new URL(socks5Proxy)
this.socksProxyAgent = socksDispatcher({
type: 5,
host: urlObj.hostname,
port: parseInt(urlObj.port)
})
}
}
/**
* Send a POST request to the RPC API of a Soroban node
* @param {string} method
* @param {*} args
* @returns
*/
async _call(method, args) {
try {
const payload = {
'method': method,
'params': [args],
'jsonrpc': '2.0',
'id': 1
}
const parameters = {
'method': 'POST',
'timeout': this.timeout,
'headers': {
'content-type': 'application/json',
'User-Agent': 'HotJava/1.1.2 FCS'
},
'body': JSON.stringify(payload)
}
// Sets socks proxy agent if required
if (this.socksProxyAgent != null) {
parameters.dispatcher = this.socksProxyAgent
}
const response = await fetch(this.url, parameters)
if (!response.ok)
throw new Error(`Received invalid status from RPC API: ${response.status}`)
const responseObj = await response.json()
return Object.hasOwn(responseObj, 'result') ? responseObj.result : null
} catch (e) {
throw new RPCCallError(`RPC error: ${method}: ${e.cause}`)
}
}
/**
* Retrieve list of entries stored in Directory for a given key
* @param {string} name
*/
async directoryList(name) {
const resp = await this._call('directory.List', {'Name': name, 'Entries': []})
return (resp != null && Object.hasOwn(resp, 'Entries')) ? resp.Entries : []
}
/**
* Store a new entry associated to a given key in the directory
* @param {*} name
* @param {*} entry
* @param {*} mode
*/
async directoryAdd(name, entry, mode='default') {
const resp = await this._call('directory.Add', {'Name': name, 'Entry': entry, 'Mode': mode})
return !(resp == null || !Object.hasOwn(resp, 'Status') || resp.Status != 'success')
}
/**
* Remove an entry from the Directory
* @param {*} name
* @param {*} entry
*/
async directoryRemove(name, entry) {
const resp = await this._call('directory.Remove', {'Name': name, 'Entry': entry})
return !(resp == null || !Object.hasOwn(resp, 'Status') || resp.Status != 'success')
}
/**
* Wait for an entry associated to a given key
* @param {*} directory
* @param {*} count
*/
async waitAndRemove(directory, count=25) {
let values = [],
total = count
count = 0
while (count < total) {
values = await this.directoryList(directory)
count += 1
if (values.length > 0 || count >= total) {
break
}
// Wait for next list
await delay(200)
}
if (count >= total) {
throw new RPCTimeoutError(`Wait on ${directory}`)
}
// Consider last entry
const value = values[values.length - 1]
await this.directoryRemove(directory, value)
return value
}
}