feat: add transaction history and epochs (#296)

This commit is contained in:
Vojtech Simetka 2023-03-31 19:02:58 +02:00 committed by GitHub
parent f744b1411f
commit a2803e6073
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 576 additions and 184 deletions

View File

@ -1,3 +1,3 @@
PUBLIC_PROVIDER=https://goerli-rollup.arbitrum.io/rpc
PUBLIC_GLOBAL_ANONYMOUS_FEED_ADDRESS=0x300c783d669185a1d2b5f46577d06bc16a60f97b
PUBLIC_ADAPTER=zkitter-js
PUBLIC_ADAPTER=in-memory

View File

@ -7,12 +7,17 @@ import type { Signer } from 'ethers'
import { create } from 'ipfs-http-client'
import {
CREATE_PERSONA_GO_PRICE,
DEFAULT_GO_AMOUNT,
NEW_POST_GO_PRICE,
NEW_POST_REP_LOSS,
NEW_POST_REP_PRICE,
NEW_POST_REP_WIN,
VOTE_GO_PRICE,
VOTE_REP_WIN,
} from '$lib/constants'
import { tokens } from '$lib/stores/tokens'
import { tokens, type TokenData } from '$lib/stores/tokens'
import { posts, type Post } from '$lib/stores/post'
import { transaction, type TransactionRecord } from '$lib/stores/transaction'
import type { Adapter } from '..'
import {
@ -69,6 +74,98 @@ function startAddition(): () => unknown {
}
}
export async function startNewEpoch() {
const newTransactions: TransactionRecord[] = []
const totalRepChange = await new Promise<number>((resolve) => {
posts.update((pState) => {
const { data } = pState
let repChange = 0
data.forEach((values, key) => {
const val = values
val.pending = []
values.pending.forEach((post) => {
const included = executeWithChance(0.5)
if (included) {
val.approved.push(post)
}
if (post.myPost) {
if (included) {
repChange += NEW_POST_REP_WIN
newTransactions.push({
timestamp: Date.now(),
goChange: 0,
repChange: NEW_POST_REP_WIN,
personaId: key,
type: 'post_included',
})
} else {
repChange -= NEW_POST_REP_LOSS
newTransactions.push({
timestamp: Date.now(),
goChange: 0,
repChange: NEW_POST_REP_WIN,
personaId: key,
type: 'post_rejected',
})
}
}
if (post.yourVote) {
if ((included && post.yourVote === '+') || (!included && post.yourVote === '-')) {
repChange += VOTE_REP_WIN
newTransactions.push({
timestamp: Date.now(),
goChange: 0,
repChange: VOTE_REP_WIN,
personaId: key,
type: 'vote_win',
})
}
}
})
data.set(key, val)
})
resolve(repChange)
return { data }
})
})
tokens.update((tState) => {
let { repStaked, repTotal, go } = tState
go = DEFAULT_GO_AMOUNT
repTotal = Math.max(repTotal + totalRepChange, 0)
repStaked = 0
return { ...tState, repStaked, repTotal, go }
})
transaction.update((tState) => {
return { ...tState, transaction: [...tState.transactions, newTransactions] }
})
}
function startEpochTimer(): () => unknown {
const interval = setInterval(() => {
tokens.update(({ timeToEpoch, epochDuration, ...rest }) => {
const newTimeToEpoch = epochDuration - (Date.now() % epochDuration)
if (timeToEpoch < newTimeToEpoch) {
startNewEpoch()
}
return { ...rest, epochDuration, timeToEpoch: newTimeToEpoch }
})
}, 1000)
return () => {
clearInterval(interval)
}
}
export class InMemoryAndIPFS implements Adapter {
private ipfs = create({
host: 'ipfs.infura.io',
@ -87,6 +184,66 @@ export class InMemoryAndIPFS implements Adapter {
const storedPosts = new Map<string, { approved: Post[]; pending: Post[]; loading: boolean }>(
getFromLocalStorage('posts', []),
)
const storedTransactions = getFromLocalStorage<TransactionRecord[]>('transactions', [])
const epochDuration = 5 * 60 * 1000
const storedTokens = getFromLocalStorage('tokens', {
go: DEFAULT_GO_AMOUNT,
repTotal: 55,
repStaked: 5,
loading: false,
goHistoricalValues: [],
repStakedHistoricalValues: [],
repTotalHistoricalValues: [],
epochDuration,
timeToEpoch: epochDuration - (Date.now() % epochDuration),
})
transaction.set({ transactions: storedTransactions })
tokens.set(storedTokens)
this.subscriptions.push(
tokens.subscribe((state) => {
let newState: TokenData | undefined = undefined
if (
state.goHistoricalValues.length === 0 ||
state.go !== state.goHistoricalValues[state.goHistoricalValues.length - 1].value
) {
newState = { ...state }
newState.goHistoricalValues.push({ timestamp: Date.now(), value: state.go })
}
if (
state.repStakedHistoricalValues.length === 0 ||
state.repStaked !==
state.repStakedHistoricalValues[state.repStakedHistoricalValues.length - 1].value
) {
if (newState === undefined) {
newState = { ...state }
}
newState.repStakedHistoricalValues.push({ timestamp: Date.now(), value: state.repStaked })
}
if (
state.repTotalHistoricalValues.length === 0 ||
state.repTotal !==
state.repTotalHistoricalValues[state.repTotalHistoricalValues.length - 1].value
) {
if (newState === undefined) {
newState = { ...state }
}
newState.repTotalHistoricalValues.push({ timestamp: Date.now(), value: state.repTotal })
}
if (newState !== undefined) {
tokens.update(() => state)
saveToLocalStorage('tokens', state)
}
}),
)
this.subscriptions.push(
transaction.subscribe(({ transactions }) => {
saveToLocalStorage('transactions', transactions)
}),
)
this.subscriptions.push(startEpochTimer())
// It takes 1 second to load all the data :)
await sleep(1000)
@ -205,8 +362,19 @@ export class InMemoryAndIPFS implements Adapter {
async publishPersona(draftPersona: DraftPersona, signer: Signer): Promise<void> {
await signer.signMessage('This "transaction" publishes persona')
// FIXME: it can happen that this ID already exists
const groupId = randomId()
function getRandomNonExistingId(): Promise<string> {
let groupId = randomId()
return new Promise((resolve) => {
personas.subscribe((s) => {
while (s.all.has(groupId)) {
groupId = randomId()
}
resolve(groupId)
})
})
}
const groupId = await getRandomNonExistingId()
personas.update((state) => {
state.all.set(groupId, {
@ -232,6 +400,17 @@ export class InMemoryAndIPFS implements Adapter {
tokens.update(({ go, ...state }) => {
return { ...state, go: go - CREATE_PERSONA_GO_PRICE }
})
transaction.update(({ transactions }) => {
transactions.push({
timestamp: Date.now(),
goChange: -CREATE_PERSONA_GO_PRICE,
repChange: 0,
personaId: groupId,
type: 'publish persona',
})
return { transactions }
})
}
async signIn(): Promise<void> {
@ -272,6 +451,17 @@ export class InMemoryAndIPFS implements Adapter {
})
posts.addPending(post, groupId)
transaction.update(({ transactions }) => {
transactions.push({
timestamp: Date.now(),
goChange: -NEW_POST_GO_PRICE,
repChange: -NEW_POST_REP_PRICE,
personaId: groupId,
type: 'publish post',
})
return { transactions }
})
}
async subscribePersonaPosts(groupId: string): Promise<() => unknown> {
@ -302,6 +492,16 @@ export class InMemoryAndIPFS implements Adapter {
go: go - VOTE_GO_PRICE,
}
})
transaction.update(({ transactions }) => {
transactions.push({
timestamp: Date.now(),
goChange: -VOTE_GO_PRICE,
repChange: 0,
type: vote === '+' ? 'promote' : 'demote',
personaId: groupId,
})
return { transactions }
})
}
startChat(chat: Chat): Promise<number> {

View File

@ -4,6 +4,7 @@ import type { Chat } from '$lib/stores/chat'
import { InMemoryAndIPFS } from './in-memory-and-ipfs'
import { ZkitterAdapter } from './zkitter'
import { ADAPTER } from '$lib/constants'
import { getFromLocalStorage } from '$lib/utils'
export interface Adapter {
// This is run when the app is mounted and should start app wide subscriptions
@ -32,8 +33,24 @@ export interface Adapter {
sendChatMessage(chatId: number, text: string): Promise<void>
subscribeToChat(chatId: number): () => void
}
export const adapters = ['in-memory', 'zkitter'] as const
export type AdapterName = (typeof adapters)[number]
export const adapterName: AdapterName = getFromLocalStorage<AdapterName>(
'adapter',
ADAPTER as AdapterName,
)
let adapter: Adapter
if (ADAPTER === 'in-memory') adapter = new InMemoryAndIPFS() as Adapter
else adapter = new ZkitterAdapter() as Adapter
switch (adapterName) {
case 'in-memory':
adapter = new InMemoryAndIPFS()
break
case 'zkitter':
adapter = new ZkitterAdapter()
break
default:
throw new Error(`Invalid adapter ${ADAPTER}`)
}
export default adapter

View File

@ -0,0 +1,24 @@
<script lang="ts">
let cls: string | undefined = undefined
export { cls as class }
interface GraphRecord {
timestamp: number
value: number
}
export let minX: number
export let maxX: number
export let minY = 0
export let maxY: number
export let values: GraphRecord[]
</script>
<div class={`root ${cls}`}>
x axis scale: {minX} - {maxX}
y axis scale: {minY} - {maxY}
values length: {values.length}
</div>
<style lang="scss">
</style>

View File

@ -1,31 +0,0 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<circle cx="25" cy="20" r="1" />
<path
d="M19.4141,30H15V25.5857l5.0337-5.0337A4.6069,4.6069,0,0,1,20,20a5,5,0,1,1,4.4478,4.9663ZM17,28h1.5859l5.2061-5.2063.5395.1238a3.0351,3.0351,0,1,0-2.249-2.2488l.1236.5393L17,26.4143Z"
/>
<rect x="6" y="8" width="2" height="8" />
<rect x="2" y="8" width="2" height="8" />
<rect x="18" y="8" width="2" height="6" />
<path
d="M14,16H12a2,2,0,0,1-2-2V10a2,2,0,0,1,2-2h2a2,2,0,0,1,2,2v4A2,2,0,0,1,14,16Zm-2-2h2V10H12Z"
/>
<rect x="2" y="18" width="2" height="8" />
<rect x="14" y="18" width="2" height="4" />
<path
d="M10,26H8a2,2,0,0,1-2-2V20a2,2,0,0,1,2-2h2a2,2,0,0,1,2,2v4A2,2,0,0,1,10,26ZM8,24h2V20H8Z"
/>
<rect x="2" y="2" width="2" height="4" />
<rect x="14" y="2" width="2" height="4" />
<rect x="18" y="2" width="2" height="4" />
<path d="M10,6H8A2,2,0,0,1,6,4V2H8V4h2V2h2V4A2,2,0,0,1,10,6Z" />
</svg>

View File

@ -1,25 +0,0 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<path d="M11,21H9V19a3.0033,3.0033,0,0,1,3-3h6v2H12a1.0011,1.0011,0,0,0-1,1Z" />
<path
d="M15,15a4,4,0,1,1,4-4A4.0045,4.0045,0,0,1,15,15Zm0-6a2,2,0,1,0,2,2A2.0021,2.0021,0,0,0,15,9Z"
/>
<path
d="M24,22a4,4,0,1,1,4-4A4.0045,4.0045,0,0,1,24,22Zm0-6a2,2,0,1,0,2,2A2.0021,2.0021,0,0,0,24,16Z"
/>
<path
d="M30,28H28V26a1.0011,1.0011,0,0,0-1-1H21a1.0011,1.0011,0,0,0-1,1v2H18V26a3.0033,3.0033,0,0,1,3-3h6a3.0033,3.0033,0,0,1,3,3Z"
/>
<path
d="M14,27.7334l-5.2344-2.791A8.9858,8.9858,0,0,1,4,17V4H24v6h2V4a2.0023,2.0023,0,0,0-2-2H4A2.0023,2.0023,0,0,0,2,4V17a10.9814,10.9814,0,0,0,5.8242,9.707L14,30Z"
/>
</svg>

View File

@ -1,14 +0,0 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<polygon points="16,14 6,24 7.4,25.4 16,16.8 24.6,25.4 26,24 " />
<rect x="4" y="8" width="24" height="2" />
</svg>

View File

@ -1,15 +0,0 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<path d="M12,4A5,5,0,1,1,7,9a5,5,0,0,1,5-5m0-2a7,7,0,1,0,7,7A7,7,0,0,0,12,2Z" />
<path d="M22,30H20V25a5,5,0,0,0-5-5H9a5,5,0,0,0-5,5v5H2V25a7,7,0,0,1,7-7h6a7,7,0,0,1,7,7Z" />
<polygon points="25 16.18 22.41 13.59 21 15 25 19 32 12 30.59 10.59 25 16.18" />
</svg>

View File

@ -1,14 +0,0 @@
<script lang="ts">
import type { IconProps } from '$lib/types'
type $$Props = IconProps
let cls: string | undefined = undefined
export { cls as class }
export let size = 20
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width={size} height={size} class={cls}>
<path d="M16,4a5,5,0,1,1-5,5,5,5,0,0,1,5-5m0-2a7,7,0,1,0,7,7A7,7,0,0,0,16,2Z" />
<path d="M26,30H24V25a5,5,0,0,0-5-5H13a5,5,0,0,0-5,5v5H6V25a7,7,0,0,1,7-7h6a7,7,0,0,1,7,7Z" />
</svg>

View File

@ -0,0 +1,17 @@
<script lang="ts">
let cls: string | undefined = undefined
export { cls as class }
export let variant: 'circle' | 'line' = 'line'
export let min = 0
export let max: number
export let current: number
</script>
<div class={`root ${cls}`}>
This will be progress bar in shape of {variant} with min value of {min}, max value {max} and current
value {current}
</div>
<style lang="scss">
</style>

View File

@ -1,24 +0,0 @@
<script lang="ts">
export let title: string | undefined = undefined
</script>
<div class="root">
<span class="title">{title}</span>
<slot />
</div>
<style lang="scss">
.root {
padding: var(--spacing-12);
@media (prefers-color-scheme: dark) {
color: var(--color-body-bg);
}
}
.title {
display: block;
margin-bottom: var(--spacing-6);
font-family: var(--font-body);
font-size: 14px;
color: var(--grey-400);
}
</style>

View File

@ -11,7 +11,11 @@ export const ADAPTER = PUBLIC_ADAPTER
export const CREATE_PERSONA_GO_PRICE = 10
export const NEW_POST_REP_PRICE = 5
export const NEW_POST_GO_PRICE = 5
export const NEW_POST_REP_WIN = 5
export const NEW_POST_REP_LOSS = 5
export const VOTE_GO_PRICE = 1
export const VOTE_REP_WIN = 1
export const DEFAULT_GO_AMOUNT = 30
export const MAX_DIMENSIONS = {
PERSONA_PICTURE: {

View File

@ -5,6 +5,7 @@ export interface Post {
text: string
images: string[]
yourVote?: '+' | '-'
myPost?: boolean
}
interface PostData {

View File

@ -1,16 +1,38 @@
import { DEFAULT_GO_AMOUNT } from '$lib/constants'
import { writable, type Writable } from 'svelte/store'
interface TokenValues {
timestamp: number
value: number
}
export interface TokenData {
go: number
repTotal: number
repStaked: number
loading: boolean
goHistoricalValues: TokenValues[]
repStakedHistoricalValues: TokenValues[]
repTotalHistoricalValues: TokenValues[]
epochDuration: number
timeToEpoch: number
}
export type TokenStore = Writable<TokenData>
function createTokenStore(): TokenStore {
const store = writable<TokenData>({ go: 30, repTotal: 55, repStaked: 5, loading: false })
const epochDuration = 8 * 60 * 60 * 1000
const store = writable<TokenData>({
go: DEFAULT_GO_AMOUNT,
repTotal: 55,
repStaked: 0,
loading: false,
goHistoricalValues: [],
repStakedHistoricalValues: [],
repTotalHistoricalValues: [],
epochDuration,
timeToEpoch: epochDuration - (Date.now() % epochDuration),
})
return store
}

View File

@ -0,0 +1,32 @@
import { writable, type Writable } from 'svelte/store'
type TransactionType =
| 'publish persona'
| 'promote'
| 'demote'
| 'publish post'
| 'vote_win'
| 'post_included'
| 'post_rejected'
export interface TransactionRecord {
timestamp: number
repChange: number
goChange: number
type: TransactionType
personaId: string
}
export interface HistoryData {
transactions: TransactionRecord[]
}
export type HistoryStore = Writable<HistoryData>
function createTokenStore(): HistoryStore {
const store = writable<HistoryData>({ transactions: [] })
return store
}
export const transaction = createTokenStore()

View File

@ -23,6 +23,18 @@ export function formatDateFromNow(timestamp: number) {
return `${Math.round(delta / year)}y`
}
export function formatEpoch(duration: number): string {
const second = 1000
const minute = second * 60
const hour = minute * 60
const hours = Math.floor(duration / hour)
const minutes = Math.floor((duration - hours * hour) / minute)
const seconds = Math.floor((duration - hours * hour - minutes * minute) / second)
return `${hours.toFixed()}h ${minutes.toFixed()}' ${seconds.toFixed()}''`
}
export function formatDateAndTime(timestamp: number) {
if (!browser) {
return ''

View File

@ -18,3 +18,16 @@
</script>
<slot />
<a class="dev" href="/dev">DEV DASHBOARD</a>
<style>
.dev {
position: fixed;
right: 0;
bottom: 0;
padding: 5px;
background: #000;
color: #fff;
font-size: 0.9rem;
}
</style>

View File

@ -1,40 +1,102 @@
<script lang="ts">
import Button from '$lib/components/button.svelte'
import ArrowRight from '$lib/components/icons/arrow-right.svelte'
import Close from '$lib/components/icons/close.svelte'
import CodeSigningService from '$lib/components/icons/code-signing-service.svelte'
import GroupSecurity from '$lib/components/icons/group-security.svelte'
import Renew from '$lib/components/icons/renew.svelte'
import UpToTop from '$lib/components/icons/up-to-top.svelte'
import UserAdmin from '$lib/components/icons/user-admin.svelte'
import User from '$lib/components/icons/user.svelte'
import Wallet from '$lib/components/icons/wallet.svelte'
import Edit from '$lib/components/icons/edit.svelte'
import Header from '$lib/components/header.svelte'
import Container from '$lib/components/container.svelte'
import Textarea from '$lib/components/textarea.svelte'
import { tokens } from '$lib/stores/tokens'
import Divider from '$lib/components/divider.svelte'
import { adapterName, adapters, type AdapterName } from '$lib/adapters'
import Dropdown from '$lib/components/dropdown.svelte'
import DropdownItem from '$lib/components/dropdown-item.svelte'
import Select from '$lib/components/select.svelte'
import { saveToLocalStorage } from '$lib/utils'
import { startNewEpoch } from '$lib/adapters/in-memory-and-ipfs'
import { formatDateAndTime } from '$lib/utils/format'
let goTokenValue = $tokens.go.toFixed()
let repTokenValue = $tokens.repTotal.toFixed()
function updateTokens() {
tokens.update((tokens) => {
tokens.go = parseInt(goTokenValue)
tokens.repTotal = parseInt(repTokenValue)
return tokens
})
}
function changeAdapter(adapterName: AdapterName) {
saveToLocalStorage('adapter', adapterName)
location.reload()
}
</script>
<h2>Buttons</h2>
<Button label="primary" variant="primary" />
<Button label="secondary" variant="secondary" />
<Button label="primary disabled" variant="primary" disabled />
<Button label="secondary disabled" variant="secondary" disabled />
<Button label="primary icon" variant="primary" icon={UpToTop} />
<Button label="secondary icon" variant="secondary" icon={UpToTop} />
<Button label="primary icon disabled" variant="primary" icon={UpToTop} disabled />
<Button label="secondary icon disabled" variant="secondary" icon={UpToTop} disabled />
<Button variant="primary" icon={UpToTop} />
<Button variant="secondary" icon={UpToTop} />
<Button variant="primary" icon={UpToTop} disabled />
<Button variant="secondary" icon={UpToTop} disabled />
<Header title="DEV DASHBOARD" onBack={() => history.back()} />
<Container>
<section>
<h2>Adapter</h2>
<Dropdown>
<Select slot="button" label="Reputation level" value={adapterName} />
<h2>Icons</h2>
{#each adapters as adapter}
<DropdownItem active={adapterName === adapter} onClick={() => changeAdapter(adapter)}
>{adapter}</DropdownItem
>
{/each}
</Dropdown>
</section>
<Divider />
{#if adapterName === 'in-memory'}
<section>
<h2>Epoch</h2>
<p>Epoch duration: {$tokens.epochDuration / 1000} seconds</p>
<p>Time to end epoch: {($tokens.timeToEpoch / 1000).toFixed()} seconds</p>
<Button label="End epoch now" variant="secondary" on:click={startNewEpoch} />
</section>
<Divider />
<section>
<Textarea label="Available GO token value" bind:value={goTokenValue} />
<Textarea label="Total REP token" bind:value={repTokenValue} />
<Button label="Update" on:click={updateTokens} />
</section>
<Divider />
<section>
<h2>GO historical values</h2>
{#each $tokens.goHistoricalValues as transaction}
<section>
{formatDateAndTime(transaction.timestamp)} |
{transaction.value} GO
</section>
{/each}
</section>
<Divider />
<section>
<h2>REP staked historical values</h2>
{#each $tokens.repStakedHistoricalValues as transaction}
<section>
{formatDateAndTime(transaction.timestamp)} |
{transaction.value} REP
</section>
{/each}
</section>
<Divider />
<section>
<h2>REP total historical values</h2>
{#each $tokens.repTotalHistoricalValues as transaction}
<section>
{formatDateAndTime(transaction.timestamp)} |
{transaction.value} REP
</section>
{/each}
</section>
{/if}
</Container>
<UpToTop />
<User />
<Wallet />
<GroupSecurity />
<ArrowRight />
<Close />
<CodeSigningService />
<Renew />
<UserAdmin />
<Edit />
<style lang="scss">
section {
margin-top: 1rem;
margin-bottom: 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@ -3,20 +3,44 @@
import Header from '$lib/components/header.svelte'
import Logout from '$lib/components/icons/logout.svelte'
import Wallet from '$lib/components/icons/wallet.svelte'
import WalletInfo from '$lib/components/wallet-info.svelte'
import { formatAddress } from '$lib/utils/format'
import SortAscending from '$lib/components/icons/sort-ascending.svelte'
import SortDescending from '$lib/components/icons/sort-descending.svelte'
import Divider from '$lib/components/divider.svelte'
import { formatAddress, formatDateAndTime, formatEpoch } from '$lib/utils/format'
import { canConnectWallet } from '$lib/services'
import { profile } from '$lib/stores/profile'
import adapter from '$lib/adapters'
import { tokens } from '$lib/stores/tokens'
import LearnMore from '$lib/components/learn-more.svelte'
import TokenInfo from '$lib/components/token-info.svelte'
import SectionTitle from '$lib/components/section-title.svelte'
import Search from '$lib/components/search.svelte'
import { transaction } from '$lib/stores/transaction'
import { ROUTES } from '$lib/routes'
import { personas } from '$lib/stores/persona'
import Progress from '$lib/components/progress.svelte'
import Graph from '$lib/components/graph.svelte'
let y: number
let error: Error | undefined = undefined
let filterQuery = ''
let sortAsc = true
</script>
<svelte:window bind:scrollY={y} />
<Header title="Account" onBack={() => history.back()} />
<Header title="Account" onBack={() => history.back()}>
<Button
variant="primary"
icon={Logout}
label="Logout"
on:click={() => ($profile = {})}
disabled={!$profile.signer}
/>
</Header>
<Divider />
<div class="content">
{#if $profile.signer === undefined}
<div class="wallet-icon-wrapper">
@ -26,7 +50,7 @@
<Button
variant="primary"
icon={Wallet}
label="Connect wallet to post"
label="Connect wallet"
on:click={adapter.signIn}
disabled={!canConnectWallet()}
/>
@ -42,26 +66,104 @@
{/if}
</div>
{:else}
<h2>Connected wallet</h2>
<div class="wallet-info-wrapper">
<WalletInfo title="Connected wallet">
{#await $profile.signer.getAddress()}
loading...
{:then address}
{formatAddress(address)}
{:catch error}
{error.message}
{/await}
</WalletInfo>
{#await $profile.signer.getAddress()}
loading...
{:then address}
{formatAddress(address)}
{:catch error}
{error.message}
{/await}
</div>
<div class="pad">
<Button
variant="primary"
icon={Logout}
label="Logout"
on:click={() => ($profile = {})}
disabled={!$profile.signer}
<Divider />
<h2>Cycle data</h2>
<div>
<h3>Current cycle</h3>
<Progress variant="circle" max={$tokens.epochDuration} current={$tokens.timeToEpoch} />
<div>{formatEpoch($tokens.timeToEpoch)} left in this cycle</div>
<LearnMore />
<div class="side-by-side">
<TokenInfo
title="Total reputation"
amount={$tokens.repTotal.toFixed()}
tokenName="REP"
explanation={`Including staked`}
/>
<TokenInfo
title="Currently available"
amount={$tokens.go.toFixed()}
tokenName="GO"
explanation="Until new cycle begins"
/>
</div>
</div>
<div>
<h2>Staked reputation</h2>
<Progress
variant="line"
max={$tokens.repTotal}
current={$tokens.repTotal - $tokens.repStaked}
/>
<p>
{$tokens.repTotal - $tokens.repStaked} out of {$tokens.repTotal} REP staked until cycle ends
</p>
<LearnMore />
</div>
<div>
<h2>Reputation over time</h2>
<Graph minX={0} maxX={100} minY={0} maxY={0} values={$tokens.repTotalHistoricalValues} />
<LearnMore />
</div>
<Divider />
<SectionTitle title="Activity">
<svelte:fragment slot="buttons">
{#if $profile.signer !== undefined}
<Button
icon={sortAsc ? SortAscending : SortDescending}
on:click={() => (sortAsc = !sortAsc)}
/>
{/if}
</svelte:fragment>
{#if $profile.signer !== undefined}
<Search bind:filterQuery />
{/if}
</SectionTitle>
{#each $transaction.transactions.sort( (a, b) => (sortAsc ? b.timestamp - a.timestamp : a.timestamp - b.timestamp), ) as t}
<div>
{#if t.goChange !== 0}
<h2>{t.goChange} GO</h2>
<div>
{#if t.type === 'publish persona'}
You created persona <a href={ROUTES.PERSONA(t.personaId)}
>{$personas.all.get(t.personaId)?.name}</a
>
{:else}
You {#if t.type === 'promote'}
promoted a post on
{:else if t.type === 'demote'}
demoted a post on
{:else if t.type === 'publish post'}
submitted a post to
{/if}
<a href={ROUTES.PERSONA_PENDING(t.personaId)}
>Pending • {$personas.all.get(t.personaId)?.name}</a
>
{/if}
<div>{formatDateAndTime(t.timestamp)}</div>
</div>
{/if}
{#if t.repChange !== 0}
<h2>Staked {t.repChange} REP</h2>
<div>
You submitted a post to <a href={ROUTES.PERSONA_PENDING(t.personaId)}
>Pending • {$personas.all.get(t.personaId)?.name}</a
>
<div>{formatDateAndTime(t.timestamp)}</div>
</div>
{/if}
</div>
{/each}
{/if}
</div>
@ -74,10 +176,19 @@
}
}
.side-by-side {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: center;
gap: var(--spacing-6);
}
.content {
display: flex;
flex-direction: column;
align-items: center;
margin-top: --var(--spacing-48);
.pad {
padding: var(--spacing-12);