Wallet Connect V2 support for BitcoinCash

Self-shill, but I think it’s valuable. Extended section of the latest BCH Podcast with Emergent reasons dives a lot into the ideas around transaction templates and wallet standards the community can converge on.

I have rewatched it already twice myself.

4 Likes

Hoping to have a write-up and demo available of what I’ve got so far either late tomorrow (Sunday) or Monday.

Just want to write up some tooling too so people can fool around with it.

2 Likes

Any such thing should keep security in mind.

In short, when the user is incapable of looking and understanding at the script or bytearray the “can we sign this thing?” shows, it probably isn’t secure.

Trusting 3rd party things that spend your money is mostly a bad idea.

1 Like

Hi all, apologies for delay. I’m just working on some tooling and experimenting with a slight refactor in my approach that might allow us (in future) to better support multi-sig/multi-entity contracts.

Basically, for bch_signTransaction_V0, the implementation I have right now returns:

  1. The transaction.
  2. The redeem script for each output.

The redeem scripts are necessary in many cases because, otherwise, the service would only have access to the Script Hash and would not be able to reconstruct the data required for redemption in a later transaction (or, in some cases, for a Settlement Service).

With a bit of customization to the LibAuth generateTransaction method, we might be able to return resolved variables/scripts instead: This would give us a few advantages:

  1. Retrieval of Redeem Script Construction Parameters is simpler (could extract by variable name instead of splitting on PUSH_OP positions).
  2. Multi-entity contracts would (probably) become possible in the sense that the bch_signTransaction_V0 RPC could return Resolved Variables even in the case where construction of a fully signed transaction was not possible (in a multi-entity setup, if we assume one party as co-ordinator, I think we may even be able to use this with WC).
  3. We can probably eliminate the need for the bch_compileScript_V0 method as we would now have access to variables resolved in referenced scripts (and the scripts themselves?). I was using this RPC as a way to construct a lockscript that could be passed as a locking parameter of another contract).

I need to investigate/experiment a bit more, but provided CashASM parser behaves as follows:

  1. A referenced script is returned as a resolved variable.
  2. A Bytecode Variable is returned as a resolved variable.
  3. Inline evaluations ARE NOT returned as a resolved variable (security).
  4. A referenced script containing the same name as a Bytecode variable takes precedence (i.e. overwrites) the value of a resolved variable (not sure if this is really essential, but it allows us to keep some information hidden from the service for datasigs - which might be useful in some cases).

… I think this is possible.

Regarding tooling, I’m aiming for something like this where it can take multiple contracts, combine them into a LibAuth template and then test them in-tool with WC by providing the tx template + data:

Should have this tool available to play with by week’s end latest, with a customized version of Cashonize that’ll work with it. Hoping people will be able to test more advanced multi-contract flows to see whether this approach would actually be workable in practice or not.

Thanks all.

4 Likes

Hi all, sorry this is taking a lot longer than anticipated. It’s kind of a “three steps forward, two steps back” exercise where I write an implementation, realize there’s a use-case gap, and then have to redesign and refactor.

To give a brief high-level outline of what I’m going for:

BRIEF OVERVIEW

  1. To help preserve privacy (inc. with HD Wallets) and improve security, the Session established between Service (“Dapp”) and Wallet uses a Sandboxed Keypair generated using sandboxedPrivateKey = hash256(sha256(masterPrivateKey) + sha256(domainName)).
  2. This PoC uses LibAuth templates. In future, these template can be hashed and whitelisted (trusted) by a wallet. For security, the Service (“Dapp”) can only specify inputs and outputs by their corresponding scripts in the provided template (no raw locking/unlocking bytecode allowed).
  3. The Service (“Dapp”) CAN request that the Wallet provides additional UTXOs to automatically meet the Satoshi and Token amounts that are summed in the outputs. In this respect, UTXO selection is done by the Wallet, but the Service can also provide inputs that it knows about.
  4. The Service (“Dapp”) CAN request a Wallet’s UTXOs provided they a) contain tokens and b) access to those token category ID’s have been granted upon Session Negotiation (sessionProperties.allowedTokens as an array of category ids or * for ALL tokens). This allows us to support token-specific use-cases (e.g. a particular NFT as input or more vague Crypto-Exchange use-cases).
  5. To prevent standards fracturing, the underlying RPC’s are intended to be transport agnostic: They should be applicable to HTTP, LibP2P any other transports (not just WalletConnect). Thus, we should be able to use them for a future HTTP-based Payment Protocols, etc.
  6. The hope is that, in future, we will be able to converge with what BitJSON is building. The idea is that a wallet CAN (if the template is natively supported) save the data payloads and use the built-in Wallet UX to execute contract actions. But, for WC, the idea is that the SERVICE (“Dapp”) stores the data associated with a given WC account.
  7. There is currently no support for batching requests (or “actions”): We could build this, but BitJSON is working on something similar (“actions”) and we should probably wait to see what his implementation looks like. These will likely replace specifying the transaction manually. NOTE: This is no longer the case and we will need basic batch/actions support. I’m reaching out to BitJSON to see if he knows approx. what this may look like in his implementation so that migration to it is less painful in future, but am going to use a very rudimentary approach in the meantime.
  8. Type-hinting is currently hacked into a variable’s description field. We want to think a little about how to handle this as they are not always static (e.g. using oracles.cash, we may set a price - but we want that to show depending upon which Oracle was chosen in another variable according to the scaling factor).
  9. Currently, the bch_signTransaction_V0 RPC returns redeem scripts for the compiled outputs, but we want to return resolved variables instead (I think this’ll both simplify things and potentially allow us to support multisig).
  10. Accounts are currently identified by address - but there are some benefits we may gain if we use the Public Key instead (e.g. ability to sign and encrypt messages for another user on a Dapp given their pubkey.)

I really hope I can get a tool that’ll be able to demo most of the above by Monday. Assuming my current approach is versatile enough, I think this is realistic. But, if I bump into another use-case gap, might be a bit longer still (apologies).

If any questions in the meantime, please feel free to ask.

5 Likes

Update here:

4 Likes

So here’s the current state of things:

Pat’s spec simply referred to as “WalletConnect” (WalletConnect + Paytaca Connector interface) has been implemented in 3 BCH wallets: Cashonize, Zapit and Paytaca.
The protocol is used by multiple dapps: Tapswap, CashTokens Studio, Cash-Ninjas mint with a few more in the works

Jim’s spec called “CashConnect” (WalletConnect + CashRPC) has been merged into mainline Cashonize but the spec is still marked as pre-alpha.
CashConnect is used by the BCH Guru price prediction game and by the CashRPC IDE

And then there is still the custom Paytaca Connect which is used by the bitcats and available as a 2nd option in the CashTokens studio. Paytaca connect was used by the Paytaca browser extension and was pioneering wallet connect functionality on BCH. Now it can be phased out, as Pat’s wallet connect spec is more general than for just browser extensions.

5 Likes

Without really knowing too much about it, I was impressed by Pat’s spec, and have been blown away by Jim’s spec, even at this early stage. The Guru beta is amazing. Can’t wait until we have that implemented across the ecosystem, and this is only the early version (a bit of work on just the UI to have more helpful links and info would already be a big step forward).

2 Likes

I maintain a list of BCH walletConnecta apps at Tokenaut.cash, it has been growing steadily through-out 2024, so the protocol is positioned to play an important role on BCH going forward.

There is a 4th wallet with support for BCH Wallet Connect on the way: Electron Cash. It will have WalletConnect support through an EC plugin but this is planned to be merged into the mainline wallet eventually.

In the Elecron Cash group the developer, ‘OPReturn’, wrote:

I think the alpha version for WalletConnect plugin for EC is almost ready. Doing final testings. Hopefully I’ll be able to do a release this week.

3 Likes

There’s now 10 dapps using the BCH WalletConnect protocol

The WalletConnect plugin Electron Cash also has been released in alpha version by now so that means 4 supporting wallets (link to the plugin)

Paytaca Connect is also being deprecated as a separate standard and will shortly be removed from Tapswap and from the CashTokens Studio as a user-option.

This means there is strong standardization of the BCH ecosystem around the WalletConnect standard which is great news for users and developers.

CashScript now also has a WalletConnect guide for contract developers to get started using the standard in their dapps.

3 Likes

has anyone been able to migrate from the deprecated “@walletconnect/modal” library?

1 Like

Cashonize and Selene are using @reown/walletkit and @walletconnect/core:

Selene reference (basically a direct rip from Cashonize adapted for Selene’s architecture):

3 Likes

There’s this recent migration blogpost by Reown for the deprecated @walletconnect/modal library: WalletConnect Modal to Reown AppKit Core - Reown Docs

It seems the new library is @reown/appkit/core and you create the modal with a createAppKit function. The styling of the modal also seems to have changed somewhat.

@kzKallisti those are the libraries on the wallet side, Dagur as an app dev is asking about the libraries for the dapp side (the modal library used to display the walletconnect qr code)

2 Likes

oops my bad, thanks for the correction :sweat_smile:

I have created @bch-wc2/interfaces and @bch-wc2/privkey-connector packages.

@bch-wc2/interfaces formalizes the specced interfaces and allows to import them to create signTransaction requests such as those in cashscript and in mainnet-js (see https://github.com/mainnet-cash/mainnet-js/releases/tag/2.7.7)

@bch-wc2/privkey-connector is a small piece of code which utilizes a private key to sign the WC2 requests locally, it is super important part for local development and automated testing.

See the repository https://github.com/mainnet-pat/bch-wc2

4 Likes

I was able to use the latest WalletConnect/AppKit libraries, not to migrate, but to start a new dApp implementation.

I haven’t tested everything, but pairing, getting the addresses, and signing a transaction already works. It also doesn’t support the custom wallets like Paytaca like the original reference, but I’ll try adding that eventually.

Set up

// config.ts

import type { AppKitNetwork } from "@reown/appkit/networks";
import type { InferredCaipNetwork } from "@reown/appkit-common";
import UniversalProvider from "@walletconnect/universal-provider";
import { AppKit, createAppKit } from "@reown/appkit/core";

export const projectId = process.env.NEXT_PUBLIC_REOWN_PROJECT_ID;

// you can configure your own network
const bch: InferredCaipNetwork = {
  id: "bch",
  chainNamespace: "bch" as const,
  caipNetworkId: "bch:bitcoincash",
  name: "Bitcoin Cash",
  nativeCurrency: { name: "Bitcoin Cash", symbol: "BCH", decimals: 8 },
  rpcUrls: { default: { http: [] } },
};

export const networks = [bch] as [AppKitNetwork, ...AppKitNetwork[]];

let provider: UniversalProvider | undefined;
let modal: AppKit | undefined;

export async function initializeProvider() {
  if (!provider) {
    provider = await UniversalProvider.init({
      projectId,
      metadata: {
        name: "Project name",
        description: "Project description",
        url: "https://example.com",
        icons: ["https://example.com/favicon.ico"],
      },
    });
  }
  return provider;
}

export function initializeModal(universalProvider?: UniversalProvider) {
  if (!projectId) {
    throw new Error("Project ID is not defined");
  }
  if (!modal && universalProvider != null) {
    modal = createAppKit({
      projectId,
      networks,
      /* @ts-expect-error https://docs.reown.com/appkit/next/core/installation#others-networks-appkit-core-2 */
      universalProvider,
      manualWCControl: true,
      features: {
        analytics: true, // Optional - defaults to your Cloud configuration
      },
    });
  }
  return modal;
}

// Provider.tsx
"use client";
import React, {
  useState,
  useEffect,
  PropsWithChildren,
  useContext,
} from "react";
import { initializeProvider, initializeModal } from "./config"; // previous config file
import UniversalProvider from "@walletconnect/universal-provider";

const requiredNamespaces = {
  bch: {
    chains: ["bch:bitcoincash"],
    methods: ["bch_getAddresses", "bch_signTransaction", "bch_signMessage"],
    events: ["addressesChanged"],
  },
};

interface Context {
  provider: UniversalProvider | null;
  session: UniversalProvider["session"] | null;
  isLoading: boolean;
  isInitialized: boolean;
  connect: () => void;
  disconnect: () => void;
  addresses: string[] | null;
}

const AppKitContext = React.createContext<Context>({
  provider: null,
  session: null,
  addresses: null,
  isLoading: false,
  isInitialized: false,
  connect: () => {},
  disconnect: () => {},
});

export const useAppKit = () => useContext(AppKitContext);

export function AppKitProvider(props: PropsWithChildren) {
  const [isInitialized, setIsInitialized] = useState(false);
  const [provider, setProvider] = useState<Context["provider"]>(null);
  const [session, setSession] = useState<Context["session"]>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [addresses, setAddresses] = useState<string[] | null>(null);

  // Initialize the Provider and AppKit on component mount, and check for existing session
  useEffect(() => {
    const init = async () => {
      const dataProvider = await initializeProvider();
      setProvider(dataProvider);
      const modal = initializeModal(dataProvider);

      modal?.subscribeEvents(({ data }) => {
        switch (data.event) {
          case "MODAL_OPEN":
            setIsLoading(false);
          default:
            return;
        }
      });

      if (dataProvider.session) {
        // check if there is a session
        setSession(dataProvider.session);
      }
      setIsInitialized(true);
    };
    init();
  }, []);

  useEffect(() => {
    if (session == null || provider == null || isInitialized === false) return;
    (async () => {
      try {
        const addresses = (await provider.client.request({
          chainId: "bch:bitcoincash",
          topic: session.topic,
          request: {
            method: "bch_getAddresses",
            params: {},
          },
        })) as string[];
        setAddresses(addresses);
      } catch (e) {
        console.error(e);
      }
    })();
  }, [session, provider, isInitialized]);

  useEffect(() => {
    // Handler for when WalletConnect generates a connection URI
    // Opens the AppKit modal with the URI and shows the connecting view
    if (provider == null) return;
    const handleDisplayUri = (uri: string) => {
      const modal = initializeModal(provider);
      modal?.open({ uri, view: "ConnectingWalletConnectBasic" });
    };

    // Handler for when a wallet successfully connects
    // Updates the session state and closes the modal
    const handleConnect = async ({
      session,
    }: {
      session: Context["session"];
    }) => {
      setSession(session);
      const modal = initializeModal(provider);
      await modal?.close();
    };

    const handleDisconnect = async () => {
      setSession(null);
    };

    // Subscribe to WalletConnect events
    provider?.on("display_uri", handleDisplayUri); // Listen for connection URI
    provider?.on("connect", handleConnect); // Listen for successful connections
    provider?.on("disconnect", handleDisconnect);

    return () => {
      provider?.removeListener("display_uri", handleDisplayUri);
      provider?.removeListener("connect", handleConnect);
      provider?.removeListener("disconnect", handleDisconnect);
    };
  }, [provider]);

  const connect = async () => {
    setIsLoading(true);
    try {
      if (!provider) {
        throw new Error("Provider is not initialized");
      }
      await provider.connect({
        namespaces: requiredNamespaces,
      });
    } catch (error) {
      console.error("Failed to connect:", error);
    } finally {
      setIsLoading(false);
    }
  };

  const disconnect = async () => {
    setIsLoading(true);
    try {
      if (!provider) {
        throw new Error("Provider is not initialized");
      }
      await provider.disconnect();
    } catch (error) {
      console.error("Failed to disconnect:", error);
    } finally {
      setSession(null);
      setIsLoading(false);
    }
  };

  return (
    <AppKitContext.Provider
      {...props}
      value={{
        provider,
        session,
        isLoading,
        connect,
        disconnect,
        isInitialized,
        addresses,
      }}
    />
  );
}

Usage

const {
  connect,
  disconnect,
  addresses,
  isLoading,
  isInitialized,
  session
} = useAppKit();

// open modal

connect()

// request transaction signature

await provider.client.request({
  chainId: "bch:bitcoincash",
  topic: session?.topic,
  request: {
    method: "bch_signTransaction",
    params: JSON.parse(stringify(wcTransactionObj)),
  },
});
4 Likes

I built a React library to make wallet integration in Bitcoin Cash dApps effortless, implementing wc2-bch-bcr with the latest version of Reown AppKit (instead of the deprecated walletconnect packages). It’s inspired by RainbowKit and wagmi, which provide a seamless DX.

I aim to bring that DX to BCH dApp development, making wallet connection integration as simple as:

  1. Install the npm package
  2. Set the corresponding provider
  3. Use the features through your app (connect, address, signTransaction, signMessage, etc.)

Just like RainbowKit does.

I’ve shipped a minimal working version of bch-connect, which is already published on npm, thoroughly documented in the repository README, and with a live example using it.

I still need to write test suites, and it likely still contains a lot of bugs and issues. But I’ll be working on improving it and eventually scaling it (for instance, adding support for cashconnect). I’ve never maintained a library before, so any feedback and criticism is very welcome.

The wallets I know currently work fine are Cashonize (web) and Zapit. Paytaca doesn’t work yet, but I don’t know exactly why — I still have to investigate how to make it work.

Here’s a glimpse of how bch-connect looks like:

import { useWallet } from "bch-connect";

export const WalletButtons = () => {
  const { connect, connectError, isConnected, disconnect, tokenAddress } =
    useWallet();

  return (
    <>
      <button onClick={connect}>
        {isConnected ? tokenAddress : "Connect wallet"}
      </button>

      {isConnected && (
        <button title="Disconnect wallet" onClick={disconnect}>
          Disconnect
        </button>
      )}

      {connectError && <p>Error connecting wallet: {connectError.message}</p>}
    </>
  );
};
3 Likes

Wow this is very cool. Our WalletConnect integration on Selene is still quite basic and shaky, so this might actually be extremely useful for us in upgrading that to be a lot cleaner & more robust of an integration.

This is also super useful tooling with BCH BLAZE coming up - I can see a LOT of interest & usage from fresh devs coming into the scene and onboarding straight to WalletConnect with this.

Very excited by this project, great job!

Bug report: Clicking “Connect Wallet” and then “Search Wallets” on the bottom of the pop up modal leads to a perma-loading stalled state.

2 Likes

I’m leaving here some information I’ve gathered while struggling with BCH Connect library, as it could be useful for other developers facing the same or similar issues in their projects.

The problem: While the wallet connection worked correctly in Cashonize web and Zapit with the implementation I made in BCH Connect, Paytaca mobile and Cashonize mobile weren’t working at all. (I’m not including the Paytaca browser extension, as I’ve tested it in all dApps listed in tokenaut.cash and it doesn’t work anywhere. It’s likely a bug in Paytaca’s codebase).

What I’ve found is that Paytaca Mobile and Cashonize Mobile only work with deprecated versions of the @walletconnect/sign-client package. From my testing, they work fine with the version 2.19.1.

So if you are implementing wallet connection in your project, you’ll need to install the deprecated @walletconnect/sign-client@2.19.1 instead of newer versions if you want to support the wallets mentioned above.

I’m currently working to add out-of-the-box support for all of this in BCH Connect, using the latest packages when possible and the deprecated ones when necessary, so developers don’t have to struggle with these kinds of issues. And hopefully, the wallet developers will be able to address these compatibility issues in future updates.

5 Likes

Thanks for a heads up. I am currently working to extract my web3modal+sign-client solution into an npm package with the same structure of hooks so that your reown based solution will become a drop in replacement when all issues will be solved. Will publish my package soonish

3 Likes