@1hive/connect-core
Version:
Access and interact with Aragon Organizations and their apps.
176 lines • 9.43 kB
JavaScript
import { isAddress } from '@ethersproject/address';
import { id } from '@ethersproject/hash';
import { ErrorInvalid } from '../../errors';
import { ANY_ENTITY, addressesEqual, includesAddress } from '../address';
import { findAppMethodFromSignature } from '../app';
import { encodeCallScript } from '../callScript';
import { canForward } from '../forwarding';
import { createDirectTransactionForApp, createForwarderTransactionBuilder, buildForwardingFeePreTransactions, } from '../transactions';
/**
* Calculate the forwarding path for a transaction to `destination`
* that invokes `directTransaction`.
*
*/
async function calculateForwardingPath(sender, directTransaction, forwardersWithPermission, forwarders, provider) {
// No forwarders can perform the requested action
if (forwardersWithPermission.length === 0) {
return { path: [], transactions: [] };
}
const createForwarderTransaction = createForwarderTransactionBuilder(sender, directTransaction);
const buildForwardingPath = async (forwarder, script, path, provider) => {
const transaction = createForwarderTransaction(forwarder, script);
// Only apply pretransactions to the first transaction in the path
// as it's the only one that will be executed by the user
try {
const forwardingFeePreTransactions = await buildForwardingFeePreTransactions(transaction, provider);
// If that happens, we give up as we should've been able to perform the action with this
// forwarding path
return {
transactions: [...forwardingFeePreTransactions, transaction],
path: [transaction, ...path],
};
}
catch (err) {
return { path: [], transactions: [] };
}
};
// Check if one of the forwarders that has permission to perform an action
// with `sig` on `address` can forward for us directly
for (const forwarder of forwardersWithPermission) {
const script = encodeCallScript([directTransaction]);
if (await canForward(forwarder, sender, script, provider)) {
return buildForwardingPath(forwarder, script, [directTransaction], provider);
}
}
// Get a list of all forwarders (excluding the forwarders with direct permission)
const filterForwarders = forwarders.filter((forwarder) => !includesAddress(forwardersWithPermission, forwarder));
// Set up the path finding queue
// The queue takes the form of Array<[Array<EthereumTransaction>, Array<String>]>
// In other words: it is an array of tuples, where the first index of the tuple
// is the current path and the second index of the tuple is the
// queue (a list of unexplored forwarder addresses) for that path
const queue = forwardersWithPermission.map((forwarderWithPermission) => {
// TODO: Fix types (type queue = [DirectTransaction[], string[]][])
return [
[
createForwarderTransaction(forwarderWithPermission, encodeCallScript([directTransaction])),
directTransaction,
],
filterForwarders,
];
});
// Find the shortest path via a breadth-first search of forwarder paths.
// We do a breadth-first instead of depth-first search because:
// - We assume that most forwarding paths will be quite short, so it should be faster
// to check in "stages" rather than exhaust single paths
// - We don't currently protect against cycles in the path, and so exhausting single
// paths can be wasteful if they result in dead ends
// TODO: We should find and return multiple paths
do {
const [path, [forwarder, ...nextQueue]] = queue.shift();
// Skip if no forwarder or the path is longer than 5
if (!forwarder || path.length > 5)
continue;
// Get the previous forwarder address
const previousForwarder = path[0].to;
// Encode the previous transaction into an EVM callscript
const script = encodeCallScript([path[0]]);
if (await canForward(previousForwarder, forwarder, script, provider)) {
if (await canForward(forwarder, sender, script, provider)) {
// The previous forwarder can forward a transaction for this forwarder,
// and this forwarder can forward for our address, so we have found a path
return buildForwardingPath(forwarder, script, path, provider);
}
else {
// The previous forwarder can forward a transaction for this forwarder,
// but this forwarder can not forward for our address, so we add it as a
// possible path in the queue for later exploration.
queue.push([
[createForwarderTransaction(forwarder, script), ...path],
// Avoid including the current forwarder as a candidate for the next step
// in the path. Note that this is naive and may result in repeating cycles,
// but the maximum path length would prevent against infinite loops
forwarders.filter((nextForwarder) => nextForwarder !== forwarder),
]);
}
}
// We add the current path on the back of the queue again, but we shorten
// the list of possible forwarders.
queue.push([path, nextQueue]);
} while (queue.length);
return { path: [], transactions: [] };
}
/**
* Calculate the transaction path for a transaction to `destination`
* that invokes `methodSignature` with `params`.
*
*/
export async function calculateTransactionPath(sender, destinationApp, methodSignature, params, apps, provider, finalForwarder //Address of the final forwarder that can perfom the action. Needed for actions that aren't in the ACL but whose execution depends on other factors
) {
var _a;
// The direct transaction we eventually want to perform
const directTransaction = await createDirectTransactionForApp(sender, destinationApp, methodSignature, params);
const finalForwarderProvided = finalForwarder
? isAddress(finalForwarder)
: false;
const method = findAppMethodFromSignature(destinationApp, methodSignature);
if (!method) {
throw new ErrorInvalid(`No method named ${methodSignature} on ${destinationApp.address}`);
}
// We can already assume the user is able to directly invoke the action if:
// - The method has no ACL requirements and no final forwarder was given, or
// - The final forwarder matches the sender
if (((method === null || method === void 0 ? void 0 : method.roles.length) === 0 && !finalForwarderProvided) ||
(finalForwarder && addressesEqual(finalForwarder, sender))) {
try {
return { path: [directTransaction], transactions: [directTransaction] };
}
catch (_) {
// If the direct transaction fails, we give up as we should have been able to
// perform the action directly
return { path: [], transactions: [] };
}
}
// Failing this, attempt transaction pathing algorithm with forwarders
const forwarders = apps
.filter((app) => app.isForwarder === true)
.map((app) => app.address);
let forwardersWithPermission = [];
if (finalForwarderProvided) {
if (finalForwarder) {
if (!includesAddress(forwarders, finalForwarder)) {
// Final forwarder was given, but did not match any available forwarders, so no path
// could be found
return { path: [], transactions: [] };
}
// Only attempt to find path with declared final forwarder; assume the final forwarder
// is able to invoke the action
forwardersWithPermission = [finalForwarder];
}
}
else {
// Find entities with the required permissions
const role = (await destinationApp.roles()).find((role) => role.hash === id(method.roles[0]));
const allowedEntities = ((_a = role === null || role === void 0 ? void 0 : role.permissions) === null || _a === void 0 ? void 0 : _a.filter((permission) => permission.allowed).map((permission) => permission.granteeAddress)) || [];
// No one has access, so of course we don't as well
if (allowedEntities.length === 0) {
return { path: [], transactions: [] };
}
// User may have permission; attempt direct transaction
if (includesAddress(allowedEntities, sender) ||
includesAddress(allowedEntities, ANY_ENTITY)) {
try {
return { path: [directTransaction], transactions: [directTransaction] };
}
catch (_) {
// Don't immediately fail as the permission could have parameters applied that
// disallows the user from the current action and forces us to use the full
// pathing algorithm
}
}
// Find forwarders with permission to perform the action
forwardersWithPermission = forwarders.filter((forwarder) => includesAddress(allowedEntities, forwarder));
}
return calculateForwardingPath(sender, directTransaction, forwardersWithPermission, forwarders, provider);
}
//# sourceMappingURL=calculatePath.js.map