Skip to content

Commit e777105

Browse files
authored
Merge pull request #57 from milktoastlab/fix-magic-eden-sales-tracking
Add support for MagicEden V2
2 parents df78db4 + 65d2f66 commit e777105

11 files changed

+279
-142
lines changed

.env

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ SUBSCRIPTION_DISCORD_CHANNEL_ID=
77
# Mint address to watch for sales
88
# This variable supports multiple addressses with comma e.g. SUBSCRIPTION_MINT_ADDRESS=add123,add456
99
SUBSCRIPTION_MINT_ADDRESS=
10+
11+
# Magic eden API
12+
MAGIC_EDEN_URL=https://api-mainnet.magiceden.dev/v2
13+
# Enter the NFT collection that you want to track
14+
MAGIC_EDEN_COLLECTION=
15+
# The discord channel to notify
16+
MAGIC_EDEN_DISCORD_CHANNEL_ID=
17+
1018
# Twitter secrets
1119
TWITTER_API_KEY=
1220
TWITTER_API_KEY_SECRET=

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ TWITTER_API_KEY=
110110
TWITTER_API_KEY_SECRET=
111111
TWITTER_ACCESS_TOKEN=
112112
TWITTER_ACCESS_TOKEN_SECRET=
113+
# Magic eden API
114+
MAGIC_EDEN_URL=https://api-mainnet.magiceden.dev/v2
115+
# Enter the NFT collection that you want to track
116+
MAGIC_EDEN_COLLECTION=
117+
# The discord channel to notify
118+
MAGIC_EDEN_DISCORD_CHANNEL_ID=
113119
```
114120
https://github.com/milktoastlab/SolanaNFTBot/blob/main/.env
115121

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

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

181+
### Magic Eden variables
182+
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.
183+
To enable this feature, you will need to add the following variables to your `.env` file:
184+
185+
__MAGIC_EDEN_COLLECTION__
186+
187+
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.
188+
```
189+
Example:
190+
https://magiceden.io/marketplace/milktoast
191+
```
192+
The collection key is "milktoast"
193+
194+
__MAGIC_EDEN_DISCORD_CHANNEL_ID__
195+
196+
This is the discord channel to notify. Same as `SUBSCRIPTION_DISCORD_CHANNEL_ID` but it doesn't support multiple channels at the moment.
197+
198+
175199
## Production deployment
176200

177201
The solana nft bot is containerized, you can deploy it on any hosting service that supports docker.

src/config/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@ interface TwitterConfig {
1313
accessSecret: string;
1414
}
1515

16+
export interface MagicEdenConfig {
17+
url: string;
18+
collection: string;
19+
discordChannelId: string;
20+
}
21+
1622
export interface Config {
1723
twitter: TwitterConfig;
1824
discordBotToken: string;
1925
queueConcurrency: number;
2026
subscriptions: Subscription[];
27+
magicEdenConfig: MagicEdenConfig;
2128
}
2229

2330
export type Env = { [key: string]: string };
@@ -76,6 +83,11 @@ export function loadConfig(env: Env): MutableConfig {
7683
discordBotToken: env.DISCORD_BOT_TOKEN || "",
7784
queueConcurrency: parseInt(env.QUEUE_CONCURRENCY || "2", 10),
7885
subscriptions: loadSubscriptions(env),
86+
magicEdenConfig: {
87+
url: env.MAGIC_EDEN_URL || "",
88+
collection: env.MAGIC_EDEN_COLLECTION || "",
89+
discordChannelId: env.MAGIC_EDEN_DISCORD_CHANNEL_ID || "",
90+
},
7991
};
8092

8193
return {
Lines changed: 0 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
import magicEden from "./magicEden";
2-
import magicEdenSaleTx from "./__fixtures__/magicEdenSaleTx";
3-
import magicEdenSaleFromBidTx from "./__fixtures__/magicEdenSaleFromBidTx";
4-
import { SaleMethod } from "./types";
5-
import { Connection } from "@solana/web3.js";
62

73
jest.mock("lib/solana/NFTData", () => {
84
return {
@@ -13,131 +9,9 @@ jest.mock("lib/solana/NFTData", () => {
139
});
1410

1511
describe("magicEden", () => {
16-
const conn = new Connection("https://test/");
17-
1812
test("itemUrl", () => {
1913
expect(magicEden.itemURL("xxx1")).toEqual(
2014
"https://magiceden.io/item-details/xxx1"
2115
);
2216
});
23-
24-
describe("parseNFTSale", () => {
25-
test("sale transaction should return NFTSale", async () => {
26-
const sale = await magicEden.parseNFTSale(conn, magicEdenSaleTx);
27-
expect(sale.transaction).toEqual(
28-
"626EgwuS6dbUKrkZujQCFjHiRsz92ALR5gNAEg2eMpZzEo88Cci6HifpDFcvgYR8j88nXUq1nRUA7UDRdvB7Y6WD"
29-
);
30-
expect(sale.token).toEqual(
31-
"8pwYVy61QiSTJGPc8yYfkVPLBBr8r17WkpUFRhNK6cjK"
32-
);
33-
expect(sale.soldAt).toEqual(new Date(1635141315000));
34-
expect(sale.marketplace).toEqual(magicEden);
35-
expect(sale.getPriceInLamport()).toEqual(3720000000);
36-
expect(sale.getPriceInSOL()).toEqual(3.72);
37-
38-
const expectedTransfers = [
39-
{
40-
to: "2NZukH2TXpcuZP4htiuT8CFxcaQSWzkkR6kepSWnZ24Q",
41-
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
42-
revenue: {
43-
amount: 74400000,
44-
symbol: "lamport",
45-
},
46-
},
47-
{
48-
to: "4eQwMqAA4c2VUD51rqfAke7kqeFLAxcxSB67rtFjDyZA",
49-
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
50-
revenue: {
51-
amount: 74400000,
52-
symbol: "lamport",
53-
},
54-
},
55-
{
56-
to: "Dz9kwoBVVzF11cHeKotQpA7t4aeCQsgRpVw4dg8zkntg",
57-
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
58-
revenue: {
59-
amount: 74400000,
60-
symbol: "lamport",
61-
},
62-
},
63-
{
64-
to: "4xHEEswq2T2E5uNoa1uw34RNKzPerayBHxX3P4SaR7cD",
65-
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
66-
revenue: {
67-
amount: 74400000,
68-
symbol: "lamport",
69-
},
70-
},
71-
{
72-
to: "33CJriD17bUScYW7eKFjM6BPfkFWPerHfdpvtw3a8JdN",
73-
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
74-
revenue: {
75-
amount: 74400000,
76-
symbol: "lamport",
77-
},
78-
},
79-
{
80-
to: "HWZybKNqMa93EmHK2ESL2v1XShcnt4ma4nFf14497jNS",
81-
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
82-
revenue: {
83-
amount: 74400000,
84-
symbol: "lamport",
85-
},
86-
},
87-
{
88-
to: "HihC794BdNCetkizxdFjVD2KiKWirGYbm2ojvRYXQd6H",
89-
from: "U7ZkJtaAwvBHt9Tw5BK8sdp2wLrEe7p1g3kFxB9WJCu",
90-
revenue: {
91-
amount: 3273600000,
92-
symbol: "lamport",
93-
},
94-
},
95-
];
96-
expect(sale.transfers.length).toEqual(expectedTransfers.length);
97-
expectedTransfers.forEach((expectedTransfer, index) => {
98-
const transfer = sale.transfers[index];
99-
expect(transfer.from).toEqual(expectedTransfer.from);
100-
expect(transfer.to).toEqual(expectedTransfer.to);
101-
expect(transfer.revenue).toEqual(expectedTransfer.revenue);
102-
});
103-
expect(sale.method).toEqual(SaleMethod.Direct);
104-
expect(sale.seller).toEqual(
105-
"HihC794BdNCetkizxdFjVD2KiKWirGYbm2ojvRYXQd6H"
106-
);
107-
});
108-
test("bidding sale transaction should return NFTSale", async () => {
109-
const sale = await magicEden.parseNFTSale(conn, magicEdenSaleFromBidTx);
110-
expect(sale.transaction).toEqual(
111-
"1cSgCBgot6w4KevVvsZc2PiST16BsEh9KAvmnbsSC9xXvput4SXLoq5pneQfczQEBw3jjcdmupG7Gp6MjG5MLzy"
112-
);
113-
expect(sale.token).toEqual(
114-
"3SxS8hpvZ6BfHXwaURJAhtxXWbwnkUGA7HPV3b7uLnjN"
115-
);
116-
expect(sale.buyer).toEqual(
117-
"2fT7A7iKwDodPj5rm4u4tXRFny9JY1ttHhHGp1PsvsAn"
118-
);
119-
expect(sale.method).toEqual(SaleMethod.Bid);
120-
expect(sale.seller).toEqual(
121-
"AJ3r8njrEnHnwmv2JmnXEYoy7EfsxWQq7UcnLUhjuVab"
122-
);
123-
});
124-
test("non-sale transaction should return null", async () => {
125-
const invalidSaleTx = {
126-
...magicEdenSaleTx,
127-
meta: {
128-
...magicEdenSaleTx.meta,
129-
preTokenBalances: [],
130-
postTokenBalances: [],
131-
},
132-
};
133-
expect(await magicEden.parseNFTSale(conn, invalidSaleTx)).toBe(null);
134-
});
135-
test("non magic eden transaction", async () => {
136-
const nonMagicEdenSaleTx = {
137-
...magicEdenSaleTx,
138-
};
139-
nonMagicEdenSaleTx.meta.logMessages = ["Program xxx invoke [1]"];
140-
expect(await magicEden.parseNFTSale(conn, nonMagicEdenSaleTx)).toBe(null);
141-
});
142-
});
14317
});

src/lib/marketplaces/magicEden.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Marketplace, NFTSale } from "./types";
2-
import { parseNFTSaleOnTx } from "./helper";
2+
import { parseNFTSaleOnTx } from "lib/marketplaces/helper";
33

44
const magicEden: Marketplace = {
55
name: "Magic Eden",
@@ -10,6 +10,8 @@ const magicEden: Marketplace = {
1010
iconURL: "https://www.magiceden.io/img/favicon.png",
1111
itemURL: (token: String) => `https://magiceden.io/item-details/${token}`,
1212
profileURL: (address: String) => `https://magiceden.io/u/${address}`,
13+
// Deprecated MagicEden doesn't work with the existing ways of parsing NFT sales
14+
// Detecting MagicEden now happens via their API
1315
parseNFTSale(web3Conn, txResp): Promise<NFTSale | null> {
1416
return parseNFTSaleOnTx(web3Conn, txResp, this);
1517
},

src/lib/marketplaces/parseNFTSaleForAllMarkets.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,11 @@ describe("parseNFTSale", () => {
2222

2323
test("sale transaction should return NFTSale", async () => {
2424
const tests = [
25-
magicEdenSaleTx,
2625
digitalEyeSaleTx,
2726
solanartSaleTx,
2827
alphaArtSaleTx,
2928
exchangeArtSaleTx,
3029
solseaSaleTx,
31-
magicEdenSaleTxV2,
3230
openSeaSaleTx,
3331
].map(async (tx) => {
3432
const sale = await parseNFTSaleForAllMarkets(conn, tx);

src/lib/marketplaces/parseNFTSaleForAllMarkets.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ export default async function parseNFTSaleForAllMarkets(
77
tx: ParsedConfirmedTransaction
88
): Promise<NFTSale | null> {
99
for (let i = 0; i < marketplaces.length; i++) {
10-
const nftSale = await marketplaces[i].parseNFTSale(web3Conn, tx);
10+
const marketplace = marketplaces[i];
11+
if (!marketplace.parseNFTSale) {
12+
continue;
13+
}
14+
const nftSale = await marketplace.parseNFTSale(web3Conn, tx);
1115
if (nftSale) {
1216
return nftSale;
1317
}

src/lib/marketplaces/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Connection, ParsedConfirmedTransaction } from "@solana/web3.js";
2+
import { MagicEdenConfig } from "config";
23
import NFTData from "lib/solana/NFTData";
34

45
export enum SaleMethod {
@@ -12,7 +13,7 @@ export interface Marketplace {
1213
iconURL: string;
1314
itemURL: (token: String) => string;
1415
profileURL: (address: String) => string;
15-
parseNFTSale: (
16+
parseNFTSale?: (
1617
web3Conn: Connection,
1718
tx: ParsedConfirmedTransaction
1819
) => Promise<NFTSale | null>;

src/server.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import notifyDiscordSale, { getStatus } from "lib/discord/notifyDiscordSale";
1010
import { Env, loadConfig } from "config";
1111
import { Worker } from "workers/types";
1212
import notifyNFTSalesWorker from "workers/notifyNFTSalesWorker";
13+
import notifyMagicEdenNFTSalesWorker from "workers/notifyMagicEdenNFTSalesWorker";
1314
import { parseNFTSale } from "lib/marketplaces";
1415
import { ParsedTransactionWithMeta } from "@solana/web3.js";
1516
import notifyTwitter from "lib/twitter/notifyTwitter";
@@ -109,19 +110,31 @@ import queue from "queue";
109110
logger.log(`Ready on http://localhost:${port}`);
110111
});
111112

112-
if (!subscriptions.length) {
113-
logger.warn("No subscriptions loaded");
114-
return;
113+
let workers: Worker[] = [];
114+
if (subscriptions.length) {
115+
workers = subscriptions.map((s) => {
116+
const project = {
117+
discordChannelId: s.discordChannelId,
118+
mintAddress: s.mintAddress,
119+
};
120+
const notifier = notifierFactory.create(project);
121+
return notifyNFTSalesWorker(notifier, web3Conn, project);
122+
});
115123
}
116124

117-
const workers: Worker[] = subscriptions.map((s) => {
118-
const project = {
119-
discordChannelId: s.discordChannelId,
120-
mintAddress: s.mintAddress,
121-
};
122-
const notifier = notifierFactory.create(project);
123-
return notifyNFTSalesWorker(notifier, web3Conn, project);
124-
});
125+
if (config.magicEdenConfig.collection) {
126+
const notifier = notifierFactory.create({
127+
discordChannelId: config.magicEdenConfig?.discordChannelId,
128+
mintAddress: "",
129+
});
130+
workers.push(
131+
notifyMagicEdenNFTSalesWorker(
132+
notifier,
133+
web3Conn,
134+
config.magicEdenConfig
135+
)
136+
);
137+
}
125138

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

0 commit comments

Comments
 (0)