If travel rule is required, the widget enables you to easily collect information from your customer about the recipient of the virtual assets without having to make big changes to your front-end. It is usually rendered during the withdrawal process after they have entered the destination wallet address, the amount and asset type they want 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 recipients 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.


Using the widget


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/[email protected]/dist/es/index.js"></script>

Or installing the library:

yarn add @notabene/[email protected]"version number"

We recommend that you specify the version when installing.


Usage

Create a new Notabene instance:

const notabene = new Notabene({
    vaspDID: 'did:ethr:0x94c38fd29ef36f5cb2f7bc771f9d5bd9f7d05f27',
    widget: 'https://beta-widget.notabene.id',
    container: '#container',
    authToken: '{CUSTOMER_TOKEN}',
    onValidStateChange: (isValid) => {
        // Use this value to determine if the transaction is ready to be created.
        console.log('is transaction valid', isValid);
    },
    onError: (err) => {
        // If any errors are encountered, they will be passed to this function
        alert(err.message)
    }
});

📘

authToken

If you do not know how to get your customerToken, please see the guide here.

Then render the widget:

notabene.renderWidget();

To update the widget as users enter transaction details:

notabene.setTransaction({
  transactionAsset: 'ETH',
  beneficiaryAccountNumber: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819',
  transactionAmount: '2000000000',
})

📘

setTransaction

If the user changes the input after the call, you need to do setTransaction again to refresh.

It can be called as many times as needed.

Once the widget determines the transaction is valid, it will call the onValidStateChange callback, to access the transaction details:

const currentTransactionInfo = notabene.tx

Re-rendering

If you need to close and re-render the widget, call the destroyWidget method like so:

notabene.destroyWidget(); // Will remove the widget
notabene.renderWidget(); // Will re-render the widget

🚧

renderWidget

Calling the renderWidget method without destroying it first will not work.


onError

ErrorDescription
zoid destroyed all componentserror if you try to destroy a widget that is not rendered

Implementation example

Below are a few example you can use to test the widget locally, just make sure to customize {{URL}}, {{originatorVASPdid}}and {{TOKEN}}:

Web

Download a sample html file to test the widget in your browser:

Download Here

Mobile/WebView

Below is a link to an example app to test the widget in a mobile environment using React Native and WebView:

React native widget example

Code

Here is how a full implementation looks like:

// Initial required information about the transaction. Without it the widget will not render!
const transaction = {
  transactionAmount: '1000000000000000000000',
  beneficiaryAccountNumber: 'BENEFICIARY_ADDRESS',
  transactionAsset: 'eth',
}

// A validation state to be c
const transactionIsValid = false;

// This is where you save the extra information returned from the widget.
let returnedTrasaction; 

const notabene = new window.Notabene({
  vaspDID: '{{originatorVASPdid}}',
  widget: 'https://beta-widget.notabene.dev',
  container: '#widget', // The container to which the widget will be rendered.
  authToken: '{{CUSTOMER TOKEN}}',
  onValidStateChange: (isValid: boolean) => {
    
    // When this is true you can send the transaction, knowing it will be a valid
    // Travel Rule transaction
    transactionIsValid = isValid;
    
    // Save the information returned from the widget, to be used when creating the 
    // Travel Rule transaction
    returnedTransaction = notabene.tx; 
    
  },
})

notabene.setTransaction(transaction) // Set the transaction for the widget to check validity.
notabene.renderWidget() // Render the widget.

URL

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

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


Using widget payload in txCreate

"nb.tx" payload that returned isValid=true:

{
    "beneficiaryGeographicAddress": "Arkham Asylum, Arkham Island\", \"New York, USA",
    "beneficiaryVASPname": "Arkham Asylum",
    "beneficiaryVASPdid": "did:ethr:0xb51b3292fa51dab917fbd1598fc3b5392a910848",
    "beneficiaryName": "Jack Oswald White",
    "beneficiaryPersonType": "NATURAL",
    "originatorEqualsBeneficiary": false,
    "beneficiaryAccountNumber": "0x6F3970f0b88B7cc706cB493Bb354585a511123",
    "transactionAmount": "2000000000000000000000",
    "transactionAsset": "ETH",
    "originatorVASPdid": "did:ethr:0x75215d5bfc19e1a4f0301b40abcf542aa6de8613",
    "originatorDid": "did:ethr:0x16c98b43dfba3aeb3a9b56bf772b13edd4755789",
    "isNonCustodial": false,
    "transactionBlockchainInfo": {
        "destination": "0x6F3970f0b88B7cc706cB493Bb354585a511123"
    },
    "isValid": true,
    "ivms": {
        "beneficiary": {
            "beneficiaryPersons": [
                {
                    "naturalPerson": {
                        "name": [
                            {
                                "nameIdentifier": [
                                    {
                                        "primaryIdentifier": "Jack Oswald White",
                                        "nameIdentifierType": "LEGL"
                                    }
                                ]
                            }
                        ],
                        "geographicAddress": [
                            {
                                "addressLine": [
                                    "Arkham Asylum, Arkham Island\", \"New York, USA"
                                ],
                                "addressType": "GEOG"
                            }
                        ]
                    }
                }
            ],
            "accountNumber": [
                "0x6F3970f0b88B7cc706cB493Bb354585a511123"
            ]
        }
    }
}

Payload used in a full txCreate call:

{
    "transactionAsset": "ETH",
    "transactionAmount": "2000000000000000000000",
    "originatorVASPdid": "did:ethr:0x75215d5bfc19e1a4f0301b40abcf542aa6de8613",
    "travelRuleBehavior": false,
    "beneficiaryVASPdid": "did:ethr:0xb51b3292fa51dab917fbd1598fc3b5392a910848",
    "beneficiaryVASPname":"Arkham Asylum",
    "transactionBlockchainInfo": {
        "origin": "0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
        "destination": "0x6F3970f0b88B7cc706cB493Bb354585a511123"
    },
    "originator": {
        "originatorPersons": [
            {
                "naturalPerson": {
                    "name": [
                        {
                            "nameIdentifier": [
                                {
                                    "primaryIdentifier": "Wayne",
                                    "secondaryIdentifier": "Bruce"
                                }
                            ]
                        }
                    ],
                    "geographicAddress": [
                        {
                            "streetName": "streetName",
                            "buildingNumber": "buildingNumber",
                            "buildingName": "buildingName",
                            "townName": "townName",
                            "countrySubDivision":"countrySubDivision",
                            "postCode": "postCode",
                            "country": "{{$randomCountryCode}}"
                        }
                    ]
                }
            }
        ],
        "accountNumber": [
            "0xdef171fe48cf0115b1d80b88dc8eab59176fee57"
        ]
    },
    "beneficiary": {
                  "beneficiaryPersons": [
                {
                    "naturalPerson": {
                        "name": [
                            {
                                "nameIdentifier": [
                                    {
                                        "primaryIdentifier": "Jack Oswald White",
                                        "nameIdentifierType": "LEGL"
                                    }
                                ]
                            }
                        ],
                        "geographicAddress": [
                            {
                                "addressLine": [
                                    "Arkham Asylum, Arkham Island\", \"New York, USA"
                                ],
                                "addressType": "GEOG"
                            }
                        ]
                    }
                }
            ],
            "accountNumber": [
                "0x6F3970f0b88B7cc706cB493Bb354585a511123"
            ]
    }
}

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 SDK:

  /**
   * Verify a signature (Used for beneficiaryOwnerProof)
   *
   * @param signature Signed message
   * @param message Message
   * @param address Address of the signer
   * @returns True if the `address` matches with the signed proof. False otherwise.
   */
  public async verifyMessage(
    signature: string,
    message: string,
    address: string
  ): Promise<boolean> {
    return (await ethers.utils.verifyMessage(message, signature)) === address;
  }


Customization

It is possible to customize the logic of the widget questions, 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)

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

beneficiaryDetails to enable collection of additional information about the beneficiary that is not required by your jurisdiction:

  • beneficiaryName - this will add a field to collect the beneficiary name.
  • beneficiaryGeographicAddress - this will add a field to collect the beneficiary address.

Passing the variables in the configuration:

const notabene = new Notabene({
    vaspDID: 'did:ethr:0x94c38fd29ef36f5cb2f7bc771f9d5bd9f7d05f27',
    widget: 'https://beta-widget.notabene.id',
    container: '#container',
    authToken: '{CUSTOMER_TOKEN}'
    transactionTypeAllowed: 'ALL', // 'ALL', 'SELF_TRANSACTION_ONLY', 'VASP_2_VASP_ONLY'
    nonCustodialDeclarationType: 'SIGNATURE', // 'SIGNATURE', 'DECLARATION',
    beneficiaryDetails: ['beneficiaryName', 'beneficiaryGeographicAddress'],
});

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",
};

Full web sample

    <!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Notabene Widget Example</title>
  <script id="notabene" async src="https://unpkg.com/@notabene/[email protected]/dist/es/index.js"></script>
  <script>
    document.querySelector('#notabene').addEventListener('load', function () {
      const nb = new Notabene({
        vaspDID: 'did:ethr:0x049fc13a4f1e79d4d03f082ca96758179a91da29',
        widget: 'https://beta-widget.notabene.dev',
        container: '#container',
        authToken: 'eyJ0eXAiOiJKV1QiLCJhby453y443Xh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9hcGkubm90YWJlbmUuaWQvc2NoZW1hcy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiQWNjZXNzVG9rZW4iXSwiY3JlZGVudGlhbFN1YmplY3QiOnsic2NvcGUiOiJhZG1pbiJ9fSwic3ViIjoiZGlkOmV0aHI6MHhlZmJiMTRiNzM0NzNjY2ZkNjEwNTQzYWY0YjFjOGZkYTAyN2M1N2JmIiwiaXNzIjoiZGlkOmV0aHI6MHhlZmJiMTRiNzM0NzNjY2ZkNjEwNTQzYWY0YjFjOGZkYTAyN2M1N2JmIn0.g5i0kTMMgSO4HIF44w51hljoW3xMJVqmEfrlyhsqAXcKeWk3XDaVOJBstaTMoo98VapOXG9783U7A0nLIJVK7wA',
                theme: {
                      primaryColor: '#0048ba',
                      secondaryColor: '#33ff00',
                      primaryFontColor: '#e32636',
                      secondaryFontColor: '#ffbf00',
                      backgroundColor: '#9966cc',
                      fontFamily: 'Roboto',
                      logo: 'https://www.aafk.no/_/image/dcaed91b-9d1f-491c-b56b-c16b6578dc58:e425c44b30ef8b855dc6dd4f7b86ba063dd12af6/wide-72-72/aafk_gold_RGB.svg',
                      mode: 'light', // default: 'light'
                },
        onValidStateChange: (isValid) => {
          console.log('is transaction valid', isValid);
          console.log('widget payload', nb.tx)
        },
                onError: (err) => {
                 // If any errors are encountered, they will be passed to this function
                 alert(err.message);
                },
        transactionTypeAllowed: 'ALL', // 'ALL', 'SELF_TRANSACTION_ONLY', 'VASP_2_VASP_ONLY'
        nonCustodialDeclarationType: 'SIGNATURE', // 'SIGNATURE', 'DECLARATION'
                beneficiaryDetails: ['beneficiaryName', 'beneficiaryGeographicAddress'],
      });
      nb.setTransaction({
        transactionAsset: 'ETH',
        beneficiaryAccountNumber: '0x6F3970f0b88B7cc706cB493Bb385585a511123',
        transactionAmount: '222222222222222222',
      });
      nb.renderWidget();
    });
  </script>
</head>

<body>
  <div id="container">
</body>

</html>


Using the widget only for self-hosted wallet verification

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

First party --> your customer is sending to their own self-hosted wallet

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

Third party --> your customer is sending to someone else's self-hosted wallet

 transactionTypeAllowed: 'SELF_TRANSACTION_ONLY',
 nonCustodialDeclarationType: 'DECLARATION',

More example projects