Particle Auth with Openfort

Using Particle Network as a Signer with Openfort

Openfort aims to take blockchain gaming to the next level through a verticalized toolkit solution, providing extensive ERC-4337 account abstraction infrastructure to create seamless, Web2-like experiences for players. Historically, Web3 gaming has been a challenging vertical due to the complexities introduced by blockchain; strategic utilization of account abstraction removes most of this friction for developers and enables a comfortable experience for players.

Specifically, Openfort has its own smart account implementation that works directly with its various APIs to assign players unique identifiers and associated on-chain identities (through instances of the aforementioned smart account). Through this suite of tools, developers can facilitate popup-less, gasless transactions originating from player accounts. This is done by leveraging session keys to sign transactions rather than requiring users to repeatedly confirm on-chain actions (a common point of friction for Web3 games), as well as Openfort’s Paymaster for conditionally sponsoring UserOperations.

Within this document, we'll walk through the general process of leveraging Particle as an authentication mechanism (a signer for the smart account) within Openfort's tech stack, and leverage session keys alongside sponsored transactions accordingly.



Part 1: Understanding Openfort's Architecture in the scope of this example

Before jumping into our example application, it's important to add some context regarding the architecture of Openfort. Currently, interaction with Openfort is possible through either a suite of API endpoints or through their SDK (in this case, we'll be using @openfort/openfort-node though they also provide Unity and UE5 SDKs.). We'll be focusing on the process of creating a new player and facilitating a signless and gasless transaction.

Achieving this requires three main steps (after logging in with Particle Auth):

Step 1: Creating a Player

As mentioned before, Openfort uses this concept of a "player" (in lieu of "user") throughout their SDKs and APIs, and thus you'll need to create a new player upon account authentication (through social logins with Particle Auth) within your application.

Upon social login, your server should send a request (either via API or SDK) to Openfort to create a new player, assigning a corresponding name (ideally the email of your player, retrieved through userInfo upon social login) and description (often the Signer address— the EOA generated by Particle Auth). This request will then return an associated player ID (and other information, such as the player's Smart Account address) and register the previously created player on the Openfort dashboard, indicating that this user now has both an EOA secured by MPC-TSS (with Particle Auth) and Smart Account usable with Openfort.

Step 2: Registering a Session Key

As mentioned, we want to leverage this account to facilitate popup-less transactions within this example application, thus we need an independent (and slightly more complex) flow for initializing this key after the initial account login and creation.

Similar to the account creation process, this will involve a number of server-side operations to create, save, and register the session key. To begin, we'll create a session key (simply a designated keypair) through Openfort's SDK (which we'll also use to save the session key locally, to be fetched for signatures later).

With a session key created, ideally initiated during the social login process (after a user has generated an EOA with Particle Auth and a player account has been made with Openfort), it'll need to be registered (initialized) through a transaction we can construct with Openfort's SDK, passing in parameters such as temporal conditions and a gas sponsorship policy that we'll need to setup within the Openfort dashboard. Upon signing this transaction (with Particle), the session key will be initialized and registered.

Step 3: Executing Transactions

At this point in the process, we've implemented a mixture of client-side and server-side actions to initiate social login, create an account with Openfort, and register (sign and locally save) a session key. Thus, to wrap up, we'll also mint an example NFT. Following a similar structure as the previous two steps, we'll construct a transaction using Openfort's SDK, passing in various Openfort-specific parameters (such as player ID, the mint structured as an Interaction, the gas sponsorship policy, etc.). This transaction will then be signed by the saved session key and settled on-chain, minting the NFT without the user needing to confirm any pop-ups or pay gas fees.

Part 2: Building the Application

Now that we've established the context regarding the process of building this application with Openfort, we'll need to implement this process programmatically. To achieve this, we'll separate each major step into two halves: an API endpoint, and client-side logic executed upon an event. We'll create the player account for the first step and construct transactions for the latter two steps through these endpoints, returning data to be used for execution.

As mentioned prior, we'll be generating an EOA through social login with Particle Auth, which will then be the signer in a Smart Account created by Openfort (a player account). This can be done through {your openfort object}.players.create, with {your openfort object} being an instance of Openfort (imported from @openfort/openfort-node), constructed using your Openfort API key (sk_*). Your Openfort API key can be retrieved from the Openfort dashboard.

This object can then be used to create a player through the aforementioned method, .players.create, in which you can pass a name (which can be set to the email or ID associated with an account generated by Particle Auth, retrieved through getUserInfo, either through API or SDK) and description, which can be the signer’s address.

// Retrieve user info either through Particle's API, as is done here, or SDK
const { uuid, googleEmail: email, wallets } = await fetchUserInfo(user_uuid, idToken);
const evm_wallet = wallets.find(wallet => wallet.chain === "evm_chain");

const playerAccountAddress = await openfort.players.create({
	name: email,
	description: evm_wallet.publicAddress,
});

This account creation process should be called immediately upon logging in with Particle Auth (through particle.auth.login or connect, see Web (Desktop) Quickstart). In this example, we've set up the login process through an API call that returns the player ID to be used in session key creation and transaction execution. This endpoint is executed automatically after logging in with Particle Auth, setting a state for the player ID.

If successful, a new entry should be made to the "Players" tab on the Openfort dashboard, logging the data previously used to initialize the account.

With an account created, we're ready to register a session key, enabling the previously covered pop-up abstraction. To do this, we'll set up another API endpoint responsible for constructing the transaction required to register the key. This should be signed by the underlying EOA generated by Particle Network. Openfort's SDK constructs ready-to-execute UserOperations derived from on-/off-chain information defined within corresponding objects, such as CreatePlayerSessionRequest (a type imported from @openfort/openfort-node). In this example, our CreatePlayerSessionRequest object will contain the following information:

  • playerId, the player ID previously generated and retrieved through the account creation process.
  • address, the public key in which the session key will exist.
    • This can be retrieved through {your openfort object}.sessionKey.address after calling {your openfort object}.createSessionKey and {your openfort object}.saveSessionKey.
  • chainId, any supported chain that you're hosting your application on (Polygon Mumbai in this example).
  • validUntil and validAfter, the temporal conditions of the session key, determining the duration of time until it's active (if not immediately) and then expires (if ever).
  • policy, a gas sponsorship policy ID defined within the Openfort dashboard.
  • externalOwnerAddress, the public key of the EOA created by Particle Auth.

With an object of this nature constructed, you can use it to generate an unsigned UserOperation through {your openfort object}.players.createSession({your CreatePlayerSessionRequest object}), as shown below.

// Retrieve user info either through Particle's API, as is done here, or SDK
const { uuid, googleEmail: email, wallets } = await fetchUserInfo(user_uuid, idToken);
const evm_wallet = wallets.find(wallet => wallet.chain === "evm_chain");

const createSessionRequest: CreatePlayerSessionRequest = {
	playerId,
	address: sessionPubKey,
	chainId: 80001,
	validUntil: 281474976710655,
	validAfter: 0,
	policy: process.env.NEXTAUTH_OPENFORT_POLICY!,
	externalOwnerAddress: evm_wallet.publicAddress,
};

const playerSession = await openfort.players.createSession(createSessionRequest);

playerSession, in this case, is an object containing the UserOperation and associated hash, which we'll need to sign with our EOA (Particle Auth) through the second half of our logic for session key initialization. After retrieving this object through the corresponding API endpoint, we'll need to sign it with {your openfort object}.sendSignatureSessionRequest, passing in data.id from the playerSession object alongside a call to {your provider object}.signMessage(json.data.nextAction.payload.userOpHash), with {your provider object} referring to a constructed ethers/web3.js/etc. object with the signMessage method available, sending a signature request to Particle, thus facilitating the initialization of the session key. See the following code snippet for an example of this.

// Retrieve user info either through Particle's API, or SDK as is done here.
let openfortResp, toastId, auth = particle.auth.getUserInfo();

openfort.createSessionKey();
await openfort.saveSessionKey();

const res = await fetch("/api/register-session", {
	method: "POST",
	headers: { "Content-Type": "application/json", Authorization: `Bearer ${auth.token}` },
	body: JSON.stringify({ user_uuid: auth.uuid, sessionPubKey: openfort.sessionKey.address, player: playerId }),
});
const json = await res.json();

openfortResp = await openfort.sendSignatureSessionRequest(json.data.id, await rpc.signMessage(json.data.nextAction.payload.userOpHash));

Now that we've initialized a session key, we'll need to use it through the execution of a sample transaction: an NFT mint. We'll be leveraging the player account (with Particle as a Signer) in tandem with the session key to mint an NFT without the end user signing any transactions. Before implementing this programmatically, you'll need to go to the Openfort dashboard and register the contract in question (or any other asset you intend on using in this case). The API ID returned from this registration process will be used in a moment to construct a transaction with Openfort's SDK.

With the contract registered (in this example, 0x38090d1636069c0ff1af6bc1737fb996b7f63ac0), we're ready to construct an Interaction object to be used in the generation of a UserOperation, which will then be signed by our session key. Similar to CreatePlayerSessionRequest, we need to import a specific object type, Interaction, from @openfort/openfort-node. Within an instance of Interaction, you'll need the following information:

  • contract, the API ID of the contract recently added to the Openfort dashboard.
  • functionName, the name of the function you plan on calling (the list of available functions is included on the contract's page within the dashboard).
  • functionArgs, the arguments for the function (which can also be found on the dashboard).

Thus, the construction of your Interaction object should look similar to the snippet shown below.

const interaction: Interaction = {
	contract: process.env.NEXTAUTH_OPENFORT_CONTRACT!,
  functionName: "mint",
  functionArgs: [playerId],
};

From here, you'll need to use this object within {your openfort object}.transactionIntents.create, facilitating the generation of a UserOperation to be signed and executed on the player's behalf through the session key. This call will look nearly identical to the previously covered CreatePlayerSessionRequest, but will rather be of type CreateTransactionIntentRequest and contain the following parameters:

  • playerId, the player ID previously generated and retrieved through the account creation process.
  • chainId, any supported chain that you're hosting your application on (Polygon Mumbai in this example).
  • optimistic, whether or not the transactionIntent is optimistic, meaning it resolves before settling on-chain.
  • interactions, the Interaction object that was just constructed.
  • policy, a gas sponsorship policy ID defined within the Openfort dashboard.
  • externalOwnerAddress, the public key of the EOA created by Particle Auth.

The result of this call can then be returned within data, as we'll be using it during execution in a moment. In total, between the Interaction and CreateTransactionIntentRequest object, your code should look like the following:

// Retrieve user info either through Particle's API, as is done here, or SDK
const { uuid, wallets } = await fetchUserInfo(user_uuid, idToken);
const evm_wallet = wallets.find(wallet => wallet.chain === "evm_chain");

const interaction: Interaction = {
	contract: process.env.NEXTAUTH_OPENFORT_CONTRACT!,
	functionName: "mint",
	functionArgs: [playerId],
};

const createTransactionIntentRequest: CreateTransactionIntentRequest = {
	player: playerId,
	chainId: Number(process.env.NEXTAUTH_OPENFORT_CHAINID!),
	optimistic: true,
	interactions: [interaction],
	policy: process.env.NEXTAUTH_OPENFORT_POLICY!,
	externalOwnerAddress: evm_wallet.publicAddress,
};

const transactionIntent = await openfort.transactionIntents.create(createTransactionIntentRequest);

Finally, the generated UserOperation needs to be signed and sent using the session key. This can be achieved through three primary steps:

For the first, you need to load the session key (which should, at this point, be initialized) that you saved a moment ago through {your openfort object}.loadSessionKey. Upon loading the session key, rather than using a provider object to sign the UserOperation hash (as we did with the session key initialization), we can use Openfort directly with {your openfort object}.signMessage(data.nextAction.payload.userOpHash). This will automatically use the session key to sign the UserOperation hash and thus execute the transaction on behalf of the user, never requiring a manual signature.

This signed UserOperation will then need to be sent through {your openfort object}.sendSignatureTransactionIntentRequest, as highlighted below:

// Retrieve user info either through Particle's API, or SDK as is done here.
const authInfo = particle.auth.getUserInfo();

const res = await fetch("/api/collect-asset", {
	method: "POST",
	headers: { "Content-Type": "application/json", Authorization: `Bearer ${authInfo.token}` },
	body: JSON.stringify({ user_uuid: authInfo.uuid, player: playerId }),
});

const { data } = await res.json();

const payload = data.nextAction.payload.userOpHash;
const sessionKeyLoaded = await openfort.loadSessionKey();
const signedTransaction = sessionKeyLoaded ? openfort.signMessage(payload) : await new RPC(provider!).signMessage(payload);

    
openfortTransactionResponse = await openfort.sendSignatureTransactionIntentRequest(data.id, signedTransaction);

All of this logic, from the account creation to the creation and utilization of the session key, can be brought together within a central App component that players will interface with. An example of what this may look like has been included below.

import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { CollectButton, RegisterButton } from '../components';
import { ParticleNetwork, ParticleProvider } from '@particle-network/auth';

function App() {
  const [particle, setParticle] = useState(null);
  const [provider, setProvider] = useState(null);
  const [playerId, setPlayerId] = useState(null);

  useEffect(() => {
    const initParticle = async () => {
      const newParticle = new ParticleNetwork({
        projectId: process.env.NEXT_PUBLIC_PROJECT_ID,
        clientKey: process.env.NEXT_PUBLIC_CLIENT_KEY,
        appId: process.env.NEXT_PUBLIC_APP_ID
      });
      setParticle(newParticle);
    };
    initParticle();
  }, []);

  const handleLogin = async () => {
    await particle.auth.login({ preferredAuthType: 'google' });
    setProvider(new ParticleProvider(particle.auth));
    validateToken();
  };

  const validateToken = async () => {
    const userInfo = particle?.auth.getUserInfo();

    const response = await fetch("/api/login", {
      method: "POST",
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${userInfo.token}` },
      body: JSON.stringify({ user_uuid: userInfo.uuid })
    });

    if (response.status === 200) {
      const { player } = await response.json();
      setPlayerId(player);
      toast.success("JWT Verified");
    } else {
      setProvider(null);
      toast.error("JWT Validation Failed");
    }
  };

  return (
    <div className="App">
      {/* Logo and Sign-in Section */}
      {!provider ? (
        <button className="sign-button" onClick={handleLogin}>Sign in with Google</button>
      ) : (
        <div className="profile-card">
          <h2>{particle.auth.getUserInfo().name}</h2>
          <div className="action-buttons">
            {playerId && <RegisterButton playerId={playerId} particle={particle} provider={provider} logout={logout} uiConsole={uiConsole} />}
            <button onClick={() => uiConsole(particle.auth.getUserInfo())}>Get User Info</button>
            {playerId && <CollectButton playerId={playerId} particle={particle} provider={provider} logout={logout} uiConsole={uiConsole} />}
          </div>
        </div>
      )}
    </div>
  );
}

export default App;


Conclusion

Openfort is a powerful tool for developers looking to produce flexible and user-friendly Web3 games. Leveraging Particle as the signer providing social logins leading to EOAs secured by MPC-TSS only further enhances this user-friendliness. To learn more about Openfort, take a look at the integration.

The example covered within this document is derived from this demo repository.

📹

A video covering the process of building this example application is also available.