Skip to content

Commit

Permalink
use separate page for read only
Browse files Browse the repository at this point in the history
Signed-off-by: ryanwolhuter <[email protected]>
  • Loading branch information
ryanwolhuter committed Oct 6, 2023
1 parent d79bc0f commit 4ef7ed9
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 396 deletions.
254 changes: 237 additions & 17 deletions src/plugins/oSnap/Create.vue
Original file line number Diff line number Diff line change
@@ -1,33 +1,253 @@
<script setup lang="ts">
import { ExtendedSpace, Proposal, Results } from '@/helpers/interfaces';
import Config from './components/Config.vue';
import { initialPluginData } from './constants';
import { OsnapModelValue, OsnapPluginData } from './types';
import { ExtendedSpace, TreasuryWallet } from '@/helpers/interfaces';
import { formatUnits } from '@ethersproject/units';
import { cloneDeep } from 'lodash';
import TransactionBuilder from './components/TransactionBuilder/TransactionBuilder.vue';
import {
BalanceResponse,
GnosisSafe,
NFT,
Network,
OsnapPluginData,
Token,
Transaction
} from './types';
import {
getGnosisSafeBalances,
getGnosisSafeCollectibles,
getIsOsnapEnabled,
getModuleAddressForTreasury
} from './utils';
defineProps<{
const props = defineProps<{
space: ExtendedSpace;
proposal: Proposal;
modelValue: OsnapModelValue;
results?: Results;
}>();
const isLoading = ref(false);
const emit = defineEmits<{
update: [value: { key: 'oSnap'; form: OsnapPluginData }];
}>();
const newPluginData = ref<OsnapPluginData>({
safe: null
});
const safes = ref<GnosisSafe[]>([]);
const tokens = ref<Token[]>([]);
const collectables = ref<NFT[]>([]);
function addTransaction(transaction: Transaction) {
if (newPluginData.value.safe === null) return;
newPluginData.value.safe.transactions.push(transaction);
update(newPluginData.value);
}
function removeTransaction(transactionIndex: number) {
if (!newPluginData.value.safe) return;
newPluginData.value.safe.transactions.splice(transactionIndex, 1);
update(newPluginData.value);
}
function updateTransaction(transaction: Transaction, transactionIndex: number) {
if (!newPluginData.value.safe) return;
newPluginData.value.safe.transactions[transactionIndex] = transaction;
update(newPluginData.value);
}
async function fetchTokens(url: string): Promise<Token[]> {
try {
const response = await fetch(url);
const data = await response.json();
return data.verifiedTokens?.tokens || data.tokens || [];
} catch {
return [];
}
}
async function fetchBalances(network: Network, safeAddress: string) {
if (!safeAddress) {
return [];
}
try {
const balances = await getGnosisSafeBalances(network, safeAddress);
const uniswapTokensPromise = fetchTokens(
'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
);
const snapshotTokensPromise = fetchTokens(
`${import.meta.env.VITE_SIDEKICK_URL}/api/moderation?list=verifiedTokens`
);
const tokensLists = await Promise.all([
uniswapTokensPromise,
snapshotTokensPromise
]);
const tokens = tokensLists.flat();
return enhanceTokensWithBalances(balances, tokens);
} catch (e) {
console.warn('Error fetching balances');
return [];
}
}
function enhanceTokensWithBalances(
balances: Partial<BalanceResponse>[],
tokens: Token[]
) {
return balances
.filter(
(balance): balance is BalanceResponse =>
!!balance.token && !!balance.tokenAddress && !!balance.balance
)
.map(balance => enhanceTokenWithBalance(balance, tokens))
.sort((a, b) => {
if (a.verified && b.verified) return 0;
if (a.verified) return -1;
return 1;
});
}
function enhanceTokenWithBalance(
balance: BalanceResponse,
tokens: Token[]
): Token {
const verifiedToken = getVerifiedToken(balance.tokenAddress, tokens);
return {
...balance.token,
address: balance.tokenAddress,
balance: balance.balance
? formatUnits(balance.balance, balance.token.decimals)
: '0',
verified: !!verifiedToken,
chainId: verifiedToken ? verifiedToken.chainId : undefined
};
}
function getVerifiedToken(tokenAddress: string, tokens: Token[]) {
return tokens.find(
token => token.address.toLowerCase() === tokenAddress.toLowerCase()
);
}
async function fetchCollectibles(network: Network, gnosisSafeAddress: string) {
try {
const response = await getGnosisSafeCollectibles(
network,
gnosisSafeAddress
);
console.log('nfts', response);
return response.results;
} catch (error) {
console.warn('Error fetching collectables');
}
return [];
}
async function createSafes() {
const treasuriesWithOsnapEnabled = (
await Promise.all(
props.space.treasuries.map(async treasury => {
const isOsnapEnabled = await getIsOsnapEnabled(
treasury.network as Network,
treasury.address
);
return isOsnapEnabled ? treasury : null;
})
)
).filter(treasury => treasury !== null) as TreasuryWallet[];
const safes: GnosisSafe[] = await Promise.all(
treasuriesWithOsnapEnabled.map(async treasury => {
const moduleAddress = await getModuleAddressForTreasury(
treasury.network as Network,
treasury.address
);
return {
safeName: treasury.name,
safeAddress: treasury.address,
network: treasury.network as Network,
transactions: [] as Transaction[],
moduleAddress,
};
})
);
return safes;
}
function updateSafe(safeIndex: string) {
newPluginData.value.safe = cloneDeep(safes.value[safeIndex]);
update(newPluginData.value);
}
const update = (newPluginData: OsnapPluginData) => {
emit('update', { key: 'oSnap', form: newPluginData });
};
watch(newPluginData, async () => {
if (!newPluginData.value.safe?.safeAddress) return;
tokens.value = await fetchBalances(
newPluginData.value.safe.network,
newPluginData.value.safe.safeAddress
);
collectables.value = await fetchCollectibles(
newPluginData.value.safe.network,
newPluginData.value.safe.safeAddress
);
});
onMounted(async () => {
isLoading.value = true;
safes.value = await createSafes();
newPluginData.value.safe = safes.value[0];
update(newPluginData.value);
isLoading.value = false;
});
</script>

<template>
<Config
v-if="space.plugins.oSnap"
:space="space"
:results="results"
:proposal="proposal"
:is-read-only="false"
:plugin-data="modelValue?.oSnap ?? initialPluginData"
@update="update"
/>
<div v-if="isLoading" class="grid min-h-[180px] place-items-center">
<h1 class="text-center">
Loading oSnap Safes <LoadingSpinner class="ml-2 inline" big />
</h1>
</div>
<div v-else class="rounded-2xl border p-4">
<div>
<h2 class="text-md">Add oSnap transactions</h2>
</div>
<h3 class="text-base">Pick a safe</h3>
<UiSelect
:model-value="
safes.findIndex(
safe => safe.safeAddress === newPluginData.safe?.safeAddress
)
"
@update:modelValue="updateSafe"
>
<template #label>Safe</template>
<option v-for="(safe, index) in safes" :key="index" :value="index">
{{ safe.safeName }}
</option>
</UiSelect>
<div class="mt-4 border-b last:border-b-0">
<TransactionBuilder
v-if="!!newPluginData.safe"
:space="space"
:safe-address="newPluginData.safe.safeAddress"
:module-address="newPluginData.safe.moduleAddress"
:tokens="tokens"
:collectables="collectables"
:network="newPluginData.safe.network"
:transactions="newPluginData.safe.transactions"
@add-transaction="addTransaction"
@remove-transaction="removeTransaction"
@update-transaction="updateTransaction"
/>
</div>
</div>
</template>
105 changes: 97 additions & 8 deletions src/plugins/oSnap/Proposal.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,111 @@
<script setup lang="ts">
import { ExtendedSpace, Proposal, Results } from '@/helpers/interfaces';
import Config from './components/Config.vue';
import { getIpfsUrl, shorten } from '@/helpers/utils';
import { isBigNumberish } from '@ethersproject/bignumber/lib/bignumber';
import { formatEther, formatUnits } from '@ethersproject/units';
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
import { EIP3770_PREFIXES } from '../safeSnap';
import HandleOutcomeUma from './components/HandleOutcomeUma.vue';
import ReadOnly from './components/Input/ReadOnly.vue';
import { GnosisSafe, Transaction } from './types';
defineProps<{
const props = defineProps<{
space: ExtendedSpace;
proposal: Proposal;
results: Results;
}>();
const ipfs = getIpfsUrl(props.proposal.ipfs) as string;
const safe = props.proposal.plugins.oSnap?.safe as GnosisSafe;
const { logo } = networks[safe.network];
const networkIcon = getIpfsUrl(logo);
const prefix = EIP3770_PREFIXES[safe.network];
const safeLink = `https://gnosis-safe.io/app/${prefix}:${safe.safeAddress}`;
const transactionsForDisplay = enrichTransactionsForDisplay(safe.transactions);
function enrichTransactionsForDisplay(transactions: Transaction[]) {
return transactions.map(enrichTransactionForDisplay);
}
function enrichTransactionForDisplay(transaction: Transaction) {
const { to, value, data } = transaction;
const commonProperties = { to, value: formatEther(value), data };
if (transaction.type === 'raw') {
return { ...commonProperties, type: 'Raw' };
}
if (transaction.type === 'contractInteraction') {
const { methodName, parameters } = transaction;
return {
...commonProperties,
type: 'Contract interaction',
'method name': methodName,
parameters: parameters?.join(', ')
};
}
if (transaction.type === 'transferFunds') {
const { token, amount: unformattedAmount } = transaction;
const amount =
isBigNumberish(unformattedAmount) && !!token?.decimals
? formatUnits(unformattedAmount, token.decimals)
: unformattedAmount;
return {
...commonProperties,
type: 'Transfer funds',
'token address':
token?.address === 'main' ? 'native token' : token?.address,
'token symbol': token?.symbol,
recipient: transaction.recipient,
amount
};
}
if (transaction.type === 'transferNFT') {
const { recipient, collectable } = transaction;
return {
...commonProperties,
type: 'Transfer NFT',
recipient,
collectable: `${collectable?.tokenName} #${collectable?.id}`,
'collectable address': collectable?.address
};
}
return { ...commonProperties, type: 'Raw' };
}
</script>

<template>
<Config
v-if="!!proposal.plugins.oSnap?.safe?.transactions?.length"
:plugin-data="proposal.plugins.oSnap"
:proposal="proposal"
:is-read-only="true"
:network="space.network"
<div>
<h2 class="mb-4 text-lg">oSnap Transactions</h2>
<h3 class="flex text-md">
<BaseAvatar class="" :src="networkIcon" size="24" />
{{ safe.safeName }}
<a
v-if="safe.safeAddress"
:href="safeLink"
class="ml-2 flex font-normal text-skin-text"
target="_blank"
>
{{ shorten(safe.safeAddress) }}
<i-ho-external-link class="ml-1" />
</a>
</h3>
<BaseLink v-if="ipfs" :link="ipfs">View transactions on IPFS</BaseLink>
<div
v-for="({ type, ...details }, index) in transactionsForDisplay"
class="my-4"
>
<h4 class="mb-2">{{ type }} transaction #{{ index + 1 }}</h4>
<ReadOnly v-for="[key, value] in Object.entries(details)" class="mb-2">
<strong class="mr-4 inline-block whitespace-nowrap">{{ key }}</strong>
<span class="break-all">{{ value }}</span>
</ReadOnly>
</div>
</div>
<HandleOutcomeUma
v-if="!!results"
:space="space"
:proposal="proposal"
:transactions="safe.transactions"
:results="results"
:module-address="safe.moduleAddress"
:network="safe.network"
/>
</template>
Loading

0 comments on commit 4ef7ed9

Please sign in to comment.