Skip to content

Commit

Permalink
Merge pull request #52 from oasisprotocol/csillag/acl-refactoring
Browse files Browse the repository at this point in the history
Refactor ACL handling
  • Loading branch information
csillag authored Sep 30, 2024
2 parents 6c648b2 + cca63ee commit a5c38aa
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 a5c38aa

Please sign in to comment.