UNPKG

near-workspaces

Version:

Write tests in TypeScript/JavaScript to run in a controlled NEAR Sandbox local environment.

388 lines 14.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ManagedTransaction = exports.SandboxManager = exports.TestnetManager = exports.CustomnetManager = exports.AccountManager = void 0; const path = __importStar(require("path")); const process = __importStar(require("process")); const nearAPI = __importStar(require("near-api-js")); const utils_1 = require("../utils"); const types_1 = require("../types"); const internal_utils_1 = require("../internal-utils"); const transaction_1 = require("../transaction"); const jsonrpc_1 = require("../jsonrpc"); const transaction_result_1 = require("../transaction-result"); const account_1 = require("./account"); const utils_2 = require("./utils"); class AccountManager { config; static create(config) { const { network } = config; switch (network) { case 'sandbox': { return new SandboxManager(config); } case 'testnet': { return new TestnetManager(config); } case 'custom': { return new CustomnetManager(config); } default: { throw new Error(`Bad network id: "${network}"; expected "testnet", "custom" or "sandbox"`); } } } accountsCreated = new Set(); _root; constructor(config) { this.config = config; } async accountView(accountId) { return this.provider.viewAccount(accountId); } getAccount(accountId) { return new account_1.Account(accountId, this); } getParentAccount(accountId) { const split = accountId.split('.'); if (split.length === 1) { return this.getAccount(accountId); } return this.getAccount(split.slice(1).join('.')); } async deleteKey(accountId) { try { await this.keyStore.removeKey(this.networkId, accountId); (0, internal_utils_1.debug)(`deleted Key for ${accountId}`); } catch { (0, internal_utils_1.debug)(`Failed to delete key for ${accountId}`); } } async init() { return this; } get root() { this._root ||= new account_1.Account(this.rootAccountId, this); return this._root; } get initialBalance() { return this.config.initialBalance ?? this.DEFAULT_INITIAL_BALANCE; } get doubleInitialBalance() { return this.initialBalance * 2n; } get provider() { return jsonrpc_1.JsonRpcProvider.from(this.config); } batch(sender, receiver) { return new ManagedTransaction(this, sender, receiver); } async getKey(accountId) { return this.keyStore.getKey(this.networkId, accountId); } async getPublicKey(accountId) { const keyPair = await this.getKey(accountId); return keyPair?.getPublicKey() ?? null; } /** Sets the provided key to store, otherwise creates a new one */ async setKey(accountId, keyPair) { const key = keyPair ?? types_1.KeyPairEd25519.fromRandom(); await this.keyStore.setKey(this.networkId, accountId, key); (0, internal_utils_1.debug)(`Setting keys for ${accountId}`); return (await this.getKey(accountId)); } async removeKey(accountId) { await this.keyStore.removeKey(this.networkId, accountId); } async deleteAccount(accountId, beneficiaryId, keyPair) { try { return await this.getAccount(accountId).delete(beneficiaryId, keyPair); } catch (error) { if (keyPair) { (0, internal_utils_1.debug)(`Failed to delete ${accountId} with different keyPair`); return this.deleteAccount(accountId, beneficiaryId); } throw error; } } async getRootKey() { const keyPair = await this.getKey(this.rootAccountId); if (!keyPair) { return this.setKey(this.rootAccountId); } return keyPair; } async balance(account) { return this.provider.accountBalance((0, utils_1.asId)(account)); } async availableBalance(account) { const balance = await this.balance(account); return BigInt(balance.available); } async exists(accountId) { return this.provider.accountExists((0, utils_1.asId)(accountId)); } async canCoverBalance(account, amount) { return amount < await this.availableBalance(account); } async executeTransaction(tx, keyPair) { const account = new nearAPI.Account(this.connection, tx.senderId); let oldKey = null; if (keyPair) { oldKey = await this.getKey(account.accountId); await this.setKey(account.accountId, keyPair); } try { const start = Date.now(); const outcome = await account.signAndSendTransaction({ receiverId: tx.receiverId, actions: tx.actions, returnError: true }); const end = Date.now(); if (oldKey) { await this.setKey(account.accountId, oldKey); } else if (keyPair) { // Sender account should only have account while execution transaction await this.deleteKey(tx.senderId); } const result = new transaction_result_1.TransactionResult(outcome, start, end, this.config); (0, internal_utils_1.txDebug)(result.summary()); return result; } catch (error) { // Add back oldKey if temporary one was used if (oldKey) { await this.setKey(account.accountId, oldKey); } if (error instanceof Error) { const key = await this.getPublicKey(tx.receiverId); (0, internal_utils_1.debug)(`TX FAILED: receiver ${tx.receiverId} with key ${key?.toString() ?? 'MISSING'} ${JSON.stringify(tx.actions, (_key, value) => typeof value === 'bigint' ? value.toString() : value).slice(0, 1000)}`); (0, internal_utils_1.debug)(error); } throw error; } } addAccountCreated(account, _sender) { this.accountsCreated.add(account); } async cleanup() { } // eslint-disable-line @typescript-eslint/no-empty-function get rootAccountId() { return this.config.rootAccountId; } set rootAccountId(value) { this.config.rootAccountId = value; } get keyStore() { return this.config.keyStore ?? this.defaultKeyStore; } get signer() { return new nearAPI.InMemorySigner(this.keyStore); } get networkId() { return this.config.network; } get connection() { return new nearAPI.Connection(this.networkId, this.provider, this.signer, `jsvm.${this.networkId}`); } } exports.AccountManager = AccountManager; class CustomnetManager extends AccountManager { get DEFAULT_INITIAL_BALANCE() { return BigInt((0, utils_1.parseNEAR)('10')); } get defaultKeyStore() { return new nearAPI.keyStores.InMemoryKeyStore(); } get connection() { return new nearAPI.Connection(this.networkId, this.provider, this.signer, `jsvm.${this.networkId}`); } get networkId() { return this.config.network; } async init() { return this; } async createFrom(config) { return new CustomnetManager(config); } } exports.CustomnetManager = CustomnetManager; class TestnetManager extends AccountManager { static KEYSTORE_PATH = path.join(process.cwd(), '.near-credentials', 'workspaces'); static numTestAccounts = 0; _testnetRoot; static get defaultKeyStore() { const keyStore = new nearAPI.keyStores.UnencryptedFileSystemKeyStore(this.KEYSTORE_PATH); return keyStore; } get masterAccountId() { const passedAccountId = this.config.testnetMasterAccountId ?? process.env.TESTNET_MASTER_ACCOUNT_ID; if (!passedAccountId) { throw new Error('Master account is not provided. You can set it in config while calling Worker.init(config); or with TESTNET_MASTER_ACCOUNT_ID env variable'); } return passedAccountId; } get fullRootAccountId() { return this.rootAccountId + '.' + this.masterAccountId; } get root() { this._testnetRoot ||= new account_1.Account(this.fullRootAccountId, this); return this._testnetRoot; } get DEFAULT_INITIAL_BALANCE() { return BigInt((0, utils_1.parseNEAR)('10')); } get defaultKeyStore() { return TestnetManager.defaultKeyStore; } get urlAccountCreator() { return new nearAPI.accountCreator.UrlAccountCreator({}, // ignored this.config.helperUrl); } async init() { this.rootAccountId ||= (0, utils_1.randomAccountId)('r-', 5, 5); if (!(await this.exists(this.fullRootAccountId))) { await this.getAccount(this.masterAccountId).createSubAccount(this.rootAccountId); } return this; } async createTopLevelAccountWithHelper(accountId, keyPair) { await this.urlAccountCreator.createAccount(accountId, keyPair.getPublicKey()); } async createAccount(accountId, keyPair) { if (accountId.includes('.')) { await this.getParentAccount(accountId).createAccount(accountId, { keyPair }); this.accountsCreated.delete(accountId); } else { await this.createTopLevelAccountWithHelper(accountId, keyPair ?? await this.getRootKey()); (0, internal_utils_1.debug)(`Created account ${accountId} with account creator`); } return this.getAccount(accountId); } async addFundsFromNetwork(accountId = this.fullRootAccountId) { const temporaryId = (0, utils_1.randomAccountId)(); try { const key = await this.getRootKey(); const account = await this.createAccount(temporaryId, key); await account.delete(accountId, key); } catch (error) { if (error instanceof Error) { await this.removeKey(temporaryId); } throw error; } } async addFunds(accountId, amount) { const parent = this.getParentAccount(accountId); if (parent.accountId === accountId) { return this.addFundsFromNetwork(accountId); } if (!(await this.canCoverBalance(parent, amount))) { await this.addFunds(parent.accountId, amount); } await parent.transfer(accountId, amount); } async deleteAccounts(accounts, beneficiaryId) { const keyPair = await this.getKey(this.rootAccountId) ?? undefined; return Promise.all(accounts.map(async (accountId) => { await this.deleteAccount(accountId, beneficiaryId, keyPair); await this.deleteKey(accountId); })); } async createFrom(config) { const currentRunAccount = TestnetManager.numTestAccounts; const prefix = currentRunAccount === 0 ? '' : currentRunAccount; TestnetManager.numTestAccounts += 1; const newConfig = { ...config, rootAccount: `t${prefix}.${config.rootAccountId}` }; return (new TestnetManager(newConfig)).init(); } async cleanup() { return this.deleteAccounts([...this.accountsCreated.values()], this.rootAccountId); } async needsFunds(accountId, amount) { return amount !== 0n && this.isRootOrTLAccount(accountId) && (!await this.canCoverBalance(accountId, amount)); } isRootOrTLAccount(accountId) { return this.rootAccountId === accountId || (0, utils_1.isTopLevelAccount)(accountId); } } exports.TestnetManager = TestnetManager; class SandboxManager extends AccountManager { async init() { if (!await this.getKey(this.rootAccountId)) { await this.setKey(this.rootAccountId, await (0, utils_2.getKeyFromFile)(this.keyFilePath)); } return this; } async createFrom(config) { return new SandboxManager(config); } get DEFAULT_INITIAL_BALANCE() { return BigInt((0, utils_1.parseNEAR)('200')); } get defaultKeyStore() { const keyStore = new nearAPI.keyStores.UnencryptedFileSystemKeyStore(this.config.homeDir); return keyStore; } get keyFilePath() { return path.join(this.config.homeDir, 'validator_key.json'); } } exports.SandboxManager = SandboxManager; class ManagedTransaction extends transaction_1.Transaction { manager; delete = false; constructor(manager, sender, receiver) { super(sender, receiver); this.manager = manager; } createAccount() { this.manager.addAccountCreated(this.receiverId, this.senderId); return super.createAccount(); } deleteAccount(beneficiaryId) { this.delete = true; return super.deleteAccount(beneficiaryId); } /** * * @param keyPair Temporary key to sign transaction * @returns */ async transact(keyPair) { const executionResult = await this.manager.executeTransaction(this, keyPair); if (executionResult.succeeded && this.delete) { await this.manager.deleteKey(this.receiverId); } return executionResult; } } exports.ManagedTransaction = ManagedTransaction; //# sourceMappingURL=account-manager.js.map