From 6c7c6d292a7b7e28f051eb0ad330c24d36f8c723 Mon Sep 17 00:00:00 2001 From: alexbolog Date: Tue, 19 Sep 2023 11:41:23 +0300 Subject: [PATCH] scripts progress --- README.md | 84 ++----- scripts/common.js | 27 ++- scripts/config.json | 25 ++- scripts/index.js | 278 +++++++++++++++++++++++- scripts/nft-staking-subsidiary-init.csv | 15 ++ scripts/package-lock.json | 17 ++ scripts/package.json | 1 + 7 files changed, 368 insertions(+), 79 deletions(-) create mode 100644 scripts/nft-staking-subsidiary-init.csv diff --git a/README.md b/README.md index 2c35ea0..222ebbf 100644 --- a/README.md +++ b/README.md @@ -230,67 +230,29 @@ Upgrades an existing instance of the contract using the same procedure explained ### Commit Pools Data -Will look in the `config.json` for uncomitted pools. Once a pool has been setup, it will be market as committed and ignored for further updates. If a pool must be updated, remove the "isCommitted" property and run the script again. - -#### Pool Settings that must be provided: - -- `name`: Ignored by the script, useful for keeping track of which pool does what -- `collectionTokenIdentifier`: The NFT/SFT collection token identifier of the pool -- `stakingModuleType`: One of the following values - - CodingDivisionSfts - - XBunnies - - Bloodshed - - Nosferatu - - VestaXDAO - - SnakesSfts -- `scoreConfiguration`: the scores based on which the reward is being distributed. Example of configuration: - -```json -{ - ... - "scoreConfiguration": { - "All": { - "base": 10, - "granular": [] - }, - "XBunnies": { - "base": 5, - "granular": [ - { - "nonces": [10, 11, 12], - "score": 10 - }, - { - "nonces": [210, 211, 212], - "score": 15 - }, - { - "nonces": [310, 311, 312, 313], - "score": 20 - }, - ] - } - } - ... -} -``` +Creating or updating a pool happens using a .CSV configuration file. +All the data that needs to be set for a pool must be formatted accordingly and saved in a .CSV file. Once prompted, specify the full path of the file and let the script do it's magic. + +The script doesn't check if the CSV data is already committed to the blockchain. Every line of the CSV will be executed as a transaction - **do not include data that is already been set unless you wish to spend more gas.** + +Possible columns: + +- token identifier +- nonce filters: + - Nonces: semi-column separated - 11;23;409;1223;etc + - Nonce range: start and end nonces, inclusive values, one per column +- score +- full set bonus score -#### Reward tokens - -Each reward token must be registered as a pool reward if it's going to be distributed to a specific pool. -These tokens can be set under `"poolSettings[ENVIRONMENT][rewardTokens]"` as key-value pairs. Example: - -```json -{ - ... - "rewardTokens": [ - { - "tokenIdentifier": "REWARDTKN-123456", - "stakingModuleType": "XBunnies" - } - ] - ... -} +**.CSV header** + +This is the parsing order and must be respected in order for the script to work properly. In case a nonce column does not apply, leave it empty. + +> [!NOTE] +> You can both include/exclude the headers, the script will ask you for confirmation before executing. + +``` +Token Identifier;Nonces;Nonce Range Start;Nonce Range End;Score;Full Set Score ``` -The same `"isCommitted"` logic is applied here as well. +You can check the scripts folder for a .CSV example used when initializing the subsidiaries. diff --git a/scripts/common.js b/scripts/common.js index 2b4e813..33b5a1e 100644 --- a/scripts/common.js +++ b/scripts/common.js @@ -40,8 +40,6 @@ const loadNetworkConfig = () => { const networkCfg = loadNetworkConfig(); const getPemAndAccount = async () => { - console.log(`Loading pem and account for ${process.env.ENVIRONMENT}...`); - console.log(`PEM Path: ${networkCfg.pem}`); const apiProvider = new ApiNetworkProvider(networkCfg.api); const pemContent = await loadPemContent(networkCfg.pem); const account = await loadUserAccount(apiProvider, pemContent); @@ -81,7 +79,7 @@ const getSmartContract = async () => { }); }; -const signAndSend = async (tx, walletPemContents) => { +const signAndSendExplicit = async (tx, walletPemContents) => { const provider = getProxyProvider(); const signer = prepareUserSigner(walletPemContents); const serializedTransaction = tx.serializeForSigning(); @@ -132,7 +130,7 @@ const deploy = async () => { transaction.setNonce(account.getNonceThenIncrement()); console.log(`Deploying contract on ${process.env.ENVIRONMENT}...`); - const txResult = await signAndSend(transaction, pem); + const txResult = await signAndSendExplicit(transaction, pem); const deployedAddress = deploymentTransactionResultHandler(txResult); if (deployedAddress !== "") { @@ -168,7 +166,7 @@ const upgrade = async () => { transaction.setNonce(account.getNonceThenIncrement()); console.log(`Upgrading contract on ${process.env.ENVIRONMENT}...`); - const txResult = await signAndSend(transaction, pem); + const txResult = await signAndSendExplicit(transaction, pem); const deployedAddress = deploymentTransactionResultHandler(txResult); if (deployedAddress !== "") { @@ -204,12 +202,29 @@ const buildDeployArgs = () => { return args; }; +const signAndSendTx = async (txInteraction) => { + let { pem, account } = await getPemAndAccount(); + let tx = txInteraction + .withChainID(networkCfg.chain) + .withSender(account.address) + .buildTransaction(); + + tx.setNonce(account.getNonceThenIncrement()); + let txResult = await signAndSendExplicit(tx, pem); + + return ( + !txResult.status.isFailed() && + !txResult.status.isInvalid() && + !txResult.status.isPending() + ); +}; + module.exports = { loadNetworkConfig, getPemAndAccount, getSmartContract, resultsParser: Parser, - signAndSend, + signAndSendTx, getProxyProvider, deploy, diff --git a/scripts/config.json b/scripts/config.json index a712d0d..a179cf1 100644 --- a/scripts/config.json +++ b/scripts/config.json @@ -1,6 +1,6 @@ { "address": { - "DEVNET": "erd1qqqqqqqqqqqqqpgq7z49p3gkskpl0f8x45scaly22pqnww2z6ppsm54eug" + "DEVNET": "erd1qqqqqqqqqqqqqpgqmzt9ntca4ffuf9j7rff5xx3pcfepasc36ppsv65tg4" }, "deploymentArgs": { "gasLimit": 85000000, @@ -38,11 +38,15 @@ "SharesSfts": { "granular": [ { - "nonces": [1], + "nonces": [ + 1 + ], "score": 500 }, { - "nonces": [2], + "nonces": [ + 2 + ], "score": 1 } ] @@ -105,7 +109,18 @@ "base": 2, "granular": [ { - "nonces": [388, 407, 25, 880, 274, 873, 1095, 175, 954, 1033], + "nonces": [ + 388, + 407, + 25, + 880, + 274, + 873, + 1095, + 175, + 954, + 1033 + ], "score": 160 } ] @@ -117,4 +132,4 @@ }, "MAINNET": [] } -} +} \ No newline at end of file diff --git a/scripts/index.js b/scripts/index.js index 4c241d7..d1648b6 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,8 +1,35 @@ require("dotenv").config(); +const { promises, stat } = require("fs"); const config = require("./config.json"); -const { loadNetworkConfig, deploy, upgrade } = require("./common"); +const { + deploy, + upgrade, + getSmartContract, + signAndSendTx, +} = require("./common"); +let chalk; + +import("chalk").then((module) => { + chalk = module.default; +}); + +const stakingModuleTypes = [ + "CodingDivisionSfts", + "Bloodshed", + "Nosferatu", + "VestaXDAO", + "SnakesSfts", + "SharesSfts", + "XBunnies", +]; const readline = require("readline"); +const { + TokenIdentifierValue, + U8Value, + U32Value, + U64Value, +} = require("@multiversx/sdk-core/out"); const run = () => { const rl = readline.createInterface({ @@ -11,7 +38,7 @@ const run = () => { }); rl.question( - "Please choose an option: \n 1. Deploy \n 2. Upgrade \n 3. Add Pool \n", + "Please choose an option: \n 1. Deploy \n 2. Upgrade \n 3. Register new staking pool \n 4. Configure pool scores\n> ", async (option) => { switch (option) { case "1": @@ -23,19 +50,256 @@ const run = () => { rl.close(); break; case "3": - handleAddPools(); - rl.close(); + rl.question( + "Enter NFT/SFT collection token identifier: ", + async (tokenIdentifier) => { + console.log("Please select a staking module type: "); + stakingModuleTypes.forEach((moduleType, index) => + console.log(`${index + 1}. ${moduleType}`) + ); + rl.question( + "Enter the number corresponding to the module type: ", + async (moduleType) => { + await handleRegisterNewStakingPool( + tokenIdentifier, + moduleType + ); + rl.close(); + } + ); + } + ); + break; + case "4": + rl.question(".CSV configuration file path: ", async (filePath) => { + rl.question( + "Does .CSV file include headers? (Y/N): ", + async (includesHeaders) => { + const hasHeaders = includesHeaders.toLowerCase() === "y"; + await handleConfigurePoolScores(filePath, hasHeaders); + rl.close(); + } + ); + }); break; default: console.log("Invalid option"); + rl.close(); } } ); }; -const handleAddPools = async () => { - const pools = config.poolSettings[process.env.ENVIRONMENT].pools; - console.log(pools); +const handleRegisterNewStakingPool = async (tokenIdentifier, moduleType) => { + let message = chalk.white("Registering new staking pool for "); + message += chalk.yellow(tokenIdentifier); + message += chalk.white(" with module type "); + message += chalk.yellow(stakingModuleTypes[moduleType - 1]); + message += chalk.white("... "); + + process.stdout.write(message + "\n"); + + let contract = await getSmartContract(); + let tx = contract.methodsExplicit + .createPool([ + new TokenIdentifierValue(tokenIdentifier), + new U8Value(moduleType), + ]) + .withChainID("D") + .withGasLimit(15_000_000); + + let status = await signAndSendTx(tx); + + message += status ? chalk.green("SUCCESS") : chalk.red("FAILED"); + process.stdout.write(message + "\n"); +}; + +const handleConfigurePoolScores = async (filePath, hasHeaders) => { + let fileContent = await promises.readFile(filePath, "utf8"); + const lines = fileContent + .split("\n") + .filter((_, index) => (hasHeaders ? index > 0 : true)) + .map((line) => line.trim()); + console.log(`${lines.length} transactions to be sent`); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + try { + await handleConfigureScore(line); + } catch (error) { + console.error(`Error processing line ${i}: ${error}`); + } + } +}; + +const handleConfigureScore = async (row) => { + let columns = row.split(","); + + let tokenIdentifier = columns[0]; + let nonces = columns[1].length === 0 ? [] : columns[1].split(";"); + let nonceRangeStart = columns[2] === "" ? undefined : columns[2]; + let nonceRangeEnd = columns[3] === "" ? undefined : columns[3]; + let score = columns[4]; + let fullSetBonusScore = columns[5] === "" ? undefined : columns[5]; + + process.stdout.write( + getPoolRowMessage( + tokenIdentifier, + nonces, + nonceRangeStart, + nonceRangeEnd, + score, + fullSetBonusScore + ) + ); + process.stdout.write("\n"); + + let status = false; + if ( + nonces.length === 0 && + nonceRangeStart === undefined && + nonceRangeEnd === undefined + ) { + status = await sendSetBaseAssetScoreTx(tokenIdentifier, score); + } + if (nonces.length > 0) { + status = await sendSetNonceAssetScoreTx(tokenIdentifier, nonces, score); + } + if (nonceRangeStart !== undefined && nonceRangeEnd !== undefined) { + status = await sendSetNonceAssetScoreByRangeTx( + tokenIdentifier, + nonceRangeStart, + nonceRangeEnd, + score + ); + } + + if (fullSetBonusScore !== undefined) { + let fullSetScoreResult = await sendSetFullSetBonusScoreTx( + tokenIdentifier, + fullSetBonusScore + ); + status = status && fullSetScoreResult; + } + // process.stdout.write(status ? chalk.green("SUCCESS") : chalk.red("FAILED")); + // process.stdout.write("\n"); + process.stdout.write( + getPoolRowMessage( + tokenIdentifier, + nonces, + nonceRangeStart, + nonceRangeEnd, + score, + fullSetBonusScore, + status + ) + "\n" + ); +}; + +const sendSetBaseAssetScoreTx = async (tokenIdentifier, score) => { + let contract = await getSmartContract(); + let tx = contract.methodsExplicit + .setBaseAssetScore([ + new TokenIdentifierValue(tokenIdentifier), + new U32Value(1), + new U32Value(score), + ]) + .withGasLimit(15_000_000); + + return await signAndSendTx(tx); +}; + +const sendSetNonceAssetScoreTx = async (tokenIdentifier, nonces, score) => { + let contract = await getSmartContract(); + let noncesArg = nonces.map((nonce) => new U64Value(nonce)); + let tx = contract.methodsExplicit + .setNonceAssetScore( + [ + new TokenIdentifierValue(tokenIdentifier), + new U8Value(1), + new U32Value(score), + ].concat(noncesArg) + ) + .withGasLimit(15_000_000 + nonces.length * 100_000); + + return await signAndSendTx(tx); +}; + +const sendSetNonceAssetScoreByRangeTx = async ( + tokenIdentifier, + nonceRangeStart, + nonceRangeEnd, + score +) => { + let contract = await getSmartContract(); + let gasLimit = 15_000_000 + (nonceRangeEnd - nonceRangeStart) * 100_000; + let tx = contract.methodsExplicit + .setNonceAssetScoreByRange([ + new TokenIdentifierValue(tokenIdentifier), + new U8Value(1), + new U32Value(score), + new U64Value(nonceRangeStart), + new U64Value(nonceRangeEnd), + ]) + .withGasLimit(gasLimit > 600_000_000 ? 600_000_000 : gasLimit); + + return await signAndSendTx(tx); +}; + +const sendSetFullSetBonusScoreTx = async (tokenIdentifier, score) => { + let contract = await getSmartContract(); + let tx = contract.methodsExplicit + .setFullSetScore([ + new TokenIdentifierValue(tokenIdentifier), + new U8Value(1), + new U32Value(score), + ]) + .withGasLimit(15_000_000); + + return await signAndSendTx(tx); +}; + +const getPoolRowMessage = ( + tokenIdentifier, + nonces, + nonceRangeStart, + nonceRangeEnd, + score, + fullSetBonusScore, + statusSuccessful +) => { + let statusMessage = chalk.white("Setting up "); + statusMessage += chalk.yellow(tokenIdentifier); + if (nonces.length > 0) { + statusMessage += chalk.white(" for "); + statusMessage += chalk.yellow(nonces.join(", ")); + statusMessage += chalk.white(" nonces"); + } + if (nonceRangeStart !== undefined && nonceRangeEnd !== undefined) { + statusMessage += chalk.white(" for nonces between "); + statusMessage += chalk.yellow(nonceRangeStart); + statusMessage += chalk.white(" and "); + statusMessage += chalk.yellow(nonceRangeEnd); + } + + statusMessage += chalk.white(" with score "); + statusMessage += chalk.yellow(score); + if (fullSetBonusScore !== undefined) { + statusMessage += chalk.white(" and full set bonus score "); + statusMessage += chalk.yellow(fullSetBonusScore); + } + statusMessage += chalk.white("... "); + + if (statusSuccessful === undefined) { + return statusMessage; + } + + if (statusSuccessful) { + statusMessage += chalk.green("SUCCESS"); + } else { + statusMessage += chalk.red("FAILED"); + } + + return statusMessage; }; run(); diff --git a/scripts/nft-staking-subsidiary-init.csv b/scripts/nft-staking-subsidiary-init.csv new file mode 100644 index 0000000..1e951a5 --- /dev/null +++ b/scripts/nft-staking-subsidiary-init.csv @@ -0,0 +1,15 @@ +Token Identifier,Nonces,NonceRangeStart,NonceRangeEnd,Base Score,Full set bonus score, +DHCD-bc9963,,,,5,25, +VESTAXDAO-e6c48c,,,,1, +VESTAXDAO-e6c48c,2,,,2, +VESTAXDAO-e6c48c,1,,,4, +BLOODSHED-a62781,,,,1, +BLOODSHED-a62781,,555,1157,3, +BLOODSHED-a62781,,153,554,4, +BLOODSHED-a62781,,1,152,11, +NOSFERATU-2b0485,,,,2, +NOSFERATU-2b0485,,301,700,4, +NOSFERATU-2b0485,,101,300,8, +NOSFERATU-2b0485,,1,100,16, +DHXBUNNIES-862129,,,,2, +DHXBUNNIES-862129,388; 407; 25; 880; 274; 873; 1095; 175; 954; 1033,,,160, \ No newline at end of file diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 786b3ea..22c3c7d 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -10,6 +10,7 @@ "@multiversx/sdk-wallet": "^4.2.0", "axios": "^1.4.0", "bignumber.js": "^9.1.1", + "chalk": "^5.3.0", "dotenv": "^16.3.1", "fs": "^0.0.1-security", "prompt-sync": "^4.2.0" @@ -311,6 +312,17 @@ "ieee754": "^1.2.1" } }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -957,6 +969,11 @@ "ieee754": "^1.2.1" } }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", diff --git a/scripts/package.json b/scripts/package.json index 66c7eb3..012a5c9 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -5,6 +5,7 @@ "@multiversx/sdk-wallet": "^4.2.0", "axios": "^1.4.0", "bignumber.js": "^9.1.1", + "chalk": "^5.3.0", "dotenv": "^16.3.1", "fs": "^0.0.1-security", "prompt-sync": "^4.2.0"