npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@futureverse/sylo-protocol-sdk

v0.0.0-pre-SM-337.1

Published

The Sylo protocol sdk provides simple, and easy to use methods for interacting with the Seekers Node infrastructure. Using the sdk client allows applications to leverage the decentralized communication services provided by Seeker Nodes.

Downloads

25

Readme

Sylo Protocol SDK

The Sylo protocol sdk provides simple, and easy to use methods for interacting with the Seekers Node infrastructure. Using the sdk client allows applications to leverage the decentralized communication services provided by Seeker Nodes.

The SDK provides various APIs for relaying messages between Ethereum based accounts on The Root Network.

Install

NPM

npm install @futureverse/sylo-protocol-sdk

Yarn

yarn add @futureverse/sylo-protocol-sdk

Client Usage

Peer Key

An application first needs to create a Peer key for use with their client. A Peer key uniquely identifies a client within the network. Note: This key is for identifying a unique client. A user may be operating over multiple clients (e.g logged into multiple experiences simultaneously), however a user is uniquely identified by an ethereum account.

import * as E from "fp-ts/lib/Either";
import { PeerKey } from "@futureverse/sylo-protocol-sdk/lib/src/client";

export async function createPeerKey(): Promise<PeerKey> {
  const peerKey = await PeerKey.create();

  // serialize into bytes
  const serialized = await PeerKey.encode(peerKey);

  const deserializedR = await PeerKey.encode(serialized);
  if (E.isLeft(deserializedR)) {
    throw new Error(
      `failed to deserialize peer key: ${JSON.stringify(deserializedR.left)}`
    );
  }

  return peerKey;
}

After creating a peer key, it is recommended to persist the peer key in storage, and re-use the same peer key. This is because a client needs to authenticate itself using its peer key whenever it interacts with a new Seeker node. See Authentication for more details.

Creating the Client

The client exists to provides a simple to use interface for interacting with the Seeker network. It is responsible for discovering and managing connections to nodes, decoding and verification of messages, and correctly complying with the ticketing mechanisms of the protocol.

import * as ethers from "ethers";
import { Client, PeerKey } from "@futureverse/sylo-protocol-sdk/lib/src/client";
import { decodedOrThrow, Address } from "@futureverse/experience-sdk";
import { PORCINI_CONTRACTS } from "@futureverse/sylo-protocol-sdk/lib/src/ethers";

export async function createSDKClient(
  wallet: ethers.Wallet,
  provider: ethers.Provider
): Promise<Client> {
  const prefix = "authentication_prefix";
  const suffix = "authentication_suffix";

  const account = decodedOrThrow(Address.decode(wallet.address));

  const peerKey = await PeerKey.create();

  return Client.create({
    account,
    signMessageFn: wallet.signMessage.bind(wallet),
    peerKey,
    network: PORCINI_CONTRACTS,
    provider,
    authenticationOpts: { prefix, suffix },
  });
}

Client Options

  • account: This is the ethereum address that represents the user behind this client. It is a string that must comply with the ethereum address format, and the create method only accepts accounts of type Address. The utility method decodedOrThrow can be used convert a regular string into the Address type (will throw if passed an invalid address).

  • signMessageFn: Messages relayed through the network must be signed by the sender, and the corresponding ticket attached to the message must be signed by the receiver. Signatures within the sylo-protocol always follow the etherum personal_sign format. This option represents a function that takes in data and returns a signature of that data, where the signer is the account.

  • peerKey: The peer key which will be used to identify the client.

  • network: This value represents a set of contract addresses for the sylo-protocol. The contracts have been deployed to mainnet and the porcini testnet. The sdk provides constant values for these deployments (MAINNET or PORCINI).

  • provider: A provider type from the ethers library which is used to connect to and read from the sylo-protocol contracts. This type does not need to have a signer attached, as the client will not attempt to perform any transactions on the users behalf.

  • authenticationOpts: An object containing the prefix and suffix authentication values. See Authentication for more details.

Starting the client

Before being able to use the client to send and receive messages, the client must first be started.

await client.start();

Starting the client will ensure the client is ready to receive messages. This involves discovering and connecting to the user's designated node for this epoch. It will also begin listening for new relay messages.

The client can then be stopped by calling client.stop.

await client.stop();

Calling stop will close any underlying connections and message listeners.

Managing Deposits

Sending clients must pay Seeker Nodes to relay messages using SYLO tokens. Prior to being able to send messages through the network, the sending client must have Sylo tokens in the account's escrow and penalty. The escrow and penalty deposits are managed by a contract on-chain. When tickets are redeemed by a Seeker Node (a ticket is acquired by relaying messages), the ticket's value is deducted from the sending account's escrow. If there is insufficient escrow balance, then the balance is burned from the penalty instead.

There is a requirement that the sender's account must have at least enough escrow and penalty to pay for the relay of a single message, otherwise the Node will refuse to relay the message.

Connecting to the Ticketing contract

import * as ethers from "ethers";
import { PORCINI_CONTRACTS } from "@futureverse/sylo-protocol-sdk/lib/src/ethers";
import {
  SyloTicketing__factory,
  TicketingParameters__factory,
} from "@futureverse/sylo-protocol-sdk/lib/src/eth-types";

async function main() {
  const provider = new ethers.JsonRpcProvider(
    "wss://porcini.au.rootnet.app/ws"
  );

  // deposits are managed by the sylo ticketing contract
  const ticketing = SyloTicketing__factory.connect(
    PORCINI_CONTRACTS.syloTicketing,
    provider
  );

  // ticket values are stored in the ticketing params contract
  const ticketingParams = TicketingParameters__factory.connect(
    PORCINI_CONTRACTS.ticketingParameters,
    provider
  );
}

Reading the current ticketing parameters

// read the current ticket value of a single relay
const winningTicketCost = await ticketingParams.faceValue();

const ticketWinChance = await ticketingParams.baseLiveWinProb();

// the expected cost of a single relay can be calculated by
// multiplying the ticket face value by its win chance.
const singleRelayCost =
  (winningTicketCost * ticketWinChance) / (2n ** 128n - 1n); // 2^128-1 is the win chance denominator

Making deposits

// Depositing enough to pay for 10 winning tickets
await ticketing.depositEscrow(10n * winningTicketCost, senderAccount);
await ticketing.depositPenalty(winningTicketCost, senderAccount);

Relay

The relay api is used for sending 1 to 1 messages.

Sending

Parameters:

  • to: The recipient of the message.
  • message: A Uint8Array value representing the message contents. This will be passed to the signMessageFn.
const message = `hello world`;
const sendResult = await client.relay.send(
  decodedOrThrow(Address.decode("0x448c8e9e1816300Dd052e77D2A44c990A2807D15")),
  Buffer.from(message)
);

if (E.isLeft(sendResult)) {
  throw new Error(
    `Expected send result to be ok: ${JSON.stringify(sendResult.left)}`
  );
}

Calling client.relay.send will return an Either result, where a Left result indicates failure. The expected errors for relay.send are:

Protocol Error

Receiving

To receive new relay messages, call the client.relay.pull method.

const iterator = await receiverClient.relay.pull();

const relayMessageR = await iterator.next();

if (E.isLeft(relayMessageR.value)) {
  throw new Error(`Unexpected relay error: ${JSON.stringify(relayMessageR)}`);
}

const relayMessage = relayMessageR.value.right;

// hello world
console.log(Buffer.from(relayMessage.message).toString());

Calling relay.pull returns an iterator that emits values of type Either<DeliveryError, RelayMessage>.

type RelayMessage = {
  sender: Address;
  message: Uint8Array;
  ticket: Ticket; // This is the protocol ticket and is handled by the sdk
  node: Node;
};

If the iterator returns a value of DeliveryError, it means that a new message was detected, but it failed to be processed for one of the following reasons:

FailedToDecodeReceiverTicketSignature - Failed to deserialize the signature object sent by the sender of this message.

FailedToDecodeDelegatedReceiverAddress - Failed to deserialize the delegated receiver object specified by the sender.

Decoding failures tend to occur if the sending/receiving clients are using incompatible versions of the protocol sdk.

PubSub

The pubsub api is used to broadcast messages to receivers who are subscribed to a particular topic.

Publishing

Parameters:

  • topicId - A string value for the topic of the message
  • message - A Uint8Array value representing the contents of the message
const message = "hello world";
const topicId = "topic id";

const publishResult = await client.publish(topicId, message);
if (E.isLeft(publishResult)) {
  throw new Error(`failed to publish ${JSON.stringify(publishResult.left)}`);
}

Subscribing

Parameters:

  • sender - The ethereum address of the message sender. Clients must indicate which sender they want to listen to messages for.
  • topicId - A string value for this topic id.
  • signal - An AbortSignal used to cancel the subscription
const controller = new AbortController();
const iterator = await receiverClient.pubsub.subscribe(sender, topicId);

const pubsubMessageR = await iterator.next();

if (E.isLeft(pubsubMessageR.value)) {
  throw new Error(`Unexpected relay error: ${JSON.stringify(relayMessageR)}`);
}

const pubsubMessage = pubsubMessageR.value.right;

// hello world
console.log(Buffer.from(pubsubMessage.message).toString());

Calling pubsub.subscribe returns an iterator that emits values of type Either<SubscribeError, PubsubMessage>.

export type PubsubMessage = {
  sender: Address;
  topicId: string;
  message: Uint8Array;
  ticket: MultiReceiverTicket;
};

If the iterator returns a value of SubscribeError, it means that a new message was detected, but it failed to be processed for one of the following reasons:

FailedToDecodeReceiverTicketSignature - Failed to deserialize the signature object sent by the sender of this message.

FailedToDecodeDelegatedReceiverAddress - Failed to deserialize the delegated receiver object specified by the sender.

Decoding failures tend to occur if the sending/receiving clients are using incompatible versions of the protocol sdk.

Authorized Signer

Whenever the client sends or receives a message, it attempts to invoke its signMessageFn to sign the message's corresponding ticket. This sign fn is also invoked when the client needs to authenticate with a node. It is common to use the protocol sdk in a browser context with a wallet provider such as Metamask. Where the signMessageFn makes a call to the personal_sign method of the Metamask API. This creates a troublesome user experience, where every invocation of the sign method also brings up a prompt for the user to confirm a signature.

If the user has trust in the application using this sdk, and wants to avoid having to confirm the prompt on every signature request, the user can create a delegated authorized account.

This is an account where it's address is stored on chain, and linked to a user's regular account. This linking allows signatures from the delegated account to be used within the protocol, in place of signatures from the user's main account. Creating a authorized account requires sending a transaction to the AuthorizedAccount contract to link the accounts together.

Creating the Authorized Account

// create a new wallet as the authorized account
const senderDelegatedAccount = ethers.Wallet.createRandom();

const authorizedAccounts = AuthorizedAccounts__factory.connect(
  PORCINI_CONTRACTS.authorizedAccounts,
  senderAccount
);

await authorizedAccounts.authorizeAccount(senderDelegatedAccount.address, [
  AuthorizedPermission.SigningTicket,
]);

Applications should securely store the newly generated wallet. WARNING: Leaking this key can allow attackers to spend from the user's escrow without their consent.

Using the Authorized Account

The authorized account can be passed in when creating the sdk client, or calling the client.setAuthorizedSigner method.

Client.create({
    account,
    signMessageFn: wallet.signMessage.bind(wallet),
    peerKey,
    network: PORCINI_CONTRACTS,
    provider,
    authenticationOpts: { prefix, suffix },
    {
      address: decodedOrThrow(Address.decode(senderDelegatedAccount.address)),
      sign: (data: Uint8Array | string) => {
        return senderDelegatedAccount.signMessage(data);
      }
    }
  });
client.setAuthorizedSigner({
  address: decodedOrThrow(Address.decode(senderDelegatedAccount.address)),
  sign: (data: Uint8Array | string) => {
    return senderDelegatedAccount.signMessage(data);
  },
});

When using the client, most functions allow passing in a signatureType value to determine which type of signature to use.

import { SignatureType } from "@futureverse/sylo-protocol-sdk/lib/src/utils/utils";

await client.relay.send(receiver, message, SignatureType.Authorized);

The signature type can also be set globally for signing functionality.

client.setSignatureType(SignatureType.Authorized);

Attached Authorized Signer

As an alternative to having the user send a transaction to create a new authorized account, a attached authorized account can instead be supplied alongside each signature. An attached authorized account does not need to be persisted onchain, but can be managed entirely locally.

The attached authorized account has 5 parameters:

  • account: The address of the delegated account
  • expiry: A unix timestamp in seconds for when the attached authorized account will expire.
  • prefix: A string used in constructing the proof message
  • suffix: A string used in constructing the proof message
  • infixOne: A string used in constructing the proof message

The prefix, suffix, and infixOne parameters enable an application to customize the message that the user will need to sign in order to create the proof. Creating the proof requires passing in a signing function which creates a signature signed by the user's wallet.

import { createNewAttachedAuthorizedAccount } from "@futureverse/sylo-protocol-sdk/lib/src/events/utils";

const attachedAuthorizedAccount = await createNewAttachedAuthorizedAccount(
  // The signFn can be the metamask personal sign request method for example
  signFn,
  decodedOrThrow(Address.decode(attachedWallet.address)),
  now,
  "============PREFIX_START============\nWelcome to the Sylo Notifications Demo App. This personal signing request is used to create a proof for a authorized account. The prefix is configured by the exprience. The next value is the address of the authorized account and must be in the message.\n============PREFIX_END============\n",
  "\n============SUFFIX_START============\nThis is the end of the message and is also configured by the experience.\n============SUFFIX_END============",
  "\n============INFIX_START============\nThis string sits between the address and the next value, which is the expiry of the authorized account\n============INFIX_END============\n"
);

clientSDK.setAttachedAuthorizedSigner({
  attachedAuthorizedAccount,
  sign: attachedWallet.signMessage.bind(attachedWallet),
});

The attached authorzied account can only be used when receiving messages.

Authentication

A client within the network identifies itself with a Peer Key. This key differs from the user's Ethereum account. However, sending and receiving messages within the network is based on the Ethereum account. Thus whenever a user interacts with a Seeker Node via the SDK client, the client needs to authenticate itself, and prove that it is being operated by the user's Ethereum account.

This is achieved by the client sending an authentication request to a node, and the node then returning a challenge which must be signed by the user's Ethereum account.

This authentication mechanism effectively links the user's Ethereum account to the client's peer key. The client will need to authenticate whenever it connects to a new Node that it hasn't authenticated to before.

Scanning

Scanning is the process of connecting to your own, or to a peer's designated Node. Every epoch, a Node is designated to service a particular ethereum account. This designation is based on an algorithm which takes the ethereum address as input, and also compares the Node's SYLO stake for that epoch against other nodes. Where having higher stake, makes it more likely for an account to be designated by that Node.

When messaging another peer, the client will need to scan for that peer's node. This process is automatically managed by the client. Connecting to a new node may also require that the client authenticates itself with that new node.

Errors

Most methods return an Either<Error, Success> value, and generally attempt to define all known errors.

Protocol Errors

All communication to a node is done through multiplexed libp2p streams. A Protocol Error indicates either a failure to write or read via a libp2p stream. The possible errors are:

BadDecoding - Failed to protobuf deserialize a byte value into a known object.

BadInput - Failed to protobuf serialize a object into bytes. Usually due to a malformed object.

UnknownType - All messages exchanged within the network follow a format of

{
  type, value;
}

where type indicates the expected message type. Every stream defines a set of message types expected to be exchanged within this stream. An UnknownType indicates receiving an unexpected message type. This may occur if the incompatible versions between the client or node.

UnexpectedType - This indicates a known message type for the stream, but it was received at an unexpected type. Usually indicates incompatible version between the client ande node.

UnexpectedEOF - The stream was unexpectedly closed. This usually occurs due to the Node closing the stream due to an unexpected error. Another possible cause may be that the underlying connection was dropped due to a network outage.