diff --git a/README.md b/README.md index 08a1977..296c582 100644 --- a/README.md +++ b/README.md @@ -92,15 +92,17 @@ The above command will compile the Typescript source code into a `dist/` directo ### 4.1. Useful Commands -| Command | Description | -|-------------------|-------------------------------------------------------------------------------------| -| `yarn build` | Builds the source code into the `dist/` directory. | -| `yarn docs:build` | Builds the documentation into the `.docusaurus/` directory. | -| `yarn docs:serve` | Serves the built documentation from the `.docusaurus/` directory. | -| `yarn docs:start` | Builds and runs the documentation in a development environment with hot reloading. | -| `yarn lint` | Runs the linter on `.js` and `.ts` files. | -| `yarn prettier` | Runs the prettier on `.js` and `.ts` files. | -| `yarn test` | Runs the tests. | +| Command | Description | +|-------------------|------------------------------------------------------------------------------------| +| `yarn build` | Builds the source code into the `dist/` directory. | +| `yarn docs:build` | Builds the documentation into the `.docusaurus/` directory. | +| `yarn docs:serve` | Serves the built documentation from the `.docusaurus/` directory. | +| `yarn docs:start` | Builds and runs the documentation in a development environment with hot reloading. | +| `yarn lint` | Runs the linter on `.js` and `.ts` files. | +| `yarn node:start` | Starts up a NEAR development node and runs it in the background. | +| `yarn node:start` | Stops the NEAR development node that was started in `yarn node:start`. | +| `yarn prettier` | Runs the prettier on `.js` and `.ts` files. | +| `yarn test` | Starts a NEAR development node and runs the tests. | [Back to top ^][table-of-contents] diff --git a/jest.config.ts b/jest.config.ts index 49a8587..3491434 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -5,6 +5,7 @@ import { pathsToModuleNameMapper } from 'ts-jest'; import { compilerOptions } from './tsconfig.json'; const config: Config = { + globalSetup: '/test/globalSetup.ts', moduleNameMapper: { ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/', diff --git a/scripts/start_node.sh b/scripts/start_node.sh index 47bbbd1..53c9dfb 100755 --- a/scripts/start_node.sh +++ b/scripts/start_node.sh @@ -20,7 +20,7 @@ function main { # start the node near-sandbox --home ./.near run > /dev/null 2>&1 & - sleep 1000 + sleep 2s printf "%b node started \n" "${INFO_PREFIX}" diff --git a/scripts/stop_node.sh b/scripts/stop_node.sh index b426492..7103469 100755 --- a/scripts/stop_node.sh +++ b/scripts/stop_node.sh @@ -5,7 +5,7 @@ SCRIPT_DIR=$(dirname "${0}") source "${SCRIPT_DIR}"/set_vars.sh -# Public: Gets the saved node PID from .near/node.pid and kills the background process. +# Public: Kills the node's process. # # Examples # @@ -17,11 +17,13 @@ function main { set_vars - printf "%b killing node process \n" "${INFO_PREFIX}" + printf "%b stopping the node \n" "${INFO_PREFIX}" # stop the node pkill -f "near-sandbox" + printf "%b stopped node \n" "${INFO_PREFIX}" + exit 0 } diff --git a/src/controllers/Social.get.test.ts b/src/controllers/Social.get.test.ts index 9772d2e..ed2ba58 100644 --- a/src/controllers/Social.get.test.ts +++ b/src/controllers/Social.get.test.ts @@ -1,10 +1,7 @@ -import { Account, connect, Near } from 'near-api-js'; -import { NearAccount, Worker } from 'near-workspaces'; -import { resolve } from 'node:path'; -import { cwd } from 'node:process'; +import type { Account } from 'near-api-js'; -// constants -import { NETWORK_ID } from '@test/constants'; +// credentials +import { account_id as socialContractAccountId } from '@test/credentials/localnet/social.test.near.json'; // controllers import Social from './Social'; @@ -13,35 +10,10 @@ import Social from './Social'; import createEphemeralAccount from '@test/helpers/createEphemeralAccount'; describe(`${Social.name}#get`, () => { - let contractAccount: NearAccount; - let near: Near; let signer: Account; - let worker: Worker; - - beforeAll(async () => { - worker = await Worker.init({ - network: NETWORK_ID, - }); - - contractAccount = await worker.rootAccount.devDeploy( - resolve(cwd(), 'test', 'contracts', 'social_db.wasm') - ); - await worker.rootAccount.call(contractAccount.accountId, 'new', {}); - - near = await connect({ - networkId: NETWORK_ID, - nodeUrl: worker.provider.connection.url, - }); - }); - - afterAll(async () => { - await worker.tearDown(); - }); beforeEach(async () => { - const result = await createEphemeralAccount({ - worker, - }); + const result = await createEphemeralAccount(); signer = result.account; }); @@ -49,7 +21,7 @@ describe(`${Social.name}#get`, () => { it('should return an empty object when the contract does not know the account', async () => { // arrange const client = new Social({ - contractId: contractAccount.accountId, + contractId: socialContractAccountId, }); // act const result = await client.get({ diff --git a/src/controllers/Social.getVersion.test.ts b/src/controllers/Social.getVersion.test.ts index e97c29a..b359857 100644 --- a/src/controllers/Social.getVersion.test.ts +++ b/src/controllers/Social.getVersion.test.ts @@ -1,10 +1,7 @@ -import { Account, connect, Near } from 'near-api-js'; -import { NearAccount, Worker } from 'near-workspaces'; -import { resolve } from 'node:path'; -import { cwd } from 'node:process'; +import type { Account } from 'near-api-js'; -// constants -import { NETWORK_ID } from '@test/constants'; +// credentials +import { account_id as socialContractAccountId } from '@test/credentials/localnet/social.test.near.json'; // controllers import Social from './Social'; @@ -13,35 +10,10 @@ import Social from './Social'; import createEphemeralAccount from '@test/helpers/createEphemeralAccount'; describe(`${Social.name}#getVersion`, () => { - let contractAccount: NearAccount; - let near: Near; let signer: Account; - let worker: Worker; - - beforeAll(async () => { - worker = await Worker.init({ - network: NETWORK_ID, - }); - - contractAccount = await worker.rootAccount.devDeploy( - resolve(cwd(), 'test', 'contracts', 'social_db.wasm') - ); - await worker.rootAccount.call(contractAccount.accountId, 'new', {}); - - near = await connect({ - networkId: NETWORK_ID, - nodeUrl: worker.provider.connection.url, - }); - }); - - afterAll(async () => { - await worker.tearDown(); - }); beforeEach(async () => { - const result = await createEphemeralAccount({ - worker, - }); + const result = await createEphemeralAccount(); signer = result.account; }); @@ -49,7 +21,7 @@ describe(`${Social.name}#getVersion`, () => { it('should return the version of the social contract', async () => { // arrange const client = new Social({ - contractId: contractAccount.accountId, + contractId: socialContractAccountId, }); // act const version = await client.getVersion({ signer }); diff --git a/src/controllers/Social.set.test.ts b/src/controllers/Social.set.test.ts index 2817675..7c9f74f 100644 --- a/src/controllers/Social.set.test.ts +++ b/src/controllers/Social.set.test.ts @@ -1,11 +1,8 @@ import { Account, providers, transactions, utils } from 'near-api-js'; import { randomBytes } from 'node:crypto'; -import { NearAccount, Worker } from 'near-workspaces'; -import { resolve } from 'node:path'; -import { cwd } from 'node:process'; -// constants -import { NETWORK_ID } from '@test/constants'; +// credentials +import { account_id as socialContractAccountId } from '@test/credentials/localnet/social.test.near.json'; // controllers import Social from './Social'; @@ -14,38 +11,16 @@ import Social from './Social'; import accountAccessKey, { IAccessKeyResponse, } from '@test/helpers/accountAccessKey'; -import createEphemeralAccount from '@test/helpers/createEphemeralAccount'; - -// utils import convertNEARToYoctoNEAR from '@app/utils/convertNEARToYoctoNEAR'; +import createEphemeralAccount from '@test/helpers/createEphemeralAccount'; describe(`${Social.name}#set`, () => { - let contractAccount: NearAccount; let keyPair: utils.KeyPairEd25519; let signer: Account; let signerAccessKeyResponse: IAccessKeyResponse; - let worker: Worker; - - beforeAll(async () => { - worker = await Worker.init({ - network: NETWORK_ID, - }); - - contractAccount = await worker.rootAccount.devDeploy( - resolve(cwd(), 'test', 'contracts', 'social_db.wasm') - ); - await worker.rootAccount.call(contractAccount.accountId, 'new', {}); - }); - - afterAll(async () => { - await worker.tearDown(); - }); beforeEach(async () => { - const result = await createEphemeralAccount({ - initialBalanceInAtomicUnits: convertNEARToYoctoNEAR('100'), - worker, - }); + const result = await createEphemeralAccount(convertNEARToYoctoNEAR('100')); keyPair = result.keyPair; signer = result.account; @@ -54,7 +29,7 @@ describe(`${Social.name}#set`, () => { it('should set storage and add the data', async () => { // arrange const client = new Social({ - contractId: contractAccount.accountId, + contractId: socialContractAccountId, }); const data = { [signer.accountId]: { diff --git a/test/constants/Config.ts b/test/constants/Config.ts index b943bd7..8ee5ca9 100644 --- a/test/constants/Config.ts +++ b/test/constants/Config.ts @@ -1,2 +1,2 @@ -export const NETWORK_ID = 'sandbox'; +export const NETWORK_ID = 'localnet'; export const NODE_URL = 'http://localhost:3030'; diff --git a/test/globalSetup.ts b/test/globalSetup.ts new file mode 100644 index 0000000..29dc5d2 --- /dev/null +++ b/test/globalSetup.ts @@ -0,0 +1,75 @@ +import { Account, connect, keyStores, utils } from 'near-api-js'; +import { resolve } from 'node:path'; +import { cwd } from 'node:process'; +import { readFile } from 'node:fs/promises'; + +// constants +import { NETWORK_ID, NODE_URL } from './constants'; + +// credentials +import { account_id as genesisAccountId } from './credentials/localnet/test.near.json'; +import { account_id as socialContractAccountId } from './credentials/localnet/social.test.near.json'; + +// helpers +import createTestAccount from './helpers/createTestAccount'; + +// utils +import convertNEARToYoctoNEAR from '../src/utils/convertNEARToYoctoNEAR'; + +export default async function globalSetup() { + const _functionName = 'globalSetup'; + const near = await connect({ + networkId: NETWORK_ID, + nodeUrl: NODE_URL, + keyStore: new keyStores.UnencryptedFileSystemKeyStore( + resolve(cwd(), 'test', 'credentials') + ), + }); + const contract = await readFile( + resolve(cwd(), 'test', 'contracts', 'social_db.wasm') + ); + let contractAccountPublicKey: utils.PublicKey; + let contractAccount: Account; + let genesisAccount: Account; + + genesisAccount = await near.account(genesisAccountId); + contractAccountPublicKey = await near.connection.signer.getPublicKey( + socialContractAccountId, + NETWORK_ID + ); + + // create the contract account + contractAccount = await createTestAccount({ + creatorAccount: genesisAccount, + initialBalanceInAtomicUnits: BigInt(convertNEARToYoctoNEAR('10')), + newAccountID: socialContractAccountId, + newAccountPublicKey: contractAccountPublicKey, + connection: near, + }); + + // deploy the account + await contractAccount.deployContract(contract); + + try { + // initialize the contract + await genesisAccount.functionCall({ + contractId: contractAccount.accountId, + methodName: 'new', + }); + // set the contract to live + await contractAccount.functionCall({ + contractId: contractAccount.accountId, + methodName: 'set_status', + args: { + status: 'Live', + }, + }); + } catch (error) { + // if the contract has already been initialized, just ignore + if (error.message.includes('The contract has already been initialized')) { + return; + } + + console.error(`${_functionName}:`, JSON.stringify(error)); + } +} diff --git a/test/helpers/createEphemeralAccount/createEphemeralAccount.ts b/test/helpers/createEphemeralAccount/createEphemeralAccount.ts index d86d5f8..bdd2e8a 100644 --- a/test/helpers/createEphemeralAccount/createEphemeralAccount.ts +++ b/test/helpers/createEphemeralAccount/createEphemeralAccount.ts @@ -1,55 +1,56 @@ -import { connect, keyStores, utils, Near } from 'near-api-js'; -import { NearAccount, randomAccountId } from 'near-workspaces'; +import { connect, keyStores, utils, KeyPair, Near, Account } from 'near-api-js'; +import { randomBytes } from 'node:crypto'; // constants -import { NETWORK_ID } from '@test/constants'; +import { NETWORK_ID, NODE_URL } from '@test/constants'; + +// credentials +import { + account_id as faucetAccountId, + secret_key as faucetSecretKey, +} from '@test/credentials/localnet/test.near.json'; // types -import type { IOptions, IResult } from './types'; +import type { IResult } from './types'; /** - * Creates an ephemeral account with a randomised account ID. - * @param {IOptions} options - the initial balance and the worker. + * Creates an ephemeral account with a randomised account ID of 8 lower case hexadecimal characters. + * @param {string} initialBalanceInAtomicUnits - [optional] the initial balance of the account in account units. + * Defaults to zero. * @returns {Promise} a promise that resolves to the account and the account's access key pair. */ -export default async function createEphemeralAccount({ - initialBalanceInAtomicUnits, - worker, -}: IOptions): Promise { - const rootAccountKeyPair = await worker.rootAccount.getKey(); // get the faucet key pair +export default async function createEphemeralAccount( + initialBalanceInAtomicUnits?: string +): Promise { + const accountId = `${randomBytes(8).toString('hex').toLowerCase()}.test.near`; + const faucetKeyPair = KeyPair.fromString(faucetSecretKey); // get the faucet key pair const keyPair = utils.KeyPairEd25519.fromRandom(); // create the new access key to be used const keyStore = new keyStores.InMemoryKeyStore(); - let account: NearAccount; + let faucetAccount: Account; let near: Near; - if (!rootAccountKeyPair) { - throw new Error( - `failed to get the root account "${worker.rootAccount.accountId}" access key` - ); - } - - // create the new account with the initial balance - account = await worker.rootAccount.createSubAccount(randomAccountId(), { - initialBalance: initialBalanceInAtomicUnits, - keyPair, - }); - // set the keys to the in-memory keystore - await keyStore.setKey( - NETWORK_ID, - worker.rootAccount.accountId, - rootAccountKeyPair - ); - await keyStore.setKey(NETWORK_ID, account.accountId, keyPair); + await keyStore.setKey(NETWORK_ID, faucetAccountId, faucetKeyPair); + await keyStore.setKey(NETWORK_ID, accountId, keyPair); near = await connect({ networkId: NETWORK_ID, - nodeUrl: worker.provider.connection.url, + nodeUrl: NODE_URL, keyStore, }); + faucetAccount = await near.account(faucetAccountId); + + // create the new account with the initial balance + await faucetAccount.createAccount( + accountId, + keyPair.publicKey, + initialBalanceInAtomicUnits + ? BigInt(initialBalanceInAtomicUnits) + : BigInt('0') + ); return { - account: await near.account(account.accountId), + account: await near.account(accountId), keyPair, }; } diff --git a/test/helpers/createEphemeralAccount/types/IOptions.ts b/test/helpers/createEphemeralAccount/types/IOptions.ts deleted file mode 100644 index 5b4b0f8..0000000 --- a/test/helpers/createEphemeralAccount/types/IOptions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Worker } from 'near-workspaces'; - -interface IOptions { - initialBalanceInAtomicUnits?: string; - worker: Worker; -} - -export default IOptions; diff --git a/test/helpers/createEphemeralAccount/types/index.ts b/test/helpers/createEphemeralAccount/types/index.ts index 8c186b3..37d8c72 100644 --- a/test/helpers/createEphemeralAccount/types/index.ts +++ b/test/helpers/createEphemeralAccount/types/index.ts @@ -1,2 +1 @@ -export type { default as IOptions } from './IOptions'; export type { default as IResult } from './IResult'; diff --git a/test/helpers/createTestAccount/createTestAccount.ts b/test/helpers/createTestAccount/createTestAccount.ts new file mode 100644 index 0000000..5826f93 --- /dev/null +++ b/test/helpers/createTestAccount/createTestAccount.ts @@ -0,0 +1,38 @@ +import type { Account } from 'near-api-js'; + +// types +import type { IOptions } from './types'; + +/** + * Creates a test account with an optional initial balance. + * @param {IOptions} options - the options needed to create the new account. + * @returns {Promise} a promise that resolves to the new account. + */ +export default async function createTestAccount({ + connection, + creatorAccount, + initialBalanceInAtomicUnits, + newAccountID, + newAccountPublicKey, +}: IOptions): Promise { + let newAccount = await connection.account(newAccountID); + + try { + // this will error if the account doesn't exist + await newAccount.getAccountBalance(); + + // if the new account exists, return it + return newAccount; + } catch (error) { + // no account exists, create a new one + } + + // create the new account + await creatorAccount.createAccount( + newAccountID, + newAccountPublicKey, + initialBalanceInAtomicUnits || BigInt('0') + ); + + return await connection.account(newAccountID); +} diff --git a/test/helpers/createTestAccount/index.ts b/test/helpers/createTestAccount/index.ts new file mode 100644 index 0000000..6db817d --- /dev/null +++ b/test/helpers/createTestAccount/index.ts @@ -0,0 +1,2 @@ +export { default } from './createTestAccount'; +export * from './types'; diff --git a/test/helpers/createTestAccount/types/IOptions.ts b/test/helpers/createTestAccount/types/IOptions.ts new file mode 100644 index 0000000..c52c77d --- /dev/null +++ b/test/helpers/createTestAccount/types/IOptions.ts @@ -0,0 +1,11 @@ +import { Account, Near, utils } from 'near-api-js'; + +interface IOptions { + connection: Near; + creatorAccount: Account; + initialBalanceInAtomicUnits?: bigint; + newAccountID: string; + newAccountPublicKey: utils.PublicKey; +} + +export default IOptions; diff --git a/test/helpers/createTestAccount/types/index.ts b/test/helpers/createTestAccount/types/index.ts new file mode 100644 index 0000000..68e7001 --- /dev/null +++ b/test/helpers/createTestAccount/types/index.ts @@ -0,0 +1 @@ +export type { default as IOptions } from './IOptions';