Skip to content

Commit

Permalink
Merge pull request #57 from milktoastlab/fix-magic-eden-sales-tracking
Browse files Browse the repository at this point in the history
Add support for MagicEden V2
  • Loading branch information
kryptoj authored Nov 18, 2022
2 parents df78db4 + 65d2f66 commit e777105
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 142 deletions.
8 changes: 8 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ SUBSCRIPTION_DISCORD_CHANNEL_ID=
# Mint address to watch for sales
# This variable supports multiple addressses with comma e.g. SUBSCRIPTION_MINT_ADDRESS=add123,add456
SUBSCRIPTION_MINT_ADDRESS=

# Magic eden API
MAGIC_EDEN_URL=https://api-mainnet.magiceden.dev/v2
# Enter the NFT collection that you want to track
MAGIC_EDEN_COLLECTION=
# The discord channel to notify
MAGIC_EDEN_DISCORD_CHANNEL_ID=

# Twitter secrets
TWITTER_API_KEY=
TWITTER_API_KEY_SECRET=
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ TWITTER_API_KEY=
TWITTER_API_KEY_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=
# Magic eden API
MAGIC_EDEN_URL=https://api-mainnet.magiceden.dev/v2
# Enter the NFT collection that you want to track
MAGIC_EDEN_COLLECTION=
# The discord channel to notify
MAGIC_EDEN_DISCORD_CHANNEL_ID=
```
https://github.com/milktoastlab/SolanaNFTBot/blob/main/.env

Expand Down Expand Up @@ -172,6 +178,24 @@ Then, click on the Keys and tokens tab, and generate the Access Token and Secret

<img src= https://user-images.githubusercontent.com/50549441/149973388-58f3a303-91f4-4e1b-ab7f-dfc2a22aa5da.png>

### Magic Eden variables
Magic eden's NFT trading program has changed to V2, which means the old way of detecting sales won't work anymore. We have updated the bot to use the new API to detect sales.
To enable this feature, you will need to add the following variables to your `.env` file:

__MAGIC_EDEN_COLLECTION__

This is the collection key to magic eden. To find our what it is, navigate to the collection page and look at the url. It should be the last part of the url.
```
Example:
https://magiceden.io/marketplace/milktoast
```
The collection key is "milktoast"

__MAGIC_EDEN_DISCORD_CHANNEL_ID__

This is the discord channel to notify. Same as `SUBSCRIPTION_DISCORD_CHANNEL_ID` but it doesn't support multiple channels at the moment.


## Production deployment

The solana nft bot is containerized, you can deploy it on any hosting service that supports docker.
Expand Down
12 changes: 12 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ interface TwitterConfig {
accessSecret: string;
}

export interface MagicEdenConfig {
url: string;
collection: string;
discordChannelId: string;
}

export interface Config {
twitter: TwitterConfig;
discordBotToken: string;
queueConcurrency: number;
subscriptions: Subscription[];
magicEdenConfig: MagicEdenConfig;
}

export type Env = { [key: string]: string };
Expand Down Expand Up @@ -76,6 +83,11 @@ export function loadConfig(env: Env): MutableConfig {
discordBotToken: env.DISCORD_BOT_TOKEN || "",
queueConcurrency: parseInt(env.QUEUE_CONCURRENCY || "2", 10),
subscriptions: loadSubscriptions(env),
magicEdenConfig: {
url: env.MAGIC_EDEN_URL || "",
collection: env.MAGIC_EDEN_COLLECTION || "",
discordChannelId: env.MAGIC_EDEN_DISCORD_CHANNEL_ID || "",
},
};

return {
Expand Down
126 changes: 0 additions & 126 deletions src/lib/marketplaces/magicEden.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import magicEden from "./magicEden";
import magicEdenSaleTx from "./__fixtures__/magicEdenSaleTx";
import magicEdenSaleFromBidTx from "./__fixtures__/magicEdenSaleFromBidTx";
import { SaleMethod } from "./types";
import { Connection } from "@solana/web3.js";

jest.mock("lib/solana/NFTData", () => {
return {
Expand All @@ -13,131 +9,9 @@ jest.mock("lib/solana/NFTData", () => {
});

describe("magicEden", () => {
const conn = new Connection("https://test/");

test("itemUrl", () => {
expect(magicEden.itemURL("xxx1")).toEqual(
"https://magiceden.io/item-details/xxx1"
);
});

describe("parseNFTSale", () => {
test("sale transaction should return NFTSale", async () => {
const sale = await magicEden.parseNFTSale(conn, magicEdenSaleTx);
expect(sale.transaction).toEqual(
"626EgwuS6dbUKrkZujQCFjHiRsz92ALR5gNAEg2eMpZzEo88Cci6HifpDFcvgYR8j88nXUq1nRUA7UDRdvB7Y6WD"
);
expect(sale.token).toEqual(
"8pwYVy61QiSTJGPc8yYfkVPLBBr8r17WkpUFRhNK6cjK"
);
expect(sale.soldAt).toEqual(new Date(1635141315000));
expect(sale.marketplace).toEqual(magicEden);
expect(sale.getPriceInLamport()).toEqual(3720000000);
expect(sale.getPriceInSOL()).toEqual(3.72);

const expectedTransfers = [
{
to: "2NZukH2TXpcuZP4htiuT8CFxcaQSWzkkR6kepSWnZ24Q",
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
revenue: {
amount: 74400000,
symbol: "lamport",
},
},
{
to: "4eQwMqAA4c2VUD51rqfAke7kqeFLAxcxSB67rtFjDyZA",
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
revenue: {
amount: 74400000,
symbol: "lamport",
},
},
{
to: "Dz9kwoBVVzF11cHeKotQpA7t4aeCQsgRpVw4dg8zkntg",
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
revenue: {
amount: 74400000,
symbol: "lamport",
},
},
{
to: "4xHEEswq2T2E5uNoa1uw34RNKzPerayBHxX3P4SaR7cD",
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
revenue: {
amount: 74400000,
symbol: "lamport",
},
},
{
to: "33CJriD17bUScYW7eKFjM6BPfkFWPerHfdpvtw3a8JdN",
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
revenue: {
amount: 74400000,
symbol: "lamport",
},
},
{
to: "HWZybKNqMa93EmHK2ESL2v1XShcnt4ma4nFf14497jNS",
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
revenue: {
amount: 74400000,
symbol: "lamport",
},
},
{
to: "HihC794BdNCetkizxdFjVD2KiKWirGYbm2ojvRYXQd6H",
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
revenue: {
amount: 3273600000,
symbol: "lamport",
},
},
];
expect(sale.transfers.length).toEqual(expectedTransfers.length);
expectedTransfers.forEach((expectedTransfer, index) => {
const transfer = sale.transfers[index];
expect(transfer.from).toEqual(expectedTransfer.from);
expect(transfer.to).toEqual(expectedTransfer.to);
expect(transfer.revenue).toEqual(expectedTransfer.revenue);
});
expect(sale.method).toEqual(SaleMethod.Direct);
expect(sale.seller).toEqual(
"HihC794BdNCetkizxdFjVD2KiKWirGYbm2ojvRYXQd6H"
);
});
test("bidding sale transaction should return NFTSale", async () => {
const sale = await magicEden.parseNFTSale(conn, magicEdenSaleFromBidTx);
expect(sale.transaction).toEqual(
"1cSgCBgot6w4KevVvsZc2PiST16BsEh9KAvmnbsSC9xXvput4SXLoq5pneQfczQEBw3jjcdmupG7Gp6MjG5MLzy"
);
expect(sale.token).toEqual(
"3SxS8hpvZ6BfHXwaURJAhtxXWbwnkUGA7HPV3b7uLnjN"
);
expect(sale.buyer).toEqual(
"2fT7A7iKwDodPj5rm4u4tXRFny9JY1ttHhHGp1PsvsAn"
);
expect(sale.method).toEqual(SaleMethod.Bid);
expect(sale.seller).toEqual(
"AJ3r8njrEnHnwmv2JmnXEYoy7EfsxWQq7UcnLUhjuVab"
);
});
test("non-sale transaction should return null", async () => {
const invalidSaleTx = {
...magicEdenSaleTx,
meta: {
...magicEdenSaleTx.meta,
preTokenBalances: [],
postTokenBalances: [],
},
};
expect(await magicEden.parseNFTSale(conn, invalidSaleTx)).toBe(null);
});
test("non magic eden transaction", async () => {
const nonMagicEdenSaleTx = {
...magicEdenSaleTx,
};
nonMagicEdenSaleTx.meta.logMessages = ["Program xxx invoke [1]"];
expect(await magicEden.parseNFTSale(conn, nonMagicEdenSaleTx)).toBe(null);
});
});
});
4 changes: 3 additions & 1 deletion src/lib/marketplaces/magicEden.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Marketplace, NFTSale } from "./types";
import { parseNFTSaleOnTx } from "./helper";
import { parseNFTSaleOnTx } from "lib/marketplaces/helper";

const magicEden: Marketplace = {
name: "Magic Eden",
Expand All @@ -10,6 +10,8 @@ const magicEden: Marketplace = {
iconURL: "https://www.magiceden.io/img/favicon.png",
itemURL: (token: String) => `https://magiceden.io/item-details/${token}`,
profileURL: (address: String) => `https://magiceden.io/u/${address}`,
// Deprecated MagicEden doesn't work with the existing ways of parsing NFT sales
// Detecting MagicEden now happens via their API
parseNFTSale(web3Conn, txResp): Promise<NFTSale | null> {
return parseNFTSaleOnTx(web3Conn, txResp, this);
},
Expand Down
2 changes: 0 additions & 2 deletions src/lib/marketplaces/parseNFTSaleForAllMarkets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@ describe("parseNFTSale", () => {

test("sale transaction should return NFTSale", async () => {
const tests = [
magicEdenSaleTx,
digitalEyeSaleTx,
solanartSaleTx,
alphaArtSaleTx,
exchangeArtSaleTx,
solseaSaleTx,
magicEdenSaleTxV2,
openSeaSaleTx,
].map(async (tx) => {
const sale = await parseNFTSaleForAllMarkets(conn, tx);
Expand Down
6 changes: 5 additions & 1 deletion src/lib/marketplaces/parseNFTSaleForAllMarkets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export default async function parseNFTSaleForAllMarkets(
tx: ParsedConfirmedTransaction
): Promise<NFTSale | null> {
for (let i = 0; i < marketplaces.length; i++) {
const nftSale = await marketplaces[i].parseNFTSale(web3Conn, tx);
const marketplace = marketplaces[i];
if (!marketplace.parseNFTSale) {
continue;
}
const nftSale = await marketplace.parseNFTSale(web3Conn, tx);
if (nftSale) {
return nftSale;
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/marketplaces/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Connection, ParsedConfirmedTransaction } from "@solana/web3.js";
import { MagicEdenConfig } from "config";
import NFTData from "lib/solana/NFTData";

export enum SaleMethod {
Expand All @@ -12,7 +13,7 @@ export interface Marketplace {
iconURL: string;
itemURL: (token: String) => string;
profileURL: (address: String) => string;
parseNFTSale: (
parseNFTSale?: (
web3Conn: Connection,
tx: ParsedConfirmedTransaction
) => Promise<NFTSale | null>;
Expand Down
35 changes: 24 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import notifyDiscordSale, { getStatus } from "lib/discord/notifyDiscordSale";
import { Env, loadConfig } from "config";
import { Worker } from "workers/types";
import notifyNFTSalesWorker from "workers/notifyNFTSalesWorker";
import notifyMagicEdenNFTSalesWorker from "workers/notifyMagicEdenNFTSalesWorker";
import { parseNFTSale } from "lib/marketplaces";
import { ParsedTransactionWithMeta } from "@solana/web3.js";
import notifyTwitter from "lib/twitter/notifyTwitter";
Expand Down Expand Up @@ -109,19 +110,31 @@ import queue from "queue";
logger.log(`Ready on http://localhost:${port}`);
});

if (!subscriptions.length) {
logger.warn("No subscriptions loaded");
return;
let workers: Worker[] = [];
if (subscriptions.length) {
workers = subscriptions.map((s) => {
const project = {
discordChannelId: s.discordChannelId,
mintAddress: s.mintAddress,
};
const notifier = notifierFactory.create(project);
return notifyNFTSalesWorker(notifier, web3Conn, project);
});
}

const workers: Worker[] = subscriptions.map((s) => {
const project = {
discordChannelId: s.discordChannelId,
mintAddress: s.mintAddress,
};
const notifier = notifierFactory.create(project);
return notifyNFTSalesWorker(notifier, web3Conn, project);
});
if (config.magicEdenConfig.collection) {
const notifier = notifierFactory.create({
discordChannelId: config.magicEdenConfig?.discordChannelId,
mintAddress: "",
});
workers.push(
notifyMagicEdenNFTSalesWorker(
notifier,
web3Conn,
config.magicEdenConfig
)
);
}

const _ = initWorkers(workers, () => {
// Add randomness between worker executions so the requests are not made all at once
Expand Down
Loading

0 comments on commit e777105

Please sign in to comment.