SafeConnect Widget

SafeConnect 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

Please use our JavaScript SDK use the Widget in your front-end withdrawal screen.


Gitlab Readme

The Gitlab readme covers the following:

  1. How to install the SDK

Note: We recommend that you specify the version when installing.

  1. How to authenticate
  2. Supported asset format
  3. How to force a re-render
  4. Error handling
  5. Customisation (covered in more detail further down on this page)
  6. Using custom asset price

Implementation examples

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


URLs

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

Developer options

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

Custom style

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

{
  primaryColor: '#fff',
  secondaryColor: '#fff',
  primaryFontColor: '#fff',
  secondaryFontColor: '#fff',
  backgroundColor: '#fff',
  fontFamily: Arial,
  logo: {LOGO_URL},
  mode: 'dark', // default: 'light'

  // If you are using the Signature flow, here is the full list of custom theme that can be applied to the WalletConnect Modal.
  '--w3m-font-family',
  '--w3m-font-feature-settings',
  '--w3m-overlay-background-color',
  '--w3m-overlay-backdrop-filter',
  '--w3m-z-index',
  '--w3m-accent-color',
  '--w3m-accent-fill-color',
  '--w3m-background-color',
  '--w3m-background-image-url',
  '--w3m-logo-image-url',
  '--w3m-background-border-radius',
  '--w3m-container-border-radius',
  '--w3m-wallet-icon-border-radius',
  '--w3m-wallet-icon-large-border-radius',
  '--w3m-wallet-icon-small-border-radius',
  '--w3m-input-border-radius',
  '--w3m-notification-border-radius',
  '--w3m-button-border-radius',
  '--w3m-secondary-button-border-radius',
  '--w3m-icon-button-border-radius',
  '--w3m-button-hover-highlight-border-radius',
}

To get a list of supported fonts, visit https://fonts.google.com/
To get more details about the WalletConnect theme variables, visit General style variables.


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

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

Field props

By default, the Widget will ask users to provide the missing information based on what is required in the originatorVASPs jurisdiction.

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

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
country☑️☑️Recipient (or Sender) Country of Residence

Example setup for fields:

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

Transaction type allowed

transactionTypeAllowed can be used to limit the type of destinations a user can withdraw to:

  • ALL - Transaction to other VASPs and unhosted wallets 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.
  • NON_CUSTODIAL_ONLY - Only transactions to unhosted wallets are allowed.

Non-custodial declaration type

Our self-hosted wallet verification tool, SafeConnect, enables customers to verify self-hosted wallet ownership on more than 200 Ethereum-based wallets + the most popular Bitcoin wallets.

nonCustodialDeclarationType sets which ownership proof you want to use:

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

After the user selects their wallet from the list above, they will be prompted to connect and then to sign a message generated by Notabene:

📘

Re-using proofs

Signature proofs can be re-used for later transfers to the same destination 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.

Fallbacks

WALLET_NOT_SUPPORTED - If a wallet does not support signing a message to produce a cryptographic signature, these are the fall-back methods available:

DECLARATION - Falls back to the self declaration flow

REJECT - Rejects the transaction and throws an error

const notabene = new Notabene({  
    widget: '<https://beta-widget.notabene.id'>,  
    container: '#container',  
    authToken: '{CUSTOMER_TOKEN}'  
    theme: {THEME},  
    dictionary: {DICTIONARY},  
    fallbacks: [{flow: "WALLET_NOT_SUPPORTED", action: "DECLARATION"}]  
});

Opt-in features

REUSE_ADDRESS_OWNERSHIP_PROOF: For a given unique customer (identified by the customerRef from the customer token used) + unique wallet address, the widget would not ask for data collection if the customer previously verified the address ownership in a previous first party self-hosted transfer.

const notabene = new Notabene({
    widget: 'https://beta-widget.notabene.id',
    container: '#container',
    authToken: '{CUSTOMER_TOKEN}'
    theme: {THEME},
    dictionary: {DICTIONARY},
    optInFeatures: ['REUSE_ADDRESS_OWNERSHIP_PROOF']
});


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;

Using the widget only for self-hosted/unhosted wallet verification

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: 'NON_CUSTODIAL_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: 'NON_CUSTODIAL_ONLY',
 nonCustodialDeclarationType: 'DECLARATION',

You also need to consider which fallback you allow for WALLET_NOT_SUPPORTED.


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