Skip to content

Commit

Permalink
feat: add basic voting for offchain spaces (#36)
Browse files Browse the repository at this point in the history
* fix: show voting buttons for offchain proposals

* refactor: move the network actions to actions.ts

* feat: add support for basic voting for offchain proposals

* fix: fix invalid import type

* fix: use correct sequencer network depending on chainId

* chore: add tests

* Update packages/sx.js/src/clients/offchain/ethereum-sig/index.ts

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>

* refactor: use same function order definition as other networks

* fix: handle offchain receipt without tx_hash

* fix: fix typing

* fix: throw error on sequencer error response

* chore: add unit test

* chore: fix tests

* fix: show voting form only for supported voting type

* chore: remove cumbersome describe block

* fix: switch to correct chain for offchain actions

* fix: avoid changing type

* chore: improve test

* fix: revert network enforcer

* fix: remove unused import

* Update packages/sx.js/src/clients/starknet/starknet-tx/index.ts

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>

* chore: update changeset

---------

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>
  • Loading branch information
wa0x6e and Sekhmet authored Feb 19, 2024
1 parent c5ab6ce commit 827b7b3
Show file tree
Hide file tree
Showing 21 changed files with 498 additions and 102 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-parrots-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": minor
---

add OffchainEthereumSig client with basic vote support
2 changes: 1 addition & 1 deletion apps/ui/src/components/ProposalVote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const isSupported = computed(() => {
network.helpers.isStrategySupported(strategy)
);
return hasSupportedAuthenticator && hasSupportedStrategies;
return hasSupportedAuthenticator && hasSupportedStrategies && props.proposal.type === 'basic';
});
</script>

Expand Down
12 changes: 7 additions & 5 deletions apps/ui/src/composables/useActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import { getReadWriteNetwork } from '@/networks';
import { getNetwork, getReadWriteNetwork } from '@/networks';
import { registerTransaction } from '@/helpers/mana';
import { convertToMetaTransactions } from '@/helpers/transactions';
import type {
Expand Down Expand Up @@ -49,7 +49,7 @@ export function useActions() {

async function handleCommitEnvelope(envelope: any, networkId: NetworkID) {
// TODO: it should work with WalletConnect, should be done before L1 transaction is broadcasted
const network = getReadWriteNetwork(networkId);
const network = getNetwork(networkId);

if (envelope?.signatureData?.commitHash && network.baseNetworkId) {
await registerTransaction(network.chainId, {
Expand All @@ -74,7 +74,7 @@ export function useActions() {
}

async function wrapPromise(networkId: NetworkID, promise: Promise<any>) {
const network = getReadWriteNetwork(networkId);
const network = getNetwork(networkId);

const envelope = await promise;

Expand All @@ -84,9 +84,11 @@ export function useActions() {
// TODO: unify send/soc to both return txHash under same property
if (envelope.signatureData || envelope.sig) {
const receipt = await network.actions.send(envelope);
const hash = receipt.transaction_hash || receipt.hash;

console.log('Receipt', receipt);
uiStore.addPendingTransaction(receipt.transaction_hash || receipt.hash, networkId);

hash && uiStore.addPendingTransaction(hash, networkId);
} else {
uiStore.addPendingTransaction(envelope.transaction_hash || envelope.hash, networkId);
}
Expand Down Expand Up @@ -185,7 +187,7 @@ export function useActions() {
async function vote(proposal: Proposal, choice: Choice) {
if (!web3.value.account) return await forceLogin();

const network = getReadWriteNetwork(proposal.network);
const network = getNetwork(proposal.network);

await wrapPromise(
proposal.network,
Expand Down
95 changes: 95 additions & 0 deletions apps/ui/src/networks/offchain/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { OffchainNetworkConfig, clients, offchainGoerli, offchainMainnet } from '@snapshot-labs/sx';
import { getSdkChoice } from './helpers';
import type { Web3Provider } from '@ethersproject/providers';
import type { StrategyParsedMetadata, Choice, Proposal } from '@/types';
import type {
ReadOnlyNetworkActions,
NetworkConstants,
NetworkHelpers,
SnapshotInfo,
VotingPower,
Connector
} from '../types';

const SCORE_URL = 'https://score.snapshot.org';
const CONFIGS: Record<number, OffchainNetworkConfig> = {
1: offchainMainnet,
5: offchainGoerli
};

export function createActions(
constants: NetworkConstants,
helpers: NetworkHelpers,
chainId: number
): ReadOnlyNetworkActions {
const networkConfig = CONFIGS[chainId];

const client = new clients.OffchainEthereumSig({
networkConfig
});

return {
vote(
web3: Web3Provider,
connectorType: Connector,
account: string,
proposal: Proposal,
choice: Choice
): Promise<any> {
const data = {
space: proposal.space.id,
proposal: proposal.proposal_id as string,
choice: getSdkChoice(choice),
authenticator: '',
strategies: [],
metadataUri: ''
};

return client.vote({
signer: web3.getSigner(),
data
});
},
send: (envelope: any) => client.send(envelope),
getVotingPower: async (
strategiesAddresses: string[],
strategiesParams: any[],
strategiesMetadata: StrategyParsedMetadata[],
voterAddress: string,
snapshotInfo: SnapshotInfo
): Promise<VotingPower[]> => {
const result = await fetch(SCORE_URL, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'get_vp',
params: {
address: voterAddress,
space: '',
strategies: strategiesParams,
network: snapshotInfo.chainId ?? chainId,
snapshot: snapshotInfo.at ?? 'latest'
}
})
});
const body = await result.json();

return body.result.vp_by_strategy.map((vp: number, index: number) => {
const strategy = strategiesParams[index];
const decimals = parseInt(strategy.params.decimals || 0);

return {
address: strategy.name,
value: BigInt(vp * 10 ** decimals),
decimals,
symbol: strategy.params.symbol,
token: strategy.params.address,
chainId: strategy.network ? parseInt(strategy.network) : undefined
} as VotingPower;
});
}
};
}
6 changes: 4 additions & 2 deletions apps/ui/src/networks/offchain/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { getNames } from '@/helpers/stamp';
import { Space, Proposal, Vote, User, NetworkID, ProposalState } from '@/types';
import { ApiSpace, ApiProposal, ApiVote } from './types';

const DEFAULT_AUTHENTICATOR = 'OffchainAuthenticator';

function getProposalState(proposal: ApiProposal): ProposalState {
if (proposal.state === 'closed') {
if (proposal.scores_total < proposal.quorum) return 'rejected';
Expand Down Expand Up @@ -61,7 +63,7 @@ function formatSpace(space: ApiSpace, networkId: NetworkID): Space {
: [],
// NOTE: ignored
created: 0,
authenticators: [],
authenticators: [DEFAULT_AUTHENTICATOR],
executors: [],
executors_types: [],
strategies: space.strategies.map(strategy => strategy.name),
Expand Down Expand Up @@ -111,7 +113,7 @@ function formatProposal(proposal: ApiProposal, networkId: NetworkID): Proposal {
avatar: '',
controller: proposal.space.admins[0] ?? '',
voting_power_symbol: proposal.space.symbol,
authenticators: [],
authenticators: [DEFAULT_AUTHENTICATOR],
executors: [],
executors_types: [],
strategies_parsed_metadata: []
Expand Down
7 changes: 7 additions & 0 deletions apps/ui/src/networks/offchain/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Choice } from '@/types';

export function getSdkChoice(choice: Choice): number {
if (choice === 'for') return 1;
if (choice === 'against') return 2;
return 3;
}
62 changes: 12 additions & 50 deletions apps/ui/src/networks/offchain/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
import { createApi } from './api';
import * as constants from './constants';
import { createActions } from './actions';
import { pinPineapple } from '@/helpers/pin';
import { Network, VotingPower, SnapshotInfo } from '@/networks/types';
import { NetworkID, StrategyParsedMetadata } from '@/types';
import { Network } from '@/networks/types';
import { NetworkID } from '@/types';
import networks from '@/helpers/networks.json';

const HUB_URLS: Partial<Record<NetworkID, string | undefined>> = {
s: 'https://hub.snapshot.org/graphql',
's-tn': 'https://testnet.hub.snapshot.org/graphql'
};
const SCORE_URL = 'https://score.snapshot.org';
const SNAPSHOT_URLS: Partial<Record<NetworkID, string | undefined>> = {
s: 'https://snapshot.org',
's-tn': 'https://testnet.snapshot.org'
};
const CHAIN_IDS: Partial<Record<NetworkID, number>> = {
s: 1,
's-tn': 5
};

export function createOffchainNetwork(networkId: NetworkID): Network {
const l1ChainId = 1;

const l1ChainId = CHAIN_IDS[networkId];
const hubUrl = HUB_URLS[networkId];
if (!hubUrl) throw new Error(`Unknown network ${networkId}`);
if (!hubUrl || !l1ChainId) throw new Error(`Unknown network ${networkId}`);

const api = createApi(hubUrl, networkId);

const helpers = {
isAuthenticatorSupported: () => false,
isAuthenticatorSupported: () => true,
isAuthenticatorContractSupported: () => false,
getRelayerAuthenticatorType: () => null,
isStrategySupported: () => false,
isStrategySupported: () => true,
isExecutorSupported: () => false,
pin: pinPineapple,
waitForTransaction: () => {
Expand Down Expand Up @@ -70,47 +73,6 @@ export function createOffchainNetwork(networkId: NetworkID): Network {
api,
constants,
helpers,
actions: {
getVotingPower: async (
strategiesAddresses: string[],
strategiesParams: any[],
strategiesMetadata: StrategyParsedMetadata[],
voterAddress: string,
snapshotInfo: SnapshotInfo
): Promise<VotingPower[]> => {
const result = await fetch(SCORE_URL, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
method: 'get_vp',
params: {
address: voterAddress,
space: '',
strategies: strategiesParams,
network: snapshotInfo.chainId ?? l1ChainId,
snapshot: snapshotInfo.at ?? 'latest'
}
})
});
const body = await result.json();

return body.result.vp_by_strategy.map((vp: number, index: number) => {
const strategy = strategiesParams[index];
const decimals = parseInt(strategy.params.decimals || 0);

return {
address: strategy.name,
value: BigInt(vp * 10 ** decimals),
decimals,
symbol: strategy.params.symbol,
token: strategy.params.address,
chainId: strategy.network ? parseInt(strategy.network) : undefined
} as VotingPower;
});
}
}
actions: createActions(constants, helpers, l1ChainId)
};
}
18 changes: 9 additions & 9 deletions apps/ui/src/networks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,22 @@ export type VotingPower = {

// TODO: make sx.js accept Signer instead of Web3Provider | Wallet

type ReadOnlyNetworkActions = {
export type ReadOnlyNetworkActions = {
getVotingPower(
strategiesAddresses: string[],
strategiesParams: any[],
strategiesMetadata: StrategyParsedMetadata[],
voterAddress: string,
snapshotInfo: SnapshotInfo
): Promise<VotingPower[]>;
vote(
web3: Web3Provider,
connectorType: Connector,
account: string,
proposal: Proposal,
choice: Choice
): Promise<any>;
send(envelope: any): Promise<any>;
};

export type NetworkActions = ReadOnlyNetworkActions & {
Expand Down Expand Up @@ -135,13 +143,6 @@ export type NetworkActions = ReadOnlyNetworkActions & {
transactions: MetaTransaction[]
);
cancelProposal(web3: Web3Provider, proposal: Proposal);
vote(
web3: Web3Provider,
connectorType: Connector,
account: string,
proposal: Proposal,
choice: Choice
);
finalizeProposal(web3: Web3Provider, proposal: Proposal);
receiveProposal(web3: Web3Provider, proposal: Proposal);
executeTransactions(web3: Web3Provider, proposal: Proposal);
Expand All @@ -167,7 +168,6 @@ export type NetworkActions = ReadOnlyNetworkActions & {
delegatee: string,
delegationContract: string
);
send(envelope: any): Promise<any>;
};

export type NetworkApi = {
Expand Down
Loading

0 comments on commit 827b7b3

Please sign in to comment.