The Widget is a Javascript SDK that enables VASPs to easily collect information from your customer about the virtual assets' recipient without making significant changes to your front-end. It is usually rendered at point of transaction by passing it the destination wallet address, the amount, and the asset type a user wish to transfer


If the fiat value of the virtual assets being sent is above the threshold for travel rule, the Widget will help collect all the information about the recipient that is required in your jurisdiction:

If your customer is sending virtual assets to themselves (by selecting the "Yes, I own this account"-checkbox), it will not collect the recipient's name or other details as that should already be known to you and available in the KYC database.

Likewise, if the destination wallet address is already known, it will not ask the customer to select where the wallet is hosted.

JavaScript SDK

This library is the JavaScript SDK for loading the Widget on a front-end.


Installation

There are two options for loading the Notabene SDK:

<script id="notabene" async src="https://unpkg.com/@notabene/javascript-sdk@version_number/dist/es/index.js"></script>

Or installing the library:

yarn add @notabene/javascript-sdk@"version_number"

We recommend that you specify the version when installing.


Usage

📘

authToken

Kee in mind to use customerToken if you do not know how to get your customerToken, please see the guide here.

Look at the usage section on the GitLab readme and our implementation examples below to learn how to use and test the Widget.

Implementation examples

Below are a few examples you can use to test the Widget locally, make sure to customize:


URL

Production URL: https://beta-widget.notabene.id

Testing URL: https://beta-widget.notabene.dev


Using widget payload in txCreate

When the Widget returns isValid=**true**the "notabene.tx" object contains all the necessary data to create a transaction apart from the originator data.

To correctly structure the body of the txCreate request, you have to extract the beneficiary data from the IVMS object, the transaction data is available in the notabene.tx object, and only the originator data has to be added retrieving it from you internal records.

Take a look at the example below to see how to orchestrate the data for a successful txCreate request:

const tx = {
  originator: {} // HERE you need to fill the originator info
  beneficiary: returnedTransaction.ivms.beneficiary,
};

const payload: TransactionCreateRequest = {
  ...returnedTransaction,
  ...tx,
};

Following is a pseudo code of how we transform the tx returned by the widget and call txCreate in our internal testing application that implements the widget.

/ buildPerson is a helper that builds the ivms object

const tx = {
  transactionBlockchainInfo: {
    origin: originatorAddress,
    destination: address,
  },
  originator: {
    ...buildPerson("originator", {
      name: originatorName,
      country: originatorCountry,
      nationalId: originatorId,
      blockchainAddress: originatorAddress,
      dateOfBirth: originatorDate,
    }),
  },
  beneficiary: widgetPayload?.originatorEqualsBeneficiary
    ? buildPerson("beneficiary", {
        name: originatorName,
        country: originatorCountry,
        nationalId: originatorId,
        blockchainAddress: address,
        dateOfBirth: originatorDate,
      })
    : widgetPayload?.ivms.beneficiary,
};

const txCreatePayload = {
  ...widgetPayload,
  ...tx,
};

Customization

It is possible to customize the logic of the Widget by setting the following parameters.

Type of Widget

It's possible to load two forms depending on the use case, the WITHDRAWAL and the POST_DEPOSIT form.

  • notabene.renderWidget();ornotabene.renderWidget('WITHDRAWAL'); will render the form used to collect beneficiary data pre-transaction;
  • notabene.renderWidget('POST_DEPOSIT'); will render a form that is used to collect missing data about the originator in a transaction created using txNotify;

Functionality

transactionTypeAllowed for limiting the type of transaction destinations you can pass:

  • ALL - All transaction destinations are allowed. (DEFAULT)
  • VASP_2_VASP_ONLY - Only transaction destinations that are VASPs are allowed.
  • SELF_TRANSACTION_ONLY - Only transaction destinations that the originator owns are allowed.

nonCustodialDeclarationType for deciding which ownership proof type you want to use:

  • SIGNATURE - The ownership proof will be signed by the originator using a wallet. (DEFAULT)

🚧

Supported Networks for signature

Cryptographic signature is supported by the following networks: Polygon, Ethererum, arbitrum-one and optimism.

More will be added

Signature proofs can be re-used, as demonstrated in this video; note that the customerRef used to get a customerToken has to be consistent and unique for every customer on your platform.


  • DECLARATION - The ownership proof will be declared by the originator using a checkbox.

Fields and validation

By default, the Widget will ask the user to provide the missing information based on the originatorVASP jurisdiction, and those input fields are required to complete the flow.

Is it possible to request the user to provide additional information using forceDisplay, is also possible to make a specific field optional by using optional

Possible fields customization support:

field nameforceDisplayoptionaldescription
counterparty--☑️Counterparty VASP or Wallet (destination or originator)
dateAndPlaceOfBirth☑️--Recipient (or Sender) Date and Place of Birth
geographicAddress☑️☑️Recipient (or Sender) Address
name☑️--Recipient (or Sender) Name
nationalIdentification☑️--️Recipient (or Sender) National Identification

Example setup for fields:

   fieldsProps: {  
     geographicAddress: {  
       forceDisplay: true,  
       optional: true  
     },  
     counterparty: {  
       optional: true  
     }  
   }

Theme

Here you can pass configuration which will customize the styles of the Widget to meet your brand needs, all values are optional:

{
  primaryColor: '#0048ba', //botton and checkbox
  secondaryColor: '#ff0000',
  primaryFontColor: '#e32636',
  secondaryFontColor: '#ffbf00',
  backgroundColor: '#9966cc',
  fontFamily: 'Roboto',
  logo: {LOGO_URL},
  mode: 'dark', // default: 'light'
}

To get a list of supported fonts, visit here.


Dictionary

Pass a dictionary object mapping in text in the Widget to your own language, for example:

To replace Recipient's physical address with a different text:

 authToken: '..Bz6_qCOyQ7tomzGjzxMpTifylRYxPm1x1gwYQfecD6-CGGawmZ4u2bIvu2wxh55xehqanCrk_aP6pXB6ZXMTPgA',
        dictionary: {
          "Recipient's physical address": "Recipient's street, city, postal, country",
        },

 onValidStateChange: (isValid) => {
          console.log(isValid)
          console.log(nb.tx)

Here is a list of all the text you can change:

Withdrawal / deposit lables = {
WITHDRAWAL: {
		address: "Recipient's physical address",
		checkingInfo: "Checking recipient information...",
		collectInfo:
			"Due to regulations, we're required to collect additional information about the recipient.",
		confirmPartyAddress:
			"I confirm the beneficiary's address belongs to the beneficiary person.",
		name: "Recipient's full name",
		partyInfo: "Recipient Information",
		selectDestination: "Where is recipient's wallet hosted?",
		selectPartyVasp: "Select recipient's VASP",
		selectParty: "Select recipient entity",
		transferFromOwnAccount: "Transferring to an account you own?",
	},
    
	POST_DEPOSIT: {
		address: "Sender's physical address",
		checkingInfo: "Checking sender information...",
		collectInfo:
			"Due to regulations, we're required to collect additional information about the person who has sent you virtual assets before we can deposit them to your account.",
		confirmPartyAddress:
			"I confirm the sender's address belongs to the sender person.",
		name: "Sender's full name",
		partyInfo: "Sender Information",
		selectDestination: "Where is sender's wallet hosted?",
		selectPartyVasp: "Select sender's VASP",
		selectParty: "Select sender entity",
		transferFromOwnAccount: "Was this transferred from an account you own?",
	}
},
commonLabels = {
	additionalInfo: "Additional Information Required",
	agreeTerms: "I agree to the terms below",
	agreeShareInfo:
		"By sending this transfer you agree to share your personal information (full name, physical address, national identification information) with the receiving exchange.",
	cancel: "Cancel",
	countryOfIssue: "Country of Issue",
	confirmAddress: "I confirm this is my address",
	connectWallet: "CONNECT WALLET",
	continue: "Continue",
	destination: "Destination",
	enterRegistrationAuthority: "Enter authority name",
	enterIdNumber: "Enter ID number",
	enterPlaceOfBirth: "Enter place of birth",
	error: "An error occurred",
	errorSorry: "Sorry for the inconvenience.",
	dateOfBirth: "Date of Birth",
	dateOfBirthFormat: "mm/dd/yyyy",
	enterName: "Enter name",
	enterAddress: "Enter physical address",
	legalPerson: "Legal Person",
	loading: "Loading...",
	messageSigned: "Message successfully signed",
	nationalIdType: "National Identification Type",
	nationalIdNumber: "National Identification Number",
	naturalPerson: "Natural Person",
	nonCustodialWallet: "Non-custodial wallet (Argent, Metamask, Rainbow, etc.)",
	noOptions: "No options",
	placeOfBirth: "Place of Birth",
	readyToSend: "Ready to Send",
	registrationAuthority: "Registration Authority",
	ownAccount: "Yes, I own this account",
	selectCountryOfIssue: "Select country of issue",
	selectIdType: "Select ID type",
	type: "Type",
	verifyWallet:
		"Please verify your non-custodial wallet by following the steps below.",
	reconnect: "RE-CONNECT",
	signMessage: "SIGN MESSAGE",
	signYourWallet: "Please sign on your wallet",
	selectWallet: "Select a wallet to connect and sign the message with.",
	transferDetails: "Transfer Details",
	tryAgain: "Try again",
	walletVerification: "Wallet verification",
};

Using the widget for self-hosted wallet verification only

If you wish to use the Widget only for a self-hosted wallet flow, use one following customization options:

The setting below will only allow first-party transactions, and all non-custodial transactions will require a signature proof:

transactionTypeAllowed: 'SELF_TRANSACTION_ONLY', 
nonCustodialDeclarationType: 'SIGNATURE',

The setting below will allow first, and third-party transactions only, first-party non-custodial transactions will require a signature proof, and third-party non-custodial transactions will require a self-declaration.

 transactionTypeAllowed: 'ALL',
 nonCustodialDeclarationType: 'DECLARATION',

Re-using wallet proof

To allow a customer to reuse the cryptographic signature in subsequent withdrawals, you need to always use a customerToken where the customerRef is a unique and constant identifier of that customer.

Validating unhosted wallet proof

After a user has signed a message which certifies that they control the wallet address:

The widget payload will contain the message details inside "beneficiaryProof":

beneficiaryAccountNumber: "0xeC96782057A6ddEa4D0E1ed74A152E8c4f5fb6D3"
beneficiaryDid: "did:ethr:0xbcd807f27af34172641d481c873e9902f6917b65"
beneficiaryProof: 
    address: "0xeC96782057A6ddEa4D0E1ed74A152E8c4f5fb6D3"
    attestation: "I certify that\n\nETH account 0xeC96782057A6ddEa4D0E1ed74A152E8c4f5fb6D3\n\nbelonged to did:ethr:0xbcd807f27af34172641d481c873e9902f6917b65\n\non Wed, 02 Nov 2022 02:58:11 GMT"
    proof: "0xd5dd487ec1ee1620b5cb810841682dde049cc6dfad06394296f37788174f5c4c7df789c4b987a1a140f56e3438e15c70cc0f76c22b655d97b0b5e09ae53d6e2b1b"
    type: "eip-191"
isNonCustodial: true
isValid: true
originatorDid: "did:ethr:0xbcd807f27af34172641d481c873e9902f6917b65"
originatorEqualsBeneficiary: true
originatorVASPdid: "did:ethr:0x940a4b2a0932733b842e4aa906761bb3d3bd8148"
transactionAmount: "15000000000"
transactionAsset: "ETH"
transactionBlockchainInfo: 
	{destination: '0xeC96782057A6ddEa4D0E1ed74A152E8c4f5fb6D3'}

This means that the signature ("proof") can be validated using the ethers or viem. Here is an example using ethers:

const { verifyMessage } = require("ethers");

const verify = async ({ message, address, signature }) => {
  try {
    const signerAddr = await verifyMessage(message, signature);
    const result = signerAddr.toLowerCase() === address.toLowerCase();
    return { result, signerAddr };
  } catch (err) {
    console.log(err);
    return { error: err.message };
  }
};

const signature =
  "0xd5dd487ec1ee1620b5cb810841682dde049cc6dfad06394296f37788174f5c4c7df789c4b987a1a140f56e3438e15c70cc0f76c22b655d97b0b5e09ae53d6e2b1b";
const message =
  "I certify that\n\nETH account 0xeC96782057A6ddEa4D0E1ed74A152E8c4f5fb6D3\n\nbelonged to did:ethr:0xbcd807f27af34172641d481c873e9902f6917b65\n\non Wed, 02 Nov 2022 02:58:11 GMT";
const address = "0xeC96782057A6ddEa4D0E1ed74A152E8c4f5fb6D3";

verify({ message, address, signature }).then(
  ({ result, signerAddr, error }) => {
    if (result) {
      console.log(
        "Signature is valid. Signer Address:",
        signerAddr,
        "=",
        address
      );
    } else {
      console.log(
        "Signature is not valid. Signer Address:",
        signerAddr,
        "!=",
        address
      );
    }
    if (error) {
      console.log("Error: " + error);
    }
  }
);

//This returns:
//Signature is valid. Signer Address: 0xeC96782057A6ddEa4D0E1ed74A152E8c4f5fb6D3 = 0xeC96782057A6ddEa4D0E1ed74A152E8c4f5fb6D3