Client-side JavaScript


Overview

This API provides to client to add some JavaScript code to contracts definition. Execution of this script may perform some useful actions, e.g. print specific contract info or create new revision.

Add and execute JavaScript

You can add JS code to contract with Java methods Definition#setJS(byte[], String, JSApiScriptParameters) or State#setJS(byte[], String, JSApiScriptParameters). One contract may contains many JS scripts. setJS method add js-file to scripts list as attach. Also, it can be .zip compressed file. In this case .zip file should contains single .js file with script text, and scriptParameters.isCompressed should be set to true. Execute any attached script with Contract#execJS(JSApiExecOptions, byte[], String...) - it will try to run jsApiEvents.main() function. Use new JSApiExecOptions() for default options. The second parameter byte[] should receive content of attached script file (whether compressed or not).

ScriptPermissions

Each attached JS has its own permissions list. You can change this permissions with JSApiScriptParameters#setPermission(ScriptPermissions permission, Boolean state) while call setJS. ScriptPermissions is enum of string values:

  • shared_folders
  • shared_storage
  • origin_storage
  • revision_storage
  • http_client

Execution time limit

You can limit time of JavaScript code execution with scriptParameters.timeLimitMillis, in call of method setJS.

Javascript parameters

All string parameters of Contract#execJS(JSApiExecOptions, byte[], String...) method will be passed into javascript as global var jsApiParams. On JS, you can get count of parameters with jsApiParams.length (here will be 0 if we have no parameters), and iterate over it with jsApiParams[i].

Event handlers

JS code can handle some types of external events. Now there are main event and handlers for http server. Any event handler should be declared as method of jsApiEvents object.

var jsApiEvents = new Object();
jsApiEvents.main = function() {
  return 'hello world';
};
jsApiEvents.httpHandler_index = function(request, response) {
  response.setBodyAsPlainText('index page');
};

Returning values

JS code can return any values to client, as result value from main event. (If js code don't have main event, you can put result values into js global var result, but this variant is deprecated.) Also, you can return from js array of values and read them on java with ScriptObjectMirror, e.g.:

var jsApiEvents = new Object();
jsApiEvents.main = function() {
  return ['22', '33'];
};

and then on java:

ScriptObjectMirror res = (ScriptObjectMirror) contract.execJS();
System.out.println("res[0]: " + res.get("0"));
System.out.println("res[1]: " + res.get("1"));

Http masks

JSApiScriptParameters has two fields domainMasks and ipMasks. This masks allows to access remote urls/IPs with JSApiHttpClient class (it can be accessed in JS by call jsApi.getHttpClient()).

domainMasks examples:

  • universa.io - allows domain universa.io, without subdomains
  • sub.universa.io - allows domain sub.universa.io, without universa.io
  • *.universa.io - allows any subdomains, and universa.io too
  • universa.io:80 - allows only http port for universa.io
  • universa.io:443 - allows only https port for universa.io
  • universa.io:8080 - allows only custom port 8080 for universa.io
  • universa.io:* - allows any port for universa.io
  • universa.io - allows http(80) and https(443) ports for universa.io by default

ipMasks examples:

  • 192.168.33.44 - allows one ip address
  • 192.168.33.* - allows ip addresses from 192.168.33.0 to 192.168.33.255
  • ports are the same as in domainMasks

JSApi

While executing, javascript receives instance of JSApi into global var jsApi. This var provides to js set of useful methods:

JSApiContract getCurrentContract()

  • return instance of JSApiContract that represents current contract

JSApiRoleBuilder getRoleBuilder()

  • return role builder object, used to create simple, list or link roles

JSApiPermissionBuilder getPermissionBuilder()

  • return permission builder object, used to create any type of permissions

JSApiSharedFolders getSharedFolders()

  • return accessor to shared folders. List of shared folders can be passed to script through JSApiExecOptions parameter of execJS method

JSApiSharedStorage getSharedStorage()

  • return accessor to clients local shared storage, available for all contracts

JSApiOriginStorage getOriginStorage()

  • return accessor to clients local shared storage, available for contracts with same origin as currentContract.origin

JSApiRevisionStorage getRevisionStorage()

  • return accessor to clients local shared storage, available for current revision of currentContract

JSApiHttpClient getHttpClient

  • return http client, need permission http_client

PublicKey bin2publicKey(byte[] binary)

  • return unpacked PublicKey from given binary

PublicKey base64toPublicKey(String s)

  • return unpacked PublicKey from given base64 string, that contains packed binary

byte[] string2bin(String s)

  • return binary representation for given string

String bin2base64(byte[] bin)

  • return base64-string representation for given binary

byte[] base64toBin(String b64)

  • return binary data decoded from base64 string

JSApiContract

Instance of this class may represent some contract. It provides access to contract data with set of methods:

String getId()

  • return hashId of contract, as base64 string

int getRevision()

  • return revision number, as int

String getOrigin()

  • return hashId of origin contract, as base64 string

String getParent()

  • return hashId of parent contract, as base64 string

long getCreatedAt()

  • return contract creation time, as unix timestamp

String getStateDataField(String fieldPath)

  • fieldPath path.to.field
  • return corresponding value from data section of contract state, as String

void setStateDataField(String fieldPath, String value)

  • fieldPath path.to.field
  • value String value to set

void setStateDataField(String fieldPath, int value)

  • fieldPath path.to.field
  • value int value to set

String getDefinitionDataField(String fieldPath)

  • fieldPath path.to.field
  • return corresponding value from data section of contract definition, as String

String getTransactionalDataField(String fieldPath)

  • fieldPath path.to.field
  • return corresponding value from data section of contract state, as String

void setTransactionalDataField(String fieldPath, String value)

  • fieldPath path.to.field
  • value String value to set

JSApiRole getIssuer()

  • return instance of JSApiRole for issuer

JSApiRole getOwner()

  • return instance of JSApiRole for owner

JSApiRole getCreator()

  • return instance of JSApiRole for creator

void setOwner(List<String> addresses)

  • addresses array of addresses to set for owner role

void registerRole(JSApiRole role)

  • role instance of JSApiRole that will be registered. Name must be unique otherwise existing role will be overwritten

boolean isPermitted(String permissionName, PublicKey... keys)

  • permissionName type of permission to check for
  • keys set of keys to check with
  • return permission allowed for keys is found

JSApiContract createRevision()

  • return new revision of current contract, as instance of JSApi_contract

void addPermission(JSApiPermission permission)

  • permission instance of JSApiPermission that will be added

void addReference(JSApiReference reference)

  • reference instance of JSApiReference that will be added, it can be created through JSApiReferenceBuilder

JSApiRoleBuilder

This object is used to create any type of roles. Methods:

JSApiSimpleRole createSimpleRole(String name, String... addresses)

  • name is role name
  • addresses set of addresses for which role will be created
  • return new role, represented by class JSApiSimpleRole

JSApiListRole createListRole(String name, String mode, JSApiRole... roles)

  • name is role name
  • mode is mode of sub-roles combining: "all", "any" or "quorum"
  • addresses set of addresses for which role will be created
  • return new role, represented by class JSApiListRole

JSApiRoleLink createRoleLink(String newRoleName, String existingRoleName)

  • return new role link, represented by class JSApiRoleLink

JSApiPermissionBuilder

This object is used to create any type of permissions. Methods:

JSApiSplitJoinPermission createSplitJoinPermission(JSApiRole role, ScriptObjectMirror params)

  • role allows to permission
  • params is parameters of permission: fieldname, minvalue, minunit, joinmatch_fields
  • return new permission, represented by JSApiSplitJoinPermission class

JSApiChangeNumberPermission createChangeNumberPermission(JSApiRole role, ScriptObjectMirror params)

  • role allows to permission
  • params is parameters of permission: fieldname, range (minvalue, maxvalue) and delta (minstep, max_step)
  • return new permission, represented by JSApiChangeNumberPermission class

JSApiChangeOwnerPermission createChangeOwnerPermission(JSApiRole role)

  • role allows to permission
  • return new permission, represented by JSApiChangeOwnerPermission class

JSApiModifyDataPermission createModifyDataPermission(JSApiRole role, ScriptObjectMirror params)

  • role allows to permission
  • params is parameters of permission: fields is map of field names and lists of allowed values
  • return new permission, represented by JSApiModifyDataPermission class

JSApiRevokePermission createRevokePermission(JSApiRole role)

  • role allows to permission
  • return new permission, represented by JSApiRevokePermission class

JSApiReferenceBuilder

This object is used to create references. Methods:

JSApiReference createReference(String type)

  • type "TRANSACTIONAL", "EXISTING_DEFINITION" or "EXISTING_STATE"
  • return new reference as JSApiReference instance

JSApiReference

This class is used for manage reference conditions.

void setConditions(ScriptObjectMirror conditions)

conditions e.g. {'all_of':['ref.issuer==ZT4tGcT821Lzv4bx2zBZ6AdmFAod5Feu44UCM9J2ZiiGRj1qyo']}, see References conditions for details

JSApiSharedFolders

This object is used for access to user's shared folders. List of shared folders can be passed to script through JSApiExecOptions parameter of execJS method.

byte[] readAllBytes(String fileName)

This method searches file in all shared folders, returns full file contents.

  • fileName is string containing target file name with relative path or without path
  • return byte array with contents of target file

public void writeNewFile(String fileName, byte[] data)

This method creates new file in first shared folder from execOptions.sharedFolders list. If file already exists - throws an exception.

  • fileName is string containing target file name with relative path or without path
  • data array of bytes that will be placed into new created file

public void rewriteExistingFile(String fileName, byte[] data)

This method searches file in all shared folders. If file was found, it's contents will be replaced.

  • fileName is string containing target file name with relative path or without path
  • data array of bytes that will be placed into target file

JSApiSharedStorage

This object is used for access to clients local shared storage, available for all contracts. Local path is ~/.universaStorage/shared/

byte[] readAllBytes(String fileName)

This method searches file in common shared folder, returns full file contents.

  • fileName is string containing target file name with relative path or without path
  • return byte array with contents of target file

void writeNewFile(String fileName, byte[] data)

This method creates new file in common shared folder. If file already exists - throws an exception.

  • fileName is string containing target file name with relative path or without path
  • data array of bytes that will be placed into new created file

void rewriteExistingFile(String fileName, byte[] data)

This method searches file in common shared folder. If file was found, it's contents will be replaced.

  • fileName is string containing target file name with relative path or without path
  • data array of bytes that will be placed into target file

JSApiOriginStorage

This object is used for access to clients local shared storage, available for contracts with same origin as currentContract.origin. Local path is ~/.universaStorage/origin/<origin>/.

byte[] readAllBytes(String fileName)

This method searches file in origin shared folder, returns full file contents.

  • fileName is string containing target file name with relative path or without path
  • return byte array with contents of target file

void writeNewFile(String fileName, byte[] data)

This method creates new file in origin shared folder. If file already exists - throws an exception.

  • fileName is string containing target file name with relative path or without path
  • data array of bytes that will be placed into new created file

void rewriteExistingFile(String fileName, byte[] data)

This method searches file in origin shared folder. If file was found, it's contents will be replaced.

  • fileName is string containing target file name with relative path or without path
  • data array of bytes that will be placed into target file

JSApiRevisionStorage

This object is used for access to clients local shared storage, available for current revision of currentContract. Also, provides read-only access to parent's revision shared folder. Local path is ~/.universaStorage/revision/<id>/.

byte[] readAllBytes(String fileName)

This method searches file in revision shared folder, returns full file contents.

  • fileName is string containing target file name with relative path or without path
  • return byte array with contents of target file

byte[] readAllBytesFromParent(String fileName)

This method searches file in parent's revision shared folder, returns full file contents.

  • fileName is string containing target file name with relative path or without path
  • return byte array with contents of target file

void writeNewFile(String fileName, byte[] data)

This method creates new file in revision shared folder. If file already exists - throws an exception.

  • fileName is string containing target file name with relative path or without path
  • data array of bytes that will be placed into new created file

void rewriteExistingFile(String fileName, byte[] data)

This method searches file in revision shared folder. If file was found, it's contents will be replaced.

  • fileName is string containing target file name with relative path or without path
  • data array of bytes that will be placed into target file

JSApiHttpClient

This object provides methods for making http requests. Target urls (or ip-addresses) should be allowed by domainMasks and/or ipMasks parameters in JSApiScriptParameters.

List sendGetRequest(String strUrl, String respType)

Sends GET request to strUrl.

  • strUrl is target url of ip address, may contains :port
  • respType one of following: text for plain text, json for DOM object or bin for binary file
  • return array of two fields, first is resp_code from server (int), second is response of type corresponding to respType

sendPostRequest(String strUrl, String respType, Map params, String contentType)

Sends POST request to strUrl.

  • strUrl is target url of ip address, may contains :port
  • respType one of following: text for plain text, json for DOM object or bin for binary file
  • params is dict with key-value parameters
  • contentType is format, in which parameters will be encoded. form for application/x-www-form-urlencoded or json for application/json
  • return array of two fields, first is resp_code from server (int), second is response of type corresponding to respType

sendPostRequestMultipart(String strUrl, String respType, Map formParams, Map files)

  • strUrl is target url of ip address, may contains :port
  • respType one of following: text for plain text, json for DOM object or bin for binary file
  • formParams is dict with key-value parameters, that will be sent to remote as text
  • files is dict of key-value parameters, that will be sent to remote as binary data
  • return array of two fields, first is resp_code from server (int), second is response of type corresponding to respType

Http server

Contract with attached javascript can implement local http server. In other words, you can implement some http handlers on javascript, attach this javascript to Contract, and run it with uniclient command --start-http-server. All http contracts should be approved, so you need to register it first.

Http endpoint handlers

For handling endpoint, you need to add event handler into jsApiEvents with any name and two parameters request (JSApiHttpRequest) and response (JSApiHttpResponse). Then, bind endpoint's path and your handler in routes-file.

JSApiHttpRequest

JSApi can handle application/x-www-form-urlencoded, application/json and multipart/form-data types of requests. In any case, all request parameters will put in Map collection corresponding to request's content type, that can be accessed with getParams method.

Map getParams()

  • return parsed key-values request parameters. For uploaded file, here will be key=filename and velue=binary with contents of file.

JSApiHttpResponse

Is used for setting response to client in http handlers.

void setResponseCode(int code)

  • code http response codes, like 200 (by default), or 404 or something else

void setBodyAsPlainText(String answer)

  • answer String with plain text, that will be return to client

void setBodyAsJson(ScriptObjectMirror bodyAsJson)

  • bodyAsJson js-object with answer structure, e.g. inline object like {value: 42}

void setBodyAsFileBinary(byte[] bodyAsBinary)

  • bodyAsBinary binary data that will be return to client as application/octet-stream

Start server, routes-file

You can start http server with uniclient --start-http-server /path/to/routes-file.json. In routes file you should specify listen port and all endpoints, like:

{
  "listenPort": "8880",
  "routes": [
    {"endpoint": "/endpoint1", "handlerName": "httpHandler_endpoint1", "contractPath": "/tmp/contract1.tp", "scriptName": "script1.js"},
    {"endpoint": "/endpoint2", "handlerName": "httpHandler_endpoint2", "contractPath": "/tmp/contract1.tp", "scriptName": "script2.js", "jsApiParams": ["param1", "param2", "param3"]},
    {"endpoint": "/endpoint3", "handlerName": "httpHandler_endpoint3", "scriptName": "script3.js", "slotId": "IEdqdKCzieGoMweJM8zsmr6fnnPSzD7u8poQkgeUXOQLzPz5scSwYlDEqlZWIU0bTsR1FRLG9+tVprjB28feOeg5GNZGFEbUV4F/5+kQXtqYGtMfQn0QWKvABd9GsUfO", "originId": "xSwLq3g5X2/Cqq5cVECONYAy6TlzxZqVGigKqlhC2hg4zaDqcuT/DRAKRXYy6RTp6itpASWYqbeXw5/6DPTB7wTmt3rCgvJzf7Tb9A23JGdFebu45CGdUODDFhgWA2bh"}
  ]
}

Each endpoint has parameters:

  • endpoint path that will be handled
  • handlerName name of handler method, that will be searched in jsApiEvents object
  • scriptName name of script, attached to contract. Each contract can have many java scripts attached, so we need to select js by it's name
  • contractPath path to contract file in local filesystem, or remote url
  • slotId if you want to automatically download latest contract from slot1, specify slotId+originId
  • originId if you want to automatically download latest contract from slot1, specify slotId+originId
  • jsApiParams put here array of strings, that will be passed to script's jsApiParams object

Http server and SLOT1

If you have save you contract with javascript endpoints in slot1, uniclient will automatically check and download latest revision of it (period of checking is 600 seconds). For this, you should specify your endpoint contract (in routes.json file) with slotId+originId. Also, in this case you can ommit contractPath parameter, your contract will be downloaded from slot1 immidiately after start http server.

Important: for correct work slot1 with http server, it's need to save script file body in contract binary. For this, use setJS method with flag putContentIntoContract=true

Http script example

Javascript, attached to contract:

var jsApiEvents = new Object();
jsApiEvents.httpHandler_getVersion = function(request, response){
  response.setBodyAsJson({
    version: 1
  });
};

Attach it to contract with setJS (putContentIntoContract=true), set jsFileName=script1.js. Don't forget to add ModifyDataPermission to field scripts if you want to change your javacript implementation later. Register in Universa network and save in SLOT1. Now you have to get originId of your contract and slotId of slot1 storage contract.

Next, create ~/routes.json file with content:

{
  "listenPort": "8880",
  "routes": [
    {"endpoint": "/contract1/getVersion", "handlerName": "httpHandler_getVersion", "scriptName": "script1.js", "slotId": "<your_slotId>", "originId": "<your_originId>"}
  ]
}

Now, start http server with command java -jar uniclient.jar --start-http-server ~/routes.json. Your endpoint will be available on http://localhost/contract1/getVersion

And last, you can create and register new revision of your contract with new javascript implementation. It will automatically updated in 5-20 minutes. Restart http server manually for immediately update.

Examples

Hello world

JS code:

print('Hello World');

Java code:

Contract contract = new Contract();
contract.setJS(js); // js with code above
contract.execJS();

JS code:

print('owner: ' + jsApi.getCurrentContract().getOwner());
print('revision: ' + jsApi.getCurrentContract().getRevision());

Java code:

Contract contract = new Contract(myPrivateKey);
contract.setJS(js); // js with code above
contract.execJS();

Create new revision with incremented value

JS code:

rev = jsApi.getCurrentContract().createRevision();
var oldValue = parseInt(rev.getStateDataField('test_value'));
var newValue = (oldValue + 1) >> 0; // '>> 0' converts js-number to int
rev.setStateDataField('test_value', newValue);
result = rev

Java code:

Contract contract = new Contract(TestKeys.privateKey(1));
Binder permParams = new Binder();
permParams.set("min_value", 1);
permParams.set("min_step", 1);
permParams.set("max_step", 1);
permParams.set("field_name", "test_value");
ChangeNumberPermission perm = new ChangeNumberPermission(contract.getOwner(), permParams);
contract.addPermission(perm);
contract.getStateData().set("test_value", 11);
contract.setJS(js); // js with code above
contract.seal();
contract = Contract.extractContractFromJs((Contract.JSApi_contract) contract.execJS());
System.out.println("revision: " + contract.getRevision());
System.out.println("test_value: " + contract.getStateData().getIntOrThrow("test_value"));