This site requires Javascript to be enabled.

Decentralized Storage

With Jackal storage, you can purchase and manage storage on-demand, without the need for intermediaries or credit cards. This approach empowers users to store their data securely and privately, while developers can build more resilient and innovative applications.

Jackal Protocol

For a full set of docs for the Jackal Protocol, check out docs.jackalprotocol.com.

Using Jackal With Jackal.js

Installing Dependencies

npm install @jackallabs/jackal.js

Initialization

To get started building with Jackal, we first need to create a StorageHandler.

import type { IClientSetup, IStorageHandler, ClientHandler } from '@jackallabs/jackal.js'const chainId = 'jackal-1'const mainnet = {  chainId,  endpoint: 'https://rpc.jackalprotocol.com',    chainConfig: {        chainId,        chainName: 'Jackal Mainnet',        rpc: 'https://rpc.jackalprotocol.com',        rest: 'https://api.jackalprotocol.com',        bip44: {            coinType: 118        },        stakeCurrency: {            coinDenom: 'JKL',            coinMinimalDenom: 'ujkl',            coinDecimals: 6        },        bech32Config: {            bech32PrefixAccAddr: 'jkl',            bech32PrefixAccPub: 'jklpub',            bech32PrefixValAddr: 'jklvaloper',            bech32PrefixValPub: 'jklvaloperpub',            bech32PrefixConsAddr: 'jklvalcons',            bech32PrefixConsPub: 'jklvalconspub'        },        currencies: [            {                coinDenom: 'JKL',                coinMinimalDenom: 'ujkl',                coinDecimals: 6            }        ],        feeCurrencies: [            {                coinDenom: 'JKL',                coinMinimalDenom: 'ujkl',                coinDecimals: 6,                gasPriceStep: {                    low: 0.002,                    average: 0.002,                    high: 0.02                }            }        ],        features: []    }}export const archwayChainId = 'archway-1'export const archwayConfig = {    chainId: archwayChainId,    chainName: 'Archway',    rpc: 'https://archway-rpc.polkachu.com',    rest: 'https://archway-api.polkachu.com',    bip44: {        coinType: 118    },    stakeCurrency: {        coinDenom: 'ARCH',        coinMinimalDenom: 'aarch',        coinDecimals: 18    },    bech32Config: {        bech32PrefixAccAddr: 'arch',        bech32PrefixAccPub: 'archpub',        bech32PrefixValAddr: 'archvaloper',        bech32PrefixValPub: 'archvaloperpub',        bech32PrefixConsAddr: 'archvalcons',        bech32PrefixConsPub: 'archvalconspub'    },    currencies: [        {            coinDenom: 'ARCH',            coinMinimalDenom: 'aarch',            coinDecimals: 18        }    ],    feeCurrencies: [        {            coinDenom: 'ARCH',            coinMinimalDenom: 'aarch',            coinDecimals: 18,            gasPriceStep: {                low: 196000000000,                average: 225400000000,                high: 254800000000            }        }    ],    features: []}const setup: IClientSetup = {    host: {        chainId: archwayChainId,        endpoint: 'https://archway-rpc.polkachu.com',        chainConfig: archwayConfig,    },    endpoint: mainnet.endpoint,    chainConfig: mainnet.chainConfig,    chainId: mainnet.chainId,    networks: ["archway", "jackal"],    selectedWallet: 'keplr',}const myClient = await ClientHandler.connect(setup)const storage: IStorageHandler = await myClient.createWasmStorageHandler()storage.loadProviderPool() // load the available provider pool

Purchasing Storage

Purchasing storage requires an IBC send to make sure the account on Jackal has the correct amount of tokens, in this code snippet we IBC send over the JKL difference needed.

async function buyStorage () {    const gb = SOME_GB_VALUE    const days = SOME_DAY_COUNT    const usd = getUSDPrice() // get USD price of storage plan, function body not included here.    const price = Math.floor(usd / PRICE_OF_JACKAL_TOKEN_IN_USD * 1000000 * 1.15)    const coin = await myClient.getJklBalance()    const hostBal = await myClient.getHostNetworkBalance(myClient.getHostAddress(), 'ibc/926432AE1C5FA4F857B36D970BE7774C7472079506820B857B75C5DE041DD7A3')    let bal = coin.amount            if (bal < price) {        if (hostBal.amount < price - bal) {          throw ('You do not have enough JKL on Archway')        }        const c: Coin = {          amount: (price - bal).toString(),          denom: 'ibc/926432AE1C5FA4F857B36D970BE7774C7472079506820B857B75C5DE041DD7A3'        }        await myClient.ibcSend(myClient.getICAJackalAddress(), c, 'channel-14')    }    const c = await myClient.getJklBalance()    bal = c.amount    while (bal < price) {        await new Promise(r => setTimeout(r, 1000))        const c = await myClient.getJklBalance()        bal = c.amount    }          await storage.purchaseStoragePlan({        gb,        days,        broadcastOptions: {          monitorTimeout: 5 * 60 // 5 minute timeout since we're using IBC        }    })}

Upload Files

Use your storage account to upload files.

// unlock full feature setawait storage.upgradeSigner()// if first time using account, initializeawait storage.initStorage()// load root directory if not already loadedawait storage.loadDirectory('Home')// upload encrypted file/* get your file into the browser */const myFiles = [/* Files */]await storage.queuePrivate(myFiles)await storage.processAllQueues()// upload public (unencrypted) file/* get your file into the browser */const myPublicFiles = [/* Files */]await storage.queuePublic(myPublicFiles)await storage.processAllQueues()

Downloading Files

Download your file.

// create a tracker to monitor download progressconst tracker = { progress: 0, chunks: [] }const myFileName = 'myFileName.txt'// Home is the default root folder for all Jackal.js accountsconst myFile = await storage.downloadFile(`Home/${myFileName}`, tracker)// do something with myFile

Embedding Jackal Into an Archway Contract

If you want to use a custom contract (i.e. embedding storage into your own contracts) you will need to follow a contract API that lets Jackal.js call the contracts interchangeably.

Here is a trimmed down version of our outpost factory contract. This contract has two main functions, CreateOutpost will create a new copy of the storage-outpost. And querying the state of this contract lets us figure out which controller contract to call to upload files.

Example Custom Contract

#[cfg(not(feature = "library"))]use cosmwasm_std::entry_point;use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};use crate::error::ContractError;use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};use crate::state::{ContractState, STATE};#[cfg_attr(not(feature = "library"), entry_point)]pub fn instantiate(    deps: DepsMut,    _env: Env,    info: MessageInfo,    msg: InstantiateMsg,) -> Result<Response, ContractError> {    STATE.save(        deps.storage,        &ContractState::new(msg.storage_outpost_code_id, info.sender.to_string()),    )?;    Ok(Response::default())}#[cfg_attr(not(feature = "library"), entry_point)]pub fn execute(    deps: DepsMut,    env: Env,    info: MessageInfo,    msg: ExecuteMsg,) -> Result<Response, ContractError> {    match msg {        ExecuteMsg::CreateOutpost {            channel_open_init_options,        } => execute::create_outpost(deps, env, info, channel_open_init_options),        ExecuteMsg::MapUserOutpost { outpost_owner} => execute::map_user_outpost(deps, env, info, outpost_owner),    }}#[cfg_attr(not(feature = "library"), entry_point)]pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {    match msg {        QueryMsg::GetContractState {} => to_json_binary(&query::state(deps)?),        QueryMsg::GetUserOutpostAddress { user_address } => to_json_binary(&query::user_outpost_address(deps, user_address)?),        QueryMsg::GetAllUserOutpostAddresses {  } => to_json_binary(&query::get_all_user_outpost_addresses(deps)?),    }}mod execute {    use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Uint128, Event, to_json_binary};    use storage_outpost::outpost_helpers::StorageOutpostContract;    use storage_outpost::types::msg::ExecuteMsg as IcaControllerExecuteMsg;    use storage_outpost::types::state::{CallbackCounter, ChannelState};    use storage_outpost::{        outpost_helpers::StorageOutpostCode,        types::msg::options::ChannelOpenInitOptions,    };    use storage_outpost::types::callback::Callback;    use serde_json_wasm::from_str;    use crate::state::{self, USER_ADDR_TO_OUTPOST_ADDR, LOCK};    use super::*;    pub fn create_outpost(        deps: DepsMut,        env: Env,        info: MessageInfo,        channel_open_init_options: ChannelOpenInitOptions,    ) -> Result<Response, ContractError> {        let state = STATE.load(deps.storage)?;        let storage_outpost_code_id = StorageOutpostCode::new(state.storage_outpost_code_id);        if let Some(value) = USER_ADDR_TO_OUTPOST_ADDR.may_load(deps.storage, &info.sender.to_string())? {            return Err(ContractError::AlreadyCreated(value))        }        let _lock = LOCK.save(deps.storage, &info.sender.to_string(), &true);        let callback = Callback {            contract: env.contract.address.to_string(),            msg: None,            outpost_owner: info.sender.to_string(),        };        let instantiate_msg = storage_outpost::types::msg::InstantiateMsg {            owner: Some(info.sender.to_string()),            admin: Some(env.contract.address.to_string()),            channel_open_init_options: Some(channel_open_init_options),            callback: Some(callback),        };        let label            = format!("storage_outpost-owned by: {}", &info.sender.to_string());        let cosmos_msg = storage_outpost_code_id.instantiate(            instantiate_msg,            label,            Some(env.contract.address.to_string()),        )?;        let mut event = Event::new("FACTORY: create_ica_contract");        event = event.add_attribute("info.sender", &info.sender.to_string());        Ok(Response::new().add_message(cosmos_msg).add_event(event))    }    pub fn map_user_outpost(        deps: DepsMut,        env: Env,        info: MessageInfo,        outpost_owner: String,    ) -> Result<Response, ContractError> {        let lock = LOCK.may_load(deps.storage, &outpost_owner)?;        if let Some(true) = lock {            LOCK.save(deps.storage, &outpost_owner, &false)?;        } else {            return Err(ContractError::MissingLock {})        }        USER_ADDR_TO_OUTPOST_ADDR.save(deps.storage, &outpost_owner, &info.sender.to_string())?;        let mut event = Event::new("FACTORY:map_user_outpost");        event = event.add_attribute("info.sender", &info.sender.to_string());        Ok(Response::new().add_event(event))    }    mod query {        use crate::state::{USER_ADDR_TO_OUTPOST_ADDR};        use super::*;        pub fn state(deps: Deps) -> StdResult<ContractState> {            STATE.load(deps.storage)        }        pub fn user_outpost_address(deps: Deps, user_address: String) -> StdResult<String> {            USER_ADDR_TO_OUTPOST_ADDR.load(deps.storage, &user_address)        }        pub fn get_all_user_outpost_addresses(deps: Deps) -> StdResult<Vec<(String, String)>> {            let mut all_entries = Vec::new();            let pairs = USER_ADDR_TO_OUTPOST_ADDR                .range(deps.storage, None, None, cosmwasm_std::Order::Ascending);            for pair in pairs {                let (key, value) = pair?;                all_entries.push((key.to_string(), value));            }            Ok(all_entries)        }    }}

Please refer to this GitHub repo for more information: https://github.com/JackalLabs/storage-outpost/blob/master/cross-contract/contracts/outpost-factory/src/contract.rs

Integrating Your Custom Contract with Jackal.js

When using a custom contract, you can make Jackal.js use it instead of the default Outpost Factory. It is as easy as adding a contract field to the createWasmStorageHandler function call.

myStorageHandler.createWasmStorageHandler({contract: 'YOUR_CONTRACT_ADDRESS_HERE'})