Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #260 from JoinColony/feat/releasing-staged-payments
Browse files Browse the repository at this point in the history
Feat: Releasing staged payments
jakubcolony authored Jul 30, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 0147382 + 9ccb90b commit 8bdecc2
Showing 26 changed files with 4,235 additions and 5,247 deletions.
5,475 changes: 1,845 additions & 3,630 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -25,8 +25,8 @@
},
"homepage": "https://github.com/JoinColony/tx-ingestor",
"dependencies": {
"@colony/colony-js": "^0.0.0-snapshot-69cce09-20240724101901",
"@colony/events": "0.0.0-snapshot-20240329154314",
"@colony/colony-js": "^0.0.0-snapshot-75444d7-20240717133215",
"@colony/events": "^0.0.0-snapshot-75444d7-20240717133215",
"aws-amplify": "^4.3.43",
"cross-fetch": "^4.0.0",
"dotenv": "^16.0.3",
2 changes: 0 additions & 2 deletions src/eventListeners/types.ts
Original file line number Diff line number Diff line change
@@ -11,10 +11,8 @@ export interface BaseEventListener {
export enum EventListenerType {
Colony = 'Colony',
Network = 'Network',
VotingReputation = 'VotingReputation',
Extension = 'Extension',
Token = 'Token',
OneTxPayment = 'OneTxPayment',
}

export interface ColonyEventListener extends BaseEventListener {
1 change: 1 addition & 0 deletions src/graphql/fragments/expenditures.graphql
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ fragment Expenditure on Expenditure {
userStakeId
createdAt
firstEditTransactionHash
type
}

# It is important this fragment contains all the fields of the ExpenditureSlot type
3,470 changes: 2,084 additions & 1,386 deletions src/graphql/generated.ts

Large diffs are not rendered by default.

11 changes: 0 additions & 11 deletions src/graphql/queries/expenditures.graphql
Original file line number Diff line number Diff line change
@@ -17,17 +17,6 @@ query GetExpenditureByNativeFundingPotIdAndColony(
}
}
}

query GetExpenditureMetadata($id: ID!) {
getExpenditureMetadata(id: $id) {
stages {
name
slotId
isReleased
}
}
}

query GetStreamingPayment($id: ID!) {
getStreamingPayment(id: $id) {
id
2 changes: 1 addition & 1 deletion src/handlers/expenditures/expenditureStateChanged.ts
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ export const handleExpenditureStateChanged: EventHandler = async (event) => {
const { storageSlot, value } = event.args;
const keys = event.args[4];

const updatedSlot = decodeUpdatedSlot(expenditure, {
const updatedSlot = decodeUpdatedSlot(expenditure.slots, {
storageSlot,
keys,
value,
68 changes: 44 additions & 24 deletions src/handlers/expenditures/helpers/createEditExpenditureAction.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { AnyColonyClient } from '@colony/colony-js';
import { utils } from 'ethers';
import { mutate, query } from '~amplifyClient';
import { isEqual, omit } from 'lodash';
import { mutate } from '~amplifyClient';
import {
ColonyActionType,
ExpenditureFragment,
ExpenditurePayout,
ExpenditureSlot,
ExpenditureStatus,
GetActionByIdDocument,
GetActionByIdQuery,
GetActionByIdQueryVariables,
ExpenditureType,
UpdateExpenditureDocument,
UpdateExpenditureMutation,
UpdateExpenditureMutationVariables,
} from '~graphql';
import provider from '~provider';
import { ContractEvent, ContractEventsSignatures } from '~types';
import {
checkActionExists,
getExpenditureDatabaseId,
mapLogToContractEvent,
toNumber,
@@ -40,6 +40,7 @@ export class NotEditActionError extends Error {
* This function gets called for both `ExpenditureStateChanged` and `ExpenditurePayoutSet` events
* It determines whether the event is part of an edit action and creates it in the DB
* Otherwise, it returns a result allowing the handler to continue processing as normal
* @TODO: Refactor once multicall limitations are resolved
*/
export const createEditExpenditureAction = async (
event: ContractEvent,
@@ -53,11 +54,18 @@ export const createEditExpenditureAction = async (
ContractEventsSignatures.OneTxPaymentMade,
);

if (!expenditure.firstEditTransactionHash || hasOneTxPaymentEvent) {
if (hasOneTxPaymentEvent) {
throw new NotEditActionError();
}

if (
!expenditure.firstEditTransactionHash ||
expenditure.firstEditTransactionHash === transactionHash
) {
/**
* If expenditure doesn't have `firstEditTransactionHash` set, it means it's the first time
* we see an ExpenditurePayoutSet event, which is normally part of expenditure creation
* Only subsequent ExpenditurePayoutSet events will be considered as edit actions
* If this is the first transaction containing the relevant events, it is
* part of expenditure creation
* Only subsequent events will be considered as edit actions
*/
throw new NotEditActionError();
}
@@ -101,30 +109,53 @@ export const createEditExpenditureAction = async (
* Determine changes to the expenditure after all relevant events have been processed
*/
let updatedSlots: ExpenditureSlot[] = expenditure.slots;
let hasUpdatedSlots = false;
let updatedStatus: ExpenditureStatus | undefined;

let shouldCreateAction = false;

for (const actionEvent of actionEvents) {
if (
actionEvent.signature === ContractEventsSignatures.ExpenditureStateChanged
) {
const { storageSlot, value } = actionEvent.args;
const keys = actionEvent.args[4];

const updatedSlot = decodeUpdatedSlot(expenditure, {
const updatedSlot = decodeUpdatedSlot(updatedSlots, {
storageSlot,
keys,
value,
});

if (updatedSlot) {
const preUpdateSlot = updatedSlots.find(
({ id }) => id === updatedSlot?.id,
);

updatedSlots = getUpdatedExpenditureSlots(
updatedSlots,
updatedSlot.id,
updatedSlot,
);

hasUpdatedSlots = true;
/**
* Special case for staged expenditure
* If the only change was claim delay set to 0, we assume it was a stage release
* Otherwise, we set the flag to create an action
*/
const hasClaimDelayChangedToZero =
preUpdateSlot?.claimDelay !== '0' && updatedSlot.claimDelay === '0';
const hasOtherChanges = !isEqual(
omit(preUpdateSlot, 'claimDelay'),
omit(updatedSlot, 'claimDelay'),
);

if (
expenditure.type !== ExpenditureType.Staged ||
!hasClaimDelayChangedToZero ||
hasOtherChanges
) {
shouldCreateAction = true;
}
}

const decodedStatus = decodeUpdatedStatus(actionEvent);
@@ -163,7 +194,7 @@ export const createEditExpenditureAction = async (
payouts: updatedPayouts,
});

hasUpdatedSlots = true;
shouldCreateAction = true;
}
}

@@ -178,7 +209,7 @@ export const createEditExpenditureAction = async (
},
);

if (hasUpdatedSlots) {
if (shouldCreateAction) {
const { agent: initiatorAddress } = event.args;

await writeActionFromEvent(event, colonyAddress, {
@@ -192,14 +223,3 @@ export const createEditExpenditureAction = async (
});
}
};

const checkActionExists = async (transactionHash: string): Promise<boolean> => {
const existingActionQuery = await query<
GetActionByIdQuery,
GetActionByIdQueryVariables
>(GetActionByIdDocument, {
id: transactionHash,
});

return !!existingActionQuery?.data?.getColonyAction;
};
44 changes: 24 additions & 20 deletions src/handlers/expenditures/helpers/decodeSetExpenditureState.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { BigNumber, BigNumberish, utils } from 'ethers';

import {
ExpenditureFragment,
ExpenditureSlot,
ExpenditureSlotFragment as ExpenditureSlot,
ExpenditureStatus,
} from '~graphql';
import { toNumber } from '~utils';
@@ -33,18 +32,17 @@ interface SetExpenditureStateParams {
* If there were no changes to the expenditure slot, it returns undefined
*/
export const decodeUpdatedSlot = (
expenditure: ExpenditureFragment,
expenditureSlots: ExpenditureSlot[],
params: SetExpenditureStateParams,
): ExpenditureSlot | undefined => {
const { storageSlot, value, keys } = params;
// The unfortunate naming of the `keys` property means we have to access it like so

let updatedSlot: ExpenditureSlot | undefined;

if (BigNumber.from(storageSlot).eq(EXPENDITURESLOTS_SLOT)) {
const slotId = toNumber(keys[0]);

const existingSlot = expenditure.slots.find(
const existingSlot = expenditureSlots.find(
({ id }) => id === toNumber(keys[0]),
);

@@ -53,27 +51,33 @@ export const decodeUpdatedSlot = (
.decode(['address'], value)
.toString();

updatedSlot = {
...existingSlot,
id: slotId,
recipientAddress,
};
if (recipientAddress !== existingSlot?.recipientAddress) {
updatedSlot = {
...existingSlot,
id: slotId,
recipientAddress,
};
}
} else if (keys[1] === EXPENDITURESLOT_CLAIMDELAY) {
const claimDelay = BigNumber.from(value).toString();

updatedSlot = {
...existingSlot,
id: slotId,
claimDelay,
};
if (claimDelay !== existingSlot?.claimDelay) {
updatedSlot = {
...existingSlot,
id: slotId,
claimDelay,
};
}
} else if (keys[1] === EXPENDITURESLOT_PAYOUTMODIFIER) {
const payoutModifier = toNumber(value);

updatedSlot = {
...existingSlot,
id: slotId,
payoutModifier,
};
if (payoutModifier !== existingSlot?.payoutModifier) {
updatedSlot = {
...existingSlot,
id: slotId,
payoutModifier,
};
}
}
}

111 changes: 58 additions & 53 deletions src/handlers/expenditures/stagedPaymentReleased.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,85 @@
import { mutate, query } from '~amplifyClient';
import { utils } from 'ethers';
import { ExtensionEventListener } from '~eventListeners';
import { ColonyActionType } from '~graphql';
import { getInterfaceByListener } from '~interfaces';
import provider from '~provider';
import { ContractEventsSignatures, EventHandler } from '~types';
import {
GetExpenditureMetadataDocument,
GetExpenditureMetadataQuery,
GetExpenditureMetadataQueryVariables,
UpdateExpenditureMetadataDocument,
UpdateExpenditureMetadataMutation,
UpdateExpenditureMetadataMutationVariables,
} from '~graphql';
import { EventHandler } from '~types';
import {
checkActionExists,
getCachedColonyClient,
getExpenditureDatabaseId,
insertAtIndex,
output,
mapLogToContractEvent,
toNumber,
verbose,
writeActionFromEvent,
} from '~utils';

export const handleStagedPaymentReleased: EventHandler = async (
event,
listener,
) => {
const { expenditureId, slot } = event.args;
/**
* @NOTE: The UI uses multicall to potentially release multiple slots in one transaction
* Since we only want to create a single action, we will get all slot release events
* the first time we see this event and skip the subsequent ones
*
* Something to refactor once https://github.com/JoinColony/colonyCDapp/issues/2317 is implemented
*/
const { transactionHash, blockNumber } = event;
const actionExists = await checkActionExists(transactionHash);
if (actionExists) {
return;
}

const { expenditureId, agent: initiatorAddress } = event.args;
const convertedExpenditureId = toNumber(expenditureId);
const convertedSlot = toNumber(slot);

const { colonyAddress } = listener as ExtensionEventListener;
const colonyClient = await getCachedColonyClient(colonyAddress);
if (!colonyClient) {
return;
}

const databaseId = getExpenditureDatabaseId(
colonyAddress,
convertedExpenditureId,
);
const releasedSlotIds = [];

const response = await query<
GetExpenditureMetadataQuery,
GetExpenditureMetadataQueryVariables
>(GetExpenditureMetadataDocument, {
id: databaseId,
const logs = await provider.getLogs({
fromBlock: blockNumber,
toBlock: blockNumber,
topics: [utils.id(ContractEventsSignatures.StagedPaymentReleased)],
});
const metadata = response?.data?.getExpenditureMetadata;

if (!metadata?.stages) {
output(
`Could not find stages data for expenditure with ID: ${databaseId}. This is a bug and needs investigating.`,
);
const iface = getInterfaceByListener(listener);
if (!iface) {
return;
}

const existingStageIndex = metadata.stages.findIndex(
(stage) => stage.slotId === convertedSlot,
);
const existingStage = metadata.stages[existingStageIndex];
for (const log of logs) {
const mappedEvent = await mapLogToContractEvent(log, iface);

// If the stage doesn't exist or it's been already set to released, we don't need to do anything
if (!existingStage || existingStage.isReleased) {
return;
if (!mappedEvent) {
continue;
}

// Check the expenditure ID matches the one in the first event
const eventExpenditureId = toNumber(mappedEvent.args.expenditureId);

if (eventExpenditureId === convertedExpenditureId) {
releasedSlotIds.push(toNumber(mappedEvent.args.slot));
}
}

const updatedStage = {
...existingStage,
isReleased: true,
};
const updatedStages = insertAtIndex(
metadata.stages,
existingStageIndex,
updatedStage,
const databaseId = getExpenditureDatabaseId(
colonyAddress,
convertedExpenditureId,
);

verbose(`Stage released in expenditure with ID: ${databaseId}`);
if (!releasedSlotIds.length) {
return;
}

await mutate<
UpdateExpenditureMetadataMutation,
UpdateExpenditureMetadataMutationVariables
>(UpdateExpenditureMetadataDocument, {
input: {
id: databaseId,
stages: updatedStages,
},
await writeActionFromEvent(event, colonyAddress, {
type: ColonyActionType.ReleaseStagedPayments,
initiatorAddress,
expenditureId: databaseId,
expenditureSlotIds: releasedSlotIds,
});
};
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ export default async (
gasEstimate: BigNumber,
): Promise<void> => {
const { args } = event;
const [, , , , expenditureId] = actionArgs;
const [, , , , expenditureId, slotId] = actionArgs;
const [, , domainId] = args;

await createMotionInDB(colonyAddress, event, {
@@ -28,5 +28,6 @@ export default async (
colonyAddress,
toNumber(expenditureId),
),
expenditureSlotIds: [toNumber(slotId)],
});
};
57 changes: 22 additions & 35 deletions src/handlers/motions/motionCreated/handlers/multicall/multicall.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Result, TransactionDescription } from 'ethers/lib/utils';
import { TransactionDescription } from 'ethers/lib/utils';
import { BigNumber, utils } from 'ethers';
import { getCachedColonyClient, output } from '~utils';
import { ContractEvent, ContractMethodSignatures } from '~types';
import { AnyColonyClient } from '@colony/colony-js';
import { getCachedColonyClient, output, parseFunctionData } from '~utils';
import { ContractEvent } from '~types';
import { multicallHandlers } from './multicallHandlers';

/**
@@ -11,38 +10,19 @@ import { multicallHandlers } from './multicallHandlers';
* It should be refactored as part of https://github.com/JoinColony/colonyCDapp/issues/2317
*/

// List all supported multicall functions
export const supportedMulticallFunctions: ContractMethodSignatures[] = [
ContractMethodSignatures.MoveFundsBetweenPots,
ContractMethodSignatures.SetExpenditureState,
ContractMethodSignatures.SetExpenditurePayout,
];

export interface DecodedFunction {
functionSignature: ContractMethodSignatures;
args: Result;
}

const decodeFunctions = (
encodedFunctions: utils.Result,
colonyClient: AnyColonyClient,
): DecodedFunction[] => {
const decodedFunctions: DecodedFunction[] = [];
encodedFunctions: string[],
interfaces: utils.Interface[],
): TransactionDescription[] => {
const decodedFunctions: TransactionDescription[] = [];
for (const functionCall of encodedFunctions) {
supportedMulticallFunctions.forEach((fragment) => {
try {
const decodedArgs = colonyClient.interface.decodeFunctionData(
fragment,
functionCall,
);
decodedFunctions.push({
functionSignature: fragment,
args: decodedArgs,
});
} catch {
// silent. We are expecting all but one of the fragments to error for each arg.
}
});
const parsedFunction = parseFunctionData(functionCall, interfaces);
if (!parsedFunction) {
output(`Failed to parse multicall function: ${functionCall}`);
continue;
}

decodedFunctions.push(parsedFunction);
}

return decodedFunctions;
@@ -53,6 +33,7 @@ export const handleMulticallMotion = async (
event: ContractEvent,
parsedAction: TransactionDescription,
gasEstimate: BigNumber,
interfaces: utils.Interface[],
): Promise<void> => {
const colonyClient = await getCachedColonyClient(colonyAddress);

@@ -70,10 +51,16 @@ export const handleMulticallMotion = async (
.toString();

// We need to determine which multicallMotion this is and pass it to the appropriate handler
const decodedFunctions = decodeFunctions(encodedFunctions, colonyClient);
const decodedFunctions = decodeFunctions(encodedFunctions, interfaces);

if (decodedFunctions.length === 0) {
return;
}

for (const [validator, handler] of multicallHandlers) {
if (validator({ decodedFunctions })) {
console.log({ decodedFunctions });
console.log('Multicall handler found: ', handler.name);
handler({
colonyAddress,
event,
Original file line number Diff line number Diff line change
@@ -21,7 +21,9 @@ export const isEditLockedExpenditureMotion: MulticallValidator = ({
ContractMethodSignatures.SetExpenditureState,
];

return signaturesToMatch.includes(decodedFunctions[0].functionSignature);
return signaturesToMatch.includes(
decodedFunctions[0].signature as ContractMethodSignatures,
);
};

export const editLockedExpenditureMotionHandler: MulticallHandler = async ({
@@ -55,7 +57,7 @@ export const editLockedExpenditureMotionHandler: MulticallHandler = async ({
}

if (
decodedFunction.functionSignature ===
decodedFunction.signature ===
ContractMethodSignatures.SetExpenditurePayout
) {
const [, , , slotId, tokenAddress, amountWithFee] = decodedFunction.args;
@@ -82,12 +84,11 @@ export const editLockedExpenditureMotionHandler: MulticallHandler = async ({
payouts: updatedPayouts,
});
} else if (
decodedFunction.functionSignature ===
ContractMethodSignatures.SetExpenditureState
decodedFunction.signature === ContractMethodSignatures.SetExpenditureState
) {
const [, , , storageSlot, , keys, value] = decodedFunction.args;
if (storageSlot.eq(EXPENDITURESLOTS_SLOT)) {
const updatedSlot = decodeUpdatedSlot(expenditure, {
const updatedSlot = decodeUpdatedSlot(updatedSlots, {
storageSlot,
keys,
value,
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ export const isFundExpenditureMotion: MulticallValidator = ({
}) => {
return decodedFunctions.every(
(decodedFunction) =>
decodedFunction.functionSignature ===
decodedFunction.signature ===
ContractMethodSignatures.MoveFundsBetweenPots,
);
};
@@ -51,7 +51,7 @@ export const fundExpenditureMotionHandler: MulticallHandler = async ({

for (const decodedFunction of decodedFunctions) {
if (
decodedFunction.functionSignature !==
decodedFunction.signature !==
ContractMethodSignatures.MoveFundsBetweenPots ||
decodedFunction.args._toPot !== targetPotId
) {
Original file line number Diff line number Diff line change
@@ -6,10 +6,15 @@ import {
fundExpenditureMotionHandler,
isFundExpenditureMotion,
} from './fundExpenditureMotion';
import {
isReleaseStagedPaymentsMotion,
releaseStagedPaymentsMotionHandler,
} from './releaseStagedPaymentsMotion';
import { MulticallHandler, MulticallValidator } from './types';

export const multicallHandlers: Array<[MulticallValidator, MulticallHandler]> =
[
[isFundExpenditureMotion, fundExpenditureMotionHandler],
[isEditLockedExpenditureMotion, editLockedExpenditureMotionHandler],
[isReleaseStagedPaymentsMotion, releaseStagedPaymentsMotionHandler],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ColonyActionType } from '~graphql';
import { createMotionInDB } from '~handlers/motions/motionCreated/helpers';
import { ContractMethodSignatures } from '~types';
import {
getDomainDatabaseId,
getExpenditureDatabaseId,
toNumber,
} from '~utils';
import { MulticallHandler, MulticallValidator } from './types';

export const isReleaseStagedPaymentsMotion: MulticallValidator = ({
decodedFunctions,
}) => {
return decodedFunctions.every(
(decodedFunction) =>
decodedFunction.signature ===
ContractMethodSignatures.ReleaseStagedPaymentViaArbitration,
);
};

export const releaseStagedPaymentsMotionHandler: MulticallHandler = async ({
colonyAddress,
event,
decodedFunctions,
gasEstimate,
}) => {
const [, , domainId] = event.args;

// @NOTE: This handler assumes the multicall is releasing stages of a single expenditure
const expenditureId = decodedFunctions[0]?.args[4];
const slotIds = decodedFunctions.map((decodedFunction) =>
toNumber(decodedFunction.args[5]),
);

await createMotionInDB(colonyAddress, event, {
type: ColonyActionType.ReleaseStagedPaymentsMotion,
fromDomainId: colonyAddress
? getDomainDatabaseId(colonyAddress, domainId)
: undefined,
gasEstimate: gasEstimate.toString(),
expenditureId: getExpenditureDatabaseId(
colonyAddress,
toNumber(expenditureId),
),
expenditureSlotIds: slotIds,
});
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { TransactionDescription } from 'ethers/lib/utils';
import { ContractEvent } from '~types';
import { DecodedFunction } from '../multicall';

interface MulticallHandlerParams {
colonyAddress: string;
event: ContractEvent;
gasEstimate: string;
decodedFunctions: DecodedFunction[];
decodedFunctions: TransactionDescription[];
}

export type MulticallHandler = ({
@@ -17,5 +17,5 @@ export type MulticallHandler = ({
export type MulticallValidator = ({
decodedFunctions,
}: {
decodedFunctions: DecodedFunction[];
decodedFunctions: TransactionDescription[];
}) => boolean;
51 changes: 12 additions & 39 deletions src/handlers/motions/motionCreated/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { BigNumber } from 'ethers';
import { BigNumber, utils } from 'ethers';
import { TransactionDescription } from 'ethers/lib/utils';
import {
AnyColonyClient,
AnyOneTxPaymentClient,
AnyStakedExpenditureClient,
AnyStagedExpenditureClient,
AnyVotingReputationClient,
} from '@colony/colony-js';
import { AnyVotingReputationClient } from '@colony/colony-js';

import { ColonyOperations, ContractEvent, MotionEvents } from '~types';
import { getDomainDatabaseId, getVotingClient, verbose } from '~utils';
import { getDomainDatabaseId, getVotingClient } from '~utils';
import { GraphQLFnReturn, mutate } from '~amplifyClient';
import {
ColonyMotion,
@@ -35,45 +29,23 @@ import {
getUserMinStake,
getMessageKey,
} from '../helpers';
import { parseFunctionData } from '~utils/parseFunction';

export interface SimpleTransactionDescription {
name: ColonyOperations.SimpleDecision;
}

interface MotionActionClients {
colonyClient?: AnyColonyClient | null;
oneTxPaymentClient?: AnyOneTxPaymentClient | null;
stakedExpenditureClient?: AnyStakedExpenditureClient | null;
stagedExpenditureClient?: AnyStagedExpenditureClient | null;
}

export const parseAction = (
export const parseMotionAction = (
action: string,
clients: MotionActionClients,
): TransactionDescription | SimpleTransactionDescription | undefined => {
interfaces: utils.Interface[],
): TransactionDescription | SimpleTransactionDescription | null => {
if (action === SIMPLE_DECISIONS_ACTION_CODE) {
return {
name: ColonyOperations.SimpleDecision,
};
}

for (const key in clients) {
const client = clients[key as keyof MotionActionClients];
if (!client) {
continue;
}
// Return the first time a client can successfully parse the motion
try {
return client.interface.parseTransaction({
data: action,
});
} catch {
continue;
}
}

verbose(`Unable to parse ${action}`);
return undefined;
return parseFunctionData(action, interfaces);
};

interface GetMotionDataArgs {
@@ -239,7 +211,7 @@ type MotionFields = Omit<
Pick<
CreateColonyMotionInput,
| 'gasEstimate'
| 'expenditureSlotId'
| 'expenditureSlotIds'
| 'editedExpenditureSlots'
| 'expenditureFunding'
>;
@@ -258,7 +230,7 @@ export const createMotionInDB = async (
} = event;
const {
gasEstimate,
expenditureSlotId,
expenditureSlotIds,
editedExpenditureSlots,
expenditureFunding,
...actionFields
@@ -300,6 +272,7 @@ export const createMotionInDB = async (
blockNumber,
rootHash,
isMotionFinalization: false,
expenditureSlotIds,
...actionFields,
};

@@ -308,7 +281,7 @@ export const createMotionInDB = async (
...motionData,
gasEstimate,
expenditureId: actionFields.expenditureId,
expenditureSlotId,
expenditureSlotIds,
editedExpenditureSlots,
expenditureFunding,
}),
30 changes: 22 additions & 8 deletions src/handlers/motions/motionCreated/motionCreated.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber, constants } from 'ethers';
import { BigNumber, constants, utils } from 'ethers';
import { StaticJsonRpcProvider } from '@ethersproject/providers';

import { ColonyOperations, EventHandler } from '~types';
@@ -9,8 +9,9 @@ import {
getOneTxPaymentClient,
getVotingClient,
verbose,
output,
} from '~utils';
import { SimpleTransactionDescription, parseAction } from './helpers';
import { SimpleTransactionDescription, parseMotionAction } from './helpers';
import {
handleEditDomainMotion,
handleAddDomainMotion,
@@ -63,12 +64,24 @@ export const handleMotionCreated: EventHandler = async (
const motion = await votingReputationClient.getMotion(motionId, {
blockTag: blockNumber,
});
const parsedAction = parseAction(motion.action, {
colonyClient,
oneTxPaymentClient,
stakedExpenditureClient,
stagedExpenditureClient,
});

/**
* @NOTE: This is not good, we should use ABIs from @colony/abis instead.
* It would avoid having to make network calls each time the motion is created
*/
const interfaces = [
colonyClient.interface,
oneTxPaymentClient?.interface,
stakedExpenditureClient?.interface,
stagedExpenditureClient?.interface,
].filter(Boolean) as utils.Interface[]; // Casting seems necessary as TS does not pick up the .filter()

const parsedAction = parseMotionAction(motion.action, interfaces);

if (!parsedAction) {
output(`Failed to parse motion action: ${motion.action}`);
return;
}

let gasEstimate: BigNumber;

@@ -238,6 +251,7 @@ export const handleMotionCreated: EventHandler = async (
event,
parsedAction,
gasEstimate,
interfaces,
);
break;
}
30 changes: 8 additions & 22 deletions src/handlers/motions/motionFinalized/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { BigNumber } from 'ethers';
import { TransactionDescription } from 'ethers/lib/utils';
import { BlockTag } from '@ethersproject/abstract-provider';
import { AnyVotingReputationClient, Extension } from '@colony/colony-js';
import { AnyVotingReputationClient } from '@colony/colony-js';

import { ColonyOperations, MotionVote } from '~types';
import {
getCachedColonyClient,
getColonyFromDB,
getDomainDatabaseId,
getStakedExpenditureClient,
getStagedExpenditureClient,
output,
parseFunctionData,
} from '~utils';
import { query, mutate } from '~amplifyClient';
import {
@@ -32,7 +31,6 @@ import {
UpdateColonyMetadataDocument,
UpdateDomainMetadataDocument,
} from '~graphql';
import { parseAction } from '../motionCreated/helpers';

export const getStakerReward = async (
motionId: string,
@@ -248,23 +246,13 @@ export const linkPendingMetadata = async (
finalizedMotion: ColonyMotion,
): Promise<void> => {
const colonyClient = await getCachedColonyClient(colonyAddress);
const oneTxPaymentClient =
(await colonyClient?.getExtensionClient(Extension.OneTxPayment)) ?? null;
const stakedExpenditureClient = await getStakedExpenditureClient(
colonyAddress,
);

const stagedExpenditureClient = await getStagedExpenditureClient(
colonyAddress,
);

const parsedAction = parseAction(action, {
colonyClient,
oneTxPaymentClient,
stakedExpenditureClient,
stagedExpenditureClient,
});

if (!colonyClient) {
return;
}

// @NOTE: We only care about handful of events from Colony contract so not passing all the interfaces
const parsedAction = parseFunctionData(action, [colonyClient?.interface]);
if (!parsedAction) {
return;
}
@@ -275,7 +263,6 @@ export const linkPendingMetadata = async (
parsedAction.name === ColonyOperations.EditDomain;
const isMotionEditingAColony =
parsedAction.name === ColonyOperations.EditColony;

if (
isMotionAddingADomain ||
isMotionEditingADomain ||
@@ -288,7 +275,6 @@ export const linkPendingMetadata = async (
>(GetColonyActionByMotionIdDocument, {
motionId: finalizedMotion.id,
})) ?? {};

const colonyAction = data?.getColonyActionByMotionId?.items;
/*
* pendingDomainMetadata is a motion data prop that we use to store the metadata of a Domain that COULD be created/edited
4 changes: 2 additions & 2 deletions src/types/events.ts
Original file line number Diff line number Diff line change
@@ -90,8 +90,8 @@ export enum ContractEventsSignatures {
StakeFractionSet = 'StakeFractionSet(address,uint256)',

// Staged Expenditures
ExpenditureMadeStaged = 'ExpenditureMadeStaged(uint256,bool)',
StagedPaymentReleased = 'StagedPaymentReleased(uint256,uint256)',
ExpenditureMadeStaged = 'ExpenditureMadeStaged(address,uint256,bool)',
StagedPaymentReleased = 'StagedPaymentReleased(address,uint256,uint256)',

// Streaming Payments
StreamingPaymentCreated = 'StreamingPaymentCreated(address,uint256)',
1 change: 1 addition & 0 deletions src/types/methods.ts
Original file line number Diff line number Diff line change
@@ -5,4 +5,5 @@ export enum ContractMethodSignatures {
MoveFundsBetweenPots = 'moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)',
SetExpenditureState = 'setExpenditureState',
SetExpenditurePayout = 'setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)',
ReleaseStagedPaymentViaArbitration = 'releaseStagedPaymentViaArbitration(uint256,uint256,uint256,uint256,uint256,uint256,address[])',
}
2 changes: 1 addition & 1 deletion src/types/motions.ts
Original file line number Diff line number Diff line change
@@ -68,7 +68,7 @@ export const motionNameMapping: { [key: string]: ColonyActionType } = {
[ColonyOperations.SetExpenditureState]:
ColonyActionType.SetExpenditureStateMotion,
[ColonyOperations.ReleaseStagedPaymentViaArbitration]:
ColonyActionType.ReleaseStagedPaymentMotion,
ColonyActionType.ReleaseStagedPaymentsMotion,
};

export enum MotionSide {
19 changes: 19 additions & 0 deletions src/utils/actionExists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { query } from '~amplifyClient';
import {
GetActionByIdDocument,
GetActionByIdQuery,
GetActionByIdQueryVariables,
} from '~graphql';

export const checkActionExists = async (
transactionHash: string,
): Promise<boolean> => {
const existingActionQuery = await query<
GetActionByIdQuery,
GetActionByIdQueryVariables
>(GetActionByIdDocument, {
id: transactionHash,
});

return !!existingActionQuery?.data?.getColonyAction;
};
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -17,3 +17,5 @@ export * from './isColonyAddress';
export * from './fundsClaims';
export * from './metadataDelta';
export * from './transactionHasEvent';
export * from './parseFunction';
export * from './actionExists';
22 changes: 22 additions & 0 deletions src/utils/parseFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { utils } from 'ethers';
import { TransactionDescription } from 'ethers/lib/utils';

/**
* Helper attempting to decode function data by trying to parse it with different contract ABIs
* until it finds one that does not throw an error
* @TODO: This should be refactored to use ABIs from @colony/abis
*/
export const parseFunctionData = (
functionData: string,
interfaces: utils.Interface[],
): TransactionDescription | null => {
for (const iface of interfaces) {
try {
const parsed = iface.parseTransaction({ data: functionData });
return parsed;
} catch (e) {
// Ignore
}
}
return null;
};

0 comments on commit 8bdecc2

Please sign in to comment.