Skip to content

Commit

Permalink
add multiple safes to builder
Browse files Browse the repository at this point in the history
  • Loading branch information
gsteenkamp89 committed Apr 24, 2024
1 parent d82ed3e commit 4e80970
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 243 deletions.
294 changes: 81 additions & 213 deletions src/plugins/oSnap/Create.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
<script setup lang="ts">
import { ExtendedSpace } from '@/helpers/interfaces';
import { formatUnits } from '@ethersproject/units';
import { cloneDeep } from 'lodash';
import SelectSafe from './components/Input/SelectSafe.vue';
import TransactionBuilder from './components/TransactionBuilder/TransactionBuilder.vue';
import {
BalanceResponse,
GnosisSafe,
NFT,
Network,
OsnapPluginData,
Token,
Transaction,
nonNullable
} from './types';
import {
getGnosisSafeBalances,
getGnosisSafeCollectibles,
getIsOsnapEnabled,
getModuleAddressForTreasury,
getNativeAsset
} from './utils';
import OsnapMarketingWidget from './components/OsnapMarketingWidget.vue';
import BotSupportWarning from './components/BotSupportWarning.vue';
import { getIsOsnapEnabled, getModuleAddressForTreasury } from './utils';
import CreateSafe from './CreateSafe.vue';
import { toChecksumAddress } from '@/helpers/utils';
const props = defineProps<{
Expand All @@ -40,121 +27,36 @@ const emit = defineEmits<{
}>();
const newPluginData = ref<OsnapPluginData>({
safe: null
safes: 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);
}
function removeTransaction(transactionIndex: number) {
if (!newPluginData.value.safe) return;
newPluginData.value.safe.transactions.splice(transactionIndex, 1);
}
function updateTransaction(transaction: Transaction, transactionIndex: number) {
if (!newPluginData.value.safe) return;
newPluginData.value.safe.transactions[transactionIndex] = transaction;
}
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 balancesWithNative = balances.map(balance => {
if (!balance.tokenAddress || !balance.token) {
return {
...balance,
token: getNativeAsset(network),
tokenAddress: 'main'
};
}
return balance;
});
const updateSafes = (safes: GnosisSafe[]) => {
newPluginData.value = { safes };
emit('update', { key: 'oSnap', form: newPluginData.value });
};
const tokens = await fetchTokens('https://tokens.uniswap.org');
const allSafes = ref<GnosisSafe[]>([]);
const configuredSafes = computed(() => newPluginData.value.safes ?? []);
return enhanceTokensWithBalances(balancesWithNative, tokens, network);
} catch (e) {
console.warn('Error fetching balances', e);
return [];
const unconfiguredSafes = computed<GnosisSafe[]>(() => {
if (newPluginData.value?.safes?.length && configuredSafes.value.length) {
return allSafes.value.filter(safe =>
!!findSafe(safe, configuredSafes.value) ? false : true
);
}
}
function enhanceTokensWithBalances(
balances: Partial<BalanceResponse>[],
tokens: Token[],
network: Network
) {
return balances
.filter(
(balance): balance is BalanceResponse =>
!!balance.token && !!balance.tokenAddress && !!balance.balance
)
.map(balance => enhanceTokenWithBalance(balance, tokens, network))
.sort((a, b) => {
if (a.address === 'main' && b.address !== 'main') return -1;
if (!(a.address === 'main') && b.address === 'main') return 1;
if (a.verified && !b.verified) return -1;
if (!a.verified && b.verified) return +1;
if (!a.balance || !b.balance) return 0;
if (parseFloat(a.balance) > parseFloat(b.balance)) return -1;
return 0;
});
}
// gets token balances and also determines if the token is verified
function enhanceTokenWithBalance(
balance: BalanceResponse,
tokens: Token[],
network: Network
): 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: network
};
}
return allSafes.value;
});
function getVerifiedToken(tokenAddress: string, tokens: Token[]) {
return tokens.find(
token => token.address.toLowerCase() === tokenAddress.toLowerCase()
function safeEqual(safe1: GnosisSafe, safe2: GnosisSafe): boolean {
return (
safe1.safeAddress === safe2.safeAddress && safe1.network === safe1.network
);
}
async function fetchCollectibles(network: Network, gnosisSafeAddress: string) {
try {
const response = await getGnosisSafeCollectibles(
network,
gnosisSafeAddress
);
return response.results;
} catch (error) {
console.warn('Error fetching collectibles');
}
return [];
function findSafe(safeToFind: GnosisSafe, from: GnosisSafe[]) {
return from.length
? from.find(safe => safeEqual(safe, safeToFind))
: undefined;
}
// maps over the treasuries and creates a safe for each one
Expand Down Expand Up @@ -199,54 +101,47 @@ async function createOsnapEnabledSafes() {
return safes;
}
// when changing safes, we create a whole new object and replace it
function updateSafe(safe: GnosisSafe) {
newPluginData.value.safe = cloneDeep(safe);
update(newPluginData.value);
function addSafeToConfigure(safe: GnosisSafe) {
updateSafes([...(newPluginData.value.safes ?? []), safe]);
}
const update = (newPluginData: OsnapPluginData) => {
emit('update', { key: 'oSnap', form: newPluginData });
};
async function loadBalancesAndCollectibles() {
if (!newPluginData.value.safe?.safeAddress) return;
isLoading.value = true;
tokens.value = await fetchBalances(
newPluginData.value.safe.network,
newPluginData.value.safe.safeAddress
);
collectables.value = await fetchCollectibles(
newPluginData.value.safe.network,
newPluginData.value.safe.safeAddress
);
function addNewSafe() {
addSafeToConfigure(unconfiguredSafes.value[0]);
}
isLoading.value = false;
function removeSafe(safeIndex: number) {
const copy = [...configuredSafes.value];
copy.splice(safeIndex, 1);
updateSafes(copy);
}
watch(
() => [
newPluginData.value.safe?.safeAddress,
newPluginData.value.safe?.network
],
async () => {
await loadBalancesAndCollectibles();
update(newPluginData.value);
}
);
function updateSafe(safe: GnosisSafe, safeIndex: number) {
const copy = [...configuredSafes.value];
copy[safeIndex] = safe;
updateSafes(copy);
}
onMounted(async () => {
isLoading.value = true;
safes.value = await createOsnapEnabledSafes();
newPluginData.value.safe = cloneDeep(safes.value[0]);
await loadBalancesAndCollectibles();
update(newPluginData.value);
allSafes.value = await createOsnapEnabledSafes();
const initialSafe = cloneDeep(allSafes.value[0]);
updateSafes([initialSafe]);
isLoading.value = false;
});
</script>

<template>
<template v-if="hasLegacyPluginInstalled">
<template v-if="!space.treasuries.length">
<h2>Warning: no treasuries</h2>
<p>
You have installed the oSnap plugin, but you don't have any treasuries.
</p>
<p>
Please add a Safe as a treasury and enable oSnap on it to use the oSnap
plugin.
</p>
</template>
<template v-else-if="hasLegacyPluginInstalled">
<div class="rounded-2xl border p-4 text-md">
<h2 class="mb-2">Warning: Multiple oSnap enabled plugins detected</h2>
<p class="mb-2">
Expand All @@ -255,66 +150,39 @@ onMounted(async () => {
</p>
</div>
</template>
<template v-else>
<div v-if="isLoading" class="grid min-h-[180px] place-items-center">

<template v-else-if="isLoading">
<div class="grid min-h-[180px] place-items-center">
<h2 class="text-center">
Loading oSnap Safes <LoadingSpinner class="ml-2 inline" big />
</h2>
</div>
<div v-else class="rounded-2xl border p-4 relative">
<OsnapMarketingWidget class="absolute top-[-16px] right-[16px]" />
<template v-if="space.treasuries.length === 0">
<h2>Warning: no treasuries</h2>
<p>
You have installed the oSnap plugin, but you don't have any
treasuries.
</p>
<p>
Please add a Safe as a treasury and enable oSnap on it to use the
oSnap plugin.
</p>
</template>
<template v-else-if="safes.length === 0">
<h2>Warning: no oSnap safes found</h2>
<p>
You have installed the oSnap plugin, but you don't have any oSnap
safes.
</p>
<p>
Please add a Safe as a treasury and enable oSnap on it to use the
oSnap plugin.
</p>
</template>
<template v-else>
<h2 class="text-md">Add oSnap transactions</h2>
<h3 class="text-base">Pick a safe</h3>
<SelectSafe
:safes="safes"
:selectedSafe="newPluginData.safe"
@updateSafe="updateSafe($event)"
/>
<BotSupportWarning
v-if="newPluginData.safe"
:safe-address="newPluginData.safe?.safeAddress"
:chain-id="newPluginData.safe?.network"
/>
<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"
:safe="newPluginData.safe"
@add-transaction="addTransaction"
@remove-transaction="removeTransaction"
@update-transaction="updateTransaction"
/>
</div>
</template>
</div>
</template>
<template v-else-if="!allSafes.length">
<h2>Warning: no oSnap safes found</h2>
<p>
You have installed the oSnap plugin, but you don't have any oSnap safes.
</p>
<p>
Please add a Safe as a treasury and enable oSnap on it to use the oSnap
plugin.
</p>
</template>
<div class="flex flex-col gap-6" v-else>
<CreateSafe
v-for="(safe, i) in newPluginData.safes"
:key="`${safe.network}:${safe.safeAddress}`"
:safe-index="i"
:safe="safe"
@remove-safe="removeSafe"
@update-safe="updateSafe"
:all-safes="allSafes"
/>
</div>
<TuneButton
v-if="unconfiguredSafes.length"
class="mt-4 w-full"
@click="addNewSafe"
>Add Batch</TuneButton
>
</template>
Loading

0 comments on commit 4e80970

Please sign in to comment.