Skip to content

Commit

Permalink
Refactor ACL handling
Browse files Browse the repository at this point in the history
All the logic relevant to specific ACLs have been moved separate files
to src/components/ACLs.

This should make it easier to test individual ACLs, and to att new ones.
  • Loading branch information
csillag committed Sep 28, 2024
1 parent 14cd73c commit cca63ee
Show file tree
Hide file tree
Showing 13 changed files with 661 additions and 538 deletions.
28 changes: 28 additions & 0 deletions frontend/src/components/ACLs/allowAll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineACL } from './common'
import { VITE_CONTRACT_ACL_ALLOWALL } from '../../constants/config'
import { denyWithReason } from '../InputFields'

export const allowAll = defineACL({
value: 'acl_allowAll',
label: 'Everybody',
costEstimation: 0.1,
useConfiguration: () => ({
fields: [],
values: undefined,
}),
getAclOptions: () => [
'0x', // Empty bytes is passed
{
address: VITE_CONTRACT_ACL_ALLOWALL,
options: { allowAll: true },
},
],
isThisMine: options => 'allowAll' in options.options,

checkPermission: async (pollACL, daoAddress, proposalId, userAddress) => {
const proof = new Uint8Array()
const result = 0n !== (await pollACL.canVoteOnPoll(daoAddress, proposalId, userAddress, proof))
const canVote = result ? true : denyWithReason('some unknown reason')
return { canVote, proof }
},
} as const)
80 changes: 80 additions & 0 deletions frontend/src/components/ACLs/allowList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { defineACL } from './common'
import { VITE_CONTRACT_ACL_VOTERALLOWLIST } from '../../constants/config'
import { abiEncode, isValidAddress } from '../../utils/poll.utils'
import { denyWithReason, useTextArrayField } from '../InputFields'
import { AclOptions } from '@oasisprotocol/blockvote-contracts'

// Split a list of addresses by newLine, comma or space
const splitAddresses = (addressSoup: string): string[] =>
addressSoup
.split('\n')
.flatMap(x => x.split(','))
.flatMap(x => x.split(' '))
.map(x => x.trim())
.filter(x => x.length > 0)

export const allowList = defineACL({
value: 'acl_allowList',
label: 'Address Whitelist',
costEstimation: 0.1,
description: 'You can specify a list of addresses that are allowed to vote.',
useConfiguration: active => {
const addresses = useTextArrayField({
name: 'addresses',
label: 'Acceptable Addresses',
visible: active,
description: 'You can just copy-paste your list here',
addItemLabel: 'Add address',
removeItemLabel: 'Remove address',
allowEmptyItems: [false, 'Please specify address, or remove this field!'],
minItems: 1,
allowDuplicates: [
false,
['This address is repeated below.', 'The same address was already listed above!'],
],
itemValidator: value =>
value && !isValidAddress(value) ? "This doesn't seem to be a valid address." : undefined,
onItemEdited: (index, value, me) => {
if (value.indexOf(',') !== -1 || value.indexOf(' ') !== -1 || value.indexOf('\n') !== -1) {
const addresses = splitAddresses(value)
const newAddresses = [...me.value]
for (let i = 0; i < addresses.length; i++) {
newAddresses[index + i] = addresses[i]
}
me.setValue(newAddresses)
}
},
validateOnChange: true,
showValidationSuccess: true,
})

return {
fields: [addresses],
values: {
addresses: addresses.value,
},
}
},

getAclOptions: (props): [string, AclOptions] => {
if (!props.addresses) throw new Error('Internal errors: parameter mismatch, addresses missing.')
return [
abiEncode(['address[]'], [props.addresses]),
{
address: VITE_CONTRACT_ACL_VOTERALLOWLIST,
options: { allowList: true },
},
]
},

isThisMine: options => 'allowList' in options.options,

checkPermission: async (pollACL, daoAddress, proposalId, userAddress) => {
const proof = new Uint8Array()
const explanation = 'This poll is only for a predefined list of addresses.'
const result = 0n !== (await pollACL.canVoteOnPoll(daoAddress, proposalId, userAddress, proof))
// console.log("whiteListAcl check:", result)
const canVote = result ? true : denyWithReason('you are not on the list of allowed addresses')
return { canVote, explanation, proof }
},
} as const)
56 changes: 56 additions & 0 deletions frontend/src/components/ACLs/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Choice, DecisionWithReason, FieldConfiguration } from '../InputFields'
import { AclOptions, IPollACL } from '@oasisprotocol/blockvote-contracts'
import { BytesLike } from 'ethers'
export type StatusUpdater = (status: string | undefined) => void

/**
* This data structure describes an ACL
*/
export type ACL<Name, ConfigInputValues, Options extends AclOptions, Extra> = Choice<Name> & {
/**
* Estimated cost per vote
*
* This is used for setting up gasless voting.
*/
costEstimation: number

/**
* Specify the fields and values needed for configuring the ACL when creating a poll
*/
useConfiguration: (selected: boolean) => { fields: FieldConfiguration; values: ConfigInputValues }

/**
* Compose the ACL options when creating a poll
*/
getAclOptions:
| ((config: ConfigInputValues, statusUpdater?: StatusUpdater) => [string, Options])
| ((config: ConfigInputValues, statusUpdater?: StatusUpdater) => Promise<[string, Options]>)

/**
* Attempt to recognize if this ACL is managing a given poll, based on ACL options
* @param options
*/
isThisMine: (options: AclOptions) => boolean

/**
* Determine if we can vote on this poll
*
* The actual contract is made available, this function just needs to interpret the result
* and compose the required messages.
*/
checkPermission: (
pollACL: IPollACL,
daoAddress: string,
proposalId: string,
userAddress: string,
options: Options,
) => Promise<
{ canVote: DecisionWithReason; explanation?: string; proof: BytesLike; error?: string } & Extra
>
}

export function defineACL<Name, ConfigInputValues, Options extends AclOptions, Extra>(
acl: ACL<Name, ConfigInputValues, Options, Extra>,
): ACL<Name, ConfigInputValues, Options, Extra> {
return acl
}
6 changes: 6 additions & 0 deletions frontend/src/components/ACLs/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.explanation {
font-style: normal;
font-weight: 400;
font-size: 15px;
color: #414749;
}
15 changes: 15 additions & 0 deletions frontend/src/components/ACLs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { allowAll } from './allowAll'
import { allowList } from './allowList'
import { tokenHolder } from './tokenHolder'
import { xchain } from './xchain'
import { AclOptions } from '../../types'

/**
* The list of supported ACLs
*/
export const acls = [allowAll, allowList, tokenHolder, xchain] as const

/**
* Find the ACL needed to manage a poll, based on ACL options
*/
export const findACLForOptions = (options: AclOptions) => acls.find(acl => acl.isThisMine(options))
90 changes: 90 additions & 0 deletions frontend/src/components/ACLs/tokenHolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { defineACL } from './common'
import { DecisionWithReason, denyWithReason, useLabel, useTextField } from '../InputFields'
import { abiEncode, getSapphireTokenDetails, isValidAddress } from '../../utils/poll.utils'
import { VITE_CONTRACT_ACL_TOKENHOLDER } from '../../constants/config'

export const tokenHolder = defineACL({
value: 'acl_tokenHolder',
costEstimation: 0.2,
label: 'Holds Token on Sapphire',
hidden: true, // We decided to hide this option, since this is not the focus
useConfiguration: active => {
const tokenAddress = useTextField({
name: 'tokenAddress',
label: 'Token Address',
visible: active,
required: [true, 'Please specify the address of the token that is the key to this poll!'],
validators: [
value => (!isValidAddress(value) ? "This doesn't seem to be a valid address." : undefined),
async (value, controls) => {
controls.updateStatus({ message: 'Fetching token details...' })
const details = await getSapphireTokenDetails(value)
if (!details) {
return "Can't find token details!"
}
tokenName.setValue(details.name)
tokenSymbol.setValue(details.symbol)
},
],
validateOnChange: true,
showValidationSuccess: true,
})

const hasValidSapphireTokenAddress =
tokenAddress.visible && tokenAddress.isValidated && !tokenAddress.hasProblems

const tokenName = useLabel({
name: 'tokenName',
visible: hasValidSapphireTokenAddress,
label: 'Selected token:',
initialValue: '',
})

const tokenSymbol = useLabel({
name: 'tokenSymbol',
visible: hasValidSapphireTokenAddress,
label: 'Symbol:',
initialValue: '',
})

return {
fields: [tokenAddress, [tokenName, tokenSymbol]],
values: {
tokenAddress: tokenAddress.value,
},
}
},

getAclOptions: props => {
if (!props.tokenAddress) throw new Error('Internal errors: parameter mismatch, addresses missing.')
return [
abiEncode(['address'], [props.tokenAddress]),
{
address: VITE_CONTRACT_ACL_TOKENHOLDER,
options: { token: props.tokenAddress },
},
]
},

isThisMine: options => 'token' in options.options,

checkPermission: async (pollACL, daoAddress, proposalId, userAddress, options) => {
const tokenAddress = options.options.token
const tokenInfo = await getSapphireTokenDetails(tokenAddress)
const explanation = `You need to hold some ${tokenInfo?.name ?? 'specific'} token (on the Sapphire network) to vote.`
const proof = new Uint8Array()
let canVote: DecisionWithReason
try {
const result = 0n !== (await pollACL.canVoteOnPoll(daoAddress, proposalId, userAddress, proof))
// console.log("tokenHolderAcl check:", result)
if (result) {
canVote = true
} else {
canVote = denyWithReason(`you don't hold any ${tokenInfo?.name} tokens`)
}
} catch {
canVote = denyWithReason(`you don't hold any ${tokenInfo?.name} tokens`)
}
return { canVote, explanation, proof }
},
} as const)
Loading

0 comments on commit cca63ee

Please sign in to comment.