diff --git a/CHANGELOG.md b/CHANGELOG.md index 28068dc..ad9c1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### [0.2.0](https://github.com/juliancwirko/buildo-begins/releases/tag/v0.2.0) (2022-06-16) +- added `buildo-begins herotag` - a command for creating Elrond herotag (dns) and checking the address of the existing one + ### [0.1.0](https://github.com/juliancwirko/buildo-begins/releases/tag/v0.1.0) (2022-06-12) - added `buildo-begins issue-esdt` command for issuing new ESDT tokens - added `buildo-begins set-special-roles-esdt` command for setting and unsetting special ESDT roles - local mint, and local burn diff --git a/README.md b/README.md index 49461fb..fb13d3f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ For now, the first version gives you basic stuff. But there will be much more: 7. `buildo-begins issue-esdt` - issue new ESDT token 8. `buildo-begins set-special-roles-esdt` - set/unset special ESDT roles 9. `buildo-begins mint-burn-esdt` - mint/burn the ESDT token supply (requires special roles) +10. `buildo-begins herotag` - create a herotag and assign it to addres and check addresses of existing ones What is awesome here is that you don't have to worry about proper nonce, decimal places, or differentiation between the NFT token id and collection ticker. The maximum amount of arguments will always be the address, token id, and amount. It will differ for each type, but these are maximum. diff --git a/esbuild.config.js b/esbuild.config.js index 1fb49b9..5f35c82 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -17,6 +17,7 @@ esbuild 'ora', 'axios', 'bignumber.js', + 'keccak', ], }) .catch(() => process.exit(1)); diff --git a/package-lock.json b/package-lock.json index 007fca2..894cf13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "buildo-begins", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "buildo-begins", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "@elrondnetwork/erdjs": "^10.2.5", @@ -15,6 +15,7 @@ "axios": "^0.27.2", "bignumber.js": "^9.0.2", "cosmiconfig": "^7.0.1", + "keccak": "^3.0.2", "ora": "5.4.1", "prompts": "^2.4.2" }, @@ -22,6 +23,7 @@ "buildo-begins": "build/index.js" }, "devDependencies": { + "@types/keccak": "^3.0.1", "@types/node": "^17.0.39", "@types/prompt": "^1.1.2", "@types/prompts": "^2.0.14", @@ -203,6 +205,19 @@ "uuid": "8.3.2" } }, + "node_modules/@elrondnetwork/erdjs-walletcore/node_modules/keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@elrondnetwork/erdjs/node_modules/bignumber.js": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", @@ -211,6 +226,19 @@ "node": "*" } }, + "node_modules/@elrondnetwork/erdjs/node_modules/keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@elrondnetwork/transaction-decoder": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@elrondnetwork/transaction-decoder/-/transaction-decoder-0.1.0.tgz", @@ -359,6 +387,15 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-/MxAVmtyyeOvZ6dGf3ciLwFRuV5M8DRIyYNFGHYI6UyBW4/XqyO0LZw+JFMvaeY3cHItQAkELclBU1x5ank6mg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -2510,13 +2547,14 @@ "dev": true }, "node_modules/keccak": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", - "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", + "integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==", "hasInstallScript": true, "dependencies": { "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" }, "engines": { "node": ">=10.0.0" @@ -3616,6 +3654,15 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" + }, + "keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "requires": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + } } } }, @@ -3660,6 +3707,17 @@ "scryptsy": "2.1.0", "tweetnacl": "1.0.3", "uuid": "8.3.2" + }, + "dependencies": { + "keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "requires": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + } + } } }, "@elrondnetwork/transaction-decoder": { @@ -3797,6 +3855,15 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-/MxAVmtyyeOvZ6dGf3ciLwFRuV5M8DRIyYNFGHYI6UyBW4/XqyO0LZw+JFMvaeY3cHItQAkELclBU1x5ank6mg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -5231,12 +5298,13 @@ "dev": true }, "keccak": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", - "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", + "integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==", "requires": { "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" } }, "kleur": { diff --git a/package.json b/package.json index bc1570a..ab0f44e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "node": "^14.13.1 || >=16.0.0" }, "types": "build/types", - "version": "0.1.0", + "version": "0.2.0", "description": "Elrond blockchain CLI helper tools", "main": "build/index.js", "bin": { @@ -31,6 +31,7 @@ "typescript" ], "devDependencies": { + "@types/keccak": "^3.0.1", "@types/node": "^17.0.39", "@types/prompt": "^1.1.2", "@types/prompts": "^2.0.14", @@ -51,6 +52,7 @@ "axios": "^0.27.2", "bignumber.js": "^9.0.2", "cosmiconfig": "^7.0.1", + "keccak": "^3.0.2", "ora": "5.4.1", "prompts": "^2.4.2" } diff --git a/src/herotag.ts b/src/herotag.ts new file mode 100644 index 0000000..1025ce5 --- /dev/null +++ b/src/herotag.ts @@ -0,0 +1,114 @@ +import prompts, { PromptObject } from 'prompts'; +import { exit } from 'process'; +import { + Transaction, + ContractCallPayloadBuilder, + ContractFunction, + TypedValue, + BytesValue, + SmartContract, +} from '@elrondnetwork/erdjs'; + +import axios from 'axios'; + +import { + areYouSureAnswer, + setup, + commonTxOperations, + dnsScAddressForHerotag, +} from './utils'; +import { chain, shortChainId, publicApi } from './config'; + +const promptQuestions: PromptObject[] = [ + { + type: 'select', + name: 'type', + message: 'What do you want to do with the herotag?\n', + validate: (value) => (!value ? 'Required!' : true), + choices: [ + { title: 'Create one', value: 'create' }, + { title: 'Check the address for one', value: 'check' }, + ], + }, + { + type: 'text', + name: 'herotag', + message: 'Please provide the herotag name (without .elrond suffix)\n', + validate: (value) => { + if (!value) return 'Required!'; + if (value.length > 25 || value.length < 3) { + return 'Length between 3 and 25 characters!'; + } + if (!new RegExp(/^[a-z0-9]+$/).test(value)) { + return 'Lowercase alphanumeric characters only!'; + } + return true; + }, + }, +]; + +export const herotag = async () => { + const { herotag, type } = await prompts(promptQuestions); + + if (!herotag) { + console.log('You have to provide the herotag name!'); + exit(9); + } + + if (type === 'check') { + try { + const response = await axios.get<{ address: string }>( + `${publicApi[chain]}/usernames/${herotag.trim()}`, + { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + } + ); + console.log( + `\nAddress of ${herotag}.elrond is: ${response?.data?.address}\n` + ); + } catch { + console.log( + '\nThere is no such herotag registered. Please also check the chain type. By default it checks on the devnet.\n' + ); + } + } else { + try { + await areYouSureAnswer(); + + const dnsScAddress = dnsScAddressForHerotag(`${herotag}.elrond`); + const heroBytes = BytesValue.fromUTF8(`${herotag}.elrond`); + + const { signer, userAccount, provider } = await setup(); + + const dnsSc = new SmartContract({ address: dnsScAddress }); + const dnsCanRegisterQuery = dnsSc.createQuery({ + func: new ContractFunction('canRegister'), + args: [heroBytes], + }); + + await provider.queryContract(dnsCanRegisterQuery); + + const args: TypedValue[] = [heroBytes]; + + const data = new ContractCallPayloadBuilder() + .setFunction(new ContractFunction('register')) + .setArgs(args) + .build(); + + const tx = new Transaction({ + data, + value: 0, + gasLimit: 50000 + 1500 * data.length() + 20000000, + receiver: dnsScAddress, + chainID: shortChainId[chain], + }); + + await commonTxOperations(tx, userAccount, signer, provider); + } catch (e) { + console.log((e as Error)?.message); + } + } +}; diff --git a/src/index.ts b/src/index.ts index c0608d5..22ed7ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { sendEgld } from './egld/send-egld'; import { sendNft } from './nft/send-nft'; import { sendSft } from './sft/send-sft'; import { sendMetaEsdt } from './meta-esdt/send-meta-esdt'; +import { herotag } from './herotag'; const COMMANDS = { derivePem: 'derive-pem', @@ -23,6 +24,7 @@ const COMMANDS = { issueEsdt: 'issue-esdt', mintBurnEsdt: 'mint-burn-esdt', setSpecialRolesEsdt: 'set-special-roles-esdt', + herotag: 'herotag', }; const args = argv; @@ -75,6 +77,9 @@ switch (command) { case COMMANDS.setSpecialRolesEsdt: setSpecialRolesEsdt(); break; + case COMMANDS.herotag: + herotag(); + break; default: break; } diff --git a/src/utils.ts b/src/utils.ts index c3c73cc..239289f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,8 +3,9 @@ import { exit, cwd } from 'process'; import prompts, { PromptObject } from 'prompts'; import { Transaction, TransactionWatcher } from '@elrondnetwork/erdjs'; import ora from 'ora'; +import keccak from 'keccak'; -import { Account } from '@elrondnetwork/erdjs'; +import { Account, SmartContract, Address } from '@elrondnetwork/erdjs'; import { parseUserKey, UserSigner } from '@elrondnetwork/erdjs-walletcore'; import { ApiNetworkProvider } from '@elrondnetwork/erdjs-network-providers'; @@ -126,3 +127,23 @@ export const commonTxOperations = async ( `Transaction link: ${elrondExplorer[chain]}/transactions/${txHash}\n` ); }; + +export const dnsScAddressForHerotag = (herotag: string) => { + const hashedHerotag = keccak('keccak256').update(herotag).digest(); + + const initialAddress = Buffer.from(Array(32).fill(1)); + const initialAddressSlice = initialAddress.slice(0, 30); + const scId = hashedHerotag.slice(31); + + const deployer_pubkey = Buffer.concat([ + initialAddressSlice, + Buffer.from([0, scId.readUIntBE(0, 1)]), + ]); + + const scAddress = SmartContract.computeAddress( + new Address(deployer_pubkey), + 0 + ); + + return scAddress; +};