Skip to main content

NeonWallet - Using Universal Provider to Connect to a dApp

coz.io ·  · 10 min read

NeonWallet uses the WalletConnect protocol, an open-source standard for connecting crypto wallets to dApps. It uses QR codes or deep-links to establish secure, encrypted sessions, enabling users to interact with dApps without exposing their private key.

1. Prerequisites#

2. Setup#

Add the following dependency to your project:

npm i @walletconnect/universal-provider

3. Initializing Universal Provider#

Universal Provider has a lot of initialization options, but the ones you'll need to use are:

  • projectId: a valid project ID, otherwise you won't be able to use WalletConnect;
  • metadata: information about your dApp and what will be shown on the wallet.
import UniversalProvider from "@walletconnect/universal-provider";
let provider: UniversalProvider;
async function initProvider() {  provider = await UniversalProvider.init({    projectId: "Project ID from https://cloud.reown.com/",    metadata: {      // Replace with actual dApp metadata      name: "dApp name",      description: "dApp description",      url: "https://link.to.your/project/",      icons: ["https://link.to.your/icon/"],    },  });}

4. Connecting to Neo blockchain#

Connecting the provider is quite easy, use the connect method and specify the following namespace:

async function startConnection() {  await provider.connect({    namespaces: {      neo3: {        methods: [          "invokeFunction",          "testInvoke",          "signMessage",          "verifyMessage",        ],        // If you want to connect to the testnet, then use `"neo3:testnet"` chain instead        chains: ["neo3:mainnet"],        events: ["connect", "disconnect"],      },    },  });}

This method should emit the display_uri event that provides the WalletConnect URI needed to initiate a connection with a wallet. To facilitate the experience we recommend using the NeonWallet connect page, but if you wish, you could use another solution or create your own.

provider.on("display_uri", (uri: string) => {  window.open(`https://neon.coz.io/connect?uri=${uri}`, "_blank")?.focus();});

Using the NeonWallet connect page, the user can connect the dApp in 3 different ways: deep-link, QR Code, or copy and paste URI.

Neon COZ connect page

After the user accepts the connection, the connect event will be emitted with the session information. You'll likely want to update your UI to reflect that the user is connected, and you can do that by listening to the event:

provider.on("connect", (connection: any) => {  console.log("Connected:", connection);
  // Here you can update your UI to reflect that the user is connected  changeUIToConnectedState();});

5. Invoking smart contracts with requests#

Now that you are connected, you can start making requests to the blockchain. To interact with a smart contract, you can use the testInvoke and invokeFunction methods.

testInvoke:#

If you want to get a value from the smart contract without persisting a transaction, you can use the testInvoke method. This method is useful for checking the state of a smart contract or getting a value without modifying the blockchain. This method doesn't cost any GAS, so you can use it freely.

async function testInvokeExample() {  if (!provider.session) {    console.error("Provider session is not initialized.");    return;  }
  // This should be something like: "neo3:mainnet:NNLi44dJNXtDNSBkofB48aTVYtb1zZrNEs"  const sessionNamespace = provider.session.namespaces.neo3.accounts[0];  const currentChain =    sessionNamespace.split(":")[0] + ":" + sessionNamespace.split(":")[1];  const currentAccount = sessionNamespace.split(":")[2];
  const testInvokeResponse = await provider.request(    {      method: "testInvoke",      params: {        invocations: [          {            scriptHash: "0xd2a4cff31913016155e38e474a2c06d08be276cf",            operation: "balanceOf",            args: [              {                type: "Hash160",                value: currentAccount,              },            ],          },        ],      },    },    currentChain  );
  console.log(testInvokeResponse);}

invokeFunction:#

If you want to invoke a smart contract and persist the transaction on the blockchain, you can use the invokeFunction method. This method will cost GAS, so make sure that the user has enough GAS in their wallet to cover the transaction fees.

async function invokeFunctionExample() {  if (!provider.session) {    console.error("Provider session is not initialized.");    return;  }
  // This should be something like: "neo3:mainnet:NNLi44dJNXtDNSBkofB48aTVYtb1zZrNEs"  const sessionNamespace = provider.session.namespaces.neo3.accounts[0];  const currentChain =    sessionNamespace.split(":")[0] + ":" + sessionNamespace.split(":")[1];  const currentAccount = sessionNamespace.split(":")[2];
  const invokeTxId = await provider.request(    {      method: "invokeFunction",      params: {        invocations: [          {            scriptHash: "0xd2a4cff31913016155e38e474a2c06d08be276cf",            operation: "transfer",            args: [              {                type: "Hash160",                value: currentAccount,              },              {                type: "Hash160",                value: currentAccount,              },              {                type: "Integer",                value: "100",              },              {                type: "String",                value: "Test transfer from WalletConnect Universal Provider",              },            ],          },        ],      },    },    currentChain  );
  console.log(invokeTxId);}

It's a good practice to check if the dApp has an account connected to it by checking if the provider session is not undefined before making requests, as shown in the examples above.

The parameters for the invokeFunction and testInvoke methods are structured the same way, both of them expect a ContractInvocationMulti from the NeonDappKitTypes package.

`invokeFunction` and `testInvoke` method parameters
export type ContractInvocationMulti = {  signers?: Signer[]  invocations: ContractInvocation[]  extraSystemFee?: number  systemFeeOverride?: number  extraNetworkFee?: number  networkFeeOverride?: number}

ContractInvocationMulti type is used to define one or multiple invocations that can be made in a single request. It allows you to specify multiple invocations, signers, and fees.

Fields like: signers, extraSystemFee, systemFeeOverride, extraNetworkFee, and networkFeeOverride are optional and can be used to customize the invocation. If not provided, the default values will be used based on the wallet's configuration and the network.

The only required field is invocations, which is an array of ContractInvocation objects. Each ContractInvocation object defines a single invocation to a smart contract.

export type ContractInvocation = {  scriptHash: string  operation: string  abortOnFail?: boolean  args?: Arg[]}

ContractInvocation has the following fields:

  • scriptHash: the hash of the smart contract to invoke;
  • operation: the name of the method to invoke on the smart contract;
  • abortOnFail: a boolean that indicates whether the invocation should abort if it fails.
  • args: an array of arguments to pass to the smart contract operation. Each argument is of type Arg, which represents the data types supported by the smart contract defined by NEP-25;
export type AnyArgType = { type: "Any"; value: any }export type StringArgType = { type: "String"; value: string }export type BooleanArgType = { type: "Boolean"; value: boolean }export type PublicKeyArgType = { type: "PublicKey"; value: string }export type Hash160ArgType = { type: "Hash160"; value: string }export type Hash256ArgType = { type: "Hash256"; value: string }export type IntegerArgType = { type: "Integer"; value: string }export type ArrayArgType = { type: "Array"; value: Arg[] }export type MapArgType = { type: "Map"; value: { key: Arg; value: Arg }[] }export type ByteArrayArgType = { type: "ByteArray"; value: string }
export type Arg =  | AnyArgType  | StringArgType  | BooleanArgType  | PublicKeyArgType  | Hash160ArgType  | Hash256ArgType  | IntegerArgType  | ArrayArgType  | MapArgType  | ByteArrayArgType

These types represent the different data types that can be used as arguments in the smart contract invocation. You can use them to define the arguments for the args field in the ContractInvocation object.

6. Disconnecting the wallet#

You can disconnect the wallet in two ways: using the disconnect method from the provider or listening to the disconnect event emitted by the wallet. You should treat both cases, as you can't be sure how the user will be disconnecting from the dApp.

async function disconnectWalletFromDapp() {  await provider.disconnect();
  // Here you can update your UI to reflect that the user is disconnected  changeUIToDisconnectedState();}
provider.on("disconnect", (payload: any) => {  console.log("Disconnected:", payload);
  // Here you can update your UI to reflect that the user is disconnected  changeUIToDisconnectedState();});

7. Extra - Integrating with other connection UIs#

The dApp can now connect and interact with the Neo blockchain using WalletConnect Universal Provider, but you might want to use a different UI for connecting to the dApp, such as a modal.

Reown provides packages that you can use to connect to the dApp if you don't want to use the NeonWallet connect page. For example: @walletconnect/modal or @reown/appkit.

WalletConnect Modal is a simple and easy to use component that allow users to connect to the dApp with a QR code or a deep-link. With minimal changes, you can use it together with the Universal Provider to connect to the Neo blockchain.

Initialize the WalletConnect Modal:
import { WalletConnectModal } from "@walletconnect/modal";const modal = new WalletConnectModal({  projectId: "Project ID from https://cloud.reown.com/",  chains: ["neo3:testnet", "neo3:mainnet"],  explorerRecommendedWalletIds: [    "f039a4bdf6d5a54065b6cc4b134e32d23bed5d391ad97f25aab5627de26a0784",  ],  themeMode: "dark",});

And then you can use the openModal method to open the modal and connect to the dApp by listening to the display_uri event:

provider.on("display_uri", (uri: string) => {  modal.openModal({ uri });});
provider.on("connect", (payload: any) => {  // Close the modal after the user connects to a wallet  modal.closeModal();  changeUIToConnectedState();});

The other methods can stay the same as in the previous examples, such as invokeFunction, testInvoke, and disconnect.

WalletConnect Modal