Skip to content

Commit

Permalink
feat: store per-treasury execution on proposals (#475)
Browse files Browse the repository at this point in the history
* feat: store per-treasury execution on proposals

oSnap proposals can have separate executions for multiple treasuries
This commit will display those in the UI as separate intities,
linked to specific treasury.

* feat: add support for safeImport transaction type
  • Loading branch information
Sekhmet authored Jul 12, 2024
1 parent 67cf130 commit aa1e297
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 45 deletions.
43 changes: 39 additions & 4 deletions apps/ui/src/components/ProposalExecutionsList.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
<script setup lang="ts">
import { Transaction as TransactionType } from '@/types';
import { sanitizeUrl, shorten } from '@/helpers/utils';
import { getNetwork } from '@/networks';
import { NetworkID, ProposalExecution } from '@/types';
defineProps<{ txs: TransactionType[] }>();
defineProps<{ executions: ProposalExecution[] }>();
function getTreasuryExplorerUrl(networkId: NetworkID, safeAddress: string) {
if (!safeAddress) return null;
try {
const network = getNetwork(networkId);
const url = network.helpers.getExplorerUrl(safeAddress, 'address');
return sanitizeUrl(url);
} catch (e) {
return null;
}
}
</script>

<template>
<div v-if="txs.length > 0" class="x-block !border-x rounded-lg">
<div
v-for="execution in executions"
:key="`${execution.networkId}:${execution.safeAddress}`"
class="x-block !border-x rounded-lg mb-3 last:mb-0"
>
<a
:href="getTreasuryExplorerUrl(execution.networkId, execution.safeAddress) || undefined"
target="_blank"
class="flex justify-between items-center px-4 py-3 border-b"
:class="{
'pointer-events-none': !getTreasuryExplorerUrl(execution.networkId, execution.safeAddress)
}"
>
<UiBadgeNetwork :id="execution.networkId" class="mr-3">
<UiStamp :id="execution.safeAddress" type="avatar" :size="32" class="rounded-md" />
</UiBadgeNetwork>
<div class="flex-1 leading-[22px]">
<h4 class="text-skin-link" v-text="execution.safeName || shorten(execution.safeAddress)" />
<div class="text-skin-text text-[17px]" v-text="shorten(execution.safeAddress)" />
</div>
</a>
<TransactionsListItem
v-for="(tx, i) in txs"
v-for="(tx, i) in execution.transactions"
:key="i"
:tx="tx"
class="border-b last:border-b-0 px-4 py-3 space-x-2 flex items-center justify-between"
Expand Down
52 changes: 52 additions & 0 deletions apps/ui/src/helpers/__snapshots__/osnap.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,58 @@ exports[`parseOSnapTransaction > should parse oSnap raw transaction 1`] = `
}
`;

exports[`parseOSnapTransaction > should parse oSnap safeImport transaction 1`] = `
{
"_form": {
"abi": [
{
"_isFragment": true,
"constant": false,
"gas": null,
"inputs": [
{
"_isParamType": true,
"arrayChildren": null,
"arrayLength": null,
"baseType": "address",
"components": null,
"indexed": null,
"name": "to",
"type": "address",
},
{
"_isParamType": true,
"arrayChildren": null,
"arrayLength": null,
"baseType": "uint256",
"components": null,
"indexed": null,
"name": "amount",
"type": "uint256",
},
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
},
],
"args": [
"0xfE1552DA65FAcAaC5B50b73CEDa4C993e16d4694",
"1000000000000000000",
],
"method": "transfer",
"recipient": "0x4F604735c1cF31399C6E711D5962b2B3E0225AD3",
},
"_type": "contractCall",
"data": "0xa9059cbb000000000000000000000000fe1552da65facaac5b50b73ceda4c993e16d46940000000000000000000000000000000000000000000000000de0b6b3a7640000",
"salt": "",
"to": "0x4F604735c1cF31399C6E711D5962b2B3E0225AD3",
"value": "0",
}
`;

exports[`parseOSnapTransaction > should parse oSnap transfer NFT transaction 1`] = `
{
"_form": {
Expand Down
57 changes: 56 additions & 1 deletion apps/ui/src/helpers/osnap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('parseOSnapTransaction', () => {
constant: false,
_isFragment: true,
stateMutability: 'nonpayable'
},
} as any,
isValid: true,
formatted: [
'0x000000000000cd17345801aa8147b8D3950260FF',
Expand All @@ -120,6 +120,61 @@ describe('parseOSnapTransaction', () => {
expect(parseOSnapTransaction(transaction)).toMatchSnapshot();
});

it('should parse oSnap safeImport transaction', () => {
const transaction = {
to: '0x4F604735c1cF31399C6E711D5962b2B3E0225AD3',
abi: '[{"type":"function","name":"transfer","constant":false,"inputs":[{"name":"to","type":"address","indexed":null,"components":null,"arrayLength":null,"arrayChildren":null,"baseType":"address","_isParamType":true},{"name":"amount","type":"uint256","indexed":null,"components":null,"arrayLength":null,"arrayChildren":null,"baseType":"uint256","_isParamType":true}],"outputs":[],"payable":false,"stateMutability":"nonpayable","gas":null,"_isFragment":true}]',
data: '0xa9059cbb000000000000000000000000fe1552da65facaac5b50b73ceda4c993e16d46940000000000000000000000000000000000000000000000000de0b6b3a7640000',
type: 'safeImport' as const,
value: '0',
method: {
gas: null,
name: 'transfer',
type: 'function',
inputs: [
{
name: 'to',
type: 'address',
indexed: null,
baseType: 'address',
components: null,
arrayLength: null,
_isParamType: true,
arrayChildren: null
},
{
name: 'amount',
type: 'uint256',
indexed: null,
baseType: 'uint256',
components: null,
arrayLength: null,
_isParamType: true,
arrayChildren: null
}
],
outputs: [],
payable: false,
constant: false,
_isFragment: true,
stateMutability: 'nonpayable'
} as any,
isValid: true,
formatted: [
'0x4F604735c1cF31399C6E711D5962b2B3E0225AD3',
0,
'0',
'0xa9059cbb000000000000000000000000fe1552da65facaac5b50b73ceda4c993e16d46940000000000000000000000000000000000000000000000000de0b6b3a7640000'
],
parameters: {
to: '0xfE1552DA65FAcAaC5B50b73CEDa4C993e16d4694',
amount: '1000000000000000000'
}
};

expect(parseOSnapTransaction(transaction)).toMatchSnapshot();
});

it('should parse oSnap raw transaction', () => {
const transaction = {
to: '0x556B14CbdA79A36dC33FcD461a04A5BCb5dC2A70',
Expand Down
39 changes: 34 additions & 5 deletions apps/ui/src/helpers/osnap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ETH_CONTRACT } from './constants';
import { JsonFragment } from '@ethersproject/abi';
import {
BaseTransaction,
ContractCallTransaction,
Expand All @@ -6,7 +8,6 @@ import {
SendTokenTransaction,
Transaction
} from '@/types';
import { ETH_CONTRACT } from './constants';

export const transactionTypes = [
'transferFunds',
Expand Down Expand Up @@ -44,10 +45,15 @@ type OSnapTransferNFTTransaction = OSnapBaseTransaction & {
type OSnapContractInteractionTransaction = OSnapBaseTransaction & {
type: 'contractInteraction';
abi: string;
method: {
name: string;
};
parameters: any[];
method: Required<JsonFragment>;
parameters: string[];
};

type OSnapSafeImportTransaction = OSnapBaseTransaction & {
type: 'safeImport';
abi: string;
method: Required<JsonFragment>;
parameters: { [key: string]: string };
};

type OSnapRawTransaction = OSnapBaseTransaction & {
Expand All @@ -58,6 +64,7 @@ type OSnapTransaction =
| OSnapTransferFundsTransaction
| OSnapTransferNFTTransaction
| OSnapContractInteractionTransaction
| OSnapSafeImportTransaction
| OSnapRawTransaction;

function parseTransferFundsTransaction(
Expand Down Expand Up @@ -120,6 +127,26 @@ function parseContractInteractionTransaction(
};
}

function parseSafeImportTransaction(
transaction: OSnapSafeImportTransaction
): ContractCallTransaction {
return {
to: transaction.to,
data: transaction.data,
value: transaction.value,
salt: '',
_type: 'contractCall',
_form: {
recipient: transaction.to,
method: transaction.method.name,
args: transaction.method.inputs.map(input =>
input.name ? transaction.parameters[input.name] : null
),
abi: JSON.parse(transaction.abi)
}
};
}

function parseRawTransaction(transaction: OSnapRawTransaction): RawTransaction {
return {
to: transaction.to,
Expand All @@ -141,6 +168,8 @@ export function parseOSnapTransaction(transaction: OSnapTransaction): Transactio
return parseTransferNFTTransaction(transaction);
case 'contractInteraction':
return parseContractInteractionTransaction(transaction);
case 'safeImport':
return parseSafeImportTransaction(transaction);
case 'raw':
return parseRawTransaction(transaction);
default:
Expand Down
62 changes: 54 additions & 8 deletions apps/ui/src/networks/common/graphqlApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from './highlight';
import { PaginationOpts, SpacesFilter, NetworkApi, ProposalsFilter } from '@/networks/types';
import { getNames } from '@/helpers/stamp';
import { BASIC_CHOICES } from '@/helpers/constants';
import { BASIC_CHOICES, CHAIN_IDS } from '@/helpers/constants';
import {
Space,
Proposal,
Expand All @@ -33,10 +33,11 @@ import {
NetworkID,
ProposalState,
Follow,
UserActivity
UserActivity,
ProposalExecution
} from '@/types';
import { ApiSpace, ApiProposal, ApiStrategyParsedMetadata } from './types';
import { clone } from '@/helpers/utils';
import { clone, compareAddresses } from '@/helpers/utils';

type ApiOptions = {
baseNetworkId?: NetworkID;
Expand Down Expand Up @@ -93,6 +94,51 @@ function processStrategiesMetadata(
return strategiesIndicies.map(index => metadataMap[index]) || [];
}

function processExecutions(
proposal: ApiProposal,
executionNetworkId: NetworkID
): ProposalExecution[] {
// NOTE: This is unstable, meaning that if executors_strategies or treasuries
// are modified in the future it will affect pass proposals.
// We should persist those values on proposal directly so it's stable.
// Right now we can't really update subgraphs because of TheGraph issue.

if (!proposal.metadata.execution) return [];

const match = proposal.space.metadata.executors_strategies.find(
strategy => strategy.address === proposal.execution_strategy
);

const treasuries = proposal.space.metadata.treasuries.map(treasury => {
const { name, network, address } = JSON.parse(treasury);

return {
name,
network,
address
};
});

const matchingTreasury = treasuries.find(treasury => {
if (!match) return null;

return (
match.treasury &&
compareAddresses(treasury.address, match.treasury) &&
match.treasury_chain === CHAIN_IDS[treasury.network]
);
});

return [
{
safeAddress: match?.treasury || '',
safeName: matchingTreasury?.name || 'Unnamed treasury',
networkId: matchingTreasury?.network || executionNetworkId,
transactions: formatExecution(proposal.metadata.execution)
}
];
}

function formatSpace(space: ApiSpace, networkId: NetworkID): Space {
return {
...space,
Expand Down Expand Up @@ -150,6 +196,9 @@ function formatProposal(
current: number,
baseNetworkId?: NetworkID
): Proposal {
const executionNetworkId =
proposal.execution_strategy_type === 'EthRelayer' && baseNetworkId ? baseNetworkId : networkId;

return {
...proposal,
space: {
Expand All @@ -173,11 +222,8 @@ function formatProposal(
title: proposal.metadata.title,
body: proposal.metadata.body,
discussion: proposal.metadata.discussion,
execution_network:
proposal.execution_strategy_type === 'EthRelayer' && baseNetworkId
? baseNetworkId
: networkId,
execution: formatExecution(proposal.metadata.execution),
execution_network: executionNetworkId,
executions: processExecutions(proposal, executionNetworkId),
has_execution_window_opened: ['Axiom', 'EthRelayer'].includes(proposal.execution_strategy_type)
? proposal.max_end <= current
: proposal.min_end <= current,
Expand Down
9 changes: 9 additions & 0 deletions apps/ui/src/networks/common/graphqlApi/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,17 @@ const PROPOSAL_FRAGMENT = gql`
name
avatar
voting_power_symbol
treasuries
executors
executors_types
executors_strategies {
id
address
destination_address
type
treasury_chain
treasury
}
}
strategies_parsed_metadata {
index
Expand Down
Loading

0 comments on commit aa1e297

Please sign in to comment.