Skip to content

Commit

Permalink
feat(chat): handle ephemeral signers for squadsx (#24)
Browse files Browse the repository at this point in the history
* feat(chat): handle ephemeral signers for squadsx

* fix(ui): add walletContext param to DiscussionForm
  • Loading branch information
joeymeere authored Nov 4, 2024
1 parent aa80065 commit 54acd3e
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 39 deletions.
56 changes: 41 additions & 15 deletions actions/castVote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import { fetchVoteRecordByPubkey } from '@hooks/queries/voteRecord'
import { findPluginName } from '@constants/plugins'
import { BN } from '@coral-xyz/anchor'
import { postComment } from './chat/postMessage'
import { withPostChatMessageEphSigner } from '@utils/ephemeral-signers/postMessageWithEphSigner'
import { getEphemeralSigners } from '@utils/ephemeral-signers'
import { Wallet } from '@solana/wallet-adapter-react'

const getVetoTokenMint = (
proposal: ProgramAccount<Proposal>,
Expand Down Expand Up @@ -158,6 +161,7 @@ const createTokenOwnerRecordIfNeeded = async ({

export async function castVote(
{ connection, wallet, programId, walletPubkey }: RpcContext,
walletContext: Wallet,
realm: ProgramAccount<Realm>,
proposal: ProgramAccount<Proposal>,
tokenOwnerRecord: PublicKey,
Expand Down Expand Up @@ -309,21 +313,43 @@ export async function castVote(
createPostMessageTicketIxs
)

await withPostChatMessage(
postMessageIxs,
chatMessageSigners,
GOVERNANCE_CHAT_PROGRAM_ID,
programId,
realm.pubkey,
proposal.account.governance,
proposal.pubkey,
tokenOwnerRecord,
governanceAuthority,
payer,
undefined,
message,
plugin?.voterWeightPk
)
// Check if the connected wallet is not SquadsX
if (walletContext.adapter.name !== 'SquadsX') {
await withPostChatMessage(
postMessageIxs,
chatMessageSigners,
GOVERNANCE_CHAT_PROGRAM_ID,
programId,
realm.pubkey,
proposal.account.governance,
proposal.pubkey,
tokenOwnerRecord,
governanceAuthority,
payer,
undefined,
message,
plugin?.voterWeightPk
)
} else {
const chatMessage = await getEphemeralSigners(walletContext, 1)

await withPostChatMessageEphSigner(
postMessageIxs,
chatMessageSigners,
GOVERNANCE_CHAT_PROGRAM_ID,
programId,
realm.pubkey,
proposal.account.governance,
proposal.pubkey,
tokenOwnerRecord,
governanceAuthority,
payer,
undefined,
message,
chatMessage[0], // Executing a chat message ix in Squads requires subbing in a custom ephemeral signer
plugin?.voterWeightPk
)
}
}

const isNftVoter = votingPlugin?.client instanceof NftVoterClient
Expand Down
62 changes: 45 additions & 17 deletions actions/chat/postMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ import {
txBatchesToInstructionSetWithSigners,
} from '@utils/sendTransactions'
import { sendSignAndConfirmTransactionsProps } from '@blockworks-foundation/mangolana/lib/transactions'
import { withPostChatMessageEphSigner } from '@utils/ephemeral-signers/postMessageWithEphSigner'
import { getEphemeralSigners } from '@utils/ephemeral-signers'
import { Wallet } from '@solana/wallet-adapter-react'

export async function postChatMessage(
{ connection, wallet, programId, walletPubkey }: RpcContext,
walletContext: Wallet,
realm: ProgramAccount<Realm>,
proposal: ProgramAccount<Proposal>,
tokeOwnerRecord: ProgramAccount<TokenOwnerRecord>,
Expand All @@ -45,21 +49,42 @@ export async function postChatMessage(
createNftTicketsIxs
)

await withPostChatMessage(
instructions,
signers,
GOVERNANCE_CHAT_PROGRAM_ID,
programId,
realm.pubkey,
proposal.account.governance,
proposal.pubkey,
tokeOwnerRecord.pubkey,
governanceAuthority,
payer,
replyTo,
body,
plugin?.voterWeightPk
)
// Check if the connected wallet is not SquadsX
if (walletContext.adapter.name !== 'SquadsX') {
await withPostChatMessage(
instructions,
signers,
GOVERNANCE_CHAT_PROGRAM_ID,
programId,
realm.pubkey,
proposal.account.governance,
proposal.pubkey,
tokeOwnerRecord.pubkey,
governanceAuthority,
payer,
replyTo,
body,
plugin?.voterWeightPk
)
} else {
const chatMessage = await getEphemeralSigners(walletContext, 1)
await withPostChatMessageEphSigner(
instructions,
signers,
GOVERNANCE_CHAT_PROGRAM_ID,
programId,
realm.pubkey,
proposal.account.governance,
proposal.pubkey,
tokeOwnerRecord.pubkey,
governanceAuthority,
payer,
replyTo,
body,
chatMessage[0], // Executing a chat message ix in Squads requires subbing in a custom ephemeral signer
plugin?.voterWeightPk
)
}

// createTicketIxs is a list of instructions that create nftActionTicket only for nft-voter-v2 plugin
// so it will be empty for other plugins or just spl-governance
Expand Down Expand Up @@ -102,15 +127,18 @@ export async function postComment(
transactionProps: sendSignAndConfirmTransactionsProps & {
lookupTableAccounts?: any
autoFee?: boolean
}) {
}
) {
try {
await sendTransactionsV3(transactionProps)
} catch (e) {
if (e.message.indexOf('Transaction too large:') !== -1) {
const numbers = e.message.match(/\d+/g)
const [size, maxSize] = numbers ? numbers.map(Number) : [0, 0]
if (size > maxSize) {
throw new Error(`You must reduce your comment by ${size - maxSize} character(s).`)
throw new Error(
`You must reduce your comment by ${size - maxSize} character(s).`
)
}
}
throw e
Expand Down
17 changes: 10 additions & 7 deletions components/chat/DiscussionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { useRouteProposalQuery } from '@hooks/queries/proposal'
import { useVotingPop } from '@components/VotePanel/hooks'
import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'
import { useLegacyVoterWeight } from '@hooks/queries/governancePower'
import {useVotingClients} from "@hooks/useVotingClients";
import { useVotingClients } from '@hooks/useVotingClients'
import { Wallet, useWallet } from '@solana/wallet-adapter-react'

const DiscussionForm = () => {
const [comment, setComment] = useState('')
Expand All @@ -27,21 +28,22 @@ const DiscussionForm = () => {
const realm = useRealmQuery().data?.result
const { result: ownVoterWeight } = useLegacyVoterWeight()
const { realmInfo } = useRealm()
const votingClients = useVotingClients();
const votingClients = useVotingClients()
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState('')

const wallet = useWalletOnePointOh()
const walletContext = useWallet()
const connected = !!wallet?.connected
const connection = useLegacyConnectionContext()
const proposal = useRouteProposalQuery().data?.result
const tokenRole = useVotingPop()
const commenterVoterTokenRecord =
tokenRole === 'community' ?
ownTokenRecord ?? ownCouncilTokenRecord :
ownCouncilTokenRecord
tokenRole === 'community'
? ownTokenRecord ?? ownCouncilTokenRecord
: ownCouncilTokenRecord

const votingClient = votingClients(tokenRole ?? 'community');// default to community if no role is provided
const votingClient = votingClients(tokenRole ?? 'community') // default to community if no role is provided
const submitComment = async () => {
setSubmitting(true)
setError('')
Expand Down Expand Up @@ -70,6 +72,7 @@ const DiscussionForm = () => {
try {
await postChatMessage(
rpcContext,
walletContext.wallet as Wallet,
realm,
proposal,
commenterVoterTokenRecord,
Expand All @@ -81,7 +84,7 @@ const DiscussionForm = () => {
setComment('')
} catch (ex) {
console.error("Can't post chat message", ex)
setError(ex.message);
setError(ex.message)
//TODO: How do we present transaction errors to users? Just the notification?
} finally {
setSubmitting(false)
Expand Down
3 changes: 3 additions & 0 deletions hooks/useSubmitVote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import { useBatchedVoteDelegators } from '@components/VotePanel/useDelegators'
import { useVotingClients } from '@hooks/useVotingClients'
import { useNftClient } from '../VoterWeightPlugins/useNftClient'
import { useRealmVoterWeightPlugins } from './useRealmVoterWeightPlugins'
import { Wallet, useWallet } from '@solana/wallet-adapter-react'

export const useSubmitVote = () => {
const wallet = useWalletOnePointOh()
const walletContext = useWallet()
const connection = useLegacyConnectionContext()
const realm = useRealmQuery().data?.result
const proposal = useRouteProposalQuery().data?.result
Expand Down Expand Up @@ -146,6 +148,7 @@ export const useSubmitVote = () => {
try {
await castVote(
rpcContext,
walletContext.wallet as Wallet,
realm,
proposal,
tokenOwnerRecordPk,
Expand Down
75 changes: 75 additions & 0 deletions utils/ephemeral-signers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { StandardWalletAdapter } from '@solana/wallet-adapter-base'
import { Wallet } from '@solana/wallet-adapter-react'
import { Keypair, PublicKey } from '@solana/web3.js'

/**
* Outputs `num` number of ephemeral signers for a transaction, designed to be used only in cases
* where SquadsX is the connected wallet, and a throwaway keypair is signing a transaction.
* @arg Wallet - A standard wallet context from @solana/wallet-adapter-react
* @arg num - The number of ephemeral signers to generate
* @returns - An array of ephemeral signer PublicKeys
*/
export async function getEphemeralSigners(
wallet: Wallet,
num: number
): Promise<PublicKey[]> {
let adapter = wallet.adapter as StandardWalletAdapter

const features = adapter.wallet.features

if (
adapter &&
'standard' in adapter &&
SquadsGetEphemeralSignersFeatureIdentifier in features
) {
const ephemeralSignerFeature = (await features[
SquadsGetEphemeralSignersFeatureIdentifier
]) as EphemeralSignerFeature

const ephemeralSigners = (await ephemeralSignerFeature.getEphemeralSigners(
num
)) as GetEphemeralSignersOutput

// WIP: Types for Solana wallet adapter features can be difficult
// @ts-ignore
return ephemeralSigners.map((signer) => new PublicKey(signer))
} else {
return [Keypair.generate().publicKey]
}
}

export type GetEphemeralSignersOutput = {
method: 'getEphemeralSigners'
result: {
ok: boolean
value: {
addresses: string[]
}
}
}

export const SquadsGetEphemeralSignersFeatureIdentifier = 'fuse:getEphemeralSigners' as const

export type WalletAdapterFeature<
FeatureName extends string,
FeatureProperties extends Record<string, any> = {},
FeatureMethods extends Record<string, (...args: any[]) => any> = {}
> = {
[K in FeatureName]: FeatureProperties &
{
[M in keyof FeatureMethods]: (
...args: Parameters<FeatureMethods[M]>
) => ReturnType<FeatureMethods[M]>
}
}

export type WalletWithEphemeralSigners = WalletAdapterFeature<
'standard',
{
'fuse:getEphemeralSigners': EphemeralSignerFeature
}
>

export type EphemeralSignerFeature = {
getEphemeralSigners: (num: number) => GetEphemeralSignersOutput
}
Loading

0 comments on commit 54acd3e

Please sign in to comment.