Skip to content

Instantly share code, notes, and snippets.

@Athiriyya
Last active July 13, 2022 18:52
Show Gist options
  • Select an option

  • Save Athiriyya/ecb8b9bf0268399f9d338903cec42293 to your computer and use it in GitHub Desktop.

Select an option

Save Athiriyya/ecb8b9bf0268399f9d338903cec42293 to your computer and use it in GitHub Desktop.
Parse Defi Kingdoms Quests, Node.js
{
"name": "parse_quests_node",
"version": "1.0.0",
"description": "Parse Defi Kingdoms quest rewards",
"main": "parse_dfk_quest.js",
"type": "module",
"scripts": {
"start": "node parse_dfk_quest.js"
},
"author": "Athiriyya",
"email" : "athiriyya@gmail.com",
"url": "https://gist.github.com/Athiriyya/ecb8b9bf0268399f9d338903cec42293",
"license": "ISC",
"dependencies": {
"@thanpolas/degenking": "^1.0.1",
"argparse": "^2.0.1",
"ethers": "^5.6.9"
}
}
// Note that this depends on @thanpolas's great Node package:
// https://github.com/degen-heroes/degenking
// Install with `npm i @thanpolas/degenking` or use the package.json in this gist
// Also needed: argparse, ethers.js
import * as url from 'node:url';
import { ethers } from 'ethers';
import { ArgumentParser } from 'argparse';
import * as degenKing from '@thanpolas/degenking';
const { GOLD_DECIMALS, JEWEL_DECIMALS, ZERO_ADDRESS } = degenKing.CONSTANTS;
const CRYSTAL_DECIMALS = 18;
import { abiQuestCoreV1, abiQuestCoreV2, ALL_ITEMS } from '@thanpolas/degenking';
const { HARMONY, DFKN } = degenKing.CONSTANTS.NETWORK_IDS;
const GOLD_ADDRESSES = [
'0xeCCC54b836cD5bf114daec723de1c0d89B0C2A7b',
'0x576C260513204392F0eC0bc865450872025CB1cA',
'0x3a4EDcf3312f44EF027acfd8c21382a5259936e7',
'0x24B46b91E0862221D39dd30FAAd63999717860Ab',
];
const JEWEL_ADDRESSES = [
'0x72Cb10C6bfA5624dD07Ef608027E366bd690048F',
'0x63882d0438AdA0dD76ed2E6B7C2D53A55284A557',
];
const CRYSTAL_ADDRESSES = [
'0xa5c47B4bEb35215fB0CF0Ea6516F9921591c3aCE',
'0x04b9dA42306B023f3572e106B11D82aAd9D32EBb',
];
// ===========
// = GLOBALS =
// ===========
let activeChainId = HARMONY;
let ITEM_NAMES = {};
export async function main() {
const demoMode = true;
if (demoMode) {
runDemos();
} else {
const args = await parseAllArgs();
let { txid, chainId } = args;
const results = await getQuestResults(txid, chainId);
console.log(`Rewards for txid ${txid} on chain ${chainId}:\n${JSON.stringify(results, null, 4)}`);
}
}
async function runDemos() {
const demos = [
// A single-hero v1 harmony quest:
['0x252a70c70bcbd32e02c1389f77cdfd0b3bc16eecdc61ba7e83b392b187383136', HARMONY],
// 2-hero v2 harmony quest:
['0x252a70c70bcbd32e02c1389f77cdfd0b3bc16eecdc61ba7e83b392b187383136', HARMONY],
// 2-hero v2 DFK Chain quest:
['0x1d3af48baeadcbc737cd1205acdd5d40da7bc8f3052811d46de087042268c72c', DFKN],
];
demos.forEach(async ([txid, chainId]) => {
const results = await getQuestResults(txid, chainId);
const resultsStr = JSON.stringify(results, null, 4);
console.log(`Rewards for txid ${txid} on chain ${chainId}:\n${resultsStr}\n\n`);
});
}
export async function parseAllArgs() {
const description = `Parse Defi Kingdoms quest results`;
const parser = new ArgumentParser({ description });
parser.add_argument('--txid', '-t', { type: 'str', help: 'Transaction ID', required: true });
parser.add_argument('--chain', '-c', {
choices: ['harmony', 'dfk'],
help: 'Which blockchain <TXID> occurred on',
default: 'harmony',
});
let args = parser.parse_args();
// Internally we use chainId rather than a string. Set that on the args namespace here.
args.chainId = args.chain === 'dfk' ? DFKN : HARMONY;
return args;
}
export async function getQuestResults(txId, chainId = undefined) {
chainId = chainId || activeChainId;
const { provider } = getRPCProviderByChain(chainId);
const txReceipt = await provider.getTransactionReceipt(txId);
const result = parseEndedQuestReceipt(txReceipt, chainId);
return result;
}
export function parseEndedQuestReceipt(txReceipt, chainId = undefined) {
chainId = chainId || activeChainId;
let result;
const heroDicts = {};
if (!txReceipt) {
return {};
}
const iface = getEthersQuestInterface(txReceipt, chainId);
txReceipt.logs.forEach((l) => {
try {
const parsedLog = iface.parseLog(l);
const { name, args } = parsedLog;
if (name === 'QuestXP') {
let { heroId, xpEarned, questId, player } = args;
heroId = heroId.toNumber();
xpEarned = xpEarned.toNumber();
if (!heroDicts[heroId]) {
heroDicts[heroId] = { heroId, rewards: [], skillUp: 0, xpEarned: 0 };
}
heroDicts[heroId].xpEarned += xpEarned;
// the `result` object contains info describing the
// entire quest. V2 training quests have no skillUp and may
// have no found items, but all completed quests should return
// some XP, so set that info here if it hasn't been set yet.
if (!result) {
result = {
questId: questId.toNumber(),
player,
rewards: [],
transactionHash: l.transactionHash,
blockNumber: txReceipt.blockNumber,
};
}
}
// QuestReward is the v1 Reward Event
else if (name === 'QuestReward') {
let { heroId, itemQuantity: quantity, rewardItem } = args;
heroId = heroId.toNumber();
if (!heroDicts[heroId]) {
heroDicts[heroId] = { heroId, rewards: [], skillUp: 0, xpEarned: 0 };
}
if (rewardItem !== ZERO_ADDRESS) {
const rewardDesc = labelForReward(rewardItem, quantity, chainId);
heroDicts[heroId].rewards.push(rewardDesc);
}
}
// RewardMinted is the V2 Reward Event
if (name === 'RewardMinted') {
let { heroId, amount: quantity, reward: rewardItem } = args;
heroId = heroId.toNumber();
if (!heroDicts[heroId]) {
heroDicts[heroId] = { heroId, rewards: [], skillUp: 0, xpEarned: 0 };
}
if (rewardItem !== ZERO_ADDRESS) {
const rewardDesc = labelForReward(rewardItem, quantity, chainId);
heroDicts[heroId].rewards.push(rewardDesc);
}
}
// Profession quests sometimes return a skill increase
else if (name === 'QuestSkillUp') {
let { heroId, skillUp } = args;
heroId = heroId.toNumber();
if (skillUp) {
if (!heroDicts[heroId]) {
heroDicts[heroId] = { heroId, rewards: [], skillUp: 0, xpEarned: 0 };
}
heroDicts[heroId].skillUp += skillUp / 10;
}
}
} catch (e) {
// Ignore unparseable logs
// FIXME: raise any errors that AREN't the unparseable logs
if (e.code === 'INVALID_ARGUMENT') {
// null logs have throw 'INVALID_ARGUMENT'; ignore them
} else {
// Unexpected error; raise
console.log(`Error parsing quest results: ${e.stack}`);
throw e;
}
}
});
/*
TODO: We'd also like to know:
- quest type (jewel mining, gold mining, foraging, etc)
- liquidity pool id for gardening quests
- time quest ended: maybe derivable from txReceipt.blockNumber?
*/
// Put all the per-hero rewards into the result object
result.rewards = Object.values(heroDicts);
return result;
}
export function labelForReward(rewardAddress, itemQuantityBigNum, chainId = undefined) {
chainId = chainId || activeChainId;
// Initialize the global ITEM_NAMES object so we can look up names
if (Object.keys(ITEM_NAMES).length === 0) {
ITEM_NAMES = createItemNamesMap(ALL_ITEMS);
}
let decimals;
if (GOLD_ADDRESSES.includes(rewardAddress)) {
decimals = GOLD_DECIMALS;
} else if (JEWEL_ADDRESSES.includes(rewardAddress)) {
decimals = JEWEL_DECIMALS;
} else if (CRYSTAL_ADDRESSES.includes(rewardAddress)) {
decimals = CRYSTAL_DECIMALS;
} else {
decimals = 0;
}
let name = ITEM_NAMES[chainId][rewardAddress];
name = name || `Unknown item: (${rewardAddress})`;
const amountStr = ethers.utils.formatUnits(itemQuantityBigNum, decimals);
const label = `${amountStr} ${name}`;
return label;
}
// ===========
// = HELPERS =
// ===========
function getRPCProviderByChain(chainId) {
// This can be used to get multi-chain behavior with degenking.
// For simpler ethers.js use, you really just need a url
chainId = chainId || activeChainId;
if (chainId === DFKN) {
return {
name: 'DFK Chain',
provider: new ethers.providers.JsonRpcProvider(
'https://subnets.avax.network/defi-kingdoms/dfk-chain/rpc',
),
chainId: DFKN,
};
} else if (chainId === HARMONY) {
return {
name: 'harmony-official',
provider: new ethers.providers.JsonRpcProvider('https://api.harmony.one/'),
chainId: HARMONY,
};
}
}
function getEthersQuestInterface(txReceipt, chainId = undefined) {
chainId = chainId || activeChainId;
const QUEST_CORE_V2 = getContractAddress('QUEST_CORE_V2', chainId);
const contractAbi = txReceipt.to == QUEST_CORE_V2 ? abiQuestCoreV2 : abiQuestCoreV1;
const iface = new ethers.utils.Interface(contractAbi);
return iface;
}
function getContractAddress(contractName, chainId = undefined) {
chainId = chainId || activeChainId;
let address;
if (chainId == DFKN) {
address = degenKing.ADDRESSES_DFKN[contractName];
} else if (chainId == HARMONY) {
address = degenKing.ADDRESSES_HARMONY[contractName];
}
// Not all degenking addresses are checksummed; do that here
return ethers.utils.getAddress(address);
}
function createItemNamesMap(degenKingItemsSource) {
// degenKing.ALL_ITEMS contains a comprehensive list
// of all known items and metadata, but it has to be
// searched in O(N) time. We just want lookups by chain and address,
// so create that here.
const itemNames = {
335: {}, // DFK Chain testnet?
53935: {}, // DFK Chain
1666600000: {}, // Harmony
1666700000: {}, // Harmony testnet?
};
degenKingItemsSource.forEach((i) => {
const { name, addresses } = i;
Object.entries(addresses).forEach(([chain, addr]) => {
itemNames[chain][addr] = name;
});
});
return itemNames;
}
// This equivalent of Python's `if __name__ == '__main__':` was found here:
// https://2ality.com/2022/07/nodejs-esm-main.html
// NOTE: import * as url from 'node:url'; required above
if (import.meta.url.startsWith('file:')) {
const modulePath = url.fileURLToPath(import.meta.url);
if (process.argv[1] === modulePath) {
main();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment