JS Node
Run and edit TypeScript code on-the-fly with Deno runtime.
Add a node
Upload your node with "Add Deno node" button in /dashboard/nodes page, need 2 files:
Node definition (.json)
Source code
Documentation
Code requirements
Your code MUST export a default class that implements CommandTrait
. The class name doesn't matter.
CommandTrait
is defined in @space-operator/flow-lib, it has 3 methods, one is required:
export interface CommandTrait {
/**
* Deserialize each inputs from `Value` to the type of your choice.
* This function will be called before passing inputs to `run()`.
* If not implemented, `Value.toJSObject()` will be called for each inputs.
*/
deserializeInputs?(inputs: Record<string, Value>): Record<string, any>;
/**
* Serialize each output to a `Value`.
* This function will be called after each `run()`.
* If not implemented, `new Value(output)` will be called for each outputs.
*/
serializeOutputs?(outputs: Record<string, any>): Record<string, Value>;
/**
* This function will be called every time the command is run.
* @param ctx Context
* @param params Map of input_name => input_value.
*/
run(ctx: Context, params: Record<string, any>): Promise<Record<string, any>>;
}
run()
will be called every time the node is run.
Input parameters:
ctx: Context
: context object contains information about the current invocation, as well as services available to the node. See documentation.params: Record<string, any>
: Input values, this is a map ofinput_name => value
Return type: run()
should return a map of output_name => output_value
on success, and throw an exception on errors.
Using BaseCommand class
We also provide a BaseCommand
class that will convert input and output values based on node definition. All you have to do is extends
it:
import * as lib from "jsr:@space-operator/flow-lib";
class MyCommand extends lib.BaseCommand {
async run(
_: Context,
params: Record<string, any>
): Promise<Record<string, any>> {
return { c: params.a + params.b };
}
}
Using libraries
We do not support import maps yet, therefore your code must use npm:
or jsr:
when importing libraries.
// Import a NPM package
import * as web3 from "npm:@solana/web3.js";
// Import a JSR package
import * as lib from "jsr:@space-operator/flow-lib";
Read more: npm: specifier
Example node - node that performs a simple addition

Source code template:
Use params
to access input values.
import { Context, CommandTrait } from "jsr:@space-operator/[email protected]";
export default class MyCommand implements CommandTrait {
async run(
_: Context,
params: Record<string, any>
): Promise<Record<string, any>> {
return { c: params.a + params.b };
}
}
Node definition template
{
"type": "deno",
"data": {
"node_id": "deno_add",
"display_name": "Deno Add",
"description": "",
"node_definition_version": "0.1",
"unique_id": "",
"version": "0.1",
"tags": [],
"related_to": [
{
"id": "",
"type": "",
"relationship": ""
}
],
"resources": {
"source_code_url": "",
"documentation_url": ""
},
"usage": {
"license": "Apache-2.0",
"license_url": "",
"pricing": {
"currency": "USDC",
"purchase_price": 0,
"price_per_run": 0,
"custom": {
"unit": "monthly",
"value": "0"
}
}
},
"authors": [
{
"name": "Space Operator",
"contact": ""
}
],
"design": {
"width": 0,
"height": 0,
"icon_url": "",
"backgroundColorDark": "#000000",
"backgroundColor": "#fff"
},
"options": {}
},
"targets": [
{
"name": "a",
"type_bounds": [
"f64"
],
"required": true,
"passthrough": false,
"defaultValue": null,
"tooltip": ""
},
{
"name": "b",
"type_bounds": [
"f64"
],
"required": true,
"passthrough": false,
"defaultValue": null,
"tooltip": ""
}
],
"sources": [
{
"name": "c",
"type": "f64",
"optional": false,
"defaultValue": "",
"tooltip": ""
}
],
"targets_form.json_schema": {},
"targets_form.ui_schema": {}
}
Example with a signature request from a wallet
Option 1 - Bundled node
Bundling a node allows you to combine the instructions with other nodes to create a single transaction. All nodes in a flow will be bundled and the user will sign one for one transaction. The flow will build the message and submit the transaction.
import * as lib from "jsr:@space-operator/flow-lib";
import * as web3 from "npm:@solana/web3.js";
import { Instructions } from "jsr:@space-operator/flow-lib/context";
interface Inputs {
from: web3.PublicKey;
to: web3.PublicKey;
amount: number;
}
export default class TransferSol extends lib.BaseCommand {
async run(
ctx: lib.Context,
params: Inputs,
): Promise<Record<string, any>> {
const result = await ctx.execute(
new Instructions(
params.from,
[params.from],
[
web3.SystemProgram.transfer({
fromPubkey: params.from,
toPubkey: params.to,
lamports: params.amount,
}),
]
),
{}
);
return {
signature: result.signature!,
};
}
}
Note, when bundling a node, you must update the Node Definition to specify if and how a command would output Solana instructions, and the order with which it will return its outputs:
before
: list of output names returned before instructions are sent.signature
: name of the signature's output port.after
: list of output names returned after instructions are sent.
{
"data": {
...
"instruction_info": {
"before": [],
"signature": "signature",
"after": []
}
},
...
}
Option 2 - Plain TypeScript Node
You can manually build a message, request signature, and submit a transaction. The flow will not collect the instructions from this node and will execute it as an independent node.
Access Solana client and requestSignature
method through the Context
.
https://jsr.io/@space-operator/flow-lib/doc/~/Context

import * as lib from "jsr:@space-operator/flow-lib";
import * as web3 from "npm:@solana/web3.js";
import { encodeBase58 } from "jsr:@std/encoding@^0.220.1/base58";
interface Inputs {
from: web3.PublicKey;
to: web3.PublicKey;
amount: number;
}
export default class TransferSol extends lib.BaseCommand {
async run(
ctx: lib.Context,
params: Inputs,
): Promise<Record<string, any>> {
// build the message
const message = new web3.TransactionMessage({
payerKey: params.from,
recentBlockhash: (await ctx.solana.getLatestBlockhash()).blockhash,
instructions: [
web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1000 }),
web3.SystemProgram.transfer({
fromPubkey: params.from,
toPubkey: params.to,
lamports: params.amount,
}),
],
}).compileToLegacyMessage();
// request signature from user
const { signature, new_message } = await ctx.requestSignature(
params.from,
message.serialize()
);
// submit
const tx = web3.Transaction.populate(
new_message ? web3.Message.from(new_message) : message,
[encodeBase58(signature)]
);
return {
signature: await ctx.solana.sendRawTransaction(tx.serialize()),
};
}
}
Packages
How it works
When running the node, the backend will import your class and run it like this:
import { start } from "jsr:@space-operator/[email protected]";
import UserCommand from "./__cmd.ts";
start(new UserCommand(), { hostname: "127.0.0.1", port: 0 });
Last updated
Was this helpful?