Skip to content

Commit

Permalink
feat: add session keys (alias) for offchain spaces (#294)
Browse files Browse the repository at this point in the history
* feat: add placeholder page for followings

* feat: redirect logged user to timeline when on homepage

* feat: add proposals timeline for followed spaces

* fix: fix proposal links when outside space route

* fix: fix invalid argument type

* chore: remove unused import

* fix: fix missing redirection on internal page nav

* fix: handle network dynamically

* feat: show space name in proposals list

* fix: avoid "no proposals" message flashing while logging in

* fix: remove auto redirection to timeline

* fix(ux): link space name to space

* fix: resolve author domain name

* fix: fix ignored space showing props

* fix(ui): fix proposal title not being truncated

* fix: rename routes

* fix: add sidebar to timeline pages

* fix(ui): add back right sidebar

* chore: remove debug output

* fix(ui): fix icon alignment

* chore: put back line break

* refactor: rename function to avoid confusion

* chore: return early on empty arg

* fiix(ux): fix missing message when user has no followed spaces

* fix(ux): fix feed app nav on small screen

* fix(ui): show right sidebar on explore page

* refactor: DRY

* fix(ux): redirect guest user to landing when visiting /home

* fix: reset proposals on account change

* fix: highlighted voted proposals

* refactor: code improvement

* refactor: use nested routes for home and explore page

* chore: remove leftover

* fix: improve routes

* chore: remove extra DIV

* fix: fix appNav visibility on small screen

* fix(ui): show search form on home and explore page

* refactor: improve code

* refactor: delegate logged in user checking to parent route

* refactor: fix order

* refactor: use computed value

* refactor: upgrade Follow to first class citizen

* fix: add missing function to unsupported networks

* fix: skip timeline for unsupported wallet

* refactor: move `loadFollows` to `useAccount` composable

* refactor: improve typing

* feat: replace space's star button by follow/unfollow status

* refactor: move starredSpaces to useAccount

* refactor: rename variable for consistency

* fix: auto load followed spaces for connected account

* fix: keep sidebar order when mixing starred and followed spaces

* fix: fix sidebar spaces not loading for guest user

* feat: support custom order for multiple wallets

* rerfactor: code improvement

* fix: fix followed space status not being loaded

* refactor: DRY

* refactor: avoid using computed value when not depending on ref

* refactor: remove redundant condition

* fix: fix wrong typing

* refactor: avoid using computed value for simple values

* feat: support offchain spaces follow and unfollow

* fix: add follow/unfollow to all networks type

* chore: update changeset

* chore: add missing line break

* refactor: use simple spaceId instead of Space object

* fix(ui): show loading icon while toggling space following

* fix(ux): disable button while working

* feat: add support for offchain alias (session key)

* fix: only prompt for alias tx signing

* fix(ui): allow sidebar scrolling

* fix: handle client_error thrown by sequencer

* chore: update changelog

* fix(ui): make sidebar scrollable

* fix(ui): remove tooltip on follow/unfollow button

* fix(ui): add red border to unfollow button

* fix: remove favorite feature on offchain spaces for starknet accounts

* Merge branch 'master' into show-followed-spaces-in-sidebar

* fix: remove network from space Id

* fix: move bookmarks to a Store

* refactor: code improvement

* refactor: code DRYing

* fix: populate spaces store

* fix: fix spaces fetching for multiple network type

* fix: fix variable name

* chore: code cleaning

* fix(ux): add missing loading icon while loading follow list in sidebar

* chore: remove debug output

* fix: finish merge conflict

* fix: fix wrong network for follow/unfollow actions

* chore: revert unrelated changes

* chore: revert unrelated changes

* chore: keep same properties order

* fix: make `from` arg optional

* fix: remove leftover file from conflict merge

* refactor: improve naming

* refactor: hard code alias to offchain network

* fix: use only recently created alias

* Update apps/ui/src/networks/offchain/api/index.ts

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

* refactor: remove redundant check

* refactor: use single public function  to handle handling alias

* refactor: add a buffer to alias expiration time

* fix: fix function signature

* fix: fix buffer computation

* refactor: variable renaming

---------

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>
  • Loading branch information
wa0x6e and Sekhmet authored May 29, 2024
1 parent 2d3b1fc commit ffb185d
Show file tree
Hide file tree
Showing 15 changed files with 209 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/thick-carrots-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": patch
---

add setAlias to OffchainEthereumSig
1 change: 1 addition & 0 deletions apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@ethersproject/abstract-signer": "^5.7.0",
"@ethersproject/address": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/constants": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/hash": "^5.7.0",
Expand Down
19 changes: 16 additions & 3 deletions apps/ui/src/composables/useActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const offchainNetworkId = offchainNetworks.filter(network => enabledNetworks.inc
export function useActions() {
const { mixpanel } = useMixpanel();
const uiStore = useUiStore();
const alias = useAlias();
const { web3 } = useWeb3();
const { getCurrentFromDuration } = useMetaStore();
const { modalAccountOpen } = useModal();
Expand Down Expand Up @@ -105,6 +106,14 @@ export function useActions() {
modalAccountOpen.value = true;
}

async function getAliasSigner() {
const network = getNetwork(offchainNetworkId);

return alias.getAliasWallet(address =>
wrapPromise(offchainNetworkId, network.actions.setAlias(auth.web3, address))
);
}

async function predictSpaceAddress(networkId: NetworkID, salt: string): Promise<string | null> {
if (!web3.value.account) {
forceLogin();
Expand Down Expand Up @@ -505,10 +514,9 @@ export function useActions() {
try {
await wrapPromise(
offchainNetworkId,
network.actions.followSpace(auth.web3, networkId, spaceId)
network.actions.followSpace(await getAliasSigner(), networkId, spaceId, web3.value.account)
);
} catch (e) {
console.log(e);
uiStore.addNotification('error', e.message);
return false;
}
Expand All @@ -527,7 +535,12 @@ export function useActions() {
try {
await wrapPromise(
offchainNetworkId,
network.actions.unfollowSpace(auth.web3, networkId, spaceId)
network.actions.unfollowSpace(
await getAliasSigner(),
networkId,
spaceId,
web3.value.account
)
);
} catch (e) {
uiStore.addNotification('error', e.message);
Expand Down
54 changes: 54 additions & 0 deletions apps/ui/src/composables/useAlias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Wallet } from '@ethersproject/wallet';
import { getDefaultProvider } from '@ethersproject/providers';
import { isHexString } from '@ethersproject/bytes';
import { enabledNetworks, getNetwork, offchainNetworks } from '@/networks';
import pkg from '../../package.json';

const ALIAS_AVAILABILITY_PERIOD = 60 * 60 * 24 * 30; // 30 days
const ALIAS_AVAILABILITY_BUFFER = 60 * 5; // 5 minutes

const aliases = useStorage(`${pkg.name}.aliases`, {} as Record<string, string>);

const networkId = offchainNetworks.filter(network => enabledNetworks.includes(network))[0];
const network = getNetwork(networkId);

export function useAlias() {
const provider = getDefaultProvider();
const { web3 } = useWeb3();

async function create(networkCreateActionFn: (address: string) => Promise<unknown>) {
const newAliasWallet = Wallet.createRandom();

await networkCreateActionFn(newAliasWallet.address);

aliases.value = {
...aliases.value,
...{
[web3.value.account]: newAliasWallet.privateKey
}
};

return new Wallet(newAliasWallet.privateKey, provider);
}

async function getExistingAliasWallet(privateKey: string) {
if (!isHexString(privateKey)) return null;

const registeredAlias = await network.api.loadAlias(
web3.value.account,
new Wallet(privateKey, provider).address,
Math.floor(Date.now() / 1000) - ALIAS_AVAILABILITY_PERIOD + ALIAS_AVAILABILITY_BUFFER
);

return registeredAlias ? new Wallet(privateKey, provider) : null;
}

async function getAliasWallet(networkCreateActionFn: (address: string) => Promise<unknown>) {
return (
(await getExistingAliasWallet(aliases.value[web3.value.account])) ||
(await create(networkCreateActionFn))
);
}

return { getAliasWallet };
}
3 changes: 3 additions & 0 deletions apps/ui/src/networks/common/graphqlApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,9 @@ export function createApi(uri: string, networkId: NetworkID, opts: ApiOptions =
},
loadFollows: async () => {
return [] as Follow[];
},
loadAlias: async () => {
return null;
}
};
}
3 changes: 2 additions & 1 deletion apps/ui/src/networks/evm/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ export function createActions(
);
},
followSpace: () => {},
unfollowSpace: () => {}
unfollowSpace: () => {},
setAlias: () => {}
};
}
24 changes: 18 additions & 6 deletions apps/ui/src/networks/offchain/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { EDITOR_APP_NAME, EDITOR_SNAPSHOT_OFFSET } from './constants';
import { getUrl } from '@/helpers/utils';
import { getProvider } from '@/helpers/provider';
import { getSwapLink } from '@/helpers/link';
import type { Web3Provider } from '@ethersproject/providers';
import { Web3Provider } from '@ethersproject/providers';
import type { Wallet } from '@ethersproject/wallet';
import type { StrategyParsedMetadata, Choice, Proposal, Space, VoteType, NetworkID } from '@/types';
import type {
ReadOnlyNetworkActions,
Expand Down Expand Up @@ -202,16 +203,27 @@ export function createActions(
};
});
},
followSpace(web3: Web3Provider, networkId: NetworkID, spaceId: string) {
followSpace(web3: Web3Provider | Wallet, networkId: NetworkID, spaceId: string, from?: string) {
return client.followSpace({
signer: web3.getSigner(),
data: { network: networkId, space: spaceId }
signer: web3 instanceof Web3Provider ? web3.getSigner() : web3,
data: { network: networkId, space: spaceId, ...(from ? { from } : {}) }
});
},
unfollowSpace(web3: Web3Provider, networkId: NetworkID, spaceId: string) {
unfollowSpace(
web3: Web3Provider | Wallet,
networkId: NetworkID,
spaceId: string,
from?: string
) {
return client.unfollowSpace({
signer: web3 instanceof Web3Provider ? web3.getSigner() : web3,
data: { network: networkId, space: spaceId, ...(from ? { from } : {}) }
});
},
setAlias(web3: Web3Provider, alias: string) {
return client.setAlias({
signer: web3.getSigner(),
data: { network: networkId, space: spaceId }
data: { alias }
});
}
};
Expand Down
24 changes: 22 additions & 2 deletions apps/ui/src/networks/offchain/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
PROPOSAL_QUERY,
USER_VOTES_QUERY,
USER_FOLLOWS_QUERY,
VOTES_QUERY
VOTES_QUERY,
ALIASES_QUERY
} from './queries';
import { PaginationOpts, SpacesFilter, NetworkApi, ProposalsFilter } from '@/networks/types';
import { getNames } from '@/helpers/stamp';
Expand All @@ -19,7 +20,8 @@ import {
NetworkID,
ProposalState,
SpaceMetadataTreasury,
Follow
Follow,
Alias
} from '@/types';
import { ApiSpace, ApiProposal, ApiVote } from './types';
import { DEFAULT_VOTING_DELAY } from '../constants';
Expand Down Expand Up @@ -380,6 +382,24 @@ export function createApi(uri: string, networkId: NetworkID): NetworkApi {
...follow,
space: { ...follow.space, network: follow.network }
}));
},
loadAlias: async (
address: string,
aliasAddress: string,
created_gt: number
): Promise<Alias | null> => {
const {
data: { aliases }
}: { data: { aliases: Alias[] } } = await apollo.query({
query: ALIASES_QUERY,
variables: {
address,
alias: aliasAddress,
created_gt
}
});

return aliases?.[0] ?? null;
}
};
}
9 changes: 9 additions & 0 deletions apps/ui/src/networks/offchain/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,12 @@ export const VOTES_QUERY = gql`
}
}
`;

export const ALIASES_QUERY = gql`
query Aliases($address: String!, $alias: String!, $created_gt: Int) {
aliases(where: { address: $address, alias: $alias, created_gt: $created_gt }) {
address
alias
}
}
`;
1 change: 1 addition & 0 deletions apps/ui/src/networks/starknet/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ export function createActions(
},
followSpace: () => {},
unfollowSpace: () => {},
setAlias: () => {},
send: (envelope: any) => starkSigClient.send(envelope) // TODO: extract it out of client to common helper
};
}
10 changes: 7 additions & 3 deletions apps/ui/src/networks/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FunctionalComponent } from 'vue';
import type { Web3Provider } from '@ethersproject/providers';
import type { Wallet } from '@ethersproject/wallet';
import type { MetaTransaction } from '@snapshot-labs/sx/dist/utils/encoding';
import type {
Space,
Expand All @@ -10,7 +11,8 @@ import type {
Choice,
NetworkID,
StrategyParsedMetadata,
Follow
Follow,
Alias
} from '@/types';

export type PaginationOpts = { limit: number; skip?: number };
Expand Down Expand Up @@ -125,8 +127,9 @@ export type ReadOnlyNetworkActions = {
proposal: Proposal,
choice: Choice
): Promise<any>;
followSpace(web3: Web3Provider, networkId: NetworkID, spaceId: string);
unfollowSpace(web3: Web3Provider, networkId: NetworkID, spaceId: string);
followSpace(web3: Web3Provider | Wallet, networkId: NetworkID, spaceId: string, from?: string);
unfollowSpace(web3: Web3Provider | Wallet, networkId: NetworkID, spaceId: string, from?: string);
setAlias(web3: Web3Provider, alias: string);
send(envelope: any): Promise<any>;
};

Expand Down Expand Up @@ -212,6 +215,7 @@ export type NetworkApi = {
sortBy?: 'vote_count-desc' | 'vote_count-asc' | 'proposal_count-desc' | 'proposal_count-asc'
): Promise<User[]>;
loadFollows(userId?: string, spaceId?: string): Promise<Follow[]>;
loadAlias(address: string, alias: string, created_gt: number): Promise<Alias | null>;
};

export type NetworkConstants = {
Expand Down
5 changes: 5 additions & 0 deletions apps/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ export type Follow = {
network: NetworkID;
};

export type Alias = {
address: string;
alias: string;
};

export type Contact = {
address: string;
name: string;
Expand Down
55 changes: 37 additions & 18 deletions packages/sx.js/src/clients/offchain/ethereum-sig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,28 @@ import {
updateProposalTypes,
cancelProposalTypes,
followSpaceTypes,
unfollowSpaceTypes
unfollowSpaceTypes,
aliasTypes
} from './types';
import type { Signer, TypedDataSigner, TypedDataField } from '@ethersproject/abstract-signer';
import type {
SignatureData,
Envelope,
Vote,
Propose,
UpdateProposal,
CancelProposal,
FollowSpace,
UnfollowSpace,
EIP712Message,
EIP712VoteMessage,
EIP712ProposeMessage,
EIP712UpdateProposal,
EIP712CancelProposalMessage,
EIP712FollowSpaceMessage,
EIP712UnfollowSpaceMessage
import {
type SignatureData,
type Envelope,
type Vote,
type Propose,
type UpdateProposal,
type CancelProposal,
type FollowSpace,
type UnfollowSpace,
type SetAlias,
type EIP712Message,
type EIP712VoteMessage,
type EIP712ProposeMessage,
type EIP712UpdateProposal,
type EIP712CancelProposalMessage,
type EIP712FollowSpaceMessage,
type EIP712UnfollowSpaceMessage,
type EIP712SetAliasMessage
} from '../types';
import type { OffchainNetworkConfig } from '../../../types';

Expand Down Expand Up @@ -61,6 +64,7 @@ export class EthereumSig {
| EIP712CancelProposalMessage
| EIP712FollowSpaceMessage
| EIP712UnfollowSpaceMessage
| EIP712SetAliasMessage
>(
signer: Signer & TypedDataSigner,
message: T,
Expand All @@ -86,7 +90,7 @@ export class EthereumSig {

public async send(
envelope: Envelope<
Vote | Propose | UpdateProposal | CancelProposal | FollowSpace | UnfollowSpace
Vote | Propose | UpdateProposal | CancelProposal | FollowSpace | UnfollowSpace | SetAlias
>
) {
const { address, signature: sig, domain, types, message } = envelope.signatureData!;
Expand Down Expand Up @@ -254,4 +258,19 @@ export class EthereumSig {
data
};
}

public async setAlias({
signer,
data
}: {
signer: Signer & TypedDataSigner;
data: SetAlias;
}): Promise<Envelope<SetAlias>> {
const signatureData = await this.sign(signer, data, aliasTypes);

return {
signatureData,
data
};
}
}
8 changes: 8 additions & 0 deletions packages/sx.js/src/clients/offchain/ethereum-sig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,11 @@ export const unfollowSpaceTypes = {
{ name: 'timestamp', type: 'uint64' }
]
};

export const aliasTypes = {
Alias: [
{ name: 'from', type: 'address' },
{ name: 'alias', type: 'address' },
{ name: 'timestamp', type: 'uint64' }
]
};
Loading

0 comments on commit ffb185d

Please sign in to comment.