[SNIP] Standardizing Starknet Wallet Connections with Write API and JSON-RPC Integration

Hey all,

let me know what you think about this SNIP I have been working on. Glad to receive feedback and suggestions!

Thanks, Janek!

Standardizing Starknet Wallet Connections with Write API and JSON-RPC Integration

snip: tbd
title: Standardizing Starknet Wallet Connections with Write API and JSON-RPC Integration
status: Draft
type: Standard
author: Janek Rahrt <janek@argent.xyz>
created: 2023-08-01

Simple Summary

A proposal to integrate Starknet Write API and wallet RPC methods, aligning the connection method closer to how Ethereum handles wallet connections, and solving the versioning problems currently faced by using starknet.js.

Abstract

This SNIP proposes a new way of connecting wallets with dapps in the Starknet ecosystem by using RPC methods, specifically integrating the methods described in Starknet Write API and wallet-specific methods, pioneered by metamask.

Motivation

The current method of exposing an interface from starknet.js to the webpage has led to problems with versioning. The coupling makes integrations more fragile and starknet.js versioning is a slow and painful process. This proposal aims to standardize the wallet connection process, making it more robust and maintainable, and decouple it from the developments in starknet.js. It will also allow for more competing starknet wallet libraries, such as viem forks etc.

Specification

Using RPC for Starknet wallet connections (1)

  • Starknet will utilize a similar approach to Ethereum’s window.ethereum.request() method for making RPC calls.
  • Developers can interact with Starknet by calling a method such as window.starknet.request(), passing in the specific method name and parameters.
  • This approach allows for a consistent and standardized way to interact with Starknet, aligning with how Ethereum handles wallet connections.

Wallet JSON-RPC API Integration (2)

  • Utilize Wallet’s Ethereum JSON-RPC API methods.
  • Implement methods including:
    • wallet_requestAccounts: Replaces the .enable() method and returns an array with the activated account address. This method prompts the user to allow the dapp to read the account address.
    • wallet_addStarknetChain: Allows the user to add a specific Starknet chain to the wallet. This method creates a confirmation asking the user to add the specified chain, and the user may choose to switch to the chain once it has been added.
    • wallet_switchStarknetChain: Enables the user to switch to a different Starknet chain within the wallet. This method creates a confirmation asking the user to switch to the chain with the specified chain ID.
    • wallet_watchAsset: Enables the user to track specific assets within Starknet ecosystem, such as ERC-20 tokens or NFTs. This method requests that the user track the specified asset in their wallet.
    • If needed, these methods can be extended by some EIP-2255 style enhancements.
  • Handle specific errors such as RPCErrors etc.

Starknet Write API Integration (3)

  • Implement Starknet Write API methods, including:
    • starknet_addInvokeTransaction: Submit a new transaction to be added to the chain. User will need to confirm in the wallet.
    • starknet_addDeclareTransaction: Submit a new class declaration transaction. User will need to confirm in the wallet.
    • starknet_addDeployAccountTransaction: Submit a new deploy account transaction. User will need to confirm in the wallet.
  • Handle specific errors such as INSUFFICIENT_ACCOUNT_BALANCE, INVALID_TRANSACTION_NONCE, etc.

Additional Starknet API

  • Implement some methods that are not covered by wallet_ methods or by the Starknet Write API
    • starknet_signTypedData: Sign a eip-712 like message. User will need to confirm in the wallet.

Implementation

The implementation of this SNIP will require the following steps:

  1. Integration with Wallet JSON-RPC API: Adapt Wallet’s JSON-RPC API methods for Starknet.
  2. Integration with Starknet Write API: Implement the methods described in Starknet Write API.
  3. Testing: Conduct thorough testing to ensure compatibility and robustness.
  4. Documentation: Update the documentation to reflect the new connection method.
  5. Deployment: Roll out the changes to the Starknet ecosystem.

Links

  1. EIP-1193
  2. Wallet JSON-RPC API documentation
  3. Starknet Write API documentation

Examples

Example 1: Requesting Starknet Accounts

window.starknet.request({ method: 'wallet_requestAccounts' }).then(accounts => {
  console.log(accounts);
});

Example 2: Adding a Starknet Chain

window.starknet.request({ method: 'wallet_addStarknetChain', params: [chainDetails] }).then(result => {
  console.log(result);
});

History

This proposal is a response to the ongoing challenges with versioning and the need for a more standardized way to connect wallets in the Starknet ecosystem. Prior efforts to address this issue were restricted by the limitations of the existing starknet.js interface.

Copyright

Copyright and related rights waived via MIT.

Hey @0xjanek,

Thank you for sharing the SNIP draft. It provides an excellent starting point for a dialogue concerning dApp to wallet APIs, especially considering the evolving landscape of Starknet and the introduction of new wallet types.

Concerning the use of window.starknet:
Given Starknet is a multi-wallet ecosystem, I recommend abstracting all wallet references to wallet, as in wallet.request(...). This approach ensures consistency and avoids confusion tied to a single wallet provider’s handle.

Concerning the removal of the AccountInterface Object (wallet.account):
The SNIP’s significant non-semantic alteration include the removal of the wallet.account attribute which’s replaced by new JSON-RPC APIs:

  • starknet_addInvokeTransaction replaces wallet.account.execute(transactions: AllowArray<Call>, abis?: Abi[], transactionsDetail?: InvocationsDetails): Promise<InvokeFunctionResponse>
  • starknet_addDeclareTransaction replaces wallet.account.declare(contractPayload: DeclareContractPayload, transactionsDetail?: InvocationsDetails): Promise<DeclareContractResponse>
  • starknet_addDeployAccountTransaction replaces wallet.account.deployAccount(contractPayload: DeployAccountContractPayload, transactionsDetail?: InvocationsDetails): Promise<DeployContractResponse>
  • starknet_signTypedData replaces wallet.account.signMessage(typedData: TypedData): Promise<Signature>

While I acknowledge that this SNIP is designed to be JS agnostic, and fully support the goal of enabling snjs to evolve with minimal dependency on wallets, we must also recognize the importance of retaining valuable Account utilities. This includes strong typing support for dApps in methods such as execute and deployAccount, as well as convenient utilities like declareAndDeploy.

To strike the right balance between decoupling and convenience, I propose that snjs’s Account object should wrap a get-starknet defined Wallet interface. This interface would be a standard that all wallets can expose, inject, or refer to in their interactions with dApps:

// from get-starknet
interface Wallet {
    id: string;
    name: string;
    version: string;
    icon: string;
    request: <T extends RpcMessage>(call: Omit<T, "result">) => Promise<T["result"]>;
    isPreauthorized: () => Promise<boolean>;
    on: <E extends WalletEvents>(event: E["type"], handleEvent: E["handler"]) => void;
    off: <E extends WalletEvents>(event: E["type"], handleEvent: E["handler"]) => void;
    isConnected: boolean;
    selectedAddress?: string; // perhaps changing to accountAddress: string ?
    chainId?: string;    
}

// dApp example
import { connect, Wallet } from "get-starknet";
import { Account } from "starknet";

const wallet: Wallet = await connect();
const [accountAddress] = await wallet.request({ method: 'wallet_requestAccounts' });
if (accountAddress) { // connected
    const account = new Account(wallet);
    // account.deployAccount(...);
    // account.declare(...);
    // account.execute(...);
}

In this approach, snjs encapsulates get-starknet, rather than the reverse; wallet APIs remain relatively stable over time, allowing snjs the freedom to evolve and progress at a more rapid pace. dApps should depend solely on snjs and get-starknet (either directly or indirectly through frameworks like starknet-react) for wallet-referencing and functionality. This arrangement ensures that the wallet is not burdened with the responsibility of returning the entire snjs Account object and its extensive dependency stack.

Additional Topics for Discussion:

  1. Provider Attribute: This may soon become a concern for both wallet providers and dApps, considering the impending deprecation of the Starknet gateway. We should explore if dApps should rely on wallet providers to subsidize their RPC calls, as it affects both snjs Account and wallet.provider APIs.
  2. Permissions Mechanism: Standardizing expected wallet permissions is crucial.
  3. Error Codes: This point aligns with the need to standardize mentioned in #2.

Thanks,

A big thank you to @0xjanek for the insightful SNIP and I appreciate the points brought up by @avimak. However, I’m not entirely on board with the proposition:

I propose that snjs’s Account object should wrap a get-starknet defined Wallet interface.

Implementing this would introduce a dependency of get-starknet on starknet.js, which I believe isn’t the best course of action. Isn’t the primary objective of this SNIP to ensure that get-starknet and starknet.js operate independently? Creating this wrapper would seem to couple them again, albeit in a different manner.

Instead, a more robust approach might be to validate the payloads of starknet_addInvokeTransaction, starknet_addDeclareTransaction, and starknet_addDeployAccountTransaction. In the event of discrepancies, errors can be thrown. Pairing this with comprehensive documentation could greatly simplify the process for wallet developers, dapp developers, and sn.js contributors.

Would love to hear further thoughts on this.

I understand that the goal is to speed up the development cycles of snjs without relying on wallet providers to update their clients for new snjs features.

The proposal is to reverse the dependencies: Instead of wallets having to work with snjs-specific objects like Account (hence Provider, etc.), we should allow snjs to integrate a get-starknet interface into its Account/Provider constructors. This keeps the same APIs and utilities while speeding up snjs development, since wallet interfaces rarely change and are validated at the get-starknet level.

I believe it’s not efficient for dApps to use get-starknet for wallet connections, snjs for Starknet interactions, and then a third library to bridge the two with strong typing and parameter validations.
It makes more sense for snjs to be aware of get-starknet wallet interface, at least optionally for transaction and message signing APIs/utilities.

In summary, using get-starknet to define the dapp-to-wallet APIs, along with strong typing and standard validations, gives us the best of both worlds: familiar and stable APIs with faster snjs development.

I like the proposal. I believe we need a method to subscribe to account and network changes. I propose window.wallet.subscribe({ event: "starknet_accoutsChanged" }, callback).

Additionally, I think even the most thorough documentation cannot replace the benefits of strong-typing and runtime checks/utilities. Code-based interfaces are usually more reliable than text-based documentation. Most dApp developers would prefer a tightly integrated system between the connected-wallet and snjs, rather than constantly adjusting their code based on possibly outdated documentation.

We should definitely look at Ethereum best practices where the wallet/browser extension injects an object conforming to EIP-1193 (EIP-1193: Ethereum Provider JavaScript API)

The JS libraries (strarknet.js equivalent) are wrapping that object to create higher level objects like provider, account, contract, … see two examples here in ethers.js and viem:

Hello,
As I have some understanding of the Starknet.js code, and as I handle the feedbacks of the Starknet.js users, I can easily summarize their wishes :

  • They are upset (remaining polite) by the get-starknet library (as nobody is designated to handle issues, and nobody has the responsibility to update quickly the code). So, they would like to have only Starknet.js.
  • They would like to have this kind of code :
import { Provider } from "starknet";
const wallets: string[] = Provider.getWalletList(); // returns ["BRAAVOS","ARGENTX", ...]
const web3provider = new Provider ({
    wallet : wallets[0],
    network : "SN_GOERLI"
});
const accountsList  = web3provider.getAccountsList() // return an array of accounts description

And they don’t want to be involved in what is happening under the hood between Starknet.js and the browser wallets.

Hey all,
These are all valid points @0xjanek , and I do support decoupling js-libs and wallets. I do not believe it is necessary for faster development of particular starknetjs but it will provide more flexibility for the ecosystem to evolve. Providers and Accounts are fairly standardized and mostly pushed by starknet changes, and most of the evolution is done in Contract and Contract Interactions during the last couple of months.

I do agree also with @avimak point, but I think instead of wrapping a new interface (get-starknet) into the Account we could create something like WalletClient new higher orger object that would then incorporate the interface from get-starknet.
In that case, it will be the same as with the seq/rpc spec, race with time to implement changes ASAP (with providing request() for manual construction same as fetch() on existing prov.)

Also, include events from eip-1193 for network change/ account address change.

Today I was refactoring the connectors in starknet-react and I noticed that, at the moment, the network change callback is a mess because the interface in get-starknet only specifies that network is a string, but the two wallets return the network in a different format! The spec should specify which format to use or, even better, say to return the hex-encoded network id so it’s unambiguous.

Sure, let’s use this PR as an opportunity to align all callbacks, including address, network, expected default connections, states, etc.

Another point I want to raise before this SNIP is finalized is about wallet discovery. One issue is that at the moment there is no reliable way to discover wallets since the user’s script could run before the wallets inject themselves. This leads to users having to poll for injected wallets every few milliseconds for the first half second or so.

Would it be possible to include a discovery mechanism like EIP-6963 in this SNIP?

I think this discussion deserves its own SNIP, but in general:

  1. I’m glad to see Braavos approach to multi-wallet support (e.g. adding id, name and icon) is being discussed for L1 providers, it’s about time …
  2. The fact that dApps and/or Provider Discovery Library relying on a specific window key for discovery/functionality is IMO the source of all multi-wallet-support evil; we handle that in get-starknet by simply scanning/discoverying all keys prefixed with starknet.
    Actually, it could’ve been safer if the starknet key wasn’t injected at all, but only wallet-specific tags like starknet_braavos, starknet_argent, etc. - cc @0xjanek
  3. There is no reason to poll for wallets, get-starknet (and other Provider Discovery Library from L1/L2) could simply register a MutationObserver and rescan for wallets once it’s triggered, pseudo example -
const callback = function(mutationsList, observer) {
  for (const mutation of mutationsList) {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeName.toLowerCase() === 'script') {
          // new script was added.
          // if it's `src` starts with `chrome-extension://`
          // rescan for available wallets
        }
      });
    }
  }
};

const observer = new MutationObserver(callback);
observer.observe(document.body, { childList: true, subtree: true });

so without even mentioning web-wallets (and other connectivity approaches) I would say -
get-starknet wallet interface already covers for unique wallet identifiers (id, name, icon, …), doesn’t prioritize window.starknet but instead includes all window.starknet* keys, and as for events - IMO eip-6963 tries to reinvent the wheel because there is a native, built-in solution, for recognizing new wallet injections after the client is already loaded and/or tries to list/discover for available wallets.

The issue with that approach is that it’s browser-specific (on Firefox the prefix is moz-extension for example), so developers will have to individually test all wallets on all browsers (and apps) which is unrealistic in many cases. By implementing the discovery mechanism in 6963 both wallet developer and dapp developers implement it once and know it will work everywhere.

Additionally, Next.js seems to change the DOM in development mode so it doesn’t work there either. I believe it’s unrealistic to break compatibility with the most popular frontend framework.

Interesting. I wonder why they didn’t use other native solutions, like giving the added/updated script element a specific id format (i.e., web3_wallet*) to clearly mark it as a change relevant for wallet-refresh - across browsers, and even across chains. MutationObserver has wide support across all browsers and has been around for a long time.

wrt next.js, it works for me -

'use client';

import { useEffect } from 'react';

const MyComponent = () => {
  useEffect(() => {
    const targetElement = document.getElementById('target');

    const callback = function (mutationsList, observer) {
      for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
          console.log('A child node has been added or removed.');
        }
      }
    };

    const config = { childList: true };
    const observer = new MutationObserver(callback);
    observer.observe(targetElement, config);

    return () => {
      observer.disconnect();
    };
  }, []);

  const addElement = () => {
    const target = document.getElementById('target');
    const newElement = document.createElement('p');
    newElement.textContent = 'Element ' + ((target.children.length ?? 0) + 1);
    target.appendChild(newElement);
  };

  return (
    <div>
      <button onClick={addElement}>Add Element</button>
      <div id="target">
      </div>
    </div>
  );
};

export default function Home() {
	return (
		<main>
			<MyComponent/>
		</main>
	);
}

I agree that if we decide to use MutationObserver the standard should include a specification for the id field of the injected script (IMHO it should be the same starknet_* id as the object injected into the window).

I’m still concerned it’s too specific to how web wallets work today, but worst case we can add a new snip if that becomes an issue.

Sure, the id can be the same as the window’s key (starknet_*).

Regarding being too web-specific - since EIP-6963 already relies on the window’s event mechanism, I don’t think this adds a new limitation.

The issue with that approach is that it’s browser-specific (on Firefox the prefix is moz-extension for example), so developers will have to individually test all wallets on all browsers (and apps) which is unrealistic in many cases. By implementing the discovery mechanism in 6963 both wallet developer and dapp developers implement it once and know it will work everywhere .

Additionally, Next.js seems to change the DOM in development mode so it doesn’t work there either. I believe it’s unrealistic to break compatibility with the most popular frontend framework

Without having read the full context, I would like to request that the API a) returns a locally-unique identifier for transactions that have been submitted to the wallet for signing and b) includes an RPC method for cancelling that request by the identifier returned from a.

The use case is in app.ekubo.org: when doing a swap, we construct an invoke transaction based on the latest quote. If we receive a new quote while the user has not yet signed and learn that the transaction the user is signing will fail, we want to cancel the signing request.

The wallet can show some kind of ‘cancelled by dapp’ screen, allowing the user to return to the dapp to create a new transaction.

@sendmoodz, this suggestion should likely be addressed and discussed in a dedicated SNIP, which might utilize the baseline API of this SNIP, such as a request message