Skip to content

Commit 795a644

Browse files
v1.4.0 - Farming (#8)
* Add util tests * Refactor to listener + more tests + ts tests * Refactor all commands + some more tests * Add all listener setup tests * Further unit tests * linkWallet tests * getWallet tests * All commands unit tested * link wallet with XUMM show link * got so bad with case rename * readd files * Update node.js.yml * Exclude dist from tests * lowercase util sleep * Add XLS-20 NFT Support * Add /richlist command * Update Readme * Add Farming & Richlist command (#7) Added farming for token values Added commands related to farming (Start farming & stop farming) Added unit tests for farming commands Added CRON web endpoint for farming progress tracker Added /richlist command * Fix import casing
1 parent e10c8bb commit 795a644

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1097
-7
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
[![Build CI](https://github.com/jacobpretorius/XRPL-Discord-Bot/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/jacobpretorius/XRPL-Discord-Bot/actions/workflows/node.js.yml)
44

5-
See [the wiki](https://github.com/jacobpretorius/XRPL-Discord-Bot/wiki) for more information and guides.
5+
A customisable open source Discord bot that brings the power of the XRPL to Discord communities.
6+
7+
See [the wiki](https://github.com/jacobpretorius/XRPL-Discord-Bot/wiki) for more information and install guides.

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "XRPL-Discord-Bot",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"description": "A customisable open-source Discord bot that brings the power of the XRPL to Discord communities.",
55
"repository": {
66
"type": "git",

src/app.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import EventFactory from './events/EventFactory';
1414
import { EventTypes } from './events/BotEvents';
1515
import { scanLinkedWallets } from './business/scanLinkedWallets';
1616
import { scanLinkedAccounts } from './business/scanLinkedAccounts';
17+
import { scanFarmingWallets } from './business/scanFarmingWallets';
1718
import xummWebhook from './integration/xumm/webhook';
1819

1920
// Discord Client
@@ -51,6 +52,10 @@ const commands = [
5152
name: 'price',
5253
description: 'Shows the last trading price on Sologenic 💲',
5354
},
55+
{
56+
name: 'richlist',
57+
description: 'Shows the top 10 community members 🙌',
58+
},
5459
];
5560

5661
const rest = new REST({ version: '9' }).setToken(SETTINGS.DISCORD.BOT_TOKEN);
@@ -101,19 +106,22 @@ discordClient.on('ready', async () => {
101106

102107
discordClient.login(SETTINGS.DISCORD.BOT_TOKEN);
103108

104-
// Webserver
109+
// Webserver setup
105110
const webServer = express();
106111

112+
// Basic web page to check its up locally
107113
webServer.get('/', async (req, res) => {
108114
res.send(
109115
'The XRPL Discord Bot is running! See <a href="https://github.com/jacobpretorius/XRPL-Discord-Bot">here for updates</a>'
110116
);
111117
});
112118

119+
// /status endpoint (useful for automated uptime monitoring)
113120
webServer.get('/status', async (req, res) => {
114121
res.send('Ok');
115122
});
116123

124+
// Endpoints used by CRON task runners
117125
webServer.get('/updateWallets', async (req, res) => {
118126
req.setTimeout(9999999);
119127
const forceRefreshRoles =
@@ -134,6 +142,19 @@ webServer.get('/updateAccounts', async (req, res) => {
134142
res.send(await scanLinkedAccounts(discordClient, LOGGER));
135143
});
136144

145+
webServer.get('/updateFarmingWallets', async (req, res) => {
146+
if (!SETTINGS.FARMING.ENABLED) {
147+
return res.send('ERROR: Farming is currently disabled in settings.ts');
148+
}
149+
150+
req.setTimeout(9999999);
151+
const forceRefreshHours =
152+
req.query.forceRefreshHours === 'true' ? true : false;
153+
154+
res.send(await scanFarmingWallets(discordClient, LOGGER, forceRefreshHours));
155+
});
156+
157+
// Endpoints used by XUMM
137158
webServer.use('/xummWebhook', bodyParser.json());
138159

139160
webServer.post('/xummWebhook', async (req, res) => {

src/business/scanFarmingWallets.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import SETTINGS from '../settings';
2+
import sleep from '../utils/sleep';
3+
import { getFarmingWallets } from '../data/getFarmingWallets';
4+
import { getUserByDiscordId } from '../data/getUserByDiscordId';
5+
import { updateWalletFarming } from '../data/updateWalletFarming';
6+
import { deleteWalletFarming } from '../data/deleteWalletFarming';
7+
import { setWalletEarned } from '../data/setWalletEarned';
8+
9+
import { Client, TextChannel } from 'discord.js';
10+
11+
const scanFarmingWallets = async (
12+
client: Client,
13+
LOGGER: any,
14+
forceRefreshHours: boolean
15+
) => {
16+
// Get all wallets
17+
const farmingWallets = await getFarmingWallets();
18+
19+
let changes = 0;
20+
let paused = 0;
21+
for (let i = 0; i < farmingWallets.length; i++) {
22+
const farmingProgress = farmingWallets[i];
23+
const farmingUser = await getUserByDiscordId(farmingProgress?.discordId);
24+
25+
if (!farmingProgress || !farmingUser) {
26+
continue;
27+
}
28+
29+
await sleep(100);
30+
31+
// Check holdings
32+
if (farmingUser.totalPoints < farmingProgress.rewardPointsRequired) {
33+
// They don't have enough anymore, pause their farming.
34+
paused += 1;
35+
await updateWalletFarming(
36+
farmingProgress.discordId,
37+
farmingProgress.rewardPointsRequired,
38+
farmingProgress.rewardGoalAmount,
39+
farmingProgress.rewardGoalHoursRequired,
40+
farmingProgress.hoursFarmed,
41+
false, // Pause this wallet farming
42+
farmingProgress.dateStarted
43+
);
44+
continue;
45+
}
46+
47+
// All good for this farmer
48+
changes += 1;
49+
let hoursFarmed = farmingProgress.hoursFarmed + 1;
50+
51+
// Check if we have to refresh wallets with enough points to continue farming
52+
if (forceRefreshHours) {
53+
// If so, credit them for all hours since date farming started
54+
// this is for if there are network issues for a long time and we
55+
// want to be nice to the community users.
56+
const startedAt = new Date(farmingProgress.dateStarted).getTime();
57+
const now = new Date().getTime();
58+
hoursFarmed = Math.floor(Math.abs(now - startedAt) / 36e5);
59+
}
60+
61+
// Check if they can claim now and save to other table
62+
const completed = hoursFarmed >= farmingProgress.rewardGoalHoursRequired;
63+
64+
if (completed) {
65+
// Completed farming, save to earned table
66+
await setWalletEarned(
67+
farmingProgress.discordId,
68+
farmingUser.discordUsername,
69+
farmingUser.discordDiscriminator,
70+
farmingProgress.rewardGoalAmount,
71+
hoursFarmed,
72+
farmingProgress.dateStarted,
73+
new Date().toISOString()
74+
);
75+
76+
// Delete from farmed table
77+
await deleteWalletFarming(farmingProgress.discordId);
78+
79+
// Post in payout channel
80+
const channel = client.channels.cache.get(
81+
SETTINGS.DISCORD.FARMING_DONE_CHANNEL_ID
82+
) as TextChannel;
83+
if (channel !== null) {
84+
channel.send(
85+
`🚨🚜 User finished farming: ${farmingUser.discordUsername}#${farmingUser.discordDiscriminator} with discord id ${farmingUser.discordId} has finished farming for ${farmingProgress.rewardGoalAmount} ${SETTINGS.FARMING.EARNINGS_NAME} with ${hoursFarmed} hours.`
86+
);
87+
}
88+
89+
// DM user
90+
const walletUser = client.users.cache.get(farmingUser.discordId);
91+
92+
if (walletUser) {
93+
await walletUser.send(
94+
`Congratulations! You have finished farming for ${farmingProgress.rewardGoalAmount} ${SETTINGS.FARMING.EARNINGS_NAME} at ${hoursFarmed} hours!`
95+
);
96+
}
97+
} else {
98+
// Didn't complete yet, save progress so far.
99+
await updateWalletFarming(
100+
farmingProgress.discordId,
101+
farmingProgress.rewardPointsRequired,
102+
farmingProgress.rewardGoalAmount,
103+
farmingProgress.rewardGoalHoursRequired,
104+
hoursFarmed,
105+
true,
106+
farmingProgress.dateStarted
107+
);
108+
}
109+
}
110+
111+
if (LOGGER !== null) {
112+
LOGGER.trackMetric({ name: 'scanFarmers-changes', value: changes });
113+
}
114+
115+
return `All done for ${farmingWallets.length} farmers with ${changes} changes and ${paused} paused`;
116+
};
117+
118+
export { scanFarmingWallets };

src/commands/adminLinkWallet.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import adminLinkWallet from './adminLinkWallet';
22
import isAdmin from '../utils/isAdmin';
33
import getWalletAddress from '../utils/getWalletAddress';
44
import { getWalletHoldings } from '../integration/xrpl/getWalletHoldings';
5-
import { getUserAccountIdByUsername } from '../integration/discord/getUserAccountIdByUsername';
65
import { updateUserWallet } from '../data/updateUserWallet';
76
import { updateUserRoles } from '../integration/discord/updateUserRoles';
87
import getUserNameFromAdminLinkWalletCommand from '../utils/getUserNameFromAdminLinkWalletCommand';

src/commands/help.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import help from './help';
22
import { Message } from 'discord.js';
33
import isAdmin from '../utils/isAdmin';
4+
import isFarmingEnabled from '../utils/isFarmingEnabled';
5+
import SETTINGS from '../settings';
46

57
jest.mock('../utils/isAdmin', () => jest.fn());
8+
jest.mock('../utils/isFarmingEnabled', () => jest.fn());
69

710
describe('help command logic', () => {
811
let message: Message;
@@ -22,6 +25,9 @@ describe('help command logic', () => {
2225
};
2326

2427
(isAdmin as jest.MockedFunction<typeof isAdmin>).mockReturnValue(false);
28+
(
29+
isFarmingEnabled as jest.MockedFunction<typeof isFarmingEnabled>
30+
).mockReturnValue(false);
2531
});
2632

2733
afterEach(() => {
@@ -76,4 +82,21 @@ describe('help command logic', () => {
7682

7783
expect(message.reply).toHaveBeenCalledWith(reply);
7884
});
85+
86+
it('Responds to help message with farming details when farming enabled', async () => {
87+
(
88+
isFarmingEnabled as jest.MockedFunction<typeof isFarmingEnabled>
89+
).mockReturnValue(true);
90+
91+
const reply = `You can
92+
- Link a wallet to your account using: 'linkwallet WALLETADDRESSHERE'
93+
- Check wallet points using: 'checkwallet WALLETADDRESSHERE'
94+
- Start farming for ${SETTINGS.FARMING.EARNINGS_NAME}: 'start farming'
95+
- Check farming progress: 'check farming'
96+
- Delete farming progress: 'stop farming'`;
97+
98+
await help(payload);
99+
100+
expect(message.reply).toHaveBeenCalledWith(reply);
101+
});
79102
});

src/commands/help.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import isAdmin from '../utils/isAdmin';
22
import { Message } from 'discord.js';
33
import { EventPayload } from '../events/BotEvents';
4+
import SETTINGS from '../settings';
5+
import isFarmingEnabled from '../utils/isFarmingEnabled';
46

57
const processCommand = async (message: Message) => {
68
let reply = `You can
79
- Link a wallet to your account using: 'linkwallet WALLETADDRESSHERE'
810
- Check wallet points using: 'checkwallet WALLETADDRESSHERE'
911
`;
1012

13+
if (isFarmingEnabled()) {
14+
reply += `- Start farming for ${SETTINGS.FARMING.EARNINGS_NAME}: 'start farming'
15+
- Check farming progress: 'check farming'
16+
- Delete farming progress: 'stop farming'`;
17+
}
18+
1119
if (isAdmin(message.author.id)) {
1220
reply += `\nAdmin commands
1321
- Link a wallet to user account using: 'adminlinkwallet WALLETADDRESSHERE DISCORDUSER#NUMBER'

src/commands/richlist.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { getTopHolders } from '../data/getTopHolders';
2+
import richlist from './richlist';
3+
import { CommandInteraction } from 'discord.js';
4+
import { jest } from '@jest/globals';
5+
6+
jest.mock('../data/getTopHolders');
7+
8+
describe('richlist interaction logic', () => {
9+
let interaction: CommandInteraction;
10+
let payload: any;
11+
12+
beforeEach(() => {
13+
interaction = {
14+
author: { id: '123' },
15+
commandName: 'richlist',
16+
reply: jest.fn(),
17+
} as unknown as CommandInteraction;
18+
19+
payload = {
20+
handled: false,
21+
interaction,
22+
};
23+
});
24+
25+
afterEach(() => {
26+
jest.resetAllMocks();
27+
});
28+
29+
it('calls interaction.reply when payload.handled is false', async () => {
30+
payload.handled = false;
31+
32+
await richlist(payload);
33+
34+
expect(interaction.reply).toHaveBeenCalled();
35+
});
36+
37+
it('does not call interaction.reply when payload.handled is true', async () => {
38+
payload.handled = true;
39+
40+
await richlist(payload);
41+
42+
expect(interaction.reply).not.toHaveBeenCalled();
43+
});
44+
45+
it('calls interaction.reply with top members if all ok', async () => {
46+
const userName = 'User';
47+
const discriminator = '123';
48+
const points = 55;
49+
50+
(
51+
getTopHolders as jest.MockedFunction<typeof getTopHolders>
52+
).mockReturnValue(
53+
Promise.resolve([
54+
{
55+
discordId: '123456',
56+
discordUsername: userName,
57+
discordDiscriminator: discriminator,
58+
previousDiscordUsername: 'Prev',
59+
previousDiscordDiscriminator: '333',
60+
totalPoints: points,
61+
wallets: [
62+
{ address: 'wallet', points: 50, verified: false },
63+
{ address: 'wallet2', points: 5, verified: true },
64+
],
65+
},
66+
])
67+
);
68+
69+
await richlist(payload);
70+
71+
expect(interaction.reply).toHaveBeenCalledWith({
72+
content: `Top 10 community holders:\n ${points}\t\t->\t\t${userName}#${discriminator}`,
73+
});
74+
});
75+
});

0 commit comments

Comments
 (0)