@onflow/flow-js-testing
Version:
This package will expose a set of utility methods, to allow Cadence code testing with libraries like Jest
1,511 lines (1,388 loc) • 128 kB
JavaScript
import * as fcl from '@onflow/fcl';
import { config as config$1, withPrefix, account, send, build, getBlock, decode } from '@onflow/fcl';
export { config } from '@onflow/fcl';
import path from 'path';
import fs from 'fs';
import { replaceImportAddresses, resolveArguments, getEnvironment, reportMissingImports, deployContract as deployContract$2, reportMissing, sendTransaction as sendTransaction$1, extractImports, extractContractParameters, generateSchema, splitArgs } from '@onflow/flow-cadut';
export { extractImports, replaceImportAddresses } from '@onflow/flow-cadut';
import * as rlp from 'rlp';
import { ec as ec$1 } from 'elliptic';
import { exec } from 'child_process';
import { createServer } from 'net';
import * as semver from 'semver';
import { satisfies } from 'semver';
import { sha3_256 } from 'js-sha3';
import { sha256 } from 'js-sha256';
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const TARGET = "flow.json";
let configPath = null;
let config = null;
function isDir(dir) {
return fs.lstatSync(dir).isDirectory();
}
function listFiles(dir) {
return new Set(fs.readdirSync(dir));
}
function parentDir(dir) {
return path.dirname(dir);
}
function findTarget(dir) {
if (!isDir(dir)) throw new Error(`Not a directory: ${dir}`);
return listFiles(dir).has(TARGET) ? path.resolve(dir, TARGET) : null;
}
function getConfigPath(dir) {
if (configPath != null) return configPath;
const filePath = findTarget(dir);
if (filePath == null) {
if (dir === parentDir(dir)) {
throw new Error("No flow.json found");
}
return getConfigPath(parentDir(dir));
}
configPath = filePath;
return configPath;
}
function flowConfig() {
if (config != null) return config;
const filePath = getConfigPath(process.cwd());
const content = fs.readFileSync(filePath, "utf8");
config = JSON.parse(content);
return config;
}
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const DEFAULT_COMPUTE_LIMIT = 9999;
/**
* Inits framework variables, storing private key of service account and base path
* where Cadence files are stored.
* @param {string} basePath - path to the folder with Cadence files to be tested.
* @param {number} [props.port] - port to use for accessAPI
* @param {number} [props.pkey] - private key to use for service account in case of collisions
*/
const init = function (basePath, props = {}) {
try {
const {
pkey = "48a1f554aeebf6bf9fe0d7b5b79d080700b073ee77909973ea0b2f6fbc902"
} = props;
const cfg = flowConfig();
config$1().put("PRIVATE_KEY", getServiceKey(cfg) ?? pkey);
config$1().put("SERVICE_ADDRESS", cfg?.accounts?.["emulator-account"]?.address ?? "f8d6e0586b0a20c7");
config$1().put("BASE_PATH", cfg?.testing?.paths ?? basePath);
config$1().put("fcl.limit", DEFAULT_COMPUTE_LIMIT);
return Promise.resolve();
} catch (e) {
return Promise.reject(e);
}
};
function getServiceKey(cfg) {
const value = cfg?.accounts?.["emulator-account"]?.key;
if (value) {
if (typeof value === "object") {
switch (value.type) {
case "hex":
return value.privateKey;
case "file":
{
const configDir = path.dirname(getConfigPath());
const resovledPath = path.resolve(configDir, value.location);
return fs.readFileSync(resovledPath, "utf8");
}
default:
return null;
}
} else if (typeof value === "string") {
if (value.startsWith("$")) {
return process.env[value.slice(1)];
} else {
return value;
}
} else {
return null;
}
}
return null;
}
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Get the Flow CLI version
* @param {string} flowCommand - the Flow CLI command name
* @returns {Promise<import("semver").SemVer>}
*/
const getFlowVersion = function (flowCommand = "flow") {
try {
return Promise.resolve(new Promise((resolve, reject) => {
exec(`${flowCommand} version --output=json`, (error, stdout) => {
if (error) {
reject("Could not determine Flow CLI version, please make sure it is installed and available in your PATH");
} else {
let versionStr;
try {
versionStr = JSON.parse(stdout).version;
} catch (error) {
// fallback to regex for older versions of the CLI without JSON output
const rxResult = /^Version: ([^\s]+)/m.exec(stdout);
if (rxResult) {
versionStr = rxResult[1];
}
}
const version = versionStr ? semver.parse(versionStr) : undefined;
if (!version) {
reject(`Invalid Flow CLI version string: ${versionStr}`);
}
resolve(version);
}
});
}));
} catch (e) {
return Promise.reject(e);
}
};
const isObject = arg => typeof arg === "object" && arg !== null;
const isString = obj => typeof obj === "string" || obj instanceof String;
const isAddress = address => /^0x[0-9a-f]{0,16}$/.test(address);
function getAvailablePorts(count = 1) {
if (count === 0) return Promise.resolve([]);
return new Promise((resolve, reject) => {
const server = createServer();
server.listen(0, () => {
const port = server.address().port;
server.close(function (err) {
try {
if (err) reject(err);
return Promise.resolve(getAvailablePorts(count - 1)).then(function (_getAvailablePorts) {
resolve([..._getAvailablePorts, port]);
});
} catch (e) {
return Promise.reject(e);
}
});
});
});
}
const getServiceAddress = function () {
try {
return Promise.resolve(config$1().get("SERVICE_ADDRESS")).then(withPrefix);
} catch (e) {
return Promise.reject(e);
}
};
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const invariant = function (fact, msg) {
if (!fact) {
const error = new Error(`INVARIANT ${msg}`);
error.stack = error.stack.split("\n").filter(d => !/at invariant/.test(d)).join("\n");
console.error("\n\n---\n\n", error, "\n\n", ...[].slice.call(arguments, 2), "\n\n---\n\n");
throw error;
}
};
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Represents a signature for an arbitrary message generated using a particular key
* @typedef {Object} SignatureObject
* @property {string} addr address of account whose key was used to sign the message
* @property {number} keyId key index on the account of they key used to sign the message
* @property {string} signature signature corresponding to the signed message hash as hex-encoded string
*/
/**
* Represents a private key object which may be used to generate a public key
* @typedef {Object} KeyObject
* @property {string | Buffer} privateKey private key for this key object
* @property {SignatureAlgorithm} [signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used with this key
* @property {HashAlgorithm} [hashAlgorithm=HashAlgorithm.SHA3_256] hash algorithm used with this key
* @property {weight} [weight=1000] desired weight of this key (default full weight)
*/
/**
* Represents a signer of a message or transaction
* @typedef {Object} SignerInfoObject
* @property {string} addr address of the signer
* @property {HashAlgorithm} [hashAlgorithm=HashAlgorithm.SHA3_256] hash algorithm used to hash the message before signing
* @property {SignatureAlgorithm} [signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used to generate the signature
* @property {number} [keyId=0] index of the key on the signers account to use
* @property {string | Buffer} [privateKey=SERVICE_KEY] private key of the signer (defaults to universal private key/service key from config)
*/
/**
* Enum for signing algorithms
* @readonly
* @enum {number}
*/
const SignatureAlgorithm = {
ECDSA_P256: 1,
ECDSA_secp256k1: 2
};
/**
* Enum for hasing algorithms
* @readonly
* @enum {number}
*/
const HashAlgorithm = {
SHA2_256: 1,
SHA3_256: 3
};
/**
* Enum for mapping hash algorithm name to hashing function
* @readonly
* @enum {function}
*/
const HashFunction = {
SHA2_256: sha256,
SHA3_256: sha3_256
};
/**
* Enum for mapping signature algorithm to elliptic instance
* @readonly
* @enum {EC}
*/
const ec = {
ECDSA_P256: new ec$1("p256"),
ECDSA_secp256k1: new ec$1("secp256k1")
};
const resolveHashAlgoKey = hashAlgorithm => {
const hashAlgorithmKey = Object.keys(HashAlgorithm).find(x => HashAlgorithm[x] === hashAlgorithm || isString(hashAlgorithm) && x.toLowerCase() === hashAlgorithm.toLowerCase());
if (!hashAlgorithmKey) throw new Error(`Provided hash algorithm "${hashAlgorithm}" is not currently supported`);
return hashAlgorithmKey;
};
const resolveSignAlgoKey = signatureAlgorithm => {
const signatureAlgorithmKey = Object.keys(SignatureAlgorithm).find(x => SignatureAlgorithm[x] === signatureAlgorithm || isString(signatureAlgorithm) && x.toLowerCase() === signatureAlgorithm.toLowerCase());
if (!signatureAlgorithmKey) throw new Error(`Provided signature algorithm "${signatureAlgorithm}" is not currently supported`);
return signatureAlgorithmKey;
};
const hashMsgHex = (msgHex, hashAlgorithm = HashAlgorithm.SHA3_256) => {
const hashAlgorithmKey = resolveHashAlgoKey(hashAlgorithm);
const hashFn = HashFunction[hashAlgorithmKey];
const hash = hashFn.create();
hash.update(Buffer.from(msgHex, "hex"));
return Buffer.from(hash.arrayBuffer());
};
const signWithKey = (privateKey, msgHex, hashAlgorithm = HashAlgorithm.SHA3_256, signatureAlgorithm = SignatureAlgorithm.ECDSA_P256) => {
const signAlgo = resolveSignAlgoKey(signatureAlgorithm);
const key = ec[signAlgo].keyFromPrivate(Buffer.from(privateKey, "hex"));
const sig = key.sign(hashMsgHex(msgHex, hashAlgorithm));
const n = 32; // half of signature length?
const r = sig.r.toArrayLike(Buffer, "be", n);
const s = sig.s.toArrayLike(Buffer, "be", n);
return Buffer.concat([r, s]).toString("hex");
};
const resolveSignerKey = function (signer) {
try {
return Promise.resolve(getServiceAddress()).then(function (addr) {
return Promise.resolve(config$1().get("PRIVATE_KEY")).then(function (privateKey) {
let keyId = 0,
hashAlgorithm = HashAlgorithm.SHA3_256,
signatureAlgorithm = SignatureAlgorithm.ECDSA_P256;
if (isObject(signer)) {
;
({
addr = addr,
keyId = keyId,
privateKey = privateKey,
hashAlgorithm = hashAlgorithm,
signatureAlgorithm = signatureAlgorithm
} = signer);
} else {
addr = signer || addr;
}
return {
addr,
keyId,
privateKey,
hashAlgorithm,
signatureAlgorithm
};
});
});
} catch (e) {
return Promise.reject(e);
}
};
const authorization = signer => function (account = {}) {
try {
return Promise.resolve(resolveSignerKey(signer)).then(function ({
addr,
keyId,
privateKey,
hashAlgorithm,
signatureAlgorithm
}) {
const signingFunction = function (data) {
try {
return Promise.resolve({
keyId,
addr: addr,
signature: signWithKey(privateKey, data.message, hashAlgorithm, signatureAlgorithm)
});
} catch (e) {
return Promise.reject(e);
}
};
return {
...account,
addr,
keyId,
signingFunction
};
});
} catch (e) {
return Promise.reject(e);
}
};
/**
* Returns an RLP-encoded public key for a particular private key as a hex-encoded string
* @param {KeyObject} keyObject
* @param {string | Buffer} keyObject.privateKey private key as hex-encoded string or Buffer
* @param {HashAlgorithm | string} [keyObject.hashAlgorithm=HashAlgorithm.SHA3_256] hasing algorithnm used to hash messages using this key
* @param {SignatureAlgorithm | string} [keyObject.signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used to generate signatures using this key
* @param {number} [keyObject.weight=1000] weight of the key
* @returns {string}
*/
const pubFlowKey = function (keyObject = {}) {
try {
return Promise.resolve(config$1().get("PRIVATE_KEY")).then(function (_config$get) {
let {
privateKey = _config$get,
hashAlgorithm = HashAlgorithm.SHA3_256,
signatureAlgorithm = SignatureAlgorithm.ECDSA_P256,
weight = 1000 // give key full weight
} = keyObject;
// Convert hex string private key to buffer if not buffer already
if (!Buffer.isBuffer(privateKey)) privateKey = Buffer.from(privateKey, "hex");
const hashAlgoName = resolveHashAlgoKey(hashAlgorithm);
const sigAlgoName = resolveSignAlgoKey(signatureAlgorithm);
const keys = ec[sigAlgoName].keyFromPrivate(privateKey);
const publicKey = keys.getPublic("hex").replace(/^04/, "");
return rlp.encode([Buffer.from(publicKey, "hex"),
// publicKey hex to binary
SignatureAlgorithm[sigAlgoName], HashAlgorithm[hashAlgoName], weight]).toString("hex");
});
} catch (e) {
return Promise.reject(e);
}
};
const prependDomainTag = (msgHex, domainTag) => {
const rightPaddedBuffer = buffer => Buffer.concat([Buffer.alloc(32 - buffer.length, 0), buffer]);
let domainTagBuffer = rightPaddedBuffer(Buffer.from(domainTag, "utf-8"));
return domainTagBuffer.toString("hex") + msgHex;
};
/**
* Signs a user message for a given signer
* @param {string | Buffer} msgHex hex-encoded string or Buffer of the message to sign
* @param {string | SignerInfoObject} signer signer address provided as string and JS Testing signs with universal private key/service key or signer info provided manually via SignerInfoObject
* @param {string} domainTag utf-8 domain tag to use when hashing message
* @returns {SignatureObject} signature object which can be validated using verifyUserSignatures
*/
const signUserMessage = function (msgHex, signer, domainTag) {
try {
if (Buffer.isBuffer(msgHex)) msgHex.toString("hex");
return Promise.resolve(resolveSignerKey(signer, true)).then(function ({
addr,
keyId,
privateKey,
hashAlgorithm,
signatureAlgorithm
}) {
if (domainTag) {
msgHex = prependDomainTag(msgHex, domainTag);
}
return {
keyId,
addr: addr,
signature: signWithKey(privateKey, msgHex, hashAlgorithm, signatureAlgorithm)
};
});
} catch (e) {
return Promise.reject(e);
}
};
/**
* Verifies whether user signatures were valid for a particular message hex
* @param {string | Buffer} msgHex hex-encoded string or buffer of message to verify
* @param {[SignatureObject]} signatures array of signatures to verify against msgHex
* @param {string} [domainTag=""] utf-8 domain tag to use when hashing message
* @returns {boolean} true if signatures are valid and total weight >= 1000
*/
const verifyUserSignatures = function (msgHex, signatures, domainTag = "") {
try {
if (Buffer.isBuffer(msgHex)) msgHex = msgHex.toString("hex");
invariant(signatures, "One or mores signatures must be provided");
// convert to array
signatures = [].concat(signatures);
invariant(signatures.length > 0, "One or mores signatures must be provided");
invariant(signatures.reduce((valid, sig) => valid && sig.signature != null && sig.keyId != null && sig.addr != null, true), "One or more signature is invalid. Valid signatures have the following keys: addr, keyId, siganture");
const address = signatures[0].addr;
invariant(signatures.reduce((same, sig) => same && sig.addr === address, true), "Signatures must belong to the same address");
return Promise.resolve(account(address)).then(function (_account) {
const keys = _account.keys;
const largestKeyId = Math.max(...signatures.map(sig => sig.keyId));
invariant(largestKeyId < keys.length, `Key index ${largestKeyId} does not exist on account ${address}`);
// Apply domain tag if needed
if (domainTag) {
msgHex = prependDomainTag(msgHex, domainTag);
}
let totalWeight = 0;
for (let i in signatures) {
const {
signature,
keyId
} = signatures[i];
const {
hashAlgoString: hashAlgo,
signAlgoString: signAlgo,
weight,
publicKey,
revoked
} = keys[keyId];
const key = ec[signAlgo].keyFromPublic(Buffer.from("04" + publicKey, "hex"));
if (revoked) return false;
const msgHash = hashMsgHex(msgHex, hashAlgo);
const sigBuffer = Buffer.from(signature, "hex");
const signatureInput = {
r: sigBuffer.slice(0, 32),
s: sigBuffer.slice(-32)
};
if (!key.verify(msgHash, signatureInput)) return false;
totalWeight += weight;
}
return totalWeight >= 1000;
});
} catch (e) {
return Promise.reject(e);
}
};
// Copyright Joyent, Inc. and other Node contributors.
var R = typeof Reflect === 'object' ? Reflect : null;
var ReflectApply = R && typeof R.apply === 'function'
? R.apply
: function ReflectApply(target, receiver, args) {
return Function.prototype.apply.call(target, receiver, args);
};
var ReflectOwnKeys;
if (R && typeof R.ownKeys === 'function') {
ReflectOwnKeys = R.ownKeys;
} else if (Object.getOwnPropertySymbols) {
ReflectOwnKeys = function ReflectOwnKeys(target) {
return Object.getOwnPropertyNames(target)
.concat(Object.getOwnPropertySymbols(target));
};
} else {
ReflectOwnKeys = function ReflectOwnKeys(target) {
return Object.getOwnPropertyNames(target);
};
}
function ProcessEmitWarning(warning) {
if (console && console.warn) console.warn(warning);
}
var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
return value !== value;
};
function EventEmitter() {
EventEmitter.init.call(this);
}
var events = EventEmitter;
var once_1 = once;
// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._eventsCount = 0;
EventEmitter.prototype._maxListeners = undefined;
// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
var defaultMaxListeners = 10;
function checkListener(listener) {
if (typeof listener !== 'function') {
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
}
}
Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
enumerable: true,
get: function() {
return defaultMaxListeners;
},
set: function(arg) {
if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
}
defaultMaxListeners = arg;
}
});
EventEmitter.init = function() {
if (this._events === undefined ||
this._events === Object.getPrototypeOf(this)._events) {
this._events = Object.create(null);
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined;
};
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
}
this._maxListeners = n;
return this;
};
function _getMaxListeners(that) {
if (that._maxListeners === undefined)
return EventEmitter.defaultMaxListeners;
return that._maxListeners;
}
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
return _getMaxListeners(this);
};
EventEmitter.prototype.emit = function emit(type) {
var args = [];
for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
var doError = (type === 'error');
var events = this._events;
if (events !== undefined)
doError = (doError && events.error === undefined);
else if (!doError)
return false;
// If there is no 'error' event listener then throw.
if (doError) {
var er;
if (args.length > 0)
er = args[0];
if (er instanceof Error) {
// Note: The comments on the `throw` lines are intentional, they show
// up in Node's output if this results in an unhandled exception.
throw er; // Unhandled 'error' event
}
// At least give some kind of context to the user
var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
err.context = er;
throw err; // Unhandled 'error' event
}
var handler = events[type];
if (handler === undefined)
return false;
if (typeof handler === 'function') {
ReflectApply(handler, this, args);
} else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
ReflectApply(listeners[i], this, args);
}
return true;
};
function _addListener(target, type, listener, prepend) {
var m;
var events;
var existing;
checkListener(listener);
events = target._events;
if (events === undefined) {
events = target._events = Object.create(null);
target._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener !== undefined) {
target.emit('newListener', type,
listener.listener ? listener.listener : listener);
// Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = target._events;
}
existing = events[type];
}
if (existing === undefined) {
// Optimize the case of one listener. Don't need the extra array object.
existing = events[type] = listener;
++target._eventsCount;
} else {
if (typeof existing === 'function') {
// Adding the second element, need to change to array.
existing = events[type] =
prepend ? [listener, existing] : [existing, listener];
// If we've already got an array, just append.
} else if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
// Check for listener leak
m = _getMaxListeners(target);
if (m > 0 && existing.length > m && !existing.warned) {
existing.warned = true;
// No error code for this since it is a Warning
// eslint-disable-next-line no-restricted-syntax
var w = new Error('Possible EventEmitter memory leak detected. ' +
existing.length + ' ' + String(type) + ' listeners ' +
'added. Use emitter.setMaxListeners() to ' +
'increase limit');
w.name = 'MaxListenersExceededWarning';
w.emitter = target;
w.type = type;
w.count = existing.length;
ProcessEmitWarning(w);
}
}
return target;
}
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
return _addListener(this, type, listener, true);
};
function onceWrapper() {
if (!this.fired) {
this.target.removeListener(this.type, this.wrapFn);
this.fired = true;
if (arguments.length === 0)
return this.listener.call(this.target);
return this.listener.apply(this.target, arguments);
}
}
function _onceWrap(target, type, listener) {
var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
var wrapped = onceWrapper.bind(state);
wrapped.listener = listener;
state.wrapFn = wrapped;
return wrapped;
}
EventEmitter.prototype.once = function once(type, listener) {
checkListener(listener);
this.on(type, _onceWrap(this, type, listener));
return this;
};
EventEmitter.prototype.prependOnceListener =
function prependOnceListener(type, listener) {
checkListener(listener);
this.prependListener(type, _onceWrap(this, type, listener));
return this;
};
// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener =
function removeListener(type, listener) {
var list, events, position, i, originalListener;
checkListener(listener);
events = this._events;
if (events === undefined)
return this;
list = events[type];
if (list === undefined)
return this;
if (list === listener || list.listener === listener) {
if (--this._eventsCount === 0)
this._events = Object.create(null);
else {
delete events[type];
if (events.removeListener)
this.emit('removeListener', type, list.listener || listener);
}
} else if (typeof list !== 'function') {
position = -1;
for (i = list.length - 1; i >= 0; i--) {
if (list[i] === listener || list[i].listener === listener) {
originalListener = list[i].listener;
position = i;
break;
}
}
if (position < 0)
return this;
if (position === 0)
list.shift();
else {
spliceOne(list, position);
}
if (list.length === 1)
events[type] = list[0];
if (events.removeListener !== undefined)
this.emit('removeListener', type, originalListener || listener);
}
return this;
};
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.removeAllListeners =
function removeAllListeners(type) {
var listeners, events, i;
events = this._events;
if (events === undefined)
return this;
// not listening for removeListener, no need to emit
if (events.removeListener === undefined) {
if (arguments.length === 0) {
this._events = Object.create(null);
this._eventsCount = 0;
} else if (events[type] !== undefined) {
if (--this._eventsCount === 0)
this._events = Object.create(null);
else
delete events[type];
}
return this;
}
// emit removeListener for all listeners on all events
if (arguments.length === 0) {
var keys = Object.keys(events);
var key;
for (i = 0; i < keys.length; ++i) {
key = keys[i];
if (key === 'removeListener') continue;
this.removeAllListeners(key);
}
this.removeAllListeners('removeListener');
this._events = Object.create(null);
this._eventsCount = 0;
return this;
}
listeners = events[type];
if (typeof listeners === 'function') {
this.removeListener(type, listeners);
} else if (listeners !== undefined) {
// LIFO order
for (i = listeners.length - 1; i >= 0; i--) {
this.removeListener(type, listeners[i]);
}
}
return this;
};
function _listeners(target, type, unwrap) {
var events = target._events;
if (events === undefined)
return [];
var evlistener = events[type];
if (evlistener === undefined)
return [];
if (typeof evlistener === 'function')
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
return unwrap ?
unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
}
EventEmitter.prototype.listeners = function listeners(type) {
return _listeners(this, type, true);
};
EventEmitter.prototype.rawListeners = function rawListeners(type) {
return _listeners(this, type, false);
};
EventEmitter.listenerCount = function(emitter, type) {
if (typeof emitter.listenerCount === 'function') {
return emitter.listenerCount(type);
} else {
return listenerCount.call(emitter, type);
}
};
EventEmitter.prototype.listenerCount = listenerCount;
function listenerCount(type) {
var events = this._events;
if (events !== undefined) {
var evlistener = events[type];
if (typeof evlistener === 'function') {
return 1;
} else if (evlistener !== undefined) {
return evlistener.length;
}
}
return 0;
}
EventEmitter.prototype.eventNames = function eventNames() {
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
};
function arrayClone(arr, n) {
var copy = new Array(n);
for (var i = 0; i < n; ++i)
copy[i] = arr[i];
return copy;
}
function spliceOne(list, index) {
for (; index + 1 < list.length; index++)
list[index] = list[index + 1];
list.pop();
}
function unwrapListeners(arr) {
var ret = new Array(arr.length);
for (var i = 0; i < ret.length; ++i) {
ret[i] = arr[i].listener || arr[i];
}
return ret;
}
function once(emitter, name) {
return new Promise(function (resolve, reject) {
function errorListener(err) {
emitter.removeListener(name, resolver);
reject(err);
}
function resolver() {
if (typeof emitter.removeListener === 'function') {
emitter.removeListener('error', errorListener);
}
resolve([].slice.call(arguments));
}
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
if (name !== 'error') {
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
}
});
}
function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
if (typeof emitter.on === 'function') {
eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
}
}
function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
if (typeof emitter.on === 'function') {
if (flags.once) {
emitter.once(name, listener);
} else {
emitter.on(name, listener);
}
} else if (typeof emitter.addEventListener === 'function') {
// EventTarget does not have `error` event semantics like Node
// EventEmitters, we do not listen for `error` events here.
emitter.addEventListener(name, function wrapListener(arg) {
// IE does not have builtin `{ once: true }` support so we
// have to do it manually.
if (flags.once) {
emitter.removeEventListener(name, wrapListener);
}
listener(arg);
});
} else {
throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
}
}
events.once = once_1;
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Enum of all logger levels
* @readonly
* @enum {number}
*/
const LOGGER_LEVELS = {
PANIC: 5,
FATAL: 4,
ERROR: 3,
WARN: 2,
INFO: 1,
DEBUG: 0,
TRACE: -1
};
// eslint-disable-next-line no-control-regex
const LOG_REGEXP = /LOG:.*?\s+(.*)/;
class Logger extends events.EventEmitter {
constructor(options) {
super(options);
this.handleMessage = this.handleMessage.bind(this);
this.process = null;
this.setMaxListeners(100);
}
/**
* Sets the emulator process to monitor logs of
* @param {import("child_process").ChildProcessWithoutNullStreams} process
* @returns {void}
*/
setProcess(process) {
if (this.process) {
this.process.stdout.removeListener("data", this.handleMessage);
this.process.stderr.removeListener("data", this.handleMessage);
}
this.process = process;
this.process.stdout.on("data", this.handleMessage);
this.process.stderr.on("data", this.handleMessage);
}
handleMessage(buffer) {
const logs = this.parseDataBuffer(buffer);
logs.forEach(({
level,
msg,
...data
}) => {
// Handle log special case
const levelMatch = level === LOGGER_LEVELS.INFO || level === LOGGER_LEVELS.DEBUG;
const logMatch = LOG_REGEXP.test(msg);
if (levelMatch && logMatch) {
let logMessage = msg.match(LOG_REGEXP).at(1);
// if message is string, remove from surrounding and unescape
if (/^"(.*)"/.test(logMessage)) {
logMessage = logMessage.substring(1, logMessage.length - 1).replace(/\\"/g, '"');
}
this.emit("log", logMessage);
}
// Emit emulator message to listeners
this.emit("message", {
level,
msg,
...data
});
});
}
fixJSON(msg) {
// TODO: Test this functionality
const split = msg.split("\n").filter(item => item !== "");
return split.length > 1 ? `[${split.join(",")}]` : split[0];
}
parseDataBuffer(dataBuffer) {
const data = dataBuffer.toString();
try {
if (data.includes("msg")) {
let messages = JSON.parse(this.fixJSON(data));
// Make data into array if not array
messages = [].concat(messages);
// Map string levels to enum
messages = messages.map(m => ({
...m,
level: LOGGER_LEVELS[m.level.toUpperCase()]
}));
return messages;
}
} catch (e) {
console.error(e);
}
return [];
}
}
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function _catch$1(body, recover) {
try {
var result = body();
} catch (e) {
return recover(e);
}
if (result && result.then) {
return result.then(void 0, recover);
}
return result;
}
const {
spawn
} = require("child_process");
const SUPPORTED_FLOW_CLI_VERSIONS = ">=2.0.0";
const SUPPORTED_PRE_RELEASE_MATCHER = "cadence-v1.0.0-preview";
const DEFAULT_HTTP_PORT = 8080;
const DEFAULT_GRPC_PORT = 3569;
const print = {
log: console.log,
service: console.log,
info: console.log,
error: console.error,
warn: console.warn
};
/** Class representing emulator */
class Emulator {
/**
* Create an emulator.
*/
constructor() {
this.initialized = false;
this.logging = false;
this.filters = [];
this.logger = new Logger();
this.execName = "flow";
}
/**
* Set logging flag.
* @param {boolean} logging - whether logs shall be printed
*/
setLogging(logging) {
this.logging = logging;
}
/**
* Log message with a specific type.
* @param {*} message - message to put into log output
* @param {"log"|"error"} type - type of the message to output
*/
log(message, type = "log") {
if (this.logging !== false) {
print[type](message);
}
}
/**
* Start emulator.
* @param {Object} options - Optional parameters to start emulator with
* @param {string} [options.flags] - Extra flags to supply to emulator
* @param {boolean} [options.logging] - Switch to enable/disable logging by default
* @param {number} [options.grpcPort] - Hardcoded GRPC port
* @param {number} [options.restPort] - Hardcoded REST/HTTP port
* @param {number} [options.adminPort] - Hardcoded admin port
* @param {number} [options.debuggerPort] - Hardcoded debug port
* @param {string} [options.execName] - Name of executable for flow-cli
* @returns Promise<*>
*/
start(options = {}) {
try {
const _this = this,
_arguments = arguments;
const {
flags,
logging = false,
signatureCheck = false,
execName
} = options;
if (execName) _this.execName = execName;
// Get version of CLI
return Promise.resolve(getFlowVersion(_this.execName)).then(function (flowVersion) {
const satisfiesVersion = satisfies(flowVersion.raw, SUPPORTED_FLOW_CLI_VERSIONS, {
includePrerelease: true
});
const satisfiesPreRelease = flowVersion.raw.includes(SUPPORTED_PRE_RELEASE_MATCHER);
if (!satisfiesVersion && !satisfiesPreRelease) {
throw new Error(`Unsupported Flow CLI version: ${flowVersion.raw}. Supported versions: ${SUPPORTED_FLOW_CLI_VERSIONS} or pre-releases tagged with ${SUPPORTED_PRE_RELEASE_MATCHER}`);
}
// populate emulator ports with available ports
return Promise.resolve(getAvailablePorts(4)).then(function (ports) {
const [grpcPort, restPort, adminPort, debuggerPort] = ports;
// override ports if specified in options
_this.grpcPort = options.grpcPort || grpcPort;
_this.restPort = options.restPort || restPort;
_this.adminPort = options.adminPort || adminPort;
_this.debuggerPort = options.debuggerPort || debuggerPort;
// Support deprecated start call using static port
if (_arguments.length > 1 || typeof _arguments[0] === "number") {
console.warn(`Calling emulator.start with the port argument is now deprecated in favour of dynamically selected ports and will be removed in future versions of flow-js-testing.
Please refrain from supplying this argument, as using it may cause unintended consequences.
More info: https://github.com/onflow/flow-js-testing/blob/master/TRANSITIONS.md#0001-deprecate-emulatorstart-port-argument`);
[_this.adminPort, options = {}] = _arguments;
const offset = _this.adminPort - DEFAULT_HTTP_PORT;
_this.grpcPort = DEFAULT_GRPC_PORT + offset;
}
// config access node
config$1().put("accessNode.api", `http://localhost:${_this.restPort}`);
_this.logging = logging;
_this.process = spawn(_this.execName, ["emulator", "--verbose", `--log-format=JSON`, `--rest-port=${_this.restPort}`, `--admin-port=${_this.adminPort}`, `--port=${_this.grpcPort}`, `--debugger-port=${_this.debuggerPort}`, `--skip-version-check`, signatureCheck ? "" : "--skip-tx-validation", flags]);
_this.logger.setProcess(_this.process);
// Listen to logger to display logs if enabled
_this.logger.on("*", (level, msg) => {
if (!_this.filters.includes(level)) return;
_this.log(`${level.toUpperCase()}: ${msg}`);
if (msg.includes("Starting") && msg.includes(_this.adminPort)) {
_this.log("EMULATOR IS UP! Listening for events!");
}
});
// Suppress logger warning while waiting for emulator
return Promise.resolve(config$1().put("logger.level", 0)).then(function () {
return new Promise((resolve, reject) => {
const cleanup = success => {
_this.initialized = success;
_this.logger.removeListener(LOGGER_LEVELS.ERROR, listener);
clearInterval(internalId);
if (success) resolve(true);else reject();
};
let internalId;
const checkLiveness = function () {
try {
const _temp = _catch$1(function () {
return Promise.resolve(send(build([getBlock(false)])).then(decode)).then(function () {
// Enable logger after emulator has come online
return Promise.resolve(config$1().put("logger.level", 2)).then(function () {
cleanup(true);
});
});
}, function () {});
return Promise.resolve(_temp && _temp.then ? _temp.then(function () {}) : void 0); // eslint-disable-line no-unused-vars, no-empty
} catch (e) {
return Promise.reject(e);
}
};
internalId = setInterval(checkLiveness, 100);
const listener = msg => {
_this.log(`EMULATOR ERROR: ${msg}`, "error");
cleanup(false);
};
_this.logger.on(LOGGER_LEVELS.ERROR, listener);
_this.process.on("close", code => {
if (_this.filters.includes("service")) {
_this.log(`EMULATOR: process exited with code ${code}`);
}
cleanup(false);
});
});
});
});
});
} catch (e) {
return Promise.reject(e);
}
}
/**
* Clear all log filters.
* @returns void
**/
clearFilters() {
this.filters = [];
}
/**
* Remove specific type of log filter.
* @param {(debug|info|warning)} type - type of message
* @returns void
**/
removeFilter(type) {
this.filters = this.filters.filter(item => item !== type);
}
/**
* Add log filter.
* @param {(debug|info|warning)} type type - type of message
* @returns void
**/
addFilter(type) {
if (!this.filters.includes(type)) {
this.filters.push(type);
}
}
/**
* Stop emulator.
* @returns Promise<*>
*/
stop() {
try {
const _this2 = this;
// eslint-disable-next-line no-undef
return Promise.resolve(new Promise(resolve => {
_this2.process.kill();
setTimeout(() => {
_this2.initialized = false;
resolve(false);
}, 50);
}));
} catch (e) {
return Promise.reject(e);
}
}
}
/** Singleton instance */
var emulator = new Emulator();
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const importManager = function () {
try {
return Promise.resolve(getManagerAddress()).then(function (managerAddress) {
return `import FlowManager from ${managerAddress}`;
});
} catch (e) {
return Promise.reject(e);
}
};
const importExists = (contractName, code) => {
return new RegExp(`import\\s+${contractName}`).test(code);
};
const builtInMethods = function (code) {
try {
let replacedCode = code.replace(/getCurrentBlock\(\).height/g, `FlowManager.getBlockHeight()`).replace(/getCurrentBlock\(\).timestamp/g, `FlowManager.getBlockTimestamp()`);
if (code === replacedCode) return Promise.resolve(code);
let injectedImports = replacedCode;
const _temp = function () {
if (!importExists("FlowManager", replacedCode)) {
return Promise.resolve(importManager()).then(function (imports) {
injectedImports = `
${imports}
${replacedCode}
`;
});
}
}();
return Promise.resolve(_temp && _temp.then ? _temp.then(function () {
return injectedImports;
}) : injectedImports);
} catch (e) {
return Promise.reject(e);
}
};
const applyTransformers = function (code, transformers) {
try {
return Promise.resolve(transformers.reduce(function (acc, transformer) {
try {
return Promise.resolve(acc).then(transformer);
} catch (e) {
return Promise.reject(e);
}
}, code));
} catch (e) {
return Promise.reject(e);
}
};
/*
* Flow JS Testing
*
* Copyright 2020-2021 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function _catch(body, recover) {
try {
var result = body();
} catch (e) {
return recover(e);
}
if (result && result.then) {
return result.then(void 0, recover);
}
return result;
} /**
* Submits transaction to emulator network and waits before it will be sealed.
* Returns transaction result.
* @param {Object} props
* @param {string} [props.name] - Name of Cadence template file
* @param {{string:string}} [props.addressMap={}] - name/address map to use as lookup table for addresses in import statements.
* @param {string} [props.code] - Cadence code of the transaction.
* @param {[any]} [props.args] - array of arguments specified as tupple, where last value is the type of preceding values.
* @param {[string]} [props.signers] - list of signers, who will authorize transaction, specified as array of addresses.
* @returns {Promise<any>}
*/
const extractParameters$1 = ixType => {
return function (params) {
try {
function _temp5(_fcl$config$get) {
function _temp4() {
function _temp2() {
// Resolve default import addresses
return Promise.resolve(getServiceAddress()).then(function (serviceAddress) {
const addressMap = {
...defaultsByName,
...deployedContracts,
FlowManager: serviceAddress
};
// Replace code import addresses
ixCode = replaceImportAddresses(ixCode, addressMap);
// Apply all the necessary transformations to the code
return Promise.resolve(applyTransformers(ixCode, [...ixTransformers, builtInMethods])).then(function (_applyTransformers) {
ixCode = _applyTransformers;
return {
code: ixCode,
signers: ixSigners,
args: ixArgs,
limit: ixLimit
};
});
});
}
//