Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chat): handle ephemeral signers for squadsx #24

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading