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) {
require("dotenv").config();
// 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'.");
process.exit(1);
}
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);
process.exit(1);
});
}
// /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("http://127.0.0.1:8545");
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 = () => {
this.auctionHouseContract.removeAllListeners("RequestCreateAuction");
console.log("Stopped listening to RequestCreateAuction events.");
};
// Attaches stopListening to the SIGINT event for graceful shutdown.
process.on("SIGINT", () => {
stopListening();
console.log("Cleanup complete. Exiting now.");
process.exit();
});
}
}
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
@emnul , what is the progress with this?
Hi @mow please follow the example here to understand how to define a custom environment for your Action and the Defender SDK example here to see how you can upload your custom action code to Defender.
When I run npm start
to try the action upload script I get this error:
NotAuthorizedException: Incorrect username or password.
at c:\Users\-\Documents\Projects\defender-sdk\examples\create-action\node_modules\amazon-cognito-identity-js\lib\Client.js:128:19
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
code: 'NotAuthorizedException'
}
What is Amazon Cognito for?
Hi this is an authentication error which means that you're likely using the wrong, missing, or invalid API keys when trying to make changes to your Defender environment. Please double check that you're correctly passing in the required credentials when using the Defender SDK.
I've checked by printing creds
to console just before const client = new Defender(creds)
at https://github.com/OpenZeppelin/defender-sdk/blob/31dd590954acc87e7ffb92893d75de66eeb68a04/examples/create-action/index.js#L13 and saw that the values for apiKey
and apiSecret
were correctly retrieved from the .env file, so the new Defender instance was passed these correct values.
The problem is with the credentials for Amazon Cognito which is in the amazon-cognito-identity-js package that defender-sdk imports, so please investigate.