Ju
SDK
This SDK serves as a comprehensive starting point for developers looking to build social applications with the Ju Protocol. It provides a streamlined API that focuses on common use-cases, ensuring a seamless development experience. Furthermore, the SDK is designed to be extensible through plugins, allowing third-party developers to enhance its functionality
Please note that this SDK has been built from scratch and is actively evolving. As a result, certain aspects of the core API and interfaces may undergo changes in different versions. However, we encourage you to utilize the SDK and provide early feedback if you wish to contribute to the future direction of this project.
Installation
npm install @ju-social/sdk @solana/web3.js
🔥 Pro Tip: Check out our examples and starter kits on the "JS Examples" repository.
Setup
The SDK is initiated through a Ju
instance, which serves as the entry point for accessing its API.
To establish communication with the cluster, the Ju instance requires a Connection instance from @solana/web3.js. This connection object enables seamless interaction with the underlying Solana blockchain network.
import { Ju } from "@ju-social/sdk";
import { Connection, clusterApiUrl } from "@solana/web3.js";
const connection = new Connection(clusterApiUrl("mainnet-beta"));
const ju = new Ju(connection);
Additionally, the SDK allows for customization of the entity it interacts with on behalf of and the choice of a storage provider for uploading metadata assets. These customizable components are known as "Identity Drivers" and "Storage Drivers" respectively.
To modify these drivers, you can use the use method on the Ju instance. This enables you to configure the SDK according to your specific requirements. In the subsequent sections, we will explore the available drivers in greater detail.
import { Ju, keypairIdentity, bundlrStorage } from "@ju-social/sdk";
import { Connection, clusterApiUrl, Keypair } from "@solana/web3.js";
const connection = new Connection(clusterApiUrl("mainnet-beta"));
const wallet = Keypair.generate();
const ju = Ju.make(connection)
.use(keypairIdentity(wallet))
.use(bundlrStorage());
You may have observed that instead of using new Ju(...) to create a Ju instance, you have the option to utilize Ju.make(...) to enhance the readability of the fluent API. This alternative syntax allows for a more expressive and concise way of working with the SDK.
SDK Usage
Once you have appropriately configured the Ju instance, you can utilize it to access modules that offer various sets of features. Presently, there is a single documented Core client module accessible through the core() method. This module allows you to interact with Ju's core protocol instructions.
To illustrate, here is an example of how you can retrieve a Profile using its address.
const profileAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const profile = await ju.core().profile.get(profileAddress);
You may pass an AbortSignal
to second argument to cancel the operation before it finishes — similarly to how you would cancel an HTTP request.
const profileAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const abortController = new AbortController();
setTimeout(() => abortController.abort(), 100);
const profile = await ju.core().profile.get(
profileAddress,
{ signal: abortController.signal }
);
Now, before we proceed to the identity and storage drivers, let's delve a bit deeper into the Core module to gain a better understanding of its functionality.
Core module
The Core module can be accessed via ju.core()
and provides the following clients:
Common
client
The Common client can be accessed via ju.core().common
and provides the following methods:
App
client
The App client can be accessed via ju.core().app
and provides the following methods:
Profile
client
The Profile client can be accessed via ju.core().profile
and provides the following methods:
Subspace
client
The Subspace client can be accessed via ju.core().subspace
and provides the following methods:
Publication
client
The Publication client can be accessed via ju.core().publication
and provides the following methods:
Connection
client
The Connection client can be accessed via ju.core().connection
and provides the following methods:
Reaction
client
The Reaction client can be accessed via ju.core().reaction
and provides the following methods:
Report
client
The Report client can be accessed via ju.core().report
and provides the following methods:
Common client
methods
Detailed description of the Common client methods.
search
Finds entities by given request string.
The search
method accepts an app
as public key and returns a SearchResultItem
object.
const app = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const requestString = 'something';
const result = await ju.core().common.search(
app,
requestString
);
findAliasByValue
Finds Alias instance by given alias string
The findAliasByValue
method accepts an app
as public key and alias
as string and returns an Alias
model or null
if nothing was finded.
const app = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const alias = 'johndoe';
const result = await ju.core().common.findAliasByValue(
app,
alias
);
findEntityByAliasValue
Finds Profile or Subspace instance by given alias string.
The findEntityByAliasValue
method accepts an app
as public key and alias
as string and returns a Profile
model or Subspace
model.
const app = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const alias = 'johndoe';
const result = await ju.core().common.findEntityByAliasValue(
app,
alias
);
App client
methods
Detailed description of the App client methods.
The following model, either returned or used by the above methods.
get
Gets the App instance by given App address.
The get
method accepts an app
as public key and returns an App
model instance.
const appAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const app = await ju.core().app.get(appAddress);
The returned App
object will have its JSON metadata already loaded so you can, for instance, access its image URL like so (provided it is present in the downloaded metadata).
const imageUrl = app.metadata.image;
You can read more about the App
model below.
create
Creates the App instance with given data.
The create
method accepts a CreateAppInput
object and returns an App
model instance.
const { app } = await ju.core().app.create(
{
appDomainName: 'testApp',
data: {
},
externalProcessors: {
}
}
);
update
Update the existing App data.
The update
method accepts an App
instance and app data
object to update current values.
const appAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const app = await ju.core().app.get(appAddress);
const result = await ju.core().app.update(
app,
{
metadataUri: 'https://example.com/updated-uri'
}
);
Profile client
methods
Detailed description of the Profile client methods.
get
Gets the Profile instance by given profile address (public key).
The get
method accepts an address
as public key and returns a Profile
model instance.
const profileAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const profile = await ju.core().profile.get(profileAddress);
The returned Profile
object will have its JSON metadata already loaded so you can, for instance, access its image URL like so (provided it is present in the downloaded metadata).
const imageUrl = profile.metadata.image;
You can read more about the Profile
model below.
create
Creates new Profile with given data.
The create
method accepts a CreateProfileInput
object and returns a Profile
model instance.
const appAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const { profile } = await ju.core().profile.create(
{
app: appAddress,
data: {
}
}
);
update
Update the existing Profile data.
The update
method accepts a Profile
instance and profile data
object to update current values.
const profileAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const profile = await ju.core().profile.get(profileAddress);
const result = await ju.core().app.update(
profile,
{
name: 'Alice',
surname: 'Smith'
}
);
delete
Delete the existing Profile.
The update
method accepts a Profile
instance and returns a SendAndConfirmTransactionResponse
as a result.
const profileAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const profile = await ju.core().profile.get(appAddress);
const result = await ju.core().app.delete(profile);
Subspace client
methods
Detailed description of the Subspace client methods.
get
Get the Subspace instance by subspace address (public key).
The get
method accepts an address
as public key and returns a Subspace
model instance.
const subspaceAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const { subspace } = await ju.core().subspace.get(subspaceAddress);
The returned Subspace
object will have its JSON metadata already loaded so you can, for instance, access its image URL like so (provided it is present in the downloaded metadata).
const imageUrl = subspace.metadata.image;
You can read more about the Subspace
model below.
create
Creates new Subspace with given data.
The create
method accepts a CreateSubspaceInput
object and returns and Subspace
model instance.
const appAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const { subspace } = await ju.core().subspace.create(
{
app: appAddress,
data: {
}
}
);
update
Update the existing Subspace data.
The update
method accepts a Subspace
instance and subspace data
object to update current values.
const subspaceAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const subspace = await ju.core().subspace.get(subspaceAddress);
const result = await ju.core().subspace.update(
subspace,
{
name: 'Nuclear Block',
}
);
delete
Delete the existing Subspace.
The update
method accepts a Subspace
instance and returns a SendAndConfirmTransactionResponse
as a result.
const subspaceAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const subspace = await ju.core().subspace.get(subspaceAddress);
const result = await ju.core().subspace.delete(subspace);
Publication client
methods
Detailed description of the Publication client methods.
get
Gets the Publication instance by publication address (public key).
The get
method accepts an address
as public key and returns a Publication
model instance.
const publicationAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const { publication } = await ju.core().publication.get(publicationAddress);
The returned Publication
object will have its JSON metadata already loaded so you can, for instance, access its image URL like so (provided it is present in the downloaded metadata).
const imageUrl = publication.metadata.image;
You can read more about the Publication
model below.
create
Creates new Publication with given data.
The create
method accepts a CreatePublicationInput
object and returns and Publication
model instance.
const publicationAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const { publication } = await ju.core().publication.create(
{
app: publicationAddress,
data: {
}
}
);
update
Update the existing Publication data.
The update
method accepts a Publication
instance and publication data
object to update current values.
const publicationAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const publication = await ju.core().publication.get(publicationAddress);
const result = await ju.core().publication.update(
publication,
{
}
);
delete
Delete the existing Publication.
The delete
method accepts a Publication
instance and returns a SendAndConfirmTransactionResponse
as a result.
const publicationAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const { publication } = await ju.core().publication.get(publicationAddress);
const result = await ju.core().publication.delete(publication);
Connection client
methods
Detailed description of the Connection client methods.
You can read more about the Connection
model below.
create
Creates new Connection with diven data.
The create
method accepts a target
as a variant of Profile
or Subspace
instance and optional externalProcessingData
as string and returnsSendAndConfirmTransactionResponse
object as a result.
const appAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const targetProfileAddress = new PublicKey("YTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeG");
const targetProfile = await ju.core().profile.get(targetProfileAddress);
const externalProcessingData = 'some-validation-string';
const result = await ju.core().connection.create(targetProfile, externalProcessingData);
update
Update existing Connection (actually approve).
The update
method accepts a initializer
as Profile
model, target
as public key, approveStatus
as boolean and returnsSendAndConfirmTransactionResponse
object as a result.
const appAddress = new PublicKey("XTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeE");
const initializerProfileAddress = new PublicKey("YTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeG");
const targetSubspaceAddress = new PublicKey("BTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeA");
const initializerProfile = await ju.core().profile.get(initializerProfileAddress);
const result = await ju.core().connection.update(
initializerProfile,
targetSubspaceAddress,
true
);
delete
Delete existing Connection.
The delete
method accepts a target
as a variant of Profile
model or Subspace
model instance and returnsSendAndConfirmTransactionResponse
object as a result.
const profileAddress = new PublicKey("BTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeA");
const targetProfile = await ju.core().profile.get(profileAddress);
const result = await ju.core().connection.delete(targetProfile);
Reaction client
methods
Detailed description of the Reaction client methods.
You can read more about the Reaction
model below.
create
Creates new Reaction to given target
Publication.
The create
method accepts a target
as Publication
model instance, reactionType
as [ReactionType
] (#ReactionType) enum variant and returns SendAndConfirmTransactionResponse
object as a result.
const targetPublicationAddress = new PublicKey("YTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeG");
const targetPublication = await ju.core().publication.get(targetPublicationAddress);
const result = await ju.core().reaction.create(
targetPublication,
0
);
delete
Delete existing Reaction by given target
.
The delete
method accepts a target
as Publication
model instance and returnsSendAndConfirmTransactionResponse
object as a result.
const publicationAddress = new PublicKey("BTe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeA");
const targetPublication = await ju.core().publication.get(publicationAddress);
const result = await ju.core().reaction.delete(targetPublication);
Core Models
description
App
model
Represents protocol Application entity
export type App<JsonMetadata extends object = AppJsonMetadata> = {
model: 'profile';
address: PublicKey;
metadata: JsonMetadata | null;
appDomainName: string;
authority: PublicKey;
metadataUri: string | null;
profileNameRequired: boolean;
profileSurnameRequired: boolean;
profileBirthdateRequired: boolean;
profileCountryRequired: boolean;
profileCityRequired: boolean;
profileMetadataRequired: boolean;
subspaceNameRequired: boolean;
subspaceMetadataRequired: boolean;
isProfileDeleteAllowed: boolean;
isSubspaceDeleteAllowed: boolean;
isPublicationDeleteAllowed: boolean;
isProfileIndividualProcessorsAllowed: boolean;
isSubspaceIndividualProcessorsAllowed: boolean;
isPublicationIndividualProcessorsAllowed: boolean;
registeringProcessor: PublicKey | null;
connectingProcessor: PublicKey | null;
publishingProcessor: PublicKey | null;
collectingProcessor: PublicKey | null;
referencingProcessor: PublicKey | null;
}
Profile
model
Represents protocol Application's Profile entity
export type Profile<JsonMetadata extends object = ProfileJsonMetadata> = {
model: 'profile';
address: PublicKey;
metadata: JsonMetadata | null;
app: PublicKey;
authority: PublicKey;
alias: string | null;
metadataUri: string | null;
statusText: string | null;
verified: boolean;
name: string | null;
surname: string | null;
birthDate: BN | null;
countryCode: number | null;
cityCode: number | null;
currentLocation: LocationCoordinates | null;
connectingProcessor: PublicKey | null;
createdAt: BN | null;
modifiedAt: BN | null;
}
Subspace
model
Represents protocol Application's Subspace entity
export type Subspace<JsonMetadata extends object = SubspaceJsonMetadata> = {
model: 'subspace';
address: PublicKey;
metadata: JsonMetadata | null;
app: PublicKey;
authority: PublicKey;
creator: PublicKey;
alias: string | null;
name: string | null;
uuid: string;
metadataUri: string | null;
connectingProcessor: PublicKey | null;
publishingProcessor: PublicKey | null;
collectingProcessor: PublicKey | null;
referencingProcessor: PublicKey | null;
}
Publication
model
Represents protocol Application's Publication entity
export type Publication<JsonMetadata extends object = PublicationJsonMetadata> = {
model: 'publication';
address: PublicKey;
metadata: JsonMetadata | null;
app: PublicKey;
profile: PublicKey;
authority: PublicKey;
subspace: PublicKey | null;
isMirror: boolean;
isReply: boolean;
targetPublication: PublicKey | null;
contentType: ContentType;
tag: string | null;
uuid: string;
metadataUri: string | null;
collectingProcessor: PublicKey | null;
referencingProcessor: PublicKey | null;
createdAt: BN | null;
modifiedAt: BN | null;
}
JsonMetadata
model
Object represents external metadata
export type JsonMetadata<Uri = string> = {
}
Additional references
uploadMetadata
When creating or updating an App, you will need a URI pointing to some JSON Metadata describing the additional App parameters. Depending on your requirement, you may do this on-chain or off-chain.
If your JSON metadata is not already uploaded, you may do this using the SDK via the uploadMetadata
method. It accepts a metadata object and returns the URI of the uploaded metadata. Where exactly the metadata will be uploaded depends on the selected StorageDriver
.
const { uri } = await ju.core().common.uploadMetadata(
{
name: "My App",
description: "My App description",
image: "https://arweave.net/123",
}
);
console.log(uri)
Some properties inside that metadata object will also require you to upload some assets to provide their URI — such as the image
property on the example above.
To make this process easier, the uploadMetadata
method will recognise any instances of JuFile
within the provided object and upload them in bulk to the current storage driver. It will then create a new version of the provided metadata where all instances of JuFile
are replaced with their URI. Finally, it will upload that replaced metadata to the storage driver and return it.
const browserFiles = event.target.files;
const { uri, metadata } = await ju.core().common.uploadMetadata(
{
name: "My Video",
image: await toJuFileFromBrowser(browserFiles[0]),
properties: {
files: [
{
type: "video/mp4",
uri: await toJuFileFromBrowser(browserFiles[1]),
},
]
}
}
);
console.log(metadata.image)
console.log(metadata.properties.files[0].uri)
console.log(uri)
Note that JuFile
s can be created in various different ways based on where the file is coming from. You can read more about JuFile
objects and how to use them here.
Identity
The current identity of a Ju
instance can be accessed via ju.identity()
and provide information on the wallet we are acting on behalf of when interacting with the SDK.
This method returns an identity client with the following interface.
class IdentityClient {
driver(): IdentityDriver;
setDriver(newDriver: IdentityDriver): void;
publicKey: PublicKey;
secretKey?: Uint8Array;
signMessage(message: Uint8Array): Promise<Uint8Array>;
verifyMessage(message: Uint8Array, signature: Uint8Array): boolean;
signTransaction(transaction: Transaction): Promise<Transaction>;
signAllTransactions(transactions: Transaction[]): Promise<Transaction[]>;
equals(that: Signer | PublicKey): boolean;
hasSecretKey(): this is KeypairSigner;
}
The IdentityClient
delegates to whichever IdentityDriver
is currently set to provide this set of methods. Thus, the implementation of these methods depends on the concrete identity driver being used. For instance, in the CLI, these methods will directly use a key pair whereas, in the browser, they will delegate to a wallet adapter.
Let’s have a quick look at the concrete identity drivers available to us.
guestIdentity
The guestIdentity
driver is the default driver and requires no parameter. It is essentially a null
driver that can be useful when we don’t need to send any signed transactions.
import { guestIdentity } from "@ju-social/sdk";
ju.use(guestIdentity());
If we try to sign a message or a transaction using this driver, an error will be thrown.
keypairIdentity
The keypairIdentity
driver accepts a Keypair
object as a parameter. This is useful when using the SDK locally such as within CLI applications.
import { keypairIdentity } from "@ju-social/sdk";
import { Keypair } from "@solana/web3.js";
const keypairFile = fs.readFileSync('/Users/username/.config/solana/id.json');
const keypair = Keypair.fromSecretKey(Buffer.from(JSON.parse(keypairFile.toString())));
ju.use(keypairIdentity(keypair));
walletAdapterIdentity
The walletAdapterIdentity
driver accepts a wallet adapter as defined by the “wallet-adapter” repo from Solana Labs. This is useful when using the SDK in a web application that requires the user to manually approve transactions.
import { walletAdapterIdentity } from "@ju-social/sdk";
import { useWallet } from '@solana/wallet-adapter-react';
const wallet = useWallet();
ju.use(walletAdapterIdentity(wallet));
Storage
You may access the storage client using ju.storage()
which will give you access to the following interface.
class StorageClient {
driver(): StorageDriver
setDriver(newDriver: StorageDriver): void;
getUploadPriceForBytes(bytes: number): Promise<Amount>;
getUploadPriceForFile(file: JuFile): Promise<Amount>;
getUploadPriceForFiles(files: JuFile[]): Promise<Amount>;
upload(file: JuFile): Promise<string>;
uploadAll(files: JuFile[]): Promise<string[]>;
uploadJson<T extends object = object>(json: T): Promise<string>;
download(uri: string, options?: RequestInit): Promise<JuFile>;
downloadJson<T extends object = object>(uri: string, options?: RequestInit): Promise<T>;
}
Similarly to the IdentityClient
, the StorageClient
delegates to the current StorageDriver
when executing these methods. We'll take a look at the storage drivers available to us, but first, let's talk about the JuFile
type which is being used throughout the StorageClient
API.
JuFile
The JuFile
type is a simple wrapper around Buffer
that adds additional context relevant to files and assets such as their filename, content type, extension, etc. It contains the following data.
type JuFile = Readonly<{
buffer: Buffer;
fileName: string;
displayName: string;
uniqueName: string;
contentType: string | null;
extension: string | null;
tags: JuFileTag[];
}>
You may use the toJuFile
function to create a JuFile
object from a Buffer
instance (or content string
) and a filename. The filename is necessary to infer the extension and the mime type of the provided file.
const file = toJuFile('The content of my file', 'my-file.txt');
You may also explicitly provide these options by passing a third parameter to the constructor.
const file = toJuFile('The content of my file', 'my-file.txt', {
displayName = 'A Nice Title For My File';
uniqueName = 'my-company/files/some-identifier';
contentType = 'text/plain';
extension = 'txt';
tags = [{ name: 'my-tag', value: 'some-value' }];
});
Note that if you want to create a JuFile
directly from a JSON object, there's a toJuFileFromJson
helper method that you can use like so.
const file = toJuFileFromJson({ foo: 42 });
In practice, you will most likely be creating JuFile
s from files either present on your computer or uploaded by some user on the browser. You can do the former by using fs.readFileSync
.
const buffer = fs.readFileSync('/path/to/my-file.txt');
const file = toJuFile(buffer, 'my-file.txt');
And the latter by using the toJuFileFromBrowser
helper method which accepts a File
object as defined in the browser.
const browserFile: File = event.target.files[0];
const file: JuFile = await toJuFileFromBrowser(browserFile);
Okay, now let’s talk about the concrete storage drivers available to us and how to set them up.
bundlrStorage
The bundlrStorage
driver is the default driver and uploads assets on Arweave using the Bundlr network.
By default, it will use the same RPC endpoint used by the Ju
instance as a providerUrl
and the mainnet address "https://node1.bundlr.network"
as the Bundlr address.
You may customise these by passing a parameter object to the bundlrStorage
method. For instance, here’s how you can use Bundlr on devnet.
import { bundlrStorage } from "@ju-social/sdk";
ju.use(bundlrStorage({
address: 'https://devnet.bundlr.network',
providerUrl: 'https://api.devnet.solana.com',
timeout: 60000,
}));
To fund your bundlr storage account you can cast it in TypeScript like so:
const bundlrStorage = ju.storage().driver() as BundlrStorageDriver;
This gives you access to useful public methods such as:
bundlrStorage.fund([juFile1, juFile2]);
bundlrStorage.fund(1000);
(await bundlrStorage.bundlr()).fund(1000);
mockStorage
The mockStorage
driver is a fake driver mostly used for testing purposes. It will not actually upload the assets anywhere but instead will generate random URLs and keep track of their content in a local dictionary. That way, once uploaded, an asset can be retrieved using the download
method.
import { mockStorage } from "@ju-social/sdk";
ju.use(mockStorage());