diff --git a/CHANGELOG.md b/CHANGELOG.md index 149c307c..07d37656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,11 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil ### Added ### Fixed - +- format error when getting a program not on chain [#429](https://github.com/entropyxyz/sdk/pull/429) ### Changed ### Broke +- registration parmas now matches core language `programDeployer` -> `programModAddress` when [#429](https://github.com/entropyxyz/sdk/pull/429) ### Dev diff --git a/dev/README.md b/dev/README.md index beda1d35..367ad37a 100644 --- a/dev/README.md +++ b/dev/README.md @@ -152,3 +152,68 @@ yarn yarn version --patch # patch|minor|major npm publish ``` + +## sdk "boot script" + + +When a new network of nodes is turned on, we need some actions to be taken before this new Entropy network is ready to use for tests. + +1. Give the nodes a moment to establish connections +2. run "jump start", which establishes who the initial signing nodes are (the network automatically "reshares" these roles periodically after jump start) +3. set up faucet program + - a) deploy faucet program to network + - b) install the faucet program for some faucet accounts + - c) fund the faucet accounts + +For convenience, at [./deploy-faucets.mjs](./deploy-faucets.mjs ) is a script we use to "deploy" and fund the faucets for our entropy network. You can refer to this section of the README and the script itself to deploy your own faucets! Here it is running with a dev environment setup from the root of this project: + +```bash +dev/bin/spin-up.sh four-nodes +``` +I would take a deep breath here to allow for the 4 nodes to connect before running the next command bellow. On that note the deploy faucet script takes 3 arguments as you can see right now this script assumes you know what your doing if you are running it. So not a lot of bells and whitelist here. + +the first argument is the `endpoint` to target, the second is the `fundingSeed` and the third `faucetLookUpSeed` + +```bash +node dev/deploy-faucets.mjs ws://127.0.0.1:9944 0x786ad0e2df456fe43dd1f91ebca22e235bc162e0bb8d53c633e8c85b2af68b7a 0x20423b5ff4984bcb8922483c98afb7eaa056c40fc431f8a314211e3d94a4222f + +``` +this example above should run in a dev enviroment the funding seed is eve and the report looks something like this and will log to your console: + +``` +{ + 'jump start status at start': 'Ready', + 'using faucet program pointer': '0x3a1d45fecdee990925286ccce71f78693ff2bb27eae62adf8cfb7d3d61e142aa', + 'faucet program pointer from deployment': '0x3a1d45fecdee990925286ccce71f78693ff2bb27eae62adf8cfb7d3d61e142aa', + 'faucet config': { + max_transfer_amount: 20000000000, + genesis_hash: 'a4b29c6895ae775fd291377fde31882f66244eaecbdc81e017ebb64d13b27b72' + }, + 'initial balance for funding account': 99999880954644628n, + 'initial funding faucet amount': 24999970238661157n, + 'modifiableKeys on chain': [ + '0x03cd98af667e48b4912c66576f5cdf18aee3764c7aad1c40b9c61d5ac012acf1f6', + '0x0228aba3e529d3b78b0cd7454a5f694eb09a648ec488b0b4b8ce7cd9abedd96c28', + '0x03b7c87542f57895d37f1a37a3460e27ec74de7216e6ed48609ffd2fac976ea94a' + ], + 'faucet look up address': '5EqZMUYz7jjaG2baQWJRzUzM7YhBP4E8TAj6GgqDqWdXriTn', + faucets: [ + { + vk: '0x03cd98af667e48b4912c66576f5cdf18aee3764c7aad1c40b9c61d5ac012acf1f6', + address: '5Gm6JA2ikMK1FfNn3MRmUPLWfi4p6eBzcFtKE1dnJvQpJgpg', + balance: '24,999,970,238,661,157' + }, + { + vk: '0x0228aba3e529d3b78b0cd7454a5f694eb09a648ec488b0b4b8ce7cd9abedd96c28', + address: '5FqourMb6c3z4rogDnaBb36Ku53ndy3eCiSdjRg2TUAztJ3X', + balance: '24,999,970,238,661,157' + }, + { + vk: '0x03b7c87542f57895d37f1a37a3460e27ec74de7216e6ed48609ffd2fac976ea94a', + address: '5CUQuSMxTzx74Zb8gsDobhaYSte9vt9a3Db5X6oio4X1pSX1', + balance: '24,999,970,238,661,157' + } + ], + endpoint: 'ws://127.0.0.1:9944' +} +``` \ No newline at end of file diff --git a/dev/deploy-faucets.mjs b/dev/deploy-faucets.mjs new file mode 100644 index 00000000..c59e1ca8 --- /dev/null +++ b/dev/deploy-faucets.mjs @@ -0,0 +1,204 @@ +import { readFileSync } from 'fs' +import { blake2AsHex, encodeAddress } from "@polkadot/util-crypto"; +import Keyring from '@entropyxyz/sdk/keys' +import Entropy, { wasmGlobalsReady } from '@entropyxyz/sdk' +import { jumpStartNetwork, createTimeLogProxy } from '@entropyxyz/sdk/testing' +import { evilMonkeyAnimation } from './fun-bucket.mjs' + +const endpoint = process.argv[2] +const fundingSeed = process.argv[3] +const faucetLookUpSeed = process.argv[4] + + + +// change this number to deploy more faucets +const FAUCET_COUNT = 3 +const BITS_PER_TOKEN = 1e10 +// change me if you change the schemas! +const POINTER = '0x3a1d45fecdee990925286ccce71f78693ff2bb27eae62adf8cfb7d3d61e142aa' + + +if (!endpoint) throw new Error('please provide arguments for endpoint, fundingSeed, faucetLookUpSeed') +if (!fundingSeed) throw new Error('please provide arguments for fundingSeed, faucetLookUpSeed') +if (!faucetLookUpSeed) throw new Error('please provide arguments for faucetLookUpSeed') +// this is just a novel reporter object that gets logged +const report = createTimeLogProxy({ endpoint }) +// run checks +checkEndpoint(endpoint) +checkSeed(faucetLookUpSeed) +checkSeed(fundingSeed) + +const faucetAddresses = [] +// actually run the function! +deployAndFundFaucet().then(() => process.exit(0)).catch((e) => { + console.warn(report) + console.error(e) + process.exit(1) +}) +// function defined +async function deployAndFundFaucet () { + let monkeyAnimationStop = evilMonkeyAnimation() + report.step = 'about to wait wasmGlobalsReady' + await wasmGlobalsReady() + report.step = 'completed waiting for wasmGlobalsReady' + report.step = 'creating fundingSeed keyring' + const moneyRing = new Keyring({ + seed: fundingSeed + }) + report.step = 'created keyring for fundingSeed' + report.step = 'creating faucetLookUpSeed keyring' + const faucetRing = new Keyring({ + seed: faucetLookUpSeed + }) + report.step = 'creating entropy for fundingSeed' + const moneyBags = new Entropy({ + endpoint, + keyring: moneyRing + }) + report.step = 'created entropy for fundingSeed' + report.step = 'creating entropy for faucetRing' + const faucetEntropy = new Entropy({ + endpoint, + keyring: faucetRing + }) + report.step = 'created entropy for faucetRing' + report.step = 'awaiting readys' + await faucetEntropy.ready + await moneyBags.ready + report.step = 'entropys ready' + report.step = 'getting jump start status' + const jumpStartStatus = (await moneyBags.substrate.query.stakingExtension.jumpStartProgress()).toHuman().jumpStartStatus + report.step = 'jump start status retrieved' + report['jump start status at start'] = jumpStartStatus + // deploy faucet program to chain if not already up + if (jumpStartStatus === 'Ready') { + report.step = 'starting jump start' + await jumpStartNetwork(moneyBags, true) + report.step = 'finished jump start' + monkeyAnimationStop() + monkeyAnimationStop = evilMonkeyAnimation() + } + report.step = 'getting faucet program on chain' + const faucetProgramInfo = await moneyBags.programs.dev.get(POINTER) + report.step = 'got faucet program on chain' + report['using faucet program POINTER'] = POINTER + if (faucetProgramInfo === null) { + const faucetProgram = readFileSync(new URL('./faucet_program.wasm', import.meta.url)) + const configurationSchema = { + type: 'object', + properties: { + max_transfer_amount: { type: "number" }, + genesis_hash: { type: "string" } + } + } + const auxDataSchema = { + type: 'object', + properties: { + amount: { type: "number" }, + string_account_id: { type: "string" }, + spec_version: { type: "number" }, + transaction_version: { type: "number" }, + } + } + report.step = 'deploying faucet program' + report['faucet program POINTER from deployment'] = await moneyBags.programs.dev.deploy(faucetProgram, configurationSchema, auxDataSchema) + report.step = 'deployed faucet program' + } + // transfer funds to faucet account "enough" to register (5 whole tokens) + report.step = 'transferring funds to faucet look up address from funding account' + await sendMoney(moneyBags, faucetRing.accounts.registration.address, BigInt(FAUCET_COUNT * BITS_PER_TOKEN)) + report.step = 'finish transfer' + let faucetCountDown = FAUCET_COUNT + const genesisHash = await moneyBags.substrate.rpc.chain.getBlockHash(0) + const userConfig = { + max_transfer_amount: 20_000_000_000, + genesis_hash: genesisHash.toString().split('0x')[1] + } + report['faucet config'] = userConfig + const faucets = [] + report.step = 'retrieving balance for funding account' + const funderBalance = BigInt((await faucetEntropy.substrate.query.system.account( + moneyRing.accounts.registration.address)).data.free) + report.step = 'balance for funding account' + report['initial balance for funding account'] = funderBalance.toLocaleString() + // dont transfer all funds ¯\_(ツ)_/¯ so if we run out of faucet funds you still have a small nest egg + const fundingAmount = funderBalance / BigInt(FAUCET_COUNT + 1) + report['initial funding faucet amount'] = fundingAmount.toLocaleString() + + while (!!faucetCountDown) { + report.step = 'registering a faucet' + const vk = await faucetEntropy.register({ + programModAddress: faucetEntropy.keyring.accounts.registration.address, + programData: [{ + program_pointer: POINTER, + program_config: userConfig, + }] + }) + report.step = 'registration complete' + const hashedKey = blake2AsHex(vk) + const faucetAddress = encodeAddress(hashedKey, 42).toString() + report.step = 'transferring funds to faucet address from funding account' + await sendMoney(moneyBags, faucetAddress, fundingAmount) + report.step = 'transfer complete' + report.step = 'retrieving balance for new faucet' + const balance = (await faucetEntropy.substrate.query.system.account( + faucetAddress)).data.toHuman() + report.step = 'balance for new faucet retrieved' + + faucets.push({ + 'verification key': vk, + address: faucetAddress, + balance: balance.free.toLocaleString() + }) + report['faucets'] = faucets + --faucetCountDown + } + report.step = 'getting modifiableKeys from chain for sanity check' + // These should be the same as faucets.map(faucet => faucet['verification key']) + const modifiableKeys = await moneyBags.substrate.query.registry.modifiableKeys(faucetRing.accounts.registration.address) + report.step = 'retrieved modifiableKeys from chain for sanity check' + report['modifiableKeys on chain'] = modifiableKeys.toHuman() + report['faucet look up address'] = faucetRing.accounts.registration.address + report.finished = true + monkeyAnimationStop() + console.log(report) +} + +function sendMoney(entropy, recipientAddress, amount) { + return new Promise(async (resolve, reject) => { + // WARN: await signAndSend is dangerous as it does not resolve + // after transaction is complete :melt: + const sender = entropy.keyring.accounts.registration.pair + entropy.substrate.tx.balances + .transferAllowDeath(recipientAddress, amount) + .signAndSend(sender, ({ status, events, dispatchError }) => { + if (dispatchError) { + let msg + if (dispatchError.isModule) { + // for module errors, we have the section indexed, lookup + const decoded = entropy.substrate.registry.findMetaError( + dispatchError.asModule + ) + const { docs, name, section } = decoded + + msg = `${section}.${name}: ${docs.join(' ')}` + } else { + // Other, CannotLookup, BadOrigin, no extra info + msg = dispatchError.toString() + } + return reject(Error(msg)) + } + + if (status.isFinalized) resolve(status) + }) + }) +} + + +function checkSeed(seed) { + if (seed.length !== 66) throw new Error('incompatible seed') +} + +function checkEndpoint (endpoint) { + if (!endpoint.startsWith('ws')) throw new Error('Please provide a ws endpoint') +} \ No newline at end of file diff --git a/dev/faucet_program.wasm b/dev/faucet_program.wasm new file mode 100644 index 00000000..cf1da081 Binary files /dev/null and b/dev/faucet_program.wasm differ diff --git a/dev/fun-bucket.mjs b/dev/fun-bucket.mjs new file mode 100644 index 00000000..c6fa7b40 --- /dev/null +++ b/dev/fun-bucket.mjs @@ -0,0 +1,20 @@ +const monkeys = ['🙉 - pandemonium', '🙈 - chaos', '🙊 - anarchy', '🐵 - entropy'] +/** + * starts a monkey animation + * @returns {function} to stop animation +**/ +export function evilMonkeyAnimation () { + const clear = () => process.stdout.write("\r\x1b[K") + let frame = 0 + process.stdout.write(monkeys[frame]) + ++frame + const animate = setInterval(() => { + clear() + process.stdout.write(monkeys[frame]) + if (frame === 3) frame = 0 + else ++frame + }, 1000) + return () => { + clearInterval(animate) + } +} \ No newline at end of file diff --git a/dev/testing-utils.mjs b/dev/testing-utils.mjs index 247523df..b0a0ddd2 100644 --- a/dev/testing-utils.mjs +++ b/dev/testing-utils.mjs @@ -150,3 +150,20 @@ export async function spinNetworkDown (networkType = 'four-nodes') { return Promise.reject(err) } } + +export function createTimeLogProxy (extraStartData={}) { + let lastStepTime + let steps = 0 + return new Proxy({ time: { start: (lastStepTime = Date.now()) }, ...extraStartData }, { + set: (o, k, v) => { + const now = Date.now() + if (k === 'finished') o.time['total time in seconds'] = (Date.now() - o.time.start)/1000 + else if (k === 'step') { + o.time[`${steps} - ${v}`] = `${(now - lastStepTime)/1000}s` + ++steps + lastStepTime = now + } + return o[k] = v + } + }) +} diff --git a/src/index.ts b/src/index.ts index 063fe9e9..e18f9da0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,14 +125,14 @@ export default class Entropy { * Registers a new account with the provided parameters. * * @param {RegistrationParams} params - The registration parameters. - * @param {SS58Address} [params.programDeployer] - The account authorized to modify programs on behalf of the user. + * @param {SS58Address} [params.programModAddress] - The account authorized to modify programs on behalf of the user. * @param {ProgramInstance[]} [params.programData] - Optional initial programs associated with the user. * @returns {Promise} A promise that resolves to the verifying key for the new account when the registration is complete. * @throws {Error} If the address is already registered or if there's a problem during registration. */ async register (params?: RegistrationParams): Promise { params = params || this.#getRegisterParamsDefault() - if (params.programDeployer && !isDeployer(params.programDeployer)) { + if (params.programModAddress && !isDeployer(params.programModAddress)) { throw new TypeError('Incompatible address type') } @@ -165,7 +165,7 @@ export default class Entropy { return { programData: [defaultProgram], - programDeployer: this.keyring.accounts.registration.address, + programModAddress: this.keyring.accounts.registration.address, } } diff --git a/src/programs/dev.ts b/src/programs/dev.ts index aa327e00..8e23ce31 100644 --- a/src/programs/dev.ts +++ b/src/programs/dev.ts @@ -78,7 +78,7 @@ export default class ProgramDev extends ExtrinsicBaseClass { const responseOption = await this.substrate.query.programs.programs(pointer) const programInfo = responseOption.toJSON() - + if (programInfo === null) return null return this.#formatProgramInterface(programInfo) } diff --git a/src/registration/index.ts b/src/registration/index.ts index 248263c1..8bc23e6f 100644 --- a/src/registration/index.ts +++ b/src/registration/index.ts @@ -9,7 +9,7 @@ export interface RegistrationParams { /** initial programs associated with the user */ programData: ProgramInstance[] /** The account authorized to modify programs on behalf of the user. */ - programDeployer?: SS58Address + programModAddress?: SS58Address } /** @@ -25,7 +25,7 @@ export interface AccountRegisteredSuccess { * */ export interface RegisteredInfo { programsData: Uint8Array - programDeployer: SS58Address + programModAddress: SS58Address versionNumber: number } @@ -72,14 +72,14 @@ export default class RegistrationManager extends ExtrinsicBaseClass { * * @param {RegistrationParams} params - The registration parameters. * @param {ProgramInstance[]} params.programData - The initial program data associated with the user. - * @param {SS58Address} [params.programDeployer] - Optional. The account authorized to modify programs on behalf of the user. + * @param {SS58Address} [params.programModAddress] - Optional. The account authorized to modify programs on behalf of the user. * * @returns {Promise} A promise that resolves to the verifying key of the registered account. * @throws {Error} If registration information is not found or any other error occurs during registration. */ async register ({ - programDeployer, + programModAddress, programData, }: RegistrationParams): Promise { // this is sloppy @@ -88,12 +88,12 @@ export default class RegistrationManager extends ExtrinsicBaseClass { // Convert the program data to the appropriate format and create a registration transaction. const registerTx = this.substrate.tx.registry.register( - programDeployer, + programModAddress, programData.map(this.#formatProgramInfo) ) // @ts-ignore: next line // Send the registration transaction and wait for the result. - const registrationTxResult = this.sendAndWaitFor(registerTx, { + const registrationTxResult = await this.sendAndWaitFor(registerTx, { section: 'registry', name: 'AccountRegistered', }).catch((error) => { @@ -108,49 +108,12 @@ export default class RegistrationManager extends ExtrinsicBaseClass { throw error } }) - const dataFromEvents = this.#getVerifiyingKeyFromRegisterEvent( - this.signer.pair.address - ) - await registrationTxResult - const verifyingKey = await dataFromEvents + // @ts-ignore: not sure where the void is coming from + const verifyingKey = registrationTxResult.toHuman().event.data[1] return verifyingKey } - /** - * Private method to get the verifying key from the registration event. - * @param {SS58Address} address - The address of the account. - * @returns {Promise} A promise that resolves to the verifying key. - * @private - */ - - #getVerifiyingKeyFromRegisterEvent (address: SS58Address): Promise { - const wantedMethods = ['FailedRegistration', 'AccountRegistered'] - let unsub - return new Promise(async (res, reject) => { - unsub = await this.substrate.query.system.events((events) => { - events.forEach(async (record) => { - const { event } = record - const { method } = event - if (wantedMethods.includes(method.toString())) { - if (method === wantedMethods[0]) { - if (event?.data?.toHuman()[0] === address) { - reject(new Error('Registration Failed')) - unsub() - } - } - if (method === wantedMethods[1]) { - if (event?.data?.toHuman()[0] === address) { - res(event?.data?.toHuman()[1]) - unsub() - } - } - } - }) - }) - }) - } - #formatProgramInfo (programInfo): ProgramInstance { const program: ProgramInstance = { program_pointer: programInfo.program_pointer } if (programInfo.program_config) program.program_config = Array.from( diff --git a/tests/programs-dev.test.ts b/tests/programs-dev.test.ts index b4ef46d5..3dcf6db1 100644 --- a/tests/programs-dev.test.ts +++ b/tests/programs-dev.test.ts @@ -35,6 +35,11 @@ test('Programs#dev: all methods', async (t) => { ) await run('jump-start network', jumpStartNetwork(entropy)) + const programNotOnChain = await run( + 'get a program not on chain', + entropy.programs.dev.get('0x6c8228950ca8dfb557d42ce11643c67ba5a3e5cee3ce7232808ea7477b846bcb') + ) + t.equal(programNotOnChain, null, 'get a program not on chain should be null') // deploy const noopProgram: any = readFileSync( @@ -58,7 +63,6 @@ test('Programs#dev: all methods', async (t) => { 'deploy', entropy.programs.dev.deploy(noopProgram, configSchema, auxDataSchema) ) - console.log('newPointer:', newPointer) const programsDeployed = await run( 'get deployed programs', entropy.programs.dev.getByDeployer(entropy.keyring.accounts.programDev.address) @@ -81,7 +85,6 @@ test('Programs#dev: all methods', async (t) => { 'get a specific program', entropy.programs.dev.get(newPointer) ) - t.deepEqual( noopProgramOnChain.bytecode, noopProgram, diff --git a/tests/register.test.ts b/tests/register.test.ts index 58e5e08f..da2d56ee 100644 --- a/tests/register.test.ts +++ b/tests/register.test.ts @@ -54,13 +54,13 @@ test('Register', async (t) => { await run( 'register', entropy.register({ - programDeployer: entropy.keyring.accounts.registration.address, + programModAddress: entropy.keyring.accounts.registration.address, programData: [{ programPointer: pointer, programConfig: '0x' }], }) ) await entropy.register({ - programDeployer: eveAddress, + programModAddress: eveAddress, programData: [{ programPointer: pointer, programConfig: '0x' }], }) .then(() => t.fail('throws error on duplicate registrations'))