mirror of
https://github.com/acid-info/Kurate.git
synced 2025-02-11 23:36:25 +00:00
feat: add transaction history and epochs (#296)
This commit is contained in:
parent
f744b1411f
commit
a2803e6073
@ -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
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
|
24
packages/ui/src/lib/components/graph.svelte
Normal file
24
packages/ui/src/lib/components/graph.svelte
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
17
packages/ui/src/lib/components/progress.svelte
Normal file
17
packages/ui/src/lib/components/progress.svelte
Normal 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>
|
@ -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>
|
@ -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: {
|
||||
|
@ -5,6 +5,7 @@ export interface Post {
|
||||
text: string
|
||||
images: string[]
|
||||
yourVote?: '+' | '-'
|
||||
myPost?: boolean
|
||||
}
|
||||
|
||||
interface PostData {
|
||||
|
@ -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
|
||||
}
|
||||
|
32
packages/ui/src/lib/stores/transaction.ts
Normal file
32
packages/ui/src/lib/stores/transaction.ts
Normal 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()
|
@ -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 ''
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user