Skip to content

Commit

Permalink
feat: getPersonas and callAsPersona (#98)
Browse files Browse the repository at this point in the history
* feat: getPersonas and callAsPersona

* fix: clean up tests

* fix: move infura key to env variable

* fix: clean up tests

* fix: more clean up
  • Loading branch information
DanThomp507 authored Dec 13, 2022
1 parent 6b0c0a1 commit ae27fec
Show file tree
Hide file tree
Showing 20 changed files with 547 additions and 130 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
.nvmrc
dist
env.json
.env
2 changes: 1 addition & 1 deletion docs/assets/search.js

Large diffs are not rendered by default.

82 changes: 48 additions & 34 deletions docs/classes/Pod.html

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions docs/classes/Proposal.html

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions docs/modules.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions scripts/approve-superproposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ async function main() {
let data;
try {
if (isMember) {
data = superPod.populateBurn(dummyAccount);
data = superPod.burnMember(dummyAccount);
} else {
data = superPod.populateMint(dummyAccount);
data = superPod.mintMember(dummyAccount);
}
await superPod.propose(data, subPod.safe);
} catch (err) {
Expand Down
26 changes: 24 additions & 2 deletions scripts/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,31 @@ const multiPodInput = [
async function main() {
const { walletOne } = setup(5);

const pod = await getPod(25);

const pod = await getPod('balloon.pod.eth');
console.log('pod', pod);
const admin = pod.admin;
console.log(admin, 'ADMIN');
const adminPod = await getPod(pod.admin);
console.log(adminPod, 'ADMIN POD');
const personas = await pod.getPersonas('0x3d76351819c5b188C0f7447fe7D1C7AA3e0325C0');
console.log('personas', personas);

// await pod.callAsPersona(
// pod.burnMember,
// ['0x8d2d96d31e86843e9B71E635beA331f9b1016055'],
// personas[0]
// );

// const pod = await getPod('balloon.pod.eth');
// console.log('pod', pod);
// const personas = await pod.getPersonas('0x3d76351819c5b188C0f7447fe7D1C7AA3e0325C0');
// console.log('personas', personas);

// await pod.callAsPersona(
// pod.mintMember,
// ['0x8d2d96d31e86843e9B71E635beA331f9b1016055'],
// personas[2],
// );
}

main();
5 changes: 1 addition & 4 deletions scripts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ export function sleep(ms) {

export function setup(network = 4) {
const networkName = network === 1 ? 'mainnet' : 'goerli';
const provider = new ethers.providers.InfuraProvider(
networkName,
'69ecf3b10bc24c6a972972666fe950c8',
);
const provider = new ethers.providers.InfuraProvider(networkName, process.env.INFURA_KEY);
init({ provider, network });

// Get two accounts
Expand Down
118 changes: 116 additions & 2 deletions src/Pod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ethers } from 'ethers';
import ENS, { labelhash } from '@ensdomains/ensjs';
import { getControllerByAddress, getDeployment } from '@orcaprotocol/contracts';
import axios from 'axios';
import { getPersonas } from './pod-utils';
import { config } from './config';
import { getPodFetchersByAddressOrEns, getPodFetchersById } from './fetchers';
import {
Expand Down Expand Up @@ -187,6 +188,12 @@ export default class Pod {
*/
memberEOAs?: string[];

/**
* @ignore
* @property Admin pod object
*/
adminPod?: Pod;

/**
* @ignore
* @property Array of Pod objects for any member pods
Expand Down Expand Up @@ -326,7 +333,9 @@ export default class Pod {
}

// All safe transactions that have a given nonce.
const safeTransactions = await getSafeTransactionsBySafe(this.safe, { nonce });
const safeTransactions = await getSafeTransactionsBySafe(this.safe, {
nonce,
});
if (safeTransactions.length === 0) throw new Error('Could not find a related safe transaction');
if (safeTransactions.length === 1) return new Proposal(this, this.nonce, safeTransactions[0]);
if (safeTransactions.length === 2)
Expand Down Expand Up @@ -418,6 +427,105 @@ export default class Pod {
return this.memberPods;
};

/**
* Calls a Pod method via a persona.
* The persona object can be fetched by using `Pod.getPersonas(address)`
* E.g.,
* ```
* callAsPersona(
* pod.mintMember,
* [newMember],
* { type: 'admin', address: userAddress },
* signer,
* )
* ```
* The sender must be a signer in the admin case, or the address of the sender.
* The sender must be a member of the relevant pod.
*
* @param method
* @param args
* @param persona
* @param sender
* @returns
*/
callAsPersona = async (
method: any,
args: Array<any>,
persona: { type: string; address: string },
sender?: ethers.Signer | string,
) => {
const senderAddress = sender instanceof ethers.Signer ? await sender.getAddress() : sender;
switch (persona.type) {
case 'admin':
// for the admin case, the sender must be a signer
if (!sender) throw new Error(`Expected sender to be signer, but received ${sender}`);
args.push(sender);
// admin personas do not require proposals
return method.apply(this, args);
case 'member':
// member personas require proposals to be created
return this.propose(await method.apply(this, args), persona.address);
case 'adminPodMember': {
let adminPod;
// if an admin pod object exists, use that
if (this.adminPod) adminPod = this.adminPod;
else {
// create a new admin pod object with admin address
adminPod = await new Pod(this.admin);
this.adminPod = adminPod;
}
let result;
try {
// admin pods require proposal creation
result = await adminPod.propose(await method.apply(this, args), senderAddress);
} catch (err) {
// Make the error message more specific
if (err.message.includes('Sender must be a member of')) {
throw new Error('Sender must be a member of the admin pod');
}
}
return result;
}
case 'subPodMember': {
// create a new pod object with sub pod address
const subPod = await new Pod(persona.address);

// check to see if sub pod is member of the pod
const isSubPodMember = await this.isMember(subPod.safe);

if (!isSubPodMember) throw new Error('Sub pod is not a member of this pod');
let result;
try {
// sub pods require proposal creation
result = await subPod.propose(
await this.propose(await method.apply(this, args), subPod.safe),
senderAddress,
);
} catch (err) {
// Make the error message more specific
if (err.message.includes('Sender must be a member of')) {
throw new Error('Sender must be a member of the sub pod');
}
}
return result;
}
default:
throw new Error(`${persona.type} is not a valid persona`);
}
};

/**
* Fetches all personas for a given address related to this pod.
* All personas return as an object indicating the type of the persona and the address of the persona.
* For members and admins, the persona address is the user's address.
* For admin pods and sub pods, the persona address is the pod address.
* @param address
*/
getPersonas(address: string) {
// Function declared separately out for test/mocking purposes.
return getPersonas(this, address);
}

/**
* Checks if user is a member of this pod
* @param address
Expand All @@ -444,7 +552,13 @@ export default class Pod {
isAdminPodMember = async (address: string): Promise<boolean> => {
const checkedAddress = checkAddress(address);
if (!this.admin) return false;
const adminPod = await new Pod(this.admin);
let adminPod;
if (this.adminPod) adminPod = this.adminPod;
else {
adminPod = await new Pod(this.admin);
this.adminPod = adminPod;
}

if (!adminPod) return false;
return adminPod.isMember(checkedAddress);
};
Expand Down
21 changes: 21 additions & 0 deletions src/pod-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type Pod from './Pod';

// Functions are put here so that they can be mocked for testing purposes.

// eslint-disable-next-line import/prefer-default-export
export async function getPersonas(
pod: Pod,
address: string,
): Promise<Array<{ type: string; address: string }>> {
const personas = [];
if (pod.isAdmin(address)) personas.push({ type: 'admin', address });
if (await pod.isAdminPodMember(address))
personas.push({ type: 'adminPodMember', address: pod.admin });
if (await pod.isMember(address)) personas.push({ type: 'member', address });

const memberSubPods = await pod.getSubPodsByMember(address);
memberSubPods.forEach(subPod => {
personas.push({ type: 'subPodMember', address: subPod.safe });
});
return personas;
}
2 changes: 1 addition & 1 deletion test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ethers } from 'ethers';
import { init, config } from '../src';

const provider = new ethers.providers.InfuraProvider('mainnet', {
infura: '69ecf3b10bc24c6a972972666fe950c8',
infura: process.env.INFURA_KEY,
});

test('init should throw if it receives something other than 1 or 5', async () => {
Expand Down
9 changes: 7 additions & 2 deletions test/pod-create.test.ts → test/pod.create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { userAddress, userAddress2, orcanautAddress } from './fixtures';

beforeAll(async () => {
const provider = new ethers.providers.InfuraProvider('goerli', {
infura: '69ecf3b10bc24c6a972972666fe950c8',
infura: process.env.INFURA_KEY,
});
init({ provider, network: 5 });
});
Expand All @@ -41,7 +41,12 @@ describe('pod create', () => {
createPod: mockCreate,
});
await createPod(
{ members: [userAddress, userAddress2], admin: userAddress, threshold: 1, name: 'test' },
{
members: [userAddress, userAddress2],
admin: userAddress,
threshold: 1,
name: 'test',
},
mockSigner,
);
expect(mockCreate).toHaveBeenCalledWith(
Expand Down
Loading

0 comments on commit ae27fec

Please sign in to comment.