Skip to content

Tutorial -- Transaction Automation

In this tutorial, you will learn how to automate transactions for your users using session keys. This is useful when you want to send transactions for your users from your server, for instance.

Refer to this code example while you follow along the tutorial. You can run the example by following instructions of the examples repo.

Installation

Session keys are enabled through the @zerodev/permissions package. The examples repo already installed this, but normally you would install permissions with:

npm
npm i @zerodev/permissions

The Architecture of Transaction Automation

In the typical architecture for transaction automation, there's an "owner" and an "agent":

  • The "owner" is the entity that controls the user's master key.
  • The "agent" is the entity that automates transactions for the owner.

For instance, your user might be using an embedded wallet (master key) with your frontend, and you might want to automate transactions for your users from your server. In this case, the frontend would be the "owner" and your server would be the "agent."

From a high level, this is how transaction automation works:

  • The agent creates a session key.
    • At this point, the session key has not been approved by the owner.
  • The agent sends the "address" of the session key to the owner for approval.
  • The owner signs the address and returns the approval (signature) to the agent.
  • The agent can now send transactions for users using the approval and the session key.

Code Flow

Agent: creating a session key

From the agent's side, create a ECDSA signer as the session key:

const sessionPrivateKey = generatePrivateKey()
 
const sessionKeySigner = await toECDSASigner({
  signer: privateKeyToAccount(sessionPrivateKey),
})

Note that if you do not wish to store the private key of the session key, you could use a remote signer instead:

const remoteSigner = await toRemoteSigner({
    apiKey,
    mode: RemoteSignerMode.Create
})
 
const sessionKeySigner = await toECDSASigner({
  signer: remoteSigner,
})

Agent: send session key "address" to the owner

For the owner to approve the session key, the agent must send the "address" of the session key to the owner. Note that the private key is never sent -- it's only the address which is the public key of the session key that's sent.

To obtain the session key address:

const sessionKeyAddress = sessionKeySigner.account.address

Send this address to the owner.

Owner: approving the session key

Now, on the owner side, approve the session key with policies:

const ecdsaValidator = await signerToEcdsaValidator(publicClient, {
  entryPoint,
  kernelVersion,
  signer,
})
 
// Create an "empty account" as the signer -- you only need the public
// key (address) to do this.
const emptyAccount = addressToEmptyAccount(sessionKeyAddress)
const emptySessionKeySigner = await toECDSASigner({ signer: emptyAccount })
 
const permissionPlugin = await toPermissionValidator(publicClient, {
  entryPoint,
  kernelVersion,
  signer: emptySessionKeySigner,
  policies: [
    // your policies
  ],
})
 
const sessionKeyAccount = await createKernelAccount(publicClient, {
  entryPoint,
  kernelVersion,
  plugins: {
    sudo: ecdsaValidator,
    regular: permissionPlugin,
  },
})
 
const approval = await serializePermissionAccount(sessionKeyAccount)

Now, send the serialized approval back to the agent.

Agent: using the session key

When the agent wants to use the session key, first recreate the signer. Presumably, you would've stored the session key somewhere:

// Using a stored private key
const sessionKeySigner = await toECDSASigner({
  signer: privateKeyToAccount(sessionPrivateKey),
})

Or if you were using a remote signer:

const remoteSignerWithGet = await toRemoteSigner({
    apiKey,
    keyAddress: remoteSignerAddress // you should've stored this
    mode: RemoteSignerMode.Get
})
 
const sessionKeySigner = await toECDSASigner({
  signer: remoteSigner,
})

Now create an account object by combining the approval (which you should've stored somewhere) with the sessionKeySigner:

const sessionKeyAccount = await deserializePermissionAccount(
  publicClient,
  entryPoint,
  kernelVersion,
  approval,
  sessionKeySigner
)

Finally, construct a Kernel client as usual:

const kernelClient = createKernelAccountClient({
  account: sessionKeyAccount,
 
  // the other params
})

Now you can send transactions with the Kernel client.

Revoking a Session Key

After a session key has been used, or if it's no longer needed, it's a good security practice to revoke it to ensure it cannot be used for any further transactions. Here's how you can revoke a session key:

First, prepare your environment for the revocation process. This involves creating a "sudo" account capable of performing privileged operations, such as uninstalling plugins.

const sudoAccount = await createKernelAccount(publicClient, {
    plugins: {
      sudo: ecdsaValidator,
    },
  // other params
});
 
const sudoKernelClient = createKernelAccountClient({
  account: sudoAccount,
  // other params
})

Now to revoke the session key by uninstalling its associated permission plugin, call uninstallPlugin on sudoKernelClient.

const txHash = await sudoKernelClient.uninstallPlugin({
    plugin: permissionPlugin,
});

Creating multiple session keys on multiple chains in one signature

Refer to this page.