Error when using Defender Relayer to transfer tokens

Are you still having troubles here? I'd be happy to share with you the code I have working for my Action. It's in Typescript however. LMK

Yes, please share. Thank you.

Mind you, I'm using rollup to compile the Typescript code into vanilla js before using it in the action.

// /src/actions/release-next-auction/index.ts

 * Defender ReleaseNextAuction Action Script
 * Purpose:
 * This script is designed to automatically calculate and release the next auction tag based on data
 * fetched from a GraphQL endpoint. It executes a blockchain transaction to perform the release,
 * utilizing a Defender Relayer (Externally Owned Account, EOA) with the ETSOracle role.
 * Deployment:
 * The script is bundled into `dist/defender/actions/release-next-auction/index.js` via Rollup and deployed
 * as an OpenZeppelin Defender Autotask using the Defender as Code plugin. The Defender Relayer's credentials
 * (API key and secret) are used for authentication with the relayer service, enabling the script to perform
 * transactions on behalf of the user.
 * Local Testing:
 * To test this Defender Action locally, ensure the NETWORK environment variable is set to 'mumbai_stage'.
 * This setup allows developers to simulate the action's behavior in a test environment similar to the
 * production configuration on the Mumbai test network.

import { initializeSigner } from "./../../../services/initializeSigner";
import { BlockchainService } from "./../../../services/blockchainService";
import { RelayerParams } from "@openzeppelin/defender-relay-client/lib/relayer";

// Main handler function to be invoked by Defender or locally for testing.
export async function handler(credentials: RelayerParams) {
  const signer = await initializeSigner(credentials);
  const blockchainService = new BlockchainService(signer);
  await blockchainService.handleRequestCreateAuctionEvent();

// Local testing entry point
if (require.main === module) {

  // Enforce the 'mumbai_stage' network for local testing
  if (process.env.NETWORK !== "mumbai_stage") {
    console.error("Local testing requires NETWORK environment variable to be set to 'mumbai_stage'.");

  type EnvInfo = {
    API_KEY: string;
    API_SECRET: string;

  const { API_KEY: apiKey, API_SECRET: apiSecret } = process.env as unknown as EnvInfo;
  handler({ apiKey, apiSecret })
    .then(() => process.exit(0))
    .catch((error: Error) => {
      console.error("An error occurred during local testing:", error);

// /src/services/initializeSigner.ts

// File: initializeSigner.ts
import { RelayerParams } from "@openzeppelin/defender-relay-client/lib/relayer";
import { DefenderRelaySigner, DefenderRelayProvider } from "@openzeppelin/defender-relay-client/lib/ethers";
import { ethers } from "ethers";

 * Initializes and returns an ethers.js Signer based on the application's environment.
 * This signer can be used to interact with the Ethereum blockchain, allowing for
 * both read and write operations on smart contracts.
 * @param credentials - When the signer is being initialized from a Defender Action
 *                      credentials are passed in automatically by Defender system.
 *                      If are testing a Defender Action locally, these credentials must be
 *                      set inside the action code. @see /src/defender/actions/release-next-auction/
 * @returns An instance of ethers.Signer, which can either be a Wallet (for local environments)
 *          or a DefenderRelaySigner (for non-local environments like "mumbai_stage").
 * @throws Error - If the NETWORK environment variable is set to an unsupported value
 *                 or if credentials are required but not provided.
export async function initializeSigner(credentials?: RelayerParams): Promise<ethers.Signer> {
  let signer: ethers.Signer;

  if (process.env.NETWORK === "localhost") {
    // For local development, use a Wallet connected to a local JSON-RPC provider.
    const provider = new ethers.providers.JsonRpcProvider("");
    const privateKey = getRequiredEnv("ETS_ORACLE_LOCALHOST_PK");
    signer = new ethers.Wallet(privateKey, provider);
  } else if (process.env.NETWORK === "mumbai_stage") {
    // For the "mumbai_stage" environment, use DefenderRelaySigner which requires credentials.
    if (!credentials) {
      throw new Error("Defender relayer credentials must be provided for the 'mumbai_stage' network.");
    const provider = new DefenderRelayProvider(credentials);
    signer = new DefenderRelaySigner(credentials, provider, { speed: "fast" });
  } else {
    // Throw an error if an unsupported NETWORK value is encountered.
    throw new Error("Unsupported network configuration.");

  return signer;

 * Helper function to get a required environment variable. Throws an error if the
 * variable is not set.
 * @param variable - The name of the environment variable to retrieve.
 * @returns The value of the environment variable.
 * @throws Error - If the specified environment variable is not set.
function getRequiredEnv(variable: string): string {
  const value = process.env[variable];
  if (value === undefined) {
    throw new Error(`${variable} environment variable is not set.`);
  return value;

// /src/services/blockchainServices.ts
 * The BlockchainService class encapsulates interactions with the blockchain, particularly
 * focusing on operations related to the ETS Auction House and Access Controls. It utilizes ethers.js
 * for blockchain interactions and the DefenderRelaySigner for transactions when deployed as an
 * OpenZeppelin Defender Autotask.
import { ethers } from "ethers";
//import { DefenderRelaySigner } from "@openzeppelin/defender-relay-client/lib/ethers";
import { DefenderRelaySigner } from "@openzeppelin/defender-sdk-relay-signer-client/lib/ethers/signer";
import { etsAuctionHouseConfig, etsAccessControlsConfig } from "../contracts";
import { TagService } from "./tagService";

// ABI and contract addresses for the ETS Auction House and Access Controls, assuming these are correctly defined in your contracts configurations.
const { abi: accessControlsAbi, address: accessControlsAddress } = etsAccessControlsConfig;
const { abi: auctionHouseAbi, address: auctionHouseAddress } = etsAuctionHouseConfig;

export class BlockchainService {
  private signer: ethers.Signer | DefenderRelaySigner; // Signer used for blockchain transactions.
  private accessControlsContract: ethers.Contract; // Contract instance for Access Controls.
  private auctionHouseContract: ethers.Contract; // Contract instance for the Auction House.

   * Constructs a new BlockchainService instance.
   * @param signer An ethers.Signer or DefenderRelaySigner used for signing transactions.
  constructor(signer: ethers.Signer | DefenderRelaySigner) {
    this.signer = signer as ethers.Signer;
    this.accessControlsContract = new ethers.Contract(accessControlsAddress, accessControlsAbi, this.signer);
    this.auctionHouseContract = new ethers.Contract(auctionHouseAddress, auctionHouseAbi, this.signer);

   * Retrieves the platform address from the Access Controls contract.
   * @returns A promise that resolves to the platform address in lowercase.
  public async getPlatformAddress(): Promise<string> {
    const platformAddress = await this.accessControlsContract.getPlatformAddress();
    return platformAddress.toLowerCase();

   * Fetches the ID of the last auction from the Auction House contract.
   * @returns A promise that resolves to the ID of the last auction as a bigint.
  public async getLastAuctionId(): Promise<bigint> {
    const lastAuctionId = await this.auctionHouseContract.getTotalCount();
    return lastAuctionId;

   * Retrieves the token ID of a specific auction by its ID.
   * @param auctionId The ID of the auction.
   * @returns A promise that resolves to the token ID of the auction as a bigint.
  public async getAuctionedTokenId(auctionId: bigint): Promise<bigint> {
    const auction = await this.auctionHouseContract.getAuction(auctionId);
    return auction.tokenId;

   * Handles the RequestCreateAuction event by selecting the next tag to be auctioned and
   * creating an auction for it on the blockchain.
  public async handleRequestCreateAuctionEvent() {
    try {
      // Check the current number of active auctions and compare it with the max auctions limit
      const activeAuctionsCount = await this.auctionHouseContract.getActiveCount();
      const maxAuctions = await this.auctionHouseContract.maxAuctions();

      if (activeAuctionsCount < maxAuctions) {
        // Only proceed if there are open slots for auctions
        const platformAccount = await this.getPlatformAddress();
        const lastAuctionId = await this.getLastAuctionId();
        const lastAuctionTokenId = (await this.getAuctionedTokenId(lastAuctionId)).toString();
        const tagService = new TagService(); // Instantiate TagService for determining the next tag.
        const tokenId = await tagService.findNextCTAG(platformAccount, lastAuctionTokenId);

        const tx = await this.auctionHouseContract.fulfillRequestCreateAuction(BigInt(tokenId));
        const receipt = await tx.wait();
        console.log(`Next token successfully released. Txn Hash: ${receipt.transactionHash}`);
      } else {
        console.log("No open auction slots available. Skipping auction creation.");
    } catch (error) {
      console.error("An unexpected error occurred: ", error);

   * Starts listening for RequestCreateAuction events from the Auction House contract and
   * processes them by triggering handleRequestCreateAuctionEvent.
  public async watchRequestCreateAuction() {
    console.log("***** Local auction oracle started *****");
    console.log("Listening for RequestCreateAuction event...");

    this.auctionHouseContract.on("RequestCreateAuction", async (...args) => {
      console.log("RequestCreateAuction event detected:", args);
      await this.handleRequestCreateAuctionEvent();

    // Defines a function to stop listening to events, intended for cleanup.
    const stopListening = () => {
      console.log("Stopped listening to RequestCreateAuction events.");

    // Attaches stopListening to the SIGINT event for graceful shutdown.
    process.on("SIGINT", () => {
      console.log("Cleanup complete. Exiting now.");

Thank you. Is the Dependency Version the latest, i.e. v2024-01-18?

Yes. my package.json:

  "dependencies": {
    "@ethereum-tag-service/contracts": "workspace:*",
    "@openzeppelin/defender-as-code": "^2.6.0",
    "@openzeppelin/defender-relay-client": "1.52.0",
    "@openzeppelin/defender-sdk": "1.5.0",
    "@openzeppelin/defender-sdk-relay-signer-client": "1.11.0",
    "axios": "1.6.1",
    "axios-retry": "3.5.0",
    "ethers": "5.5.3"
  "devDependencies": {
    "@rollup/plugin-commonjs": "^16.0.0",
    "@rollup/plugin-json": "^4.1.0",
    "@rollup/plugin-node-resolve": "^10.0.0",
    "@rollup/plugin-typescript": "^6.1.0",
    "@sunodo/wagmi-plugin-hardhat-deploy": "github:ethereum-tag-service/wagmi-plugin-hardhat-deploy#ets-temp",
    "@types/node": "^20.10.4",
    "@wagmi/cli": "^1.5.2",
    "builtin-modules": "^3.1.0",
    "dotenv": "^16.3.1",
    "dotenv-cli": "^7.3.0",
    "rollup": "^2.33.3",
    "serverless": "^3.38.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.4.2"

@mow fyi, this is my rollup.config.ts:

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import json from "@rollup/plugin-json";
import builtins from "builtin-modules";

export default {
  input: "src/defender/actions/release-next-auction/index.ts",
  output: {
    file: "dist/defender/actions/release-next-auction/index.js",
    format: "cjs",
    //inlineDynamicImports: true, // Inline dynamic imports
  plugins: [resolve({ preferBuiltins: true }), commonjs(), json({ compact: true }), typescript()],
  external: [...builtins, "ethers", "axios", /^@openzeppelin\/defender-relay-client(\/.*)?$/],

Hi guys, can anyone guide me on how to send ERC20 tokens using openzepelin sdk , using relayer.My main focus is to send the token while openzeppelin pays for gas transaction, want to accomplish this at the backend.Will appreciate this help