near-workspaces
Version:
Write tests in TypeScript/JavaScript to run in a controlled NEAR Sandbox local environment.
388 lines • 14.6 kB
JavaScript
"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