Skip to content

Commit

Permalink
Merge pull request #1250 from hypercerts-org/feature/marketplace-defe…
Browse files Browse the repository at this point in the history
…nder-autotasks

Feature/marketplace defender autotasks
  • Loading branch information
Jipperism authored Dec 22, 2023
2 parents 1a3b8d8 + 13a4852 commit 0f3e7d8
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 24 deletions.
2 changes: 1 addition & 1 deletion defender/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"dependencies": {
"@graphql-mesh/cache-localforage": "^0.95.7",
"@hypercerts-org/contracts": "0.8.11",
"@hypercerts-org/contracts": "1.0.0",
"@openzeppelin/defender-autotask-client": "1.50.0",
"@openzeppelin/defender-autotask-utils": "1.50.0",
"@openzeppelin/defender-base-client": "1.49.0",
Expand Down
168 changes: 168 additions & 0 deletions defender/src/auto-tasks/execute-taker-bid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
AutotaskEvent,
BlockTriggerEvent,
} from "@openzeppelin/defender-autotask-utils";
import { getNetworkConfigFromName } from "../networks";
import { createClient } from "@supabase/supabase-js";
import fetch from "node-fetch";
import { BigNumber, ethers } from "ethers";
import { MissingDataError, NotImplementedError } from "../errors";
import {
HypercertExchangeAbi,
HypercertMinterAbi,
} from "@hypercerts-org/contracts";

export async function handler(event: AutotaskEvent) {
console.log(
"Event: ",
JSON.stringify(
{ ...event, secrets: "HIDDEN", credentials: "HIDDEN" },
null,
2,
),
);
const network = getNetworkConfigFromName(event.autotaskName);
const { SUPABASE_URL, SUPABASE_SECRET_API_KEY } = event.secrets;
const ALCHEMY_KEY = event.secrets[network.alchemyKeyEnvName];

const client = createClient(SUPABASE_URL, SUPABASE_SECRET_API_KEY, {
global: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
fetch: (...args) => fetch(...args),
},
});

let provider;

if (ALCHEMY_KEY) {
provider = new ethers.providers.AlchemyProvider(
network.networkKey,
ALCHEMY_KEY,
);
} else if (network.rpc) {
provider = new ethers.providers.JsonRpcProvider(network.rpc);
} else {
throw new Error("No provider available");
}

// Check data availability
const body = event.request.body;
if (!("type" in body) || body.type !== "BLOCK") {
throw new NotImplementedError("Event body is not a BlockTriggerEvent");
}
const blockTriggerEvent = body as BlockTriggerEvent;
const contractAddress = blockTriggerEvent.matchedAddresses[0];
const fromAddress = blockTriggerEvent.transaction.from;
const txnLogs = blockTriggerEvent.transaction.logs;
const tx = await provider.getTransaction(blockTriggerEvent.hash);

if (!contractAddress) {
throw new MissingDataError(`body.matchedAddresses is missing`);
} else if (!fromAddress) {
throw new MissingDataError(`body.transaction.from is missing`);
} else if (!txnLogs) {
throw new MissingDataError(`body.transaction.logs is missing`);
} else if (!tx) {
throw new MissingDataError(`tx is missing`);
}

console.log("Contract address", contractAddress);
console.log("From address", fromAddress);

// TODO: Update contracts so we can use ABI from the @hypercerts-org/contracts package
const hypercertsMinterContractInterface = new ethers.utils.Interface(
HypercertMinterAbi,
);

// Parse TransferSingle events
const parsedLogs = txnLogs.map((l) => {
//Ignore unknown events
try {
return hypercertsMinterContractInterface.parseLog(l);
} catch (e) {
console.log("Failed to parse log", l);
return null;
}
});
console.log("Parsed logs: ", JSON.stringify(parsedLogs, null, 2));
const transferSingleEvents = parsedLogs.filter(
(e) => e !== null && e.name === "TransferSingle",
);

console.log(
"TransferSingle Events: ",
JSON.stringify(transferSingleEvents, null, 2),
);

if (transferSingleEvents.length !== 1) {
throw new MissingDataError(
`Unexpected saw ${transferSingleEvents.length} TransferSingle events`,
);
}

// Get claimID
const signerAddress = transferSingleEvents[0].args["from"] as string;
const itemId = BigNumber.from(transferSingleEvents[0].args["id"]).toString();

const hypercertExchangeContractInterface = new ethers.utils.Interface(
HypercertExchangeAbi,
);
// Parse TakerBid events
const takerBidEvents = txnLogs
.map((l) => {
//Ignore unknown events
try {
return hypercertExchangeContractInterface.parseLog(l);
} catch (e) {
console.log("Failed to parse log", l);
return null;
}
})
.filter((e) => e !== null && e.name === "TakerBid");

console.log("TakerBid Events: ", JSON.stringify(takerBidEvents, null, 2));

if (takerBidEvents.length !== 1) {
throw new MissingDataError(
`Unexpected saw ${takerBidEvents.length} TakerBid events`,
);
}

// Get claimID
const orderNonce = BigNumber.from(
takerBidEvents[0].args["nonceInvalidationParameters"][1],
).toString();
console.log(
"Signer Address: ",
signerAddress,
"Order nonce: ",
orderNonce,
"Fraction ID: ",
itemId,
"Chain ID: ",
network.chainId,
);

// Remove from DB
if (await tx.wait(5).then((receipt) => receipt.status === 1)) {
const deleteResult = await client
.from("marketplace-orders")
.delete()
.eq("signer", signerAddress)
.eq("chainId", network.chainId)
.eq("orderNonce", orderNonce)
.containedBy("itemIds", [itemId])
.select()
.throwOnError();
console.log("Deleted", deleteResult);

if (!deleteResult) {
throw new Error(
`Could not remove from database. Delete result: ${JSON.stringify(
deleteResult,
)}`,
);
}
}
}
11 changes: 7 additions & 4 deletions defender/src/create-sentinel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { abi } from "./HypercertMinterABI.js";
import config from "./config.js";
import { NetworkConfig } from "./networks";
import { SentinelClient } from "@openzeppelin/defender-sentinel-client";
Expand All @@ -13,12 +12,16 @@ export const createSentinel = async ({
autotaskID,
functionConditions = [],
eventConditions = [],
contractAddress,
abi,
}: {
name: string;
network: NetworkConfig;
autotaskID: string;
eventConditions?: EventCondition[];
functionConditions?: FunctionCondition[];
contractAddress: string;
abi: any;
}) => {
const client = new SentinelClient(config.credentials);
await client
Expand All @@ -27,8 +30,8 @@ export const createSentinel = async ({
network: network.networkKey,
confirmLevel: 1, // if not set, we pick the blockwatcher for the chosen network with the lowest offset
name,
addresses: [network.contractAddress],
abi: abi,
addresses: [contractAddress],
abi,
paused: false,
eventConditions,
functionConditions,
Expand All @@ -41,7 +44,7 @@ export const createSentinel = async ({
`Created sentinel`,
res.name,
"- monitoring address",
network.contractAddress,
contractAddress,
"- linked to autotask",
autotaskID,
);
Expand Down
17 changes: 11 additions & 6 deletions defender/src/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,28 @@ export const NETWORKS: SupportedNetworks = {
* We'll then subsequently use `getNetworkConfigFromName`
* to extract the network name from within the Autotask
* @param network
* @param contract
* @param name - name pre-encoding
* @returns
*/
export const encodeName = (network: NetworkConfig, name: string) =>
`[${network.networkKey}] ${name}`;
export const encodeName = (
network: NetworkConfig,
contract: "minter" | "exchange",
name: string,
) => `[${network.networkKey}][${contract}] ${name}`;

export const decodeName = (
encodedName: string,
): { networkKey: string; name: string } => {
const regex = /^\[(.+)\]\s(.+)$/;
): { networkKey: string; contract: string; name: string } => {
const regex = /^\[(.+)\]\[(.+)\]\s(.+)$/;
const match = encodedName.match(regex);
if (!match) {
throw new Error(`Invalid encoded name: ${encodedName}`);
}
const networkKey = match[1];
const name = match[2];
return { networkKey, name };
const contract = match[2];
const name = match[3];
return { networkKey, contract, name };
};

/**
Expand Down
65 changes: 59 additions & 6 deletions defender/src/rollout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,43 @@ import { createTask } from "./create-autotask";
import { createSentinel } from "./create-sentinel";
import { ApiError } from "./errors";
import { NetworkConfig, encodeName } from "./networks";
import {
HypercertExchangeAbi,
deployments,
asDeployedChain,
HypercertMinterAbi,
} from "@hypercerts-org/contracts";

export const rollOut = async (networks: NetworkConfig[]) => {
return await Promise.all(
networks.map(async (network) => {
console.log(
"Contract address",
network.chainId.toString(),
asDeployedChain(network.chainId.toString()),
deployments[asDeployedChain(network.chainId.toString())],
deployments[asDeployedChain(network.chainId.toString())]
.HypercertExchange,
);
// On allowlist created
const autoTaskOnAllowlistCreated = await createTask(
encodeName(network, "on-allowlist-created"),
encodeName(network, "minter", "on-allowlist-created"),
"on-allowlist-created",
);
if (!autoTaskOnAllowlistCreated) {
throw new ApiError(
encodeName(
network,
"minter",
"Could not create autoTask for on-allowlist-created",
),
);
}
await createSentinel({
name: encodeName(network, "AllowlistCreated"),
name: encodeName(network, "minter", "AllowlistCreated"),
network: network,
contractAddress: network.contractAddress,
abi: HypercertMinterAbi,
eventConditions: [
{ eventSignature: "AllowlistCreated(uint256,bytes32)" },
],
Expand All @@ -30,20 +47,23 @@ export const rollOut = async (networks: NetworkConfig[]) => {

// On batch minted
const autoTaskOnBatchMintClaimsFromAllowlists = await createTask(
encodeName(network, "batch-mint-claims-from-allowlists"),
encodeName(network, "minter", "batch-mint-claims-from-allowlists"),
"batch-mint-claims-from-allowlists",
);
if (!autoTaskOnBatchMintClaimsFromAllowlists) {
throw new ApiError(
encodeName(
network,
"minter",
"Could not create autoTask for batch-mint-claims-from-allowlists",
),
);
}
await createSentinel({
name: encodeName(network, "batchMintClaimsFromAllowlists"),
name: encodeName(network, "minter", "batchMintClaimsFromAllowlists"),
network: network,
contractAddress: network.contractAddress,
abi: HypercertMinterAbi,
autotaskID: autoTaskOnBatchMintClaimsFromAllowlists.autotaskId,
functionConditions: [
{
Expand All @@ -55,20 +75,23 @@ export const rollOut = async (networks: NetworkConfig[]) => {

// On single minted from allowlist
const autoTaskOnMintClaimFromAllowlist = await createTask(
encodeName(network, "mint-claim-from-allowlist"),
encodeName(network, "minter", "mint-claim-from-allowlist"),
"mint-claim-from-allowlist",
);
if (!autoTaskOnMintClaimFromAllowlist) {
throw new ApiError(
encodeName(
network,
"minter",
"Could not create autoTask for mint-claim-from-allowlist",
),
);
}
await createSentinel({
name: encodeName(network, "mintClaimFromAllowlist"),
name: encodeName(network, "minter", "mintClaimFromAllowlist"),
network: network,
contractAddress: network.contractAddress,
abi: HypercertMinterAbi,
autotaskID: autoTaskOnMintClaimFromAllowlist.autotaskId,
functionConditions: [
{
Expand All @@ -77,6 +100,36 @@ export const rollOut = async (networks: NetworkConfig[]) => {
},
],
});

// On execute taker bid
const autoTaskExecuteTakerBid = await createTask(
encodeName(network, "exchange", "execute-taker-bid"),
"execute-taker-bid",
);
if (!autoTaskExecuteTakerBid) {
throw new ApiError(
encodeName(
network,
"exchange",
"Could not create autoTask for execute-taker-bid",
),
);
}
await createSentinel({
name: encodeName(network, "exchange", "executeTakerBid"),
network: network,
autotaskID: autoTaskExecuteTakerBid.autotaskId,
contractAddress:
deployments[asDeployedChain(network.chainId.toString())]
.HypercertExchange,
abi: HypercertExchangeAbi,
functionConditions: [
{
functionSignature:
"executeTakerBid((address,bytes),(uint8,uint256,uint256,uint256,uint256,uint8,address,address,address,uint256,uint256,uint256,uint256[],uint256[],bytes),bytes,(bytes32,(bytes32,uint8)[]))",
},
],
});
}),
);
};
Loading

0 comments on commit 0f3e7d8

Please sign in to comment.