Space Operator
  • Welcome
  • Visual Builder
    • Flows
      • API Key & POST Request
      • WebSocket
        • Signature Requests
      • Nested Flows
      • Learn Solana
      • Add flows to your websites
      • Iterate with Localhost
    • Nodes
      • Web Assembly Nodes
        • space-cli
        • space-lib
        • Examples in Rust
          • Rectangle
          • Filter
          • Regex
        • Uploading WASM binary via UI
      • Native Nodes
        • Code Template
        • Node Definition
        • ValueSet
        • Submitting Native Nodes
        • Tracing
      • Mock Nodes
      • JS Node
      • API Input Node
  • Self-hosting
    • Docker Compose
    • Serving HTTPS
    • Lightsail Instance
    • Export data to your instance
  • FAQ
    • Servers
  • References
    • Flows
    • Flow Deployment
Powered by GitBook
On this page
  • Documentation
  • Code requirements
  • Using BaseCommand class
  • Using libraries
  • Example node - node that performs a simple addition
  • Example with a signature request from a wallet
  • Packages
  • How it works

Was this helpful?

  1. Visual Builder
  2. Nodes

JS Node

Run and edit TypeScript code on-the-fly with Deno runtime.

PreviousMock NodesNextAPI Input Node

Last updated 1 year ago

Was this helpful?

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 , 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:

  • params: Record<string, any> : Input values, this is a map of input_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

// 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";

Example node - node that performs a simple addition

Source code template:

Use params to access input values.

import { Context, CommandTrait } from "jsr:@space-operator/flow-lib@0.5.0";

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.

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/deno-command-rpc@0.5.0";
import UserCommand from "./__cmd.ts";

start(new UserCommand(), { hostname: "127.0.0.1", port: 0 });

ctx: Context: context object contains information about the current invocation, as well as services available to the node. See .

We do not support yet, therefore your code must use npm: or jsr: when importing libraries.

Read more:

Access Solana client and requestSignature method through the Context.

@space-operator/flow-lib
@space-operator/flow-lib
documentation
import maps
npm: specifier
https://jsr.io/@space-operator/flow-lib/doc/~/Context
https://jsr.io/@space-operator/flow-libjsr.io
https://jsr.io/@space-operator/deno-command-rpcjsr.io