-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: ryanwolhuter <[email protected]>
- Loading branch information
1 parent
d79bc0f
commit 4ef7ed9
Showing
14 changed files
with
366 additions
and
396 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.