Getting Started

Connect Overview

Demo Video

Example of a User going through this flow, then initiating a Transfer.

Quickstart

Setup: Create your API Private Key and API User

Implementation:

  1. Initiate Connect

  2. Load the Widget for user interaction

  3. Exchange temporary token for Access Token

  4. Use the delegated connection!

1. Create your API Private Key and API User

You need a Private Key to sign all your API requests to Narval. EdDSA is preferred, but you can use a secp256k1 or p256 key too if you prefer.

Here’s a script to generate a new keypair. You can use an AWS KMS or other KMS too.

EdDSA KeyGen Script

npm i @narval-xyz/armory-sdk

// tsx generate-key.ts
import { Alg, generateJwk } from '@narval-xyz/armory-sdk'
import { privateKeyToHex, publicKeyToHex } from '@narval-xyz/armory-sdk/signature'

const main = async () => {
  const key = await generateJwk(Alg.EDDSA)
  const privateKeyHex = await privateKeyToHex(key)
  const publicKeyHex = await publicKeyToHex(key)

  console.log({
    key,
    privateKeyHex,
    publicKeyHex
  })
}

main()
  .then(() => console.log('done'))
  .catch(console.error)

2. Initiate Connect

Implement this in your Server-side code

// App Server
import { ConnectClient } from "@narval-xyz/armory-sdk";
interface ConnectResponse {
  interact: {
    redirect: string; // The redirect url to start the interaction.
    finish: string; // The finish nonce
  };
  continue: {
    access_token: {
      value: string; // The continue token value
    };
    uri: string; // The uri to continue the interaction.
  };
}

const apiUserPrivateKey = process.env.NARVAL_API_USER_PRIVATE_KEY;
const clientId = process.env.NARVAL_CLIENT_ID;

async function initiateNarvalConnection() {
  const connectClient = await ConnectClient.init({
    host: "<https://auth.armory.narval.xyz>",
    clientId: "xxx",
    signer: apiUserPrivateKey,
  });

  const connectRequest = {
    label: `DEV - ${userId} connect token`,
    userId: userId, // Optional, reference for your app's internal userIds
    actions: ["connection:read", "connection:delete", "connection:stake"],
    providers: ["bitgo"],
    finish: {
      uri: `my-local-app/${crypto.randomUUID()}`, // Must be unique; can be any value; only needs to be a real uri if doing a redirect callback
      nonce: crypto.randomUUID(), // Random nonce, you'll double-check this at the end as a security precaution
    },
  };
  const data: ConnectResponse = await connectClient.initiateConnect(
    connectRequest
  );

  // Persist the request & response to use at the end of the flow.
  await db.connectRequest.create({
    data: {
      finishUri: connectRequest.finish.uri,
      nonce: connectRequest.finish.nonce,
      grantResponse: data,
    },
  });

  return {
    connectUrl: data.interact.redirect,
  };
}

3. Open Widget

Implement this in your Client-side application.

npm i @narval-xyz/connect-react

Add the NarvalProvider on any pages you will use the Widget, as high in the tree as possible.

import { NarvalProvider } from '@narval-xyz/connect-react'
// ...
<NarvalProvider config={{ clientId: YOUR_CLIENT_ID }}>
  {{children}}
</NarvalProvider>

use hooks to open the widget

import { useNarvalConnect } from '@narval-xyz/connect-react'
// ... your other code
const widget = useNarvalConnect({
  debug: false,
  onSuccess: (data: { interactRef: string; hash: string; finishUri: string }) => {
    console.log('iframe on success', data)
    handleWidgetOnSuccess(data)
  },
  onExit: () => {
    console.log('iframe on exit')
  },
  onError: (error) => {
    console.log('iframe on error', error)
  }
})

const handleClick = async () => {
  // Call the server to initiate the connection
  const { connectUrl } = await fetch(/*Route to Initiate Connect handler*/)
  widget.open(connectUrl)
}
// ...
<button onClick={() => handleClick()}>Create Connection</button>

4. Get the Access Token

Implement this in your Server-side code.

interface ConnectCompleteResponse {
  connectionId: string;
  accessToken: {
    label: string;
    value: string;
  };
}

async function completeNarvalConnection(data: {
  interactRef: string, hash: string, finishUri: string
}) {
  // 1. Look up persisted connectRequest
  const connectRequest = await db.connectRequest.findUnique({
    where: {
      finishUri: data.finishUri,
    },
  });

  // 2. Validate the hash matches your expected hash
  const verificationHash = ConnectClient.verificationHash({
    nonce: connectRequest.nonce,
    finish: connectRequest.grantResponse.interact.finish,
    interactRef: data.interactRef,
    host: "<https://auth.armory.narval.xyz>",
  });

  if (data.hash !== verificationHash)
    throw new Error("Hash verification failed");

  // 3. Get your Access Token
  const response: ConnectCompleteResponse = await connectClient.completeConnect(
    {
      continueToken: connectRequest.grantResponse.continue.access_token.value,
      interactRef: data.interactRef,
    }
  );

  // Persist the response data and use `response.accessToken.value`
  // to make API calls with the Connection!
}

5. Use the Connection!

Implement this in your Server-side code

const accounts = await connectClient.listProviderAccounts({
  connectionId,
  accessToken,
})

const response = await connectClient.sendTransfer({
  connectionId,
  accessToken,
  data: {
    idempotenceId: new Date().getTime().toString(),
    source: {
      type: 'account',
      id: accountId
    },
    destination: {
      address: '0xAAA...AAA',
    },
    asset: {
      assetId: 'USDC'
    },
    amount: '1'
  }
})

Last updated