Skip to main content

Demo app

In order to showcase what is possible with Arcana we built this proof of concept application which you can play around with here - https://demo.arcana.network/

In case you want to try integrating the Arcana SDKs into this front end itself here is the GitHub repo for it - https://github.com/arcana-network/sdk-demo

Get Started#

Installation and setup#

Get started by cloning the repository and running npm install inside the project directory to install node packages. Make sure you are using the latest LTS version of Node (v16).

git clone git@github.com:arcana-network/sdk-demo.git
cd sdk-demo
npm install

Once everything is installed and ready, start the development server and go to http://localhost:3000 to see the app login page.

npm run dev

Great! Before we get started integrating the Arcana SDKs, we'll need to create and configure an OAuth Client ID to enable sign-in with Google and create an app on the Arcana Dashboard.

Create a Google OAuth Client ID#

  • Go to the Google Cloud Console and create a new OAuth Client ID.
  • Remember to set the origin to [http://localhost:3000](http://localhost:3000) and the redirect url to http://localhost:3000/auth/redirect.
  • Copy and save the client id.

Create an Arcana App#

  • Go to the Arcana Dashboard and create a new account if you don't have one.
  • Create a new app. Select login type as "Google" and paste the client id from the previous step.
  • Once the app is created, note down the app id.

Configure environment variables#

Copy the .env.example file to .env and fill in the app id and google client id.

cp .env.example .env

You may need to restart the dev server for the .env changes to get picked.

User authentication#

To implement the user authentication workflow using Arcana's auth SDK, start by installing the @arcana/auth package.

npm install @arcana/auth

We'll use labeled comments to work through the implementation details. You can follow along by searching for the comment label mentioned in the code snippets.

First, go to use/arcanaAuth.js and import the AuthProvider class from @arcana/auth and create an instance.

// In src/use/arcanaAuth.js
// Import the AuthProvider class.
import { AuthProvider } from "@arcana/auth";
// AUTH-1: Create an instance of Arcana AuthProvider.
const authInstance = new AuthProvider({
appID: ARCANA_APP_ID,
network: "testnet",
oauthCreds: [
{
type: "google",
clientId: GOOGLE_CLIENT_ID,
},
],
redirectUri: `${window.location.origin}/auth/redirect`,
});

Here's a breakdown of the configuration parameters:

  • appID: This is the app id associated with your Arcana app.
  • network: The network we'll be using for authentication. Set this to "testnet" for now.
  • oauthCreds: A list of configs for each OAuth provider you intend to use in your app. Since we'll start with Google OAuth, we'll add a config for it here.
  • redirectUri: The page where the OAuth provider should redirect to on successful authentication. Make sure this matches the URI you configured in the Google Cloud Console.

The authInstance has a number of helpers for the entire authentication workflow.

Check if a user is logged in

When a user revisits a page they have logged into previously, we may skip ahead to the sign in process without requiring any user actions. The authInstance object has a helper to check if the user can be signed in directly.

// In src/use/arcanaAuth.js
function isLoggedIn() {
// AUTH-2: Check if the user is already logged in.
return authInstance.isLoggedIn();
}

Sign in a user

To sign in a user, we go through the following steps:

  • Check if the user has a an active session. If so, then skip ahead to fetching the user's information.
  • If the user is not already signed in, call authInstance.loginWithSocial("google") to trigger Google's sign-in process. This will open a popup window and ask the user to sign in with their Google credentials. On success, the popup will redirect to URL configured in on the Google Cloud Console.
  • Make sure to handle the auth flow on the redirect page to capture the user tokens by calling authInstance.handleRedirectPage with the current window location in the next step.
// In src/use/arcanaAuth.js
// AUTH-3: Sign in a user.
if (!isLoggedIn()) {
store.dispatch("showLoader", "Logging in...");
// AUTH-3a: If user does not have an active session,
// trigger the Google authentication process.
await authInstance.loginWithSocial("google");
}
// In src/use/arcanaAuth.js
function handleRedirect() {
// AUTH-4: Handle auth flow on the redirect page.
AuthProvider.handleRedirectPage(window.location);
}

Once signed-in, we can fetch and save the user's information like their name, email address and profile picture.

// In src/use/arcanaAuth.js
// AUTH-3b: Fetch the user's information and save it.
const { userInfo, privateKey } = await authInstance.getUserInfo();
store.dispatch("addBasicDetails", {
email: userInfo.id,
profileImage: userInfo.picture,
givenName: userInfo.name,
});

Similarly, we can get the user's public key and create a wallet for signing transactions. Note that we use the padPublicKey utility that takes the public key object returned from getPublicKey and turns it into a padded string.

// In src/use/arcanaAuth.js
// AUTH-3c: Fetch the user's public key and create a wallet.
const publicKey = await authInstance.getPublicKey({
verifier: "google",
id: userInfo.id,
});
const actualPublicKey = padPublicKey(publicKey);
const wallet = new Wallet(privateKey);
store.dispatch("addCryptoDetails", {
walletAddress: wallet.address,
privateKey: privateKey,
publicKey: actualPublicKey,
});

Sign out logged in users

Finally, we should allow signed-in users to log out from the app.

// In src/use/arcanaAuth.js
async function logout() {
// AUTH-5: Log a user out.
await authInstance.logout();
store.dispatch("clearStore");
}

That's it! Let's go through the entire process again and see how the app connects these individual pieces.

  • If a user lands on the AppLogin page or clicks the sign in button, we call the login method to trigger the authentication process.
  • At the end of Google's authentication flow, the popup redirects to the AuthRedirect page, which calls the handleRedirect function to capture the user tokens.
  • After successful sign in, we save the user's profile details and create a wallet with their private and public keys. This wallet will be used later for signing transactions.
  • If the user clicks the sign out button on the UserProfile component, we call the logout methods and clear the session storage.

File storage and sharing#

Go ahead install the @arcana/storage package to get started with implementing the file storage and sharing functionality.

npm install @arcana/storage

Similar to the way we created a use/arcanaAuth.js file to encapsulate user authentication methods, we'll be using the use/arcanaStorage.js file to implement methods for uploading and downloading a file, and sharing file with other users.

Go to use/arcanaStorage.js and create an instance of the Arcana class from @arcana/storage.

// In src/use/arcanaStorage.js
// Import the Arcana class and rename to StorageProvider.
import { Arcana as StorageProvider } from "@arcana/storage/dist/standalone/storage.umd";
onBeforeMount(() => {
if (!storageInstanceRef.value) {
// STORAGE-1: Create an instance of Arcana StorageProvider.
storageInstanceRef.value = new StorageProvider({
appId: ARCANA_APP_ID,
privateKey: store.getters.privateKey,
email: store.getters.email,
});
}
storageInstance = storageInstanceRef.value;
});

In the configuration, we provide:

  • appId: The associated Arcana app id.
  • privateKey: The authenticated user's private key.
  • email: The authenticated user's email address.

With that out of the way, we are ready to use the storageInstance methods to implement the file related operations.

Get a user's upload and download limits

Based on the app configuration, each user may be limited to certain upload and download limits. To get the current usage for the authenticated user, we may use the getUploadLimit and getDownloadLimit functions.

// In src/use/arcanaStorage.js
async function fetchStorageLimits() {
// STORAGE-2: Fetch the user's upload and download limits.
const access = await storageInstance.getAccess();
const [storageUsed, totalStorage] = await access.getUploadLimit();
const [bandwidthUsed, totalBandwidth] = await access.getDownloadLimit();
store.dispatch("updateStorage", {
totalStorage,
storageUsed,
});
store.dispatch("updateBandwidth", {
totalBandwidth,
bandwidthUsed,
});
}
  1. We get the access object by calling storageInstance.getAccess(). The access object has convenient methods for several storage actions.
  2. We call access.getUploadLimit() to get the authenticated user's current file storage usage and call access.getDownloadLimit() to get the current bandwidth usage.
  3. We save limits fetched to our store for later use.

Get a user's uploaded files

Users can upload files to using the storage SDK. To fetch a user's uploaded files, use the myFiles function on the storageInstance.

// In src/use/arcanaStorage.js
async function fetchMyFiles() {
store.dispatch("showLoader", "Fetching uploaded files...");
// STORAGE-3: Fetch the user's uploaded files.
const myFiles = await storageInstance.myFiles();
store.dispatch("updateMyFiles", myFiles);
store.dispatch("hideLoader");
}

Get a user's shared files

Arcana's storage SDK allows sharing files with other users. To fetch files shared with the user, use the sharedFiles function on the storageInstance.

// In src/use/arcanaStorage.js
async function fetchSharedFiles() {
store.dispatch("showLoader", "Fetching shared files...");
// STORAGE-4: Fetch the user's uploaded files.
const sharedFiles = await storageInstance.sharedFiles();
store.dispatch("updateSharedWithMe", sharedFiles);
store.dispatch("hideLoader");
}

Try out these methods on the app. Login using your Google credentials and navigate to the "My Files" page from the sidebar.

When the page is loaded, we fetch the user's storage limits and use it to update the storage and bandwidth statuses at the bottom of the sidebar. Next, we fetch the user's uploaded files and display it in the files list.

// In src/pages/MyFiles.vue
export default {
name: "MyFiles",
setup() {
onMounted(async () => {
...
await fetchStorageLimits();
await fetchMyFiles();
});
},
...
};

A similar setup happens on navigating to the "Shared With Me" page. Instead of fetching the user's uploaded files, we fetch and display files shared with the user.

Since we haven't uploaded any files yet, there isn't much to see yet. Let's implement uploading and downloading of files next.

Upload a file

To upload a file, we start by getting an uploader by calling storageInstance.getUploader().

// In src/use/arcanaStorage.js
async function upload(fileToUpload) {
store.dispatch("showLoader", "Encrypting file...");
let uploadStart = Date.now(),
uploadDate = new Date(),
totalSize,
did;
// STORAGE-5: Upload a file.
store.dispatch("showLoader", "Uploading file to distributed storage...");
const uploader = await storageInstance.getUploader();
// ... continued ...
}

Next we call uploader.upload by passing in the file blob. This starts the upload process.

// In src/use/arcanaStorage.js
// ... continued ...
uploader.upload(fileToUpload).then((fileDid) => {
did = fileDid;
});
// ... continued ...

For a better user experience, we use pass a handler uploader.onProgress to show the upload progress.

// In src/use/arcanaStorage.js
// ... continued ...
uploader.onProgress = (uploaded, total) => {
store.dispatch(
"showLoader",
`Uploaded ${bytes(uploaded)} out of ${bytes(total)}`
);
totalSize = total;
};
// ... continued ...

Any errors during the upload process trigger the uploader.onError function. Here we can check for common errors such as the user running out of space or failure to authenticate.

// In src/use/arcanaStorage.js
// ... continued ...
uploader.onError = (error) => {
if (error.message === NO_SPACE) {
toast(
"Upload failed. Storage limit exceeded. Upgrade your account to continue",
errorToast
);
} else if (error.code === UNAUTHORIZED) {
toast("Upload failed. Kindly login and try again", errorToast);
} else {
toast("Something went wrong. Try again", errorToast);
}
store.dispatch("hideLoader");
};
// ... continued ...

On success, the uploader.onSuccess function is called. This is a good place to re-fetch the user's storage limits, save the uploaded file in the local files lists and celebrate with a success message.

// In src/use/arcanaStorage.js
// ... continued ...
uploader.onSuccess = () => {
// Re-fetch the user's storage limits.
fetchStorageLimits();
// Save the uploaded file in the local files list.
let myFiles = [...store.getters.myFiles];
myFiles.push({
did,
createdAt: uploadDate,
size: totalSize,
});
store.dispatch("updateMyFiles", myFiles);
// Announce the upload success.
toast("Upload Success", successToast);
toast(
"Transaction successfully updated in arcana network's blockchain",
successToast
);
store.dispatch("hideLoader");
};

With that we have completed our upload functionality. Go ahead use the floating "Upload File" button at the bottom right of the screen to upload a file. On success, the uploaded file appears in the files list with a pseudonymous file id, size and a set of actions to download, share and delete the file.

Download a file

Downloading a file follows the same pattern as uploading. We start by getting a downloader by calling storageInstance.getDownloader().

// In src/use/arcanaStorage.js
async function download(file) {
store.dispatch(
"showLoader",
"Downloading chunks from distributed storage..."
);
let did = file.fileId;
did = did.substring(0, 2) !== "0x" ? "0x" + did : did;
// STORAGE-6: Download a file.
const downloader = await storageInstance.getDownloader();
// ... continued ...
}

Using the file did, we trigger the download process by calling downloader.download(did).

// In src/use/arcanaStorage.js
// ... continued ...
downloader.download(did);
// ... continued ...

On progress, we show downloaded bytes out of the total file size.

// In src/use/arcanaStorage.js
// ... continued ...
downloader.onProgress = (downloaded, total) => {
store.dispatch(
"showLoader",
`Completed ${bytes(downloaded)} out of ${bytes(total)}`
);
};
// ... continued ...

If an error occurs, we catch in onError and show an appropriate error message.

// In src/use/arcanaStorage.js
// ... continued ...
downloader.onError = (error) => {
if (error.message === NO_SPACE) {
toast(
"Download failed. Bandwidth limit exceeded. Upgrade your account to continue",
errorToast
);
} else if (error.code === UNAUTHORIZED) {
toast("Seems like you don't have access to download this file", errorToast);
} else {
toast("Something went wrong. Try again", errorToast);
}
store.dispatch("hideLoader");
};
// ... continued ...

On success, re-fetch the user's storage limits and show a success message.

// In src/use/arcanaStorage.js
// ... continued ...
downloader.onSuccess = () => {
fetchStorageLimits();
toast("All chunks downloaded", successToast);
toast(
"Transaction successfully updated in arcana network's blockchain",
successToast
);
store.dispatch("hideLoader");
};

Give it a whirl by clicking the download action for a file from the files list. Notice how the bandwidth status changes after a file has been successfully downloaded.

Sharing a file with other users

Arcana's storage platform allows sharing uploaded files with other users. To share a file, we use the access object from storageInstance.getAccess and call its share method. The share method takes the file did, the public key of the recipient and a validity param that controls how the long the user will have access to the shared file.

// In src/use/arcanaStorage.js
async function share(fileToShare, email) {
store.dispatch("showLoader", "Sharing file...");
let did = fileToShare.fileId;
did = did.substring(0, 2) != "0x" ? "0x" + did : did;
const publicKey = await authInstance.getPublicKey({
verifier: "google",
id: email,
});
const actualPublicKey = padPublicKey(publicKey);
const validity = 1000000;
try {
// STORAGE-7: Share a file.
const access = await storageInstance.getAccess();
await access.share([did], [actualPublicKey], [validity]);
toast(`Shared file successfully with ${email}`, successToast);
} catch (error) {
toast("Something went wrong. Try again", errorToast);
}
store.dispatch("hideLoader");
}

Use the share action for a file from the files list and enter an alternate email address to try out the share functionality. Once sharing is successful, you can log out and log in to the alternate email address. Go to the "Shared with Me" page and you should see the shared file in the list.

Get shared users and revoke access

After sharing a file, the user may wish to revoke access. We can accomplish this is two steps: firstly, for a file, fetch the list of users that the file has been shared with and second, select a user and revoke their access.

To get the list of shared users, we once again use the access object and call the getSharedUsers function with the file id.

// In src/use/arcanaStorage.js
async function getSharedUsers(did) {
try {
// STORAGE-8: Get a list of shared users.
const access = await storageInstance.getAccess();
const fileId = did.substring(0, 2) !== "0x" ? "0x" + did : did;
const users = await access.getSharedUsers(fileId);
return users;
} catch (error) {
toast("Something went wrong while fetching shared users list", errorToast);
}
}

To revoke access, we call access.revoke with the file id and the email address of the shared user.

// In src/use/arcanaStorage.js
async function revoke(fileToRevoke, address) {
store.dispatch("showLoader", "Revoking file access...");
const did = fileToRevoke.fileId;
const fileId = did.substring(0, 2) !== "0x" ? "0x" + did : did;
try {
// STORAGE-9: Revoke access to a shared file.
const access = await storageInstance.getAccess();
await access.revoke(fileId, address);
toast(`File Access Revoked`, successToast);
} catch (error) {
toast("Something went wrong. Try again", errorToast);
}
store.dispatch("hideLoader");
}

Use the revoke action to selected from a list of shared users and remove access.

Delete a file

The final functionality to round out our app is the ability to delete an uploaded file. We call deleteFile on the access object with the did of the file to be deleted. On success, we re-fetch the user's storage limits and remove the deleted file from the local files list.

// In src/use/arcanaStorage.js
async function remove(fileToDelete) {
store.dispatch("showLoader", "Deleting file...");
let did = fileToDelete.fileId;
did = did.substring(0, 2) != "0x" ? "0x" + did : did;
try {
// STORAGE-10: Delete a file.
const access = await storageInstance.getAccess();
await access.deleteFile(did);
fetchStorageLimits();
let myFiles = [...store.getters.myFiles];
myFiles = myFiles.filter((file) => file.fileId !== fileToDelete.fileId);
store.dispatch("updateMyFiles", myFiles);
toast("File deleted", successToast);
} catch (error) {
toast("Something went wrong. Try again", errorToast);
}
store.dispatch("hideLoader");
}