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:
Initiate Connect
Load the Widget for user interaction
Exchange temporary token for Access Token
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.
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