Psychedelic / cap-js

CAP-js is a client for CAP's HTTP API - An Open Internet Service to store transaction history for NFTs/Tokens on the Internet Computer - https://cap.ooo/
GNU General Public License v3.0
6 stars 1 forks source link

CAP-js

The client library for CAP - the Open Internet Service (OIS).

A client library for the CAP Open Internet Service (OIS), implemented in JavaScript. The interface is based on the candid file Cap Candid allowing dApps to interact with the main canister. In addition, this client library will support endpoint caching using Kyasshu.

The CAP-js library is utilized to integrate UIs/FEs/Apps to CAP to query & surface data from CAP. Any frontend can query the transaction/activity history for any Token or NFTs that uses CAP. (Instead of having to integrate manually with each asset!)

IMPORTANT: CAP is currently in development 🚧 and will release in the first week of November, thus it is not on mainnet or usable yet. You might see our documentation is light on the SDK/Main repo still. We're delayed in this to focus on testing, but will soon update this page with guides & detailed examples for developers.

Table of Contents

Getting Started

⚠️ ⚠️ Library not stable, will stay unstable until Cap is finalized ⚠️ ⚠️

Install

yarn add @psychedelic/cap-js

Usage

To interface with Router and Root toolkits, you have to instantiate them in the client application.

The Router instance is long lived and can persist across your application lifetime, as the Canister Id it depends on is of fixed value (e.g. calls to the method get_index_canisters will be sent to the same canister id in the network you're connected to). For this reason, it's recommended ⚡️ to create the instance in the application top-level and reuse during the application lifetime.

The Root instance in the other hand is short-lived and only useful during the token contract use cases (e.g. calls to the method get_transaction are sent to the particular token contract canister id which is unique). As such, it's necessary 🤝 to create a new instance for each token contract canister id and reuse the instance in the context of the token contract.

These are available as:

import { CapRouter, CapRoot } from '@psychedelic/cap-js';

Here's an example of how to instantiate the CapRouter, which is similar to CapRoot:

import { CapRouter } from '@psychedelic/cap-js';

const getCapRouterInstance = async ({
  canisterId,
  host,
}: {
  canisterId?: string,
  host?: string,
}) => await CapRouter.init({
  host,
  canisterId,
});

// On a hypotetical application top-level or entry-point
(async () => {
    const capRouter = new getCapRouterInstance({
        canisterId: 'rrkah-fqaaa-aaaaa-aaaaq-cai',
        host: 'http://localhost:8000',
    });
})();

💡 The root canister id and host can be computed by an environment variable. Although, these parameters can be omited and the mainnet values are set by default.

Also, there's a Hosts object that can be used to retrieve defaults:

import { Hosts } from '@psychedelic/cap-js';

// The Mainnet
const mainnetHost = Hosts.mainnet;

Similarily, a CAP CanisterInfo object is available that provides defaults:

import { CanisterInfo } from '@psychedelic/cap-js';

// The `ic-history-router` mainnet canister id
const ICHistoryRouterCanisterId = CanisterInfo['ic-history-router'].mainnet;

Quick usage examples

In order to get transactions of a particular token (e.g: XTC token):

const tokenId = 'aanaa-xaaaa-aaaah-aaeiq-cai'   // XTC Canister Id

const { canister: rootTokenId } = capRouter.get_token_contract_root_bucket({canister: tokenId, witness})

const capRootXTC = await CapRoot.init({
  canisterId: rootTokenId[0],
})

const xtcTransactions = await capRootXTC.get_transactions()

Or instead, pass an instance of capRouter to have the inner call to get_token_contract_root_bucket handled for you:

const tokenId = 'aanaa-xaaaa-aaaah-aaeiq-cai'   // XTC Canister Id

const capRootXTC = await CapRoot.init({ tokenId, router: capRouter })

const xtcTransactions = await capRootXTC.get_transactions()

Alternatively, for the case where you don't have a CapRouter instance:

const tokenId = 'aanaa-xaaaa-aaaah-aaeiq-cai'   // XTC Canister Id

const capRootXTC = await CapRoot.init({
    tokenId,
    routerCanisterId: 'rrkah-fqaaa-aaaaa-aaaaq-cai',
    host: 'http://localhost:8000'
})

const xtcTransactions = await capRootXTC.get_transactions()

You're advised to understand Candid, the interface description language for the Dfinity Internet Computer Services, because ultimately, that's where the latest method signatures for the Service endpoints are defined, to avoid any typos described in the documentation.

Once you start looking at the original source-code, you might start questioning some decisions.

For example, why cap-js at time of writing is wrapping the Actor methods which are accessible directly in the actor (e.g. capRoot.actor.get_transactions())?

As it's mainly a wrapper to original method accessible in the actor and that's for your own convenience (e.g. provides default values) it also provides a chance to the Cache layer to intercept and handle those requests.

Since Cache is available (optionally) it helps to have a common API to switch between.

const tnx = await caRoot.get_transactions(...);
const tnx = await capCache.get_transactions(...);

Find more about here

Router Canister

capRouter.get_index_canisters(witness)

Return all Cap index canisters (Router and any replicas, when scaled)

Parameters

Name Type Description
witness boolean? The optional Certified response, defaults to false

Returns

Type Description
GetIndexCanistersResponse An object returning all index canisters. If witness = true the certified response will be appended to the response

Example

const indexCanisters = await capRouter.get_index_canisters(false);
console.log(indexCanisters);
{
    witness: [] | [Witness];
    canisters: Array<Principal>;
}

capRouter.get_token_contract_root_bucket({canister, witness})

For a given token contract, return the entry root bucket

Parameters

Name Type Description
canister Principal The canister Id of the requested root bucket
witness boolean? The optional witness for the Certified response

Returns

Type Description
GetTokenContractRootBucketResponse An object returning root canister for a given token. If witness = true the certified response will be appended to the response

Example

const tokenRootBucket = await capRouter.get_token_contract_root_bucket({
 canister: "aaaa-aa",
 witness: false,
});
console.log(tokenRootBucket);
{
    witness: [] | [Witness];
    canister: [] | [Principal];
}

capRouter.get_user_root_buckets({user, witness})

query a users root bucket, each user root bucket exposes an interface to interact with and get transactions

Parameters

Name Type Description
user Principal The user Id of the requested root bucket
witness boolean? The optional witness for the Certified response

Returns

Type Description
GetUserRootBucketsResponse An object returning root canister for a given user. If witness = true the certified response will be appended to the response

Example

const userRootBucket = await capRouter.get_user_root_buckets({
 user: "avesb-mgo2l-ds25i-g7kd4-3he5l-z7ary-3biiq-sojiw-xjgbk-ich5l-mae",
 witness: false,
});
console.log(userRootBucket);
{
    witness: [] | [Witness];
    contracts: Array<Principal>;
}

capRouter.insert_new_users(contractId, users)

insert new users for a token contract

Parameters

Name Type Description
contractId Principal The token contract Id
users Array of Principal A list of user principal

Returns

Type Description
Promise<undefined> an empty response ()

capRouter.install_bucket_code(principal)

instantiate a new bucket canister for a token

Parameters

Name Type Description
principal Principal principal Id of a newly created empty canister

Returns

Type Description
Promise<undefined> an empty response ()

Example

# create root bucket canister
dfx canister --wallet=$(dfx identity get-wallet) call aaaaa-aa create_canister "(record { cycles=(4_000_000_000_000:nat64); controller=(opt principal \"$(dfx identity get-principal)\") })"

# set cap as the controller for the root bucket
dfx canister --wallet=$(dfx identity get-wallet) call aaaaa-aa update_settings "(record { canister_id=(principal \"r7inp-6aaaa-aaaaa-aaabq-cai\"); settings=(record { controller = opt principal \"rrkah-fqaaa-aaaaa-aaaaq-cai\"; null; null; null; }) })"

# Check controller of bucket canister
dfx canister info r7inp-6aaaa-aaaaa-aaabq-cai

Notes

Root Canister

capRoot.get_transaction(id, witness)

Return a specifc transaction based on global transaction Id from a token contract (tokenId)

Parameters

Name Type Description
id bigint The global txnId of a transaction to return
witness boolean? The optional witness for the Certified response

Returns

Type Description
GetTransactionResponse An object returning either Delegate or Found. If witness = true the certified response will be appended to the response

Example

const transaction = await capRoot.get_transaction(
    BigInt(1), 
    false,
)
console.log(transaction);
{
    Delegate: [Principal, [] | [Witness]];
}
# or
{ 
    Found: [[] | [Event], [] | [Witness]] 
}

Notes

capRoot.get_transactions({page, witness})

Get all transactions for a single token contract

Parameters

Name Type Description
page number? The optional number of the page to query for transctions, each page can hold up to 64 transactions. Defaults to page 0.
witness boolean? The optional witness for the Certified response

Returns

Type Description
GetTransactionsResponseBorrowed An object returning an array of data as well as the page queried. If witness = true the certified response will be appended to the response

Example

const tokenTxns = await capRoot.get_transactions({
 page: 4,
 witness: false,
});

// or

const tokenTxns = await capRoot.get_transactions({
 witness: false,
}); // this will default to page 0

console.log(tokenTxns);
{
    data: Array<Event>;
    page: number;
    witness: [] | [Witness];
}

Notes

capRoot.get_user_transactions(({page, user, witness})

Get all user transactions for the token contract

Name Type Description
page number? The optional number of the page to query for transctions, each page can hold up to 64 transactions. Defaults to page 0.
user Principal The user principal of the requested transactions
witness boolean? The optional witness for the Certified response

Returns

Type Description
GetTransactionsResponseBorrowed An object returning an array of data as well as the page queried. If witness = true the certified response will be appended to the response

Example

import { cap } from '@psychedelic/cap-js'

const userTxns = await capRoot.get_user_transactions({
 userId: "avesb-mgo2l-ds25i-g7kd4-3he5l-z7ary-3biiq-sojiw-xjgbk-ich5l-mae",
 page: 4,
 witness: false,
});

// or

const userTxns = await capRoot.get_user_transactions({
 userId: "avesb-mgo2l-ds25i-g7kd4-3he5l-z7ary-3biiq-sojiw-xjgbk-ich5l-mae",
}); // this will default to page `0` and witness `false`

console.log(userTxns);
{
    data: Array<Event>;
    page: number;
    witness: [] | [Witness];
}

capRoot.insert({operation, caller, details})

Insert a transaction event to the token contract

Name Type Description
operation operation
caller principal
details vec record

Returns

Type Description
Promise<bigint> a number

Notes

capRoot.get_bucket_for({})

ToDo

capRoot.get_next_canisters({})

ToDo

time : () -> (nat64) query;

capRoot.time([options])

ToDo

Kyasshu Layer

kyasshu is a cache layer, that is originally used in the for xtc token service by utilizing lambda, sqs, redis and dynamodb. Why would you want to use Kyasshu? Because caching allows you to efficiently reuse previously retrieved data, improving your application performance.

capRoot.get_all_user_transactions(userId, LastEvaluatedKey)

Return all of the user transactions for userId, if LastEvaluatedKey is returned, you must provide it in subsequent calls to query the rest of the data.

Parameters

Name Type Description
userId principal The user Id of the requested transactions
LastEvaluatedKey string? The optional LastEvaluatedKey, If LastEvaluatedKey is empty, then the "last page" of results has been processed and there is no more data to be retrieved. If LastEvaluatedKey is not empty, it does not necessarily mean that there is more data in the result set. The only way to know when you have reached the end of the result set is when LastEvaluatedKey is empty.

Returns

Type Description
GetTransactionsResponseBorrowed An object returning an array of data as well as the page queried. If witness = true the certified response will be appended to the response

Example

const capCache = new CapCache();

let userTxns = await capCache.get_all_user_transactions({
    user: Principal.from("zxt4e-ian3w-g4ll2-3n5mz-lfqkc-eyj7k-yg6jl-rsbud-f6sft-zdfq3-pae"),
});

userTxns = await capCache.get_all_user_transactions({
    user: Principal.from("zxt4e-ian3w-g4ll2-3n5mz-lfqkc-eyj7k-yg6jl-rsbud-f6sft-zdfq3-pae"),
    LastEvaluatedKey: userTxns.LastEvaluatedKey,
});

console.log(userTxns);
{
    Items?: {
            [key: string]: Event;
    }[] | undefined;
    LastEvaluatedKey?: {
            [key: string]: any;
    } | undefined;
}

Notes

API

Local Development

Roadmap

Testing

To run tests, run the following command

  npm run test

Roadmap

Contributing

Contributions are always welcome!

License

MIT