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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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