From 4a42a94a6697bdc050a81f171d6f734d73b0e26f Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Thu, 7 Oct 2021 21:11:28 -0400 Subject: [PATCH 01/33] eden-subchain-client --- packages/box/src/handlers/subchain.ts | 2 +- packages/box/src/subchain-storage.ts | 2 +- packages/eden-subchain-client/README.md | 41 +++++++++++++ .../docs/nodejs_quickstart.md | 54 ++++++++++++++++++ .../docs/react_quickstart.md | 57 +++++++++++++++++++ packages/eden-subchain-client/package.json | 19 +++++++ .../src}/EdenSubchain.ts | 1 - .../src}/ReactSubchain.tsx | 24 ++------ .../src}/SubchainClient.ts | 44 +++++++------- .../src}/SubchainProtocol.ts | 0 .../src}/index.ts | 0 .../eden-subchain-client/tsconfig.build.json | 12 ++++ packages/eden-subchain-client/tsconfig.json | 8 +++ .../src/components/header.tsx | 2 +- .../example-history-app/src/pages/_app.tsx | 37 +++++++----- .../src/pages/graphiql.tsx | 3 +- .../example-history-app/src/pages/index.tsx | 2 +- .../src/pages/simple_members.tsx | 2 +- packages/webapp/src/_app/hooks/box-queries.ts | 5 +- packages/webapp/src/pages/_app.tsx | 28 ++++----- packages/webapp/src/pages/as/index.tsx | 2 +- packages/webapp/src/pages/graphiql.tsx | 4 +- 22 files changed, 270 insertions(+), 79 deletions(-) create mode 100644 packages/eden-subchain-client/README.md create mode 100644 packages/eden-subchain-client/docs/nodejs_quickstart.md create mode 100644 packages/eden-subchain-client/docs/react_quickstart.md create mode 100644 packages/eden-subchain-client/package.json rename packages/{common/src/subchain => eden-subchain-client/src}/EdenSubchain.ts (98%) rename packages/{common/src/subchain => eden-subchain-client/src}/ReactSubchain.tsx (85%) rename packages/{common/src/subchain => eden-subchain-client/src}/SubchainClient.ts (82%) rename packages/{common/src/subchain => eden-subchain-client/src}/SubchainProtocol.ts (100%) rename packages/{common/src/subchain => eden-subchain-client/src}/index.ts (100%) create mode 100644 packages/eden-subchain-client/tsconfig.build.json create mode 100644 packages/eden-subchain-client/tsconfig.json diff --git a/packages/box/src/handlers/subchain.ts b/packages/box/src/handlers/subchain.ts index 28d53db21..aaf29776d 100644 --- a/packages/box/src/handlers/subchain.ts +++ b/packages/box/src/handlers/subchain.ts @@ -10,7 +10,7 @@ import { ClientStatus, ServerMessage, sanitizeClientStatus, -} from "@edenos/common/dist/subchain/SubchainProtocol"; +} from "@edenos/eden-subchain-client/dist/SubchainProtocol"; const storage = new Storage(); const dfuseReceiver = new DfuseReceiver(storage); diff --git a/packages/box/src/subchain-storage.ts b/packages/box/src/subchain-storage.ts index 662477d7d..e80472bc8 100644 --- a/packages/box/src/subchain-storage.ts +++ b/packages/box/src/subchain-storage.ts @@ -1,4 +1,4 @@ -import { EdenSubchain } from "@edenos/common/dist/subchain"; +import { EdenSubchain } from "@edenos/eden-subchain-client/dist/EdenSubchain"; import * as config from "./config"; import * as fs from "fs"; import logger from "./logger"; diff --git a/packages/eden-subchain-client/README.md b/packages/eden-subchain-client/README.md new file mode 100644 index 000000000..7e06d6fbe --- /dev/null +++ b/packages/eden-subchain-client/README.md @@ -0,0 +1,41 @@ +# EdenOS Subchain Client + +## Overview + +The Eden webapp uses a subchain to track history. This subchain contains a subset of eosio blocks relevant to the Eden contract's history. `eden-micro-chain.wasm`, which runs in both nodejs and in browsers, produces and consumes this subchain. It answers GraphQL queries about the contract history. Here's a typical setup: + +### Box server maintains the chain +``` ++--------+ +----------+ +------+ +----------+ +-----------+ +| dfuse | => | Relevant | => | wasm | => | subchain | => | websocket | +| client | | History | | | | blocks | | | ++--------+ +----------+ | | +----------+ +-----------+ + | | +----------+ +-----------+ + | | => | current | => | http GET | + | | | state | | | + +------+ +----------+ +-----------+ +``` + +### nodejs and web clients consume the chain +``` ++-----------+ +----------+ +------+ +----------+ +--------+ +| websocket | => | subchain | => | wasm | <= | GraphQL | <= | client | +| | | blocks | | | | Query | | code | ++-----------+ +----------+ | | +----------+ | | ++-----------+ +----------+ | | +----------+ | | +| http GET | => | initial | => | | => | GraphQL | => | | +| | | state | | | | Response | | | ++-----------+ +----------+ +------+ +----------+ +--------+ +``` + +When a client starts up, it fetches a copy of `eden-micro-chain.wasm` and a copy of the most-recent state from the Box server. It then subscribes to block updates through a websocket connection. + +https://genesis.eden.eoscommunity.org/ uses https://box.prod.eoscommunity.org . Third parties may host apps which use https://box.prod.eoscommunity.org to gain access to Eden history. + +Third parties may also host their own box server instances. The box server sources live at https://github.com/eoscommunity/Eden/tree/main/packages/box . The box container is available as `ghcr.io/eoscommunity/eden-box:`, https://github.com/eoscommunity/Eden/pkgs/container/eden-box . + +## Quick-start guides + +[Nodejs Quickstart](docs/nodejs_quickstart.md) + +[React Quickstart](docs/react_quickstart.md) diff --git a/packages/eden-subchain-client/docs/nodejs_quickstart.md b/packages/eden-subchain-client/docs/nodejs_quickstart.md new file mode 100644 index 000000000..8c528f199 --- /dev/null +++ b/packages/eden-subchain-client/docs/nodejs_quickstart.md @@ -0,0 +1,54 @@ +# Nodejs Quickstart + +## Minimal Example + +This starts up, runs a single query, and exits: + +``` +const SubchainClient = require('./dist/SubchainClient.js').default; +const fetch = require('node-fetch'); +const ws = require('ws'); + +const box = "box.prod.eoscommunity.org/v1/subchain"; + +(async () => { + try { + // Start up the client + const client = new SubchainClient(ws); + await client.instantiateStreaming({ + wasmResponse: fetch(`https://${box}/eden-micro-chain.wasm`), + stateResponse: fetch(`https://${box}/state`), + blocksUrl: `wss://${box}/eden-microchain`, + }); + + // Run a query + const member = client.subchain.query(` + { + members(ge:"dlarimer.gm", le:"dlarimer.gm") { + edges { + node { + account + participating + profile { + name + social + } + } + } + } + }`).data?.members.edges[0]?.node; + + // Show result + console.log(JSON.stringify(member, null, 4)); + process.exit(0); + } catch (e) { + console.error(e); + process.exit(1); + } +})(); +``` +## Notes + +Keep the client object around after it has started. It will receive blocks over time through its websocket connection. It has automatic retry when the websocket goes down. Don't frequently create new instances; this will waste resources. + +The `query` method only throws an exception when something major goes wrong. If this happens then discard the client object and instantiate a new one. Once it throws, it cannot recover. diff --git a/packages/eden-subchain-client/docs/react_quickstart.md b/packages/eden-subchain-client/docs/react_quickstart.md new file mode 100644 index 000000000..58c0f9790 --- /dev/null +++ b/packages/eden-subchain-client/docs/react_quickstart.md @@ -0,0 +1,57 @@ +# Nodejs Quickstart + +## Minimal Example + +This displays the current Eden treasury. It automatically updates whenever the treasury changes. + +``` +import React from "react"; +import ReactDOM from "react-dom"; +import { + EdenChainContext, + useCreateEdenChain, + useQuery, +} from "../dist2/ReactSubchain"; + +const box = "box.prod.eoscommunity.org/v1/subchain"; + +// Top of the react tree +function Top() { + // Start up the client + const subchain = useCreateEdenChain({ + wasmResponse: fetch(`https://${box}/eden-micro-chain.wasm`), + stateResponse: fetch(`https://${box}/state`), + blocksUrl: `wss://${box}/eden-microchain`, + }); + + return ( + // Provide the client to the children + + + + ); +} + +const treasuryQuery = ` +{ + masterPool { + amount + } +}`; + +// Display the Eden treasury +function Treasury() { + // This queries the subchain. It automatically triggers a rerender + // any time new blocks arrive. + const result = useQuery(treasuryQuery); + + // The query result, when available, is in result.data + return ( +

+ The treasury contains {result.data?.masterPool.amount} +

+ ); +} + +ReactDOM.render(, document.body); +``` diff --git a/packages/eden-subchain-client/package.json b/packages/eden-subchain-client/package.json new file mode 100644 index 000000000..3de20b05c --- /dev/null +++ b/packages/eden-subchain-client/package.json @@ -0,0 +1,19 @@ +{ + "name": "@edenos/eden-subchain-client", + "description": "Eden Subchain client", + "version": "0.1.0", + "license": "MIT", + "main": "dist/index", + "types": "dist/index", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn run clean && yarn run compile", + "clean": "rimraf -rf ./dist", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "yarn run build", + "lint": "eslint --ext .js,.ts src", + "test": "echo" + } +} diff --git a/packages/common/src/subchain/EdenSubchain.ts b/packages/eden-subchain-client/src/EdenSubchain.ts similarity index 98% rename from packages/common/src/subchain/EdenSubchain.ts rename to packages/eden-subchain-client/src/EdenSubchain.ts index 1a12bd521..f54b94f97 100644 --- a/packages/common/src/subchain/EdenSubchain.ts +++ b/packages/eden-subchain-client/src/EdenSubchain.ts @@ -64,7 +64,6 @@ export class EdenSubchain { this.imports ); } else { - console.log("instantiateStreaming missing; using fallback"); const module = await WebAssembly.compile( await (await Promise.resolve(response)).arrayBuffer() ); diff --git a/packages/common/src/subchain/ReactSubchain.tsx b/packages/eden-subchain-client/src/ReactSubchain.tsx similarity index 85% rename from packages/common/src/subchain/ReactSubchain.tsx rename to packages/eden-subchain-client/src/ReactSubchain.tsx index e7d07f88f..044dbf9ad 100644 --- a/packages/common/src/subchain/ReactSubchain.tsx +++ b/packages/eden-subchain-client/src/ReactSubchain.tsx @@ -1,15 +1,8 @@ -import SubchainClient from "./SubchainClient"; +import SubchainClient, { SubchainClientOptions } from "./SubchainClient"; import { useContext, useEffect, useState, createContext } from "react"; export function useCreateEdenChain( - edenAccount: string, - tokenAccount: string, - atomicAccount: string, - atomicmarketAccount: string, - wasmUrl: string, - stateUrl: string, - wsUrl: string, - slowMo: boolean + options: SubchainClientOptions ): SubchainClient | null { const [subchain, setSubchain] = useState(null); useEffect(() => { @@ -18,17 +11,8 @@ export function useCreateEdenChain( (async () => { try { console.log("create SubchainClient"); - client = new SubchainClient(); - await client.instantiateStreaming( - edenAccount, - tokenAccount, - atomicAccount, - atomicmarketAccount, - fetch(wasmUrl), - fetch(stateUrl), - wsUrl, - slowMo - ); + client = new SubchainClient(WebSocket); + await client.instantiateStreaming(options); setSubchain(client); } catch (e) { console.error(e); diff --git a/packages/common/src/subchain/SubchainClient.ts b/packages/eden-subchain-client/src/SubchainClient.ts similarity index 82% rename from packages/common/src/subchain/SubchainClient.ts rename to packages/eden-subchain-client/src/SubchainClient.ts index aadecaf2a..fb629aedf 100644 --- a/packages/common/src/subchain/SubchainClient.ts +++ b/packages/eden-subchain-client/src/SubchainClient.ts @@ -5,30 +5,34 @@ import { sanitizeServerMessage, } from "./SubchainProtocol"; +export interface SubchainClientOptions { + edenAccount?: string; + tokenAccount?: string; + atomicAccount?: string; + atomicmarketAccount?: string; + wasmResponse: PromiseLike; + stateResponse: PromiseLike; + blocksUrl: string; + slowmo?: boolean; +} + export default class SubchainClient { subchain = new EdenSubchain(); shuttingDown = false; blocksUrl = ""; slowmo = false; - ws: WebSocket | null = null; + ws: any; notifications: ((client: SubchainClient) => void)[] = []; - async instantiateStreaming( - edenAccount: string, - tokenAccount: string, - atomicAccount: string, - atomicmarketAccount: string, - wasmResponse: PromiseLike, - stateResponse: PromiseLike, - blocksUrl: string, - slowmo = false - ) { - this.blocksUrl = blocksUrl; - this.slowmo = slowmo; + constructor(public WebSocket: any) {} + + async instantiateStreaming(options: SubchainClientOptions) { + this.blocksUrl = options.blocksUrl; + this.slowmo = !!options.slowmo; if (this.shuttingDown) return this.shutdown(); const [, state] = await Promise.all([ - this.subchain.instantiateStreaming(wasmResponse), - stateResponse.then((resp) => { + this.subchain.instantiateStreaming(options.wasmResponse), + options.stateResponse.then((resp) => { if (resp.ok) return resp.arrayBuffer(); return null; }), @@ -38,10 +42,10 @@ export default class SubchainClient { this.subchain.setMemory(state); } else { this.subchain.initializeMemory( - edenAccount, - tokenAccount, - atomicAccount, - atomicmarketAccount + options.edenAccount || "genesis.eden", + options.tokenAccount || "eosio.token", + options.atomicAccount || "atomicassets", + options.atomicmarketAccount || "atomicmarket" ); } this.connect(); @@ -49,7 +53,7 @@ export default class SubchainClient { connect() { if (this.shuttingDown) return this.shutdown(); - this.ws = new WebSocket(this.blocksUrl); + this.ws = new this.WebSocket(this.blocksUrl); this.ws.onclose = () => { console.error("Closed connection to " + this.blocksUrl); this.ws = null; diff --git a/packages/common/src/subchain/SubchainProtocol.ts b/packages/eden-subchain-client/src/SubchainProtocol.ts similarity index 100% rename from packages/common/src/subchain/SubchainProtocol.ts rename to packages/eden-subchain-client/src/SubchainProtocol.ts diff --git a/packages/common/src/subchain/index.ts b/packages/eden-subchain-client/src/index.ts similarity index 100% rename from packages/common/src/subchain/index.ts rename to packages/eden-subchain-client/src/index.ts diff --git a/packages/eden-subchain-client/tsconfig.build.json b/packages/eden-subchain-client/tsconfig.build.json new file mode 100644 index 000000000..e71223c31 --- /dev/null +++ b/packages/eden-subchain-client/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "resolveJsonModule": true, + "strict": true, + "outDir": "./dist", + "jsx": "react" + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/packages/eden-subchain-client/tsconfig.json b/packages/eden-subchain-client/tsconfig.json new file mode 100644 index 000000000..d6d23b783 --- /dev/null +++ b/packages/eden-subchain-client/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "resolveJsonModule": true, + "strict": true, + "jsx": "react" + } +} \ No newline at end of file diff --git a/packages/example-history-app/src/components/header.tsx b/packages/example-history-app/src/components/header.tsx index 818d3d2f4..4cb4fec6c 100644 --- a/packages/example-history-app/src/components/header.tsx +++ b/packages/example-history-app/src/components/header.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { useQuery } from "@edenos/common/dist/subchain"; +import { useQuery } from "@edenos/eden-subchain-client/dist/ReactSubchain"; export default function Header() { const info = useQuery(`{ diff --git a/packages/example-history-app/src/pages/_app.tsx b/packages/example-history-app/src/pages/_app.tsx index b3712c562..3ff848cb2 100644 --- a/packages/example-history-app/src/pages/_app.tsx +++ b/packages/example-history-app/src/pages/_app.tsx @@ -1,8 +1,9 @@ import { AppProps } from "next/app"; +import fetch from "node-fetch"; import { useCreateEdenChain, EdenChainContext, -} from "@edenos/common/dist/subchain"; +} from "@edenos/eden-subchain-client/dist/ReactSubchain"; import "../../../../node_modules/graphiql/graphiql.min.css"; if ( @@ -19,21 +20,27 @@ if ( } const MyApp = ({ Component, pageProps }: AppProps) => { - const subchain = useCreateEdenChain( - process.env.NEXT_PUBLIC_EDEN_CONTRACT || "genesis.eden", - process.env.NEXT_PUBLIC_TOKEN_CONTRACT || "eosio.token", - process.env.NEXT_PUBLIC_AA_CONTRACT || "atomicassets", - process.env.NEXT_PUBLIC_AA_MARKET_CONTRACT || "atomicmarket", - process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL || - "http://localhost:3032/v1/subchain/eden-micro-chain.wasm", - process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true" - ? "bad_state_file_name_for_slow_mo" - : process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL || - "http://localhost:3032/v1/subchain/state", - process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL || + const subchain = useCreateEdenChain({ + edenAccount: process.env.NEXT_PUBLIC_EDEN_CONTRACT || "genesis.eden", + tokenAccount: process.env.NEXT_PUBLIC_TOKEN_CONTRACT || "eosio.token", + atomicAccount: process.env.NEXT_PUBLIC_AA_CONTRACT || "atomicassets", + atomicmarketAccount: + process.env.NEXT_PUBLIC_AA_MARKET_CONTRACT || "atomicmarket", + wasmResponse: fetch( + process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL || + "http://localhost:3032/v1/subchain/eden-micro-chain.wasm" + ) as any, + stateResponse: fetch( + process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true" + ? "bad_state_file_name_for_slow_mo" + : process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL || + "http://localhost:3032/v1/subchain/state" + ) as any, + blocksUrl: + process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL || "ws://localhost:3032/v1/subchain/eden-microchain", - process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true" - ); + slowmo: process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true", + }); return ( diff --git a/packages/example-history-app/src/pages/graphiql.tsx b/packages/example-history-app/src/pages/graphiql.tsx index 5669f524e..2f2f7c566 100644 --- a/packages/example-history-app/src/pages/graphiql.tsx +++ b/packages/example-history-app/src/pages/graphiql.tsx @@ -3,7 +3,8 @@ import GraphiQL from "graphiql"; import Header from "../components/header"; import { buildSchema, GraphQLSchema } from "graphql"; import { useContext } from "react"; -import { EdenChainContext, EdenSubchain } from "@edenos/common/dist/subchain"; +import { EdenChainContext } from "@edenos/eden-subchain-client/dist/ReactSubchain"; +import { EdenSubchain } from "@edenos/eden-subchain-client/dist/EdenSubchain"; function createFetcher(subchain: EdenSubchain) { return async ({ query }: { query: string }) => subchain.query(query); diff --git a/packages/example-history-app/src/pages/index.tsx b/packages/example-history-app/src/pages/index.tsx index ec812e003..f428192d1 100644 --- a/packages/example-history-app/src/pages/index.tsx +++ b/packages/example-history-app/src/pages/index.tsx @@ -1,6 +1,6 @@ import Head from "next/head"; import Header from "../components/header"; -import { usePagedQuery } from "@edenos/common/dist/subchain"; +import { usePagedQuery } from "@edenos/eden-subchain-client/dist/ReactSubchain"; const query = ` { diff --git a/packages/example-history-app/src/pages/simple_members.tsx b/packages/example-history-app/src/pages/simple_members.tsx index 4d2ef18ab..408000350 100644 --- a/packages/example-history-app/src/pages/simple_members.tsx +++ b/packages/example-history-app/src/pages/simple_members.tsx @@ -1,7 +1,7 @@ import Head from "next/head"; import Header from "../components/header"; import { Fragment } from "react"; -import { usePagedQuery } from "@edenos/common/dist/subchain"; +import { usePagedQuery } from "@edenos/eden-subchain-client/dist/ReactSubchain"; const query = ` { diff --git a/packages/webapp/src/_app/hooks/box-queries.ts b/packages/webapp/src/_app/hooks/box-queries.ts index ea2d5c006..aed247f05 100644 --- a/packages/webapp/src/_app/hooks/box-queries.ts +++ b/packages/webapp/src/_app/hooks/box-queries.ts @@ -1,4 +1,7 @@ -import { Query, useQuery } from "@edenos/common/dist/subchain"; +import { + Query, + useQuery, +} from "@edenos/eden-subchain-client/dist/ReactSubchain"; import dayjs from "dayjs"; import { MemberAccountData } from "members"; import { assetFromString } from "_app"; diff --git a/packages/webapp/src/pages/_app.tsx b/packages/webapp/src/pages/_app.tsx index fbaadfa0e..37c7f70d8 100644 --- a/packages/webapp/src/pages/_app.tsx +++ b/packages/webapp/src/pages/_app.tsx @@ -14,7 +14,7 @@ import * as advancedFormat from "dayjs/plugin/advancedFormat"; import { useCreateEdenChain, EdenChainContext, -} from "@edenos/common/dist/subchain"; +} from "@edenos/eden-subchain-client/dist/ReactSubchain"; import { EdenUALProvider, Store, Toaster } from "_app"; @@ -45,18 +45,20 @@ Modal.setAppElement("#__next"); export const queryClient = new QueryClient(); const WebApp = ({ Component, pageProps }: AppProps) => { - const subchain = useCreateEdenChain( - process.env.NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT!, - process.env.NEXT_PUBLIC_TOKEN_CONTRACT!, - process.env.NEXT_PUBLIC_AA_CONTRACT!, - process.env.NEXT_PUBLIC_AA_MARKET_CONTRACT!, - process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL!, - process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true" - ? "bad_state_file_name_for_slow_mo" - : process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL!, - process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL!, - process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true" - ); + const subchain = useCreateEdenChain({ + edenAccount: process.env.NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT!, + tokenAccount: process.env.NEXT_PUBLIC_TOKEN_CONTRACT!, + atomicAccount: process.env.NEXT_PUBLIC_AA_CONTRACT!, + atomicmarketAccount: process.env.NEXT_PUBLIC_AA_MARKET_CONTRACT!, + wasmResponse: fetch(process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL!), + stateResponse: fetch( + process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true" + ? "bad_state_file_name_for_slow_mo" + : process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL! + ), + blocksUrl: process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL!, + slowmo: process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true", + }); return ( diff --git a/packages/webapp/src/pages/as/index.tsx b/packages/webapp/src/pages/as/index.tsx index d9c335ff1..19df155a5 100644 --- a/packages/webapp/src/pages/as/index.tsx +++ b/packages/webapp/src/pages/as/index.tsx @@ -1,5 +1,5 @@ import { useRouter } from "next/router"; -import { usePagedQuery } from "@edenos/common/dist/subchain"; +import { usePagedQuery } from "@edenos/eden-subchain-client/dist/ReactSubchain"; import { Container, SideNavLayout, Heading } from "_app"; export const AsPage = () => { diff --git a/packages/webapp/src/pages/graphiql.tsx b/packages/webapp/src/pages/graphiql.tsx index dbd4ae316..1c828f8ab 100644 --- a/packages/webapp/src/pages/graphiql.tsx +++ b/packages/webapp/src/pages/graphiql.tsx @@ -4,9 +4,9 @@ import { buildSchema, GraphQLSchema } from "graphql"; import { useContext } from "react"; import { EdenChainContext, - EdenSubchain, useQuery, -} from "@edenos/common/dist/subchain"; +} from "@edenos/eden-subchain-client/dist/ReactSubchain"; +import { EdenSubchain } from "@edenos/eden-subchain-client/dist/EdenSubchain"; import "../../../../node_modules/graphiql/graphiql.min.css"; function createFetcher(subchain: EdenSubchain) { From 40bc439ae92f85f8c0d927d4bd462ba2b4c4c4ba Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Thu, 7 Oct 2021 22:54:41 -0400 Subject: [PATCH 02/33] eden-subchain-client --- .../docs/nodejs_quickstart.md | 49 +++++++++++-------- .../docs/react_quickstart.md | 8 ++- packages/eden-subchain-client/package.json | 2 +- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/eden-subchain-client/docs/nodejs_quickstart.md b/packages/eden-subchain-client/docs/nodejs_quickstart.md index 8c528f199..3e068a358 100644 --- a/packages/eden-subchain-client/docs/nodejs_quickstart.md +++ b/packages/eden-subchain-client/docs/nodejs_quickstart.md @@ -2,27 +2,34 @@ ## Minimal Example -This starts up, runs a single query, and exits: +This starts up, runs a single query, and exits. + +It needs the following NPM packages: +* `@edenos/eden-subchain-client` +* `eosjs` (eden-subchain-client needs it) +* `node-fetch` +* `ws` ``` -const SubchainClient = require('./dist/SubchainClient.js').default; -const fetch = require('node-fetch'); +const SubchainClient = require('@edenos/eden-subchain-client/dist/SubchainClient.js').default; const ws = require('ws'); const box = "box.prod.eoscommunity.org/v1/subchain"; (async () => { - try { - // Start up the client - const client = new SubchainClient(ws); - await client.instantiateStreaming({ - wasmResponse: fetch(`https://${box}/eden-micro-chain.wasm`), - stateResponse: fetch(`https://${box}/state`), - blocksUrl: `wss://${box}/eden-microchain`, - }); - - // Run a query - const member = client.subchain.query(` + try { + const fetch = (await import('node-fetch')).default; + + // Start up the client + const client = new SubchainClient(ws); + await client.instantiateStreaming({ + wasmResponse: fetch(`https://${box}/eden-micro-chain.wasm`), + stateResponse: fetch(`https://${box}/state`), + blocksUrl: `wss://${box}/eden-microchain`, + }); + + // Run a query + const member = client.subchain.query(` { members(ge:"dlarimer.gm", le:"dlarimer.gm") { edges { @@ -38,13 +45,13 @@ const box = "box.prod.eoscommunity.org/v1/subchain"; } }`).data?.members.edges[0]?.node; - // Show result - console.log(JSON.stringify(member, null, 4)); - process.exit(0); - } catch (e) { - console.error(e); - process.exit(1); - } + // Show result + console.log(JSON.stringify(member, null, 4)); + process.exit(0); + } catch (e) { + console.error(e); + process.exit(1); + } })(); ``` ## Notes diff --git a/packages/eden-subchain-client/docs/react_quickstart.md b/packages/eden-subchain-client/docs/react_quickstart.md index 58c0f9790..57fdefc7c 100644 --- a/packages/eden-subchain-client/docs/react_quickstart.md +++ b/packages/eden-subchain-client/docs/react_quickstart.md @@ -4,6 +4,12 @@ This displays the current Eden treasury. It automatically updates whenever the treasury changes. +It needs the following NPM packages: +* `@edenos/eden-subchain-client` +* `eosjs` (eden-subchain-client needs it) +* `react` +* `react-dom` + ``` import React from "react"; import ReactDOM from "react-dom"; @@ -11,7 +17,7 @@ import { EdenChainContext, useCreateEdenChain, useQuery, -} from "../dist2/ReactSubchain"; +} from "@edenos/eden-subchain-client/dist/ReactSubchain"; const box = "box.prod.eoscommunity.org/v1/subchain"; diff --git a/packages/eden-subchain-client/package.json b/packages/eden-subchain-client/package.json index 3de20b05c..9ccf0ea35 100644 --- a/packages/eden-subchain-client/package.json +++ b/packages/eden-subchain-client/package.json @@ -1,7 +1,7 @@ { "name": "@edenos/eden-subchain-client", "description": "Eden Subchain client", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "main": "dist/index", "types": "dist/index", From 99f946761d7f3c528cebd8de450c71132f539bd9 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Fri, 8 Oct 2021 10:56:43 -0400 Subject: [PATCH 03/33] Move fetch --- packages/eden-subchain-client/README.md | 2 +- .../docs/nodejs_quickstart.md | 2 +- .../docs/react_quickstart.md | 7 +++-- packages/eden-subchain-client/package.json | 2 +- .../src/ReactSubchain.tsx | 29 ++++++++++++++++--- .../src/SubchainProtocol.ts | 2 -- 6 files changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/eden-subchain-client/README.md b/packages/eden-subchain-client/README.md index 7e06d6fbe..056bc96c3 100644 --- a/packages/eden-subchain-client/README.md +++ b/packages/eden-subchain-client/README.md @@ -30,7 +30,7 @@ The Eden webapp uses a subchain to track history. This subchain contains a subse When a client starts up, it fetches a copy of `eden-micro-chain.wasm` and a copy of the most-recent state from the Box server. It then subscribes to block updates through a websocket connection. -https://genesis.eden.eoscommunity.org/ uses https://box.prod.eoscommunity.org . Third parties may host apps which use https://box.prod.eoscommunity.org to gain access to Eden history. +https://genesis.eden.eoscommunity.org/ uses https://box.prod.eoscommunity.org . Third parties may host apps which also use https://box.prod.eoscommunity.org to gain access to Eden history. Third parties may also host their own box server instances. The box server sources live at https://github.com/eoscommunity/Eden/tree/main/packages/box . The box container is available as `ghcr.io/eoscommunity/eden-box:`, https://github.com/eoscommunity/Eden/pkgs/container/eden-box . diff --git a/packages/eden-subchain-client/docs/nodejs_quickstart.md b/packages/eden-subchain-client/docs/nodejs_quickstart.md index 3e068a358..770716bc0 100644 --- a/packages/eden-subchain-client/docs/nodejs_quickstart.md +++ b/packages/eden-subchain-client/docs/nodejs_quickstart.md @@ -10,7 +10,7 @@ It needs the following NPM packages: * `node-fetch` * `ws` -``` +```js const SubchainClient = require('@edenos/eden-subchain-client/dist/SubchainClient.js').default; const ws = require('ws'); diff --git a/packages/eden-subchain-client/docs/react_quickstart.md b/packages/eden-subchain-client/docs/react_quickstart.md index 57fdefc7c..d5a078bc1 100644 --- a/packages/eden-subchain-client/docs/react_quickstart.md +++ b/packages/eden-subchain-client/docs/react_quickstart.md @@ -10,7 +10,7 @@ It needs the following NPM packages: * `react` * `react-dom` -``` +```js import React from "react"; import ReactDOM from "react-dom"; import { @@ -25,8 +25,9 @@ const box = "box.prod.eoscommunity.org/v1/subchain"; function Top() { // Start up the client const subchain = useCreateEdenChain({ - wasmResponse: fetch(`https://${box}/eden-micro-chain.wasm`), - stateResponse: fetch(`https://${box}/state`), + fetch, + wasmUrl: `https://${box}/eden-micro-chain.wasm`, + stateUrl: `https://${box}/state`, blocksUrl: `wss://${box}/eden-microchain`, }); diff --git a/packages/eden-subchain-client/package.json b/packages/eden-subchain-client/package.json index 9ccf0ea35..bf1112764 100644 --- a/packages/eden-subchain-client/package.json +++ b/packages/eden-subchain-client/package.json @@ -1,7 +1,7 @@ { "name": "@edenos/eden-subchain-client", "description": "Eden Subchain client", - "version": "0.1.1", + "version": "0.1.2", "license": "MIT", "main": "dist/index", "types": "dist/index", diff --git a/packages/eden-subchain-client/src/ReactSubchain.tsx b/packages/eden-subchain-client/src/ReactSubchain.tsx index 044dbf9ad..690d3b859 100644 --- a/packages/eden-subchain-client/src/ReactSubchain.tsx +++ b/packages/eden-subchain-client/src/ReactSubchain.tsx @@ -1,18 +1,39 @@ import SubchainClient, { SubchainClientOptions } from "./SubchainClient"; import { useContext, useEffect, useState, createContext } from "react"; +export interface UseSubchainClientOptions { + fetch: any; + edenAccount?: string; + tokenAccount?: string; + atomicAccount?: string; + atomicmarketAccount?: string; + wasmUrl: string; + stateUrl: string; + blocksUrl: string; + slowmo?: boolean; +} + export function useCreateEdenChain( - options: SubchainClientOptions + options: UseSubchainClientOptions ): SubchainClient | null { const [subchain, setSubchain] = useState(null); useEffect(() => { - if (typeof window !== "undefined") { + if (options.fetch) { let client: SubchainClient; (async () => { try { - console.log("create SubchainClient"); + const fetch = options.fetch; client = new SubchainClient(WebSocket); - await client.instantiateStreaming(options); + await client.instantiateStreaming({ + edenAccount: options.edenAccount, + tokenAccount: options.tokenAccount, + atomicAccount: options.atomicAccount, + atomicmarketAccount: options.atomicmarketAccount, + wasmResponse: fetch(options.wasmUrl), + stateResponse: fetch(options.stateUrl), + blocksUrl: options.blocksUrl, + slowmo: options.slowmo, + }); setSubchain(client); } catch (e) { console.error(e); diff --git a/packages/eden-subchain-client/src/SubchainProtocol.ts b/packages/eden-subchain-client/src/SubchainProtocol.ts index f56f1c8b8..3ee64a5f2 100644 --- a/packages/eden-subchain-client/src/SubchainProtocol.ts +++ b/packages/eden-subchain-client/src/SubchainProtocol.ts @@ -1,5 +1,3 @@ -import { array } from "zod"; - export interface BlockInfo { num: number; id: string; From b94a506b6ae1c1d7c399845cf54d401adb07c958 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Fri, 15 Oct 2021 11:18:05 -0400 Subject: [PATCH 04/33] eden-subchain-client --- .../example-history-app/src/pages/_app.tsx | 25 +++++++------------ packages/webapp/src/pages/_app.tsx | 16 ++++++------ 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/packages/example-history-app/src/pages/_app.tsx b/packages/example-history-app/src/pages/_app.tsx index 3ff848cb2..bedeb8c1e 100644 --- a/packages/example-history-app/src/pages/_app.tsx +++ b/packages/example-history-app/src/pages/_app.tsx @@ -21,24 +21,17 @@ if ( const MyApp = ({ Component, pageProps }: AppProps) => { const subchain = useCreateEdenChain({ - edenAccount: process.env.NEXT_PUBLIC_EDEN_CONTRACT || "genesis.eden", - tokenAccount: process.env.NEXT_PUBLIC_TOKEN_CONTRACT || "eosio.token", - atomicAccount: process.env.NEXT_PUBLIC_AA_CONTRACT || "atomicassets", - atomicmarketAccount: - process.env.NEXT_PUBLIC_AA_MARKET_CONTRACT || "atomicmarket", - wasmResponse: fetch( - process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL || - "http://localhost:3032/v1/subchain/eden-micro-chain.wasm" - ) as any, - stateResponse: fetch( + fetch, + edenAccount: process.env.NEXT_PUBLIC_EDEN_CONTRACT, + tokenAccount: process.env.NEXT_PUBLIC_TOKEN_CONTRACT, + atomicAccount: process.env.NEXT_PUBLIC_AA_CONTRACT, + atomicmarketAccount: process.env.NEXT_PUBLIC_AA_MARKET_CONTRACT, + wasmUrl: process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL!, + stateUrl: process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true" ? "bad_state_file_name_for_slow_mo" - : process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL || - "http://localhost:3032/v1/subchain/state" - ) as any, - blocksUrl: - process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL || - "ws://localhost:3032/v1/subchain/eden-microchain", + : process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL!, + blocksUrl: process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL!, slowmo: process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true", }); return ( diff --git a/packages/webapp/src/pages/_app.tsx b/packages/webapp/src/pages/_app.tsx index 37c7f70d8..7f20b8421 100644 --- a/packages/webapp/src/pages/_app.tsx +++ b/packages/webapp/src/pages/_app.tsx @@ -46,16 +46,16 @@ export const queryClient = new QueryClient(); const WebApp = ({ Component, pageProps }: AppProps) => { const subchain = useCreateEdenChain({ - edenAccount: process.env.NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT!, - tokenAccount: process.env.NEXT_PUBLIC_TOKEN_CONTRACT!, - atomicAccount: process.env.NEXT_PUBLIC_AA_CONTRACT!, - atomicmarketAccount: process.env.NEXT_PUBLIC_AA_MARKET_CONTRACT!, - wasmResponse: fetch(process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL!), - stateResponse: fetch( + fetch, + edenAccount: process.env.NEXT_PUBLIC_EDEN_CONTRACT, + tokenAccount: process.env.NEXT_PUBLIC_TOKEN_CONTRACT, + atomicAccount: process.env.NEXT_PUBLIC_AA_CONTRACT, + atomicmarketAccount: process.env.NEXT_PUBLIC_AA_MARKET_CONTRACT, + wasmUrl: process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL!, + stateUrl: process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true" ? "bad_state_file_name_for_slow_mo" - : process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL! - ), + : process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL!, blocksUrl: process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL!, slowmo: process.env.NEXT_PUBLIC_SUBCHAIN_SLOW_MO === "true", }); From e7d026c774ac8029c1fadce97c01cb2f733d0283 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Fri, 15 Oct 2021 12:01:11 -0400 Subject: [PATCH 05/33] eden-subchain-client --- packages/example-history-app/src/pages/_app.tsx | 3 +-- packages/webapp/src/pages/_app.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/example-history-app/src/pages/_app.tsx b/packages/example-history-app/src/pages/_app.tsx index bedeb8c1e..e5ebc5f23 100644 --- a/packages/example-history-app/src/pages/_app.tsx +++ b/packages/example-history-app/src/pages/_app.tsx @@ -1,5 +1,4 @@ import { AppProps } from "next/app"; -import fetch from "node-fetch"; import { useCreateEdenChain, EdenChainContext, @@ -21,7 +20,7 @@ if ( const MyApp = ({ Component, pageProps }: AppProps) => { const subchain = useCreateEdenChain({ - fetch, + fetch: global.window?.fetch, // undefined for nodejs to prevent lambda perf issues edenAccount: process.env.NEXT_PUBLIC_EDEN_CONTRACT, tokenAccount: process.env.NEXT_PUBLIC_TOKEN_CONTRACT, atomicAccount: process.env.NEXT_PUBLIC_AA_CONTRACT, diff --git a/packages/webapp/src/pages/_app.tsx b/packages/webapp/src/pages/_app.tsx index 7f20b8421..bf9a772f1 100644 --- a/packages/webapp/src/pages/_app.tsx +++ b/packages/webapp/src/pages/_app.tsx @@ -46,7 +46,7 @@ export const queryClient = new QueryClient(); const WebApp = ({ Component, pageProps }: AppProps) => { const subchain = useCreateEdenChain({ - fetch, + fetch: global.window?.fetch, // undefined for nodejs to prevent lambda perf issues edenAccount: process.env.NEXT_PUBLIC_EDEN_CONTRACT, tokenAccount: process.env.NEXT_PUBLIC_TOKEN_CONTRACT, atomicAccount: process.env.NEXT_PUBLIC_AA_CONTRACT, From b4f7f47fac4474d958ac59a526b2499445f7dafd Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Fri, 15 Oct 2021 12:20:40 -0400 Subject: [PATCH 06/33] update package deps --- packages/example-history-app/package.json | 1 + packages/webapp/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/example-history-app/package.json b/packages/example-history-app/package.json index 70a77c29e..7b65022bf 100644 --- a/packages/example-history-app/package.json +++ b/packages/example-history-app/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@edenos/common": "^0.1.0", + "@edenos/eden-subchain-client": "^0.1.2", "next": "^11.0.1", "react": "^17.0.2", "react-dom": "^17.0.2" diff --git a/packages/webapp/package.json b/packages/webapp/package.json index 84607c9e8..6ad6842c5 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -13,6 +13,7 @@ "dependencies": { "@culturehq/add-to-calendar": "^1.1.2", "@edenos/common": "^0.1.0", + "@edenos/eden-subchain-client": "^0.1.2", "@headlessui/react": "^1.4.1", "@popperjs/core": "^2.9.3", "async-sema": "^3.1.1", From eac679dfc6f6e7ab3b3bd97f5b427785a59cd6b8 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Fri, 15 Oct 2021 12:25:29 -0400 Subject: [PATCH 07/33] update package deps --- packages/box/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/box/package.json b/packages/box/package.json index c6842c006..48165c976 100644 --- a/packages/box/package.json +++ b/packages/box/package.json @@ -16,6 +16,7 @@ "dependencies": { "@dfuse/client": "^0.3.20", "@edenos/common": "^0.1.0", + "@edenos/eden-subchain-client": "^0.1.2", "axios": "^0.21.1", "body-parser": "^1.19.0", "cids": "^1.1.6", From c8a60d490909c14676e078a8fc96d6d4da849d79 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Fri, 15 Oct 2021 12:48:40 -0400 Subject: [PATCH 08/33] build issues --- packages/eden-subchain-client/tsconfig.build.json | 1 + packages/eden-subchain-client/tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/eden-subchain-client/tsconfig.build.json b/packages/eden-subchain-client/tsconfig.build.json index e71223c31..83db9ed56 100644 --- a/packages/eden-subchain-client/tsconfig.build.json +++ b/packages/eden-subchain-client/tsconfig.build.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.build.json", "compilerOptions": { "resolveJsonModule": true, + "target": "ES2019", // todo: upgrade webpack so it supports ?. "strict": true, "outDir": "./dist", "jsx": "react" diff --git a/packages/eden-subchain-client/tsconfig.json b/packages/eden-subchain-client/tsconfig.json index d6d23b783..a68727de0 100644 --- a/packages/eden-subchain-client/tsconfig.json +++ b/packages/eden-subchain-client/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "resolveJsonModule": true, + "target": "ES2019", // todo: upgrade webpack so it supports ?. "strict": true, "jsx": "react" } From 0e34063d89a6193b7fb820e02efade734b526993 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Fri, 15 Oct 2021 14:07:09 -0400 Subject: [PATCH 09/33] build issues --- docker/eden-webapp.Dockerfile | 2 ++ .../eden-subchain-client/tsconfig.build.json | 1 - packages/eden-subchain-client/tsconfig.json | 1 - yarn.lock | 18 ++++-------------- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/docker/eden-webapp.Dockerfile b/docker/eden-webapp.Dockerfile index c948d5ae2..ec7ba9366 100644 --- a/docker/eden-webapp.Dockerfile +++ b/docker/eden-webapp.Dockerfile @@ -8,6 +8,7 @@ ENV PATH /app/node_modules/.bin:$PATH COPY package.json yarn.lock ./ COPY ./packages/common/package.json ./packages/common/ +COPY ./packages/eden-subchain-client/package.json ./packages/eden-subchain-client/ COPY ./packages/webapp/package.json ./packages/webapp/ RUN yarn install --frozen-lockfile @@ -17,6 +18,7 @@ FROM node:alpine AS builder WORKDIR /app COPY ./packages/common ./packages/common +COPY ./packages/eden-subchain-client ./packages/eden-subchain-client COPY ./packages/webapp ./packages/webapp COPY .eslintignore .eslintrc.js .prettierrc.json lerna.json package.json tsconfig.build.json tsconfig.json yarn.lock ./ diff --git a/packages/eden-subchain-client/tsconfig.build.json b/packages/eden-subchain-client/tsconfig.build.json index 83db9ed56..e71223c31 100644 --- a/packages/eden-subchain-client/tsconfig.build.json +++ b/packages/eden-subchain-client/tsconfig.build.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.build.json", "compilerOptions": { "resolveJsonModule": true, - "target": "ES2019", // todo: upgrade webpack so it supports ?. "strict": true, "outDir": "./dist", "jsx": "react" diff --git a/packages/eden-subchain-client/tsconfig.json b/packages/eden-subchain-client/tsconfig.json index a68727de0..d6d23b783 100644 --- a/packages/eden-subchain-client/tsconfig.json +++ b/packages/eden-subchain-client/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "resolveJsonModule": true, - "target": "ES2019", // todo: upgrade webpack so it supports ?. "strict": true, "jsx": "react" } diff --git a/yarn.lock b/yarn.lock index 376036384..61f4e9ad8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3355,20 +3355,10 @@ camelize@^1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= -caniuse-lite@^1.0.30001173, caniuse-lite@^1.0.30001179, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001196: - version "1.0.30001207" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001207.tgz#364d47d35a3007e528f69adb6fecb07c2bb2cc50" - integrity sha512-UPQZdmAsyp2qfCTiMU/zqGSWOYaY9F9LL61V8f+8MrubsaDGpaHD9HRV/EWZGULZn0Hxu48SKzI5DgFwTvHuYw== - -caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001228: - version "1.0.30001248" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz#26ab45e340f155ea5da2920dadb76a533cb8ebce" - integrity sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw== - -caniuse-lite@^1.0.30001219: - version "1.0.30001222" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001222.tgz#2789b8487282cbbe1700924f53951303d28086a9" - integrity sha512-rPmwUK0YMjfMlZVmH6nVB5U3YJ5Wnx3vmT5lnRO3nIKO8bJ+TRWMbGuuiSugDJqESy/lz+1hSrlQEagCtoOAWQ== +caniuse-lite@^1.0.30001173, caniuse-lite@^1.0.30001179, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228: + version "1.0.30001267" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001267.tgz" + integrity sha512-r1mjTzAuJ9W8cPBGbbus8E0SKcUP7gn03R14Wk8FlAlqhH9hroy9nLqmpuXlfKEw/oILW+FGz47ipXV2O7x8lg== caseless@~0.12.0: version "0.12.0" From e8b49838b4820a36694368c973c0bb2df7217bf5 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Tue, 26 Oct 2021 16:15:25 -0400 Subject: [PATCH 10/33] Fix missing induction videos --- contracts/eden/src/eden-micro-chain.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 54d24ede2..c482ac1d9 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -1279,6 +1279,8 @@ void inductdonate(const action_context& context, obj.member.profile = induction.induction.profile; obj.member.inductionVideo = induction.induction.video; obj.member.createdAt = eosio::block_timestamp(context.block.timestamp); + if (obj.member.inductionVideo.empty()) + obj.member.inductionVideo = get_status().status.genesisVideo; }); transfer_funds(context.block.timestamp, payer, master_pool, quantity, history_desc::inductdonate); From ec66f8852597dded4005a1d4d993e154b600fa62 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Tue, 26 Oct 2021 16:24:58 -0400 Subject: [PATCH 11/33] force box build --- docker/eden-box.Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/eden-box.Dockerfile b/docker/eden-box.Dockerfile index 098260f11..a3c564cde 100644 --- a/docker/eden-box.Dockerfile +++ b/docker/eden-box.Dockerfile @@ -44,3 +44,4 @@ USER box EXPOSE 3032 CMD ["yarn", "start", "--stream"] + From b9c91fb97b9831f52ed6b855c431ac680efbec6b Mon Sep 17 00:00:00 2001 From: SparkPlug0025 <79721020+sparkplug0025@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:15:17 -0400 Subject: [PATCH 12/33] Ephemeral Chain Runners (#587) * Init cltester setup * Setup Chain build step * Setup genesis ephemeral data * Complete genesis runner * Adjust build step name * Complete nodeos runner * Fix runners test cases * fix atomic market files * Fix chain runners --- .github/workflows/build.yml | 20 +- contracts/eden/CMakeLists.txt | 21 +- .../eden/tests/include/nodeos-runner.hpp | 34 ++ contracts/eden/tests/include/tester-base.hpp | 506 +++++++++++++++++ contracts/eden/tests/run-full-election.cpp | 15 + contracts/eden/tests/run-genesis.cpp | 12 + contracts/eden/tests/test-eden.cpp | 527 +----------------- docker/eden-chain.Dockerfile | 23 + 8 files changed, 638 insertions(+), 520 deletions(-) create mode 100644 contracts/eden/tests/include/nodeos-runner.hpp create mode 100644 contracts/eden/tests/include/tester-base.hpp create mode 100644 contracts/eden/tests/run-full-election.cpp create mode 100644 contracts/eden/tests/run-genesis.cpp create mode 100644 docker/eden-chain.Dockerfile diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f840324d..96206d783 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,6 +141,25 @@ jobs: build/eden.wasm build/eden-micro-chain.wasm + - name: 📃 Upload Ephemeral Eden Chains Runners + if: steps.filter.outputs.src == 'true' + uses: actions/upload-artifact@v2 + with: + name: Ephemeral Eden Chains Runners + path: | + build/atomicassets.abi + build/atomicassets.wasm + build/atomicmarket.abi + build/atomicmarket.wasm + build/bios.wasm + build/boot.wasm + build/eden.abi + build/eden.wasm + build/token.abi + build/token.wasm + build/run-genesis.wasm + build/run-full-election.wasm + build-micro-chain: name: Build Micro Chain runs-on: ubuntu-latest @@ -464,4 +483,3 @@ jobs: file: docker/eden-webapp.Dockerfile tags: ${{ steps.prep.outputs.tags }} context: . - diff --git a/contracts/eden/CMakeLists.txt b/contracts/eden/CMakeLists.txt index 2a9712780..ec019e00d 100644 --- a/contracts/eden/CMakeLists.txt +++ b/contracts/eden/CMakeLists.txt @@ -42,23 +42,28 @@ add_custom_command(TARGET eden-abigen POST_BUILD COMMAND ${ROOT_BINARY_DIR}/cltester eden-abigen.wasm >${ROOT_BINARY_DIR}/eden.abi ) -function(add_test_eden suffix) - add_executable(test-eden${suffix} tests/test-eden.cpp src/globals.cpp src/accounts.cpp src/members.cpp src/atomicassets.cpp src/elections.cpp) - target_include_directories(test-eden${suffix} PUBLIC include) - target_include_directories(test-eden${suffix} PUBLIC +function(add_test_eden test_file suffix) + add_executable(${test_file}${suffix} tests/${test_file}.cpp src/globals.cpp src/accounts.cpp src/members.cpp src/atomicassets.cpp src/elections.cpp) + target_include_directories(${test_file}${suffix} PUBLIC include) + target_include_directories(${test_file}${suffix} PUBLIC ../token/include ../boot/include ../../external/atomicassets-contract/include ../../libraries/clchain/include + ./tests/include ) - target_link_libraries(test-eden${suffix} catch2 cltestlib${suffix}) - set_target_properties(test-eden${suffix} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${ROOT_BINARY_DIR}) + target_link_libraries(${test_file}${suffix} catch2 cltestlib${suffix}) + set_target_properties(${test_file}${suffix} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${ROOT_BINARY_DIR}) endfunction() -add_test_eden("") -add_test_eden("-debug") +add_test_eden("test-eden" "") +add_test_eden("test-eden" "-debug") eden_tester_test(test-eden) +# Chain Runners +add_test_eden("run-genesis" "") +add_test_eden("run-full-election" "") + file(CREATE_LINK ${CMAKE_CURRENT_SOURCE_DIR}/tests/data ${ROOT_BINARY_DIR}/eden-test-data SYMBOLIC) function(add_eden_microchain suffix) diff --git a/contracts/eden/tests/include/nodeos-runner.hpp b/contracts/eden/tests/include/nodeos-runner.hpp new file mode 100644 index 000000000..1bd7a10e7 --- /dev/null +++ b/contracts/eden/tests/include/nodeos-runner.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +struct nodeos_runner +{ + eden_tester tester; + std::string runner_name; + + nodeos_runner(std::string runner_name) : runner_name(runner_name) {} + + void start_nodeos() + { + // tolerance blocks + tester.chain.start_block(); + tester.chain.start_block(); + + // copy state + auto chain_path = tester.chain.get_path(); + eosio::execute("rm -rf " + runner_name); + eosio::execute("mkdir -p " + runner_name + "/blocks"); + eosio::execute("cp " + chain_path + "/blocks/blocks.log " + runner_name + "/blocks"); + + // run nodeos + eosio::execute("nodeos -d " + runner_name + + " " // + "--config-dir config " // + "--plugin eosio::chain_api_plugin " // + "--plugin eosio::producer_api_plugin " // + "--plugin eosio::state_history_plugin " // + "--trace-history --disable-replay-opts " // + "-e -p eosio"); + } +}; diff --git a/contracts/eden/tests/include/tester-base.hpp b/contracts/eden/tests/include/tester-base.hpp new file mode 100644 index 000000000..95a4f85ed --- /dev/null +++ b/contracts/eden/tests/include/tester-base.hpp @@ -0,0 +1,506 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CATCH_CONFIG_RUNNER +#include + +using namespace eosio; +using namespace std::literals; +namespace atomicassets = eden::atomicassets; +namespace atomicmarket = eden::atomicmarket; +using atomicassets::attribute_map; +namespace actions = eden::actions; +using user_context = test_chain::user_context; +using eden::accounts; +using eden::members; + +namespace eosio +{ + std::ostream& operator<<(std::ostream& os, + const std::pair& p) + { + os << '{' << eosio::convert_to_json(p.first.to_time_point()) << ':' << p.second << '}'; + return os; + } +} // namespace eosio + +eosio::time_point s2t(const std::string& time) +{ + uint64_t value; + eosio::check(eosio::string_to_utc_microseconds(value, time.data(), time.data() + time.size()), + "bad time"); + return eosio::time_point{eosio::microseconds(value)}; +} + +void chain_setup(test_chain& t) +{ + t.set_code("eosio"_n, "boot.wasm"); + t.as("eosio"_n).act(); + t.start_block(); // preactivate feature activates protocol features at the start of a block + t.set_code("eosio"_n, "bios.wasm"); +} + +void token_setup(test_chain& t) +{ + t.create_code_account("eosio.token"_n); + t.set_code("eosio.token"_n, "token.wasm"); + t.as("eosio.token"_n).act("eosio.token"_n, s2a("100000000.0000 EOS")); + t.as("eosio.token"_n).act("eosio.token"_n, s2a("100000000.0000 EOS"), ""); + t.as("eosio.token"_n).act("eosio.token"_n, s2a("1000000.0000 OTHER")); + t.as("eosio.token"_n).act("eosio.token"_n, s2a("1000000.0000 OTHER"), ""); +} + +void atomicmarket_setup(test_chain& t) +{ + t.create_code_account("atomicmarket"_n); + t.set_code("atomicmarket"_n, "atomicmarket.wasm"); + t.as("atomicmarket"_n).act(); + t.as("atomicmarket"_n) + .act("eosio.token"_n, eosio::symbol("EOS", 4)); +} + +void atomicassets_setup(test_chain& t) +{ + t.create_code_account("atomicassets"_n); + t.set_code("atomicassets"_n, "atomicassets.wasm"); + t.as("atomicassets"_n).act(); +} + +void eden_setup(test_chain& t) +{ + atomicassets_setup(t); + atomicmarket_setup(t); + t.set_code("eden.gm"_n, "eden.wasm"); +} + +auto get_token_balance(eosio::name owner) +{ + return token::contract::get_balance("eosio.token"_n, owner, symbol_code{"EOS"}); +} + +auto get_eden_account(eosio::name owner) +{ + return accounts{"eden.gm"_n}.get_account(owner); +} + +auto get_eden_membership(eosio::name account) +{ + return members{"eden.gm"_n}.get_member(account); +} + +auto get_globals() +{ + eden::tester_clear_global_singleton(); + eden::globals globals("eden.gm"_n); + return globals.get(); +} + +template +auto get_table_size() +{ + T tb("eden.gm"_n, eden::default_scope); + return std::distance(tb.begin(), tb.end()); +} + +template +auto get_table_size(eosio::name scope) +{ + T tb("eden.gm"_n, scope.value); + return std::distance(tb.begin(), tb.end()); +} + +template +void dump_table() +{ + T tb("eden.gm"_n, eden::default_scope); + for (const auto& record : tb) + { + std::cout << eosio::convert_to_json(record) << std::endl; + } +} + +std::vector make_names(std::size_t count) +{ + std::vector result; + if (count <= 31 * 31) + { + auto test_account_base = "edenmember"_n; + for (std::size_t i = 32; i < 1024 && result.size() != count; ++i) + { + if (i % 32) + { + result.push_back(eosio::name(test_account_base.value + (i << 4))); + } + } + } + else + { + auto test_account_base = "edenmembr"_n; + for (int i = 1024; i < 32768 && result.size() != count; ++i) + { + if (i % 32 && (i / 32) % 32) + { + result.push_back(eosio::name(test_account_base.value + (i << 4))); + } + } + } + return result; +} + +static const eden::new_member_profile alice_profile{ + "Alice", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", + "Alice was beginning to get very tired of sitting by her sister on the bank, and of " + "having nothing to do: once or twice she had peeped into the book her sister was " + "reading, but it had no pictures or conversations in it, \"and what is the use of a " + "book,\" thought Alice \"without pictures or conversations?\"", + "{\"blog\":\"alice.example.com\"}", "Lewis Carroll"}; + +static const eden::new_member_profile pip_profile{ + "Philip Pirrip", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", + "My father's family name being Pirrip and my Christian name Phillip, my infant " + "tongue could make of both names nothing longer or more explicit than Pip. So, I " + "called myself Pip and came to be called Pip.", + "{\"blog\":\"pip.example.com\"}", "Charles Dickens"}; + +static const eden::new_member_profile egeon_profile{ + "Egeon", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", + "In Syracusa was I born, and wed\nUnto a woman happy but for me,\nAnd by me, had not " + "our hap been bad.\nWith her I liv'd in joy; our wealth increas'd\nBy prosperous " + "voyages I often made\nTo Epidamnum, till my factor's death,", + "{\"blog\":\"egeon.example.com\"}", "William Shakespeare"}; + +static const eden::new_member_profile bertie_profile{ + "Bertie Wooster", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", + "I'm a bit short on brain myself; the old bean would appear to have been constructed more for " + "ornament than for use, don't you know; but give me five minutes to talk the thing over with " + "Jeeves, and I'm game to advise any one about anything.", + "{\"blog\":\"bertie.example.com\"}"}; + +static const eden::new_member_profile ahab_profile{ + "Captain Ahab", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", + "Ahab's been in colleges, as well as 'mong the cannibals; been used to deeper wonders than the " + "waves; fixed his fiery lance in mightier, stranger foes than whales. His lance! aye, the " + "keenest and the surest that out of all our isle" + "{\"blog\":\"ahab.example.com\"}"}; + +struct eden_tester +{ + test_chain chain; + user_context eosio_token = chain.as("eosio.token"_n); + user_context eden_gm = chain.as("eden.gm"_n); + user_context alice = chain.as("alice"_n); + user_context pip = chain.as("pip"_n); + user_context egeon = chain.as("egeon"_n); + user_context bertie = chain.as("bertie"_n); + user_context ahab = chain.as("ahab"_n); + + explicit eden_tester(std::function f = [] {}) + { + chain_setup(chain); + token_setup(chain); + chain.create_code_account("eden.gm"_n); + f(); + eden_setup(chain); + for (auto account : {"alice"_n, "pip"_n, "egeon"_n, "bertie"_n, "ahab"_n}) + { + chain.create_account(account); + chain.as("eosio.token"_n) + .act("eosio.token"_n, account, s2a("1000.0000 EOS"), "memo"); + chain.as("eosio.token"_n) + .act("eosio.token"_n, account, s2a("1000.0000 OTHER"), + "memo"); + } + } + void genesis() + { + eden_gm.act("Eden", eosio::symbol("EOS", 4), s2a("10.0000 EOS"), + std::vector{"alice"_n, "pip"_n, "egeon"_n}, + "QmTYqoPYf7DiVebTnvwwFdTgsYXg2RnuPrt8uddjfW2kHS", + attribute_map{}, s2a("1.0000 EOS"), 7 * 24 * 60 * 60, "", 6, + "15:30"); + + alice.act(1, alice_profile); + pip.act(2, pip_profile); + egeon.act(3, egeon_profile); + + alice.act("alice"_n, "eden.gm"_n, s2a("100.0000 EOS"), "memo"); + pip.act("pip"_n, "eden.gm"_n, s2a("10.0000 EOS"), "memo"); + egeon.act("egeon"_n, "eden.gm"_n, s2a("10.0000 EOS"), "memo"); + + alice.act("alice"_n, 1, s2a("10.0000 EOS")); + pip.act("pip"_n, 2, s2a("10.0000 EOS")); + egeon.act("egeon"_n, 3, s2a("10.0000 EOS")); + } + + void create_accounts(const std::vector& test_accounts) + { + for (auto account : test_accounts) + { + chain.start_block(); + chain.create_account(account); + chain.as("eosio.token"_n) + .act("eosio.token"_n, account, s2a("1000.0000 EOS"), "memo"); + } + } + + void finish_induction(uint64_t induction_id, + eosio::name inviter, + eosio::name invitee, + const std::vector& witnesses) + { + chain.as(invitee).act(invitee, "eden.gm"_n, s2a("10.0000 EOS"), + "memo"); + + std::string video = "QmTYqoPYf7DiVebTnvwwFdTgsYXg2RnuPrt8uddjfW2kHS"; + eden::new_member_profile profile{invitee.to_string(), + "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", + "Hi, I'm the coolest " + invitee.to_string() + " ever!", + "{\"blog\":\"" + invitee.to_string() + ".example.com\"}"}; + chain.as(invitee).act(induction_id, profile); + chain.as(inviter).act(inviter, induction_id, video); + + auto hash_data = eosio::convert_to_bin(std::tuple(video, profile)); + auto induction_hash = eosio::sha256(hash_data.data(), hash_data.size()); + + chain.as(inviter).act(inviter, induction_id, induction_hash); + for (auto witness : witnesses) + { + chain.as(witness).act(witness, induction_id, induction_hash); + } + chain.as(invitee).act(invitee, induction_id, s2a("10.0000 EOS")); + CHECK(get_eden_membership(invitee).status() == eden::member_status::active_member); + }; + + void induct(eosio::name account) + { + alice.act(42, "alice"_n, account, std::vector{"pip"_n, "egeon"_n}); + finish_induction(42, "alice"_n, account, {"pip"_n, "egeon"_n}); + } + + void induct_n(std::size_t count) + { + auto members = make_names(count); + create_accounts(members); + for (auto a : members) + { + chain.start_block(); + induct(a); + } + } + + void electseed(eosio::time_point_sec block_time, const char* expected = nullptr) + { + // This isn't a valid bitcoin block, but it meets the requirements that we actually check. + char buf[80] = + "\4\0\0\0" + "00000000000000000000000000000000" + "00000000000000000000000000000000" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00"; + uint32_t time = block_time.sec_since_epoch(); + memcpy(buf + 68, &time, 4); + expect(eden_gm.trace(eosio::bytes{std::vector(buf, buf + sizeof(buf))}), + expected); + } + + void skip_to(std::string time) + { + uint64_t value; + eosio::check(eosio::string_to_utc_microseconds(value, time.data(), time.data() + time.size()), + "bad time"); + skip_to(eosio::time_point{eosio::microseconds(value)}); + } + void skip_to(eosio::time_point tp) + { + chain.finish_block(); + auto head_tp = chain.get_head_block_info().timestamp.to_time_point(); + auto skip = (tp - head_tp).count() / 1000 - 500; + chain.start_block(skip); + } + + void setup_election(uint32_t batch_size = 10000) + { + while (true) + { + auto trace = alice.trace(batch_size); + if (trace.except) + { + expect(trace, "Nothing to do"); + break; + } + chain.start_block(); + } + } + + eosio::block_timestamp next_election_time() const + { + return *eden::elections{"eden.gm"_n}.get_next_election_time(); + } + + auto get_current_groups() const + { + std::map> groups; + eden::vote_table_type vote_tb("eden.gm"_n, eden::default_scope); + eden::current_election_state_singleton state("eden.gm"_n, eden::default_scope); + auto config = std::get(state.get()).config; + for (auto row : vote_tb.get_index<"bygroup"_n>()) + { + groups[config.member_index_to_group(row.index)].push_back(row.member); + } + return groups; + }; + + void generic_group_vote(const auto& groups, uint8_t round, bool add_video = false) + { + for (const auto& [group_id, members] : groups) + { + chain.start_block(); + auto winner = *std::min_element(members.begin(), members.end()); + for (eosio::name member : members) + { + chain.as(member).act(round, member, winner); + if (add_video) + chain.as(member).act( + round, member, "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws"); + } + } + chain.start_block(60 * 60 * 1000); + alice.act(256); + }; + + void electdonate(eosio::name member) { chain.as(member).act(member, true); } + + void electdonate_all() + { + eden::member_table_type members_tb{"eden.gm"_n, eden::default_scope}; + std::vector members(members_tb.begin(), members_tb.end()); + int i = 0; + for (auto member : members) + { + if (++i % 25 == 0) + { + chain.start_block(); + } + if (member.election_participation_status() == eden::not_in_election) + { + chain.as(member.account()).act(member.account(), true); + } + } + } + + void run_election(bool auto_donate = true, uint32_t batch_size = 10000, bool add_video = false) + { + if (auto_donate) + { + electdonate_all(); + } + skip_to(next_election_time().to_time_point() - eosio::days(1)); + electseed(next_election_time().to_time_point() - eosio::days(1)); + skip_to(next_election_time().to_time_point()); + + setup_election(batch_size); + + uint8_t round = 0; + + while (get_table_size() > 11) + { + generic_group_vote(get_current_groups(), round++, add_video); + } + + if (get_table_size() != 0) + { + chain.start_block(); + electseed(chain.get_head_block_info().timestamp.to_time_point()); + chain.start_block(2 * 60 * 60 * 1000); + alice.act(256); + } + } + + void distribute(uint32_t batch_size = 256) + { + while (true) + { + auto trace = alice.trace(batch_size); + if (trace.except) + { + expect(trace, "Nothing to do"); + break; + } + chain.start_block(); + } + } + + void set_balance(eosio::asset amount) + { + eden::account_table_type account_tb{"eden.gm"_n, "owned"_n.value}; + auto balance = account_tb.get("master"_n.value).balance(); + if (balance < amount) + { + eosio_token.act("eosio.token"_n, "eden.gm"_n, amount - balance, + "memo"); + } + else if (balance > amount) + { + eden_gm.act("eosio.token"_n, balance - amount, "memo"); + } + } + + template + eosio::asset get_total_balance(const T& table) + { + eosio::asset result{s2a("0.0000 EOS")}; + for (auto item : table) + { + result += item.balance(); + } + return result; + } + + eosio::asset get_total_balance() + { + eden::account_table_type user_accounts{"eden.gm"_n, eden::default_scope}; + eden::account_table_type internal_accounts{"eden.gm"_n, "owned"_n.value}; + return get_total_balance(user_accounts) + get_total_balance(internal_accounts) + + get_total_budget(); + } + + eosio::asset get_total_budget() + { + eden::distribution_account_table_type distributions{"eden.gm"_n, eden::default_scope}; + return get_total_balance(distributions); + }; + + auto get_budgets_by_period() const + { + std::map result; + eden::distribution_account_table_type distributions{"eden.gm"_n, eden::default_scope}; + for (auto t : distributions) + { + auto [iter, _] = result.insert(std::pair(t.distribution_time(), s2a("0.0000 EOS"))); + iter->second += t.balance(); + } + return result; + }; + + void write_dfuse_history(const char* filename) + { + chain.start_block(); + chain.start_block(); + dfuse_subchain::write_history(filename, chain); + } +}; diff --git a/contracts/eden/tests/run-full-election.cpp b/contracts/eden/tests/run-full-election.cpp new file mode 100644 index 000000000..7296ad2f4 --- /dev/null +++ b/contracts/eden/tests/run-full-election.cpp @@ -0,0 +1,15 @@ +#define CATCH_CONFIG_MAIN + +#include + +TEST_CASE("Setup Eden chain with full election") +{ + nodeos_runner r("chain-full-election"); + + r.tester.genesis(); + r.tester.run_election(true, 10000, true); + r.tester.induct_n(100); + r.tester.run_election(true, 10000, true); + + r.start_nodeos(); +} diff --git a/contracts/eden/tests/run-genesis.cpp b/contracts/eden/tests/run-genesis.cpp new file mode 100644 index 000000000..8a244d769 --- /dev/null +++ b/contracts/eden/tests/run-genesis.cpp @@ -0,0 +1,12 @@ +#define CATCH_CONFIG_MAIN + +#include + +TEST_CASE("Setup Eden chain with basic completed genesis") +{ + nodeos_runner r("chain-genesis"); + + r.tester.genesis(); + + r.start_nodeos(); +} diff --git a/contracts/eden/tests/test-eden.cpp b/contracts/eden/tests/test-eden.cpp index be6f03774..c51648cd4 100644 --- a/contracts/eden/tests/test-eden.cpp +++ b/contracts/eden/tests/test-eden.cpp @@ -1,48 +1,6 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #define CATCH_CONFIG_RUNNER -#include - -using namespace eosio; -using namespace std::literals; -namespace atomicassets = eden::atomicassets; -namespace atomicmarket = eden::atomicmarket; -using atomicassets::attribute_map; -namespace actions = eden::actions; -using user_context = test_chain::user_context; -using eden::accounts; -using eden::members; - -namespace eosio -{ - std::ostream& operator<<(std::ostream& os, - const std::pair& p) - { - os << '{' << eosio::convert_to_json(p.first.to_time_point()) << ':' << p.second << '}'; - return os; - } -} // namespace eosio - -eosio::time_point s2t(const std::string& time) -{ - uint64_t value; - eosio::check(eosio::string_to_utc_microseconds(value, time.data(), time.data() + time.size()), - "bad time"); - return eosio::time_point{eosio::microseconds(value)}; -} bool write_expected = false; @@ -89,11 +47,13 @@ struct CompareFile for (auto& ttrace : history->traces) { std::visit( - [&](auto& ttrace) { + [&](auto& ttrace) + { for (auto& atrace : ttrace.action_traces) { std::visit( - [&](auto& atrace) { + [&](auto& atrace) + { if (atrace.receiver == "eosio.null"_n && atrace.act.name == "eden.events"_n) { @@ -121,465 +81,6 @@ struct CompareFile } // write_events }; // CompareFile -void chain_setup(test_chain& t) -{ - t.set_code("eosio"_n, "boot.wasm"); - t.as("eosio"_n).act(); - t.start_block(); // preactivate feature activates protocol features at the start of a block - t.set_code("eosio"_n, "bios.wasm"); -} - -void token_setup(test_chain& t) -{ - t.create_code_account("eosio.token"_n); - t.set_code("eosio.token"_n, "token.wasm"); - t.as("eosio.token"_n).act("eosio.token"_n, s2a("100000000.0000 EOS")); - t.as("eosio.token"_n).act("eosio.token"_n, s2a("100000000.0000 EOS"), ""); - t.as("eosio.token"_n).act("eosio.token"_n, s2a("1000000.0000 OTHER")); - t.as("eosio.token"_n).act("eosio.token"_n, s2a("1000000.0000 OTHER"), ""); -} - -void atomicmarket_setup(test_chain& t) -{ - t.create_code_account("atomicmarket"_n); - t.set_code("atomicmarket"_n, "atomicmarket.wasm"); - t.as("atomicmarket"_n).act(); - t.as("atomicmarket"_n) - .act("eosio.token"_n, eosio::symbol("EOS", 4)); -} - -void atomicassets_setup(test_chain& t) -{ - t.create_code_account("atomicassets"_n); - t.set_code("atomicassets"_n, "atomicassets.wasm"); - t.as("atomicassets"_n).act(); -} - -void eden_setup(test_chain& t) -{ - atomicassets_setup(t); - atomicmarket_setup(t); - t.set_code("eden.gm"_n, "eden.wasm"); -} - -auto get_token_balance(eosio::name owner) -{ - return token::contract::get_balance("eosio.token"_n, owner, symbol_code{"EOS"}); -} - -auto get_eden_account(eosio::name owner) -{ - return accounts{"eden.gm"_n}.get_account(owner); -} - -auto get_eden_membership(eosio::name account) -{ - return members{"eden.gm"_n}.get_member(account); -} - -auto get_globals() -{ - eden::tester_clear_global_singleton(); - eden::globals globals("eden.gm"_n); - return globals.get(); -} - -template -auto get_table_size() -{ - T tb("eden.gm"_n, eden::default_scope); - return std::distance(tb.begin(), tb.end()); -} - -template -auto get_table_size(eosio::name scope) -{ - T tb("eden.gm"_n, scope.value); - return std::distance(tb.begin(), tb.end()); -} - -template -void dump_table() -{ - T tb("eden.gm"_n, eden::default_scope); - for (const auto& record : tb) - { - std::cout << eosio::convert_to_json(record) << std::endl; - } -} - -std::vector make_names(std::size_t count) -{ - std::vector result; - if (count <= 31 * 31) - { - auto test_account_base = "edenmember"_n; - for (std::size_t i = 32; i < 1024 && result.size() != count; ++i) - { - if (i % 32) - { - result.push_back(eosio::name(test_account_base.value + (i << 4))); - } - } - } - else - { - auto test_account_base = "edenmembr"_n; - for (int i = 1024; i < 32768 && result.size() != count; ++i) - { - if (i % 32 && (i / 32) % 32) - { - result.push_back(eosio::name(test_account_base.value + (i << 4))); - } - } - } - return result; -} - -static const eden::new_member_profile alice_profile{ - "Alice", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", - "Alice was beginning to get very tired of sitting by her sister on the bank, and of " - "having nothing to do: once or twice she had peeped into the book her sister was " - "reading, but it had no pictures or conversations in it, \"and what is the use of a " - "book,\" thought Alice \"without pictures or conversations?\"", - "{\"blog\":\"alice.example.com\"}", "Lewis Carroll"}; - -static const eden::new_member_profile pip_profile{ - "Philip Pirrip", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", - "My father's family name being Pirrip and my Christian name Phillip, my infant " - "tongue could make of both names nothing longer or more explicit than Pip. So, I " - "called myself Pip and came to be called Pip.", - "{\"blog\":\"pip.example.com\"}", "Charles Dickens"}; - -static const eden::new_member_profile egeon_profile{ - "Egeon", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", - "In Syracusa was I born, and wed\nUnto a woman happy but for me,\nAnd by me, had not " - "our hap been bad.\nWith her I liv'd in joy; our wealth increas'd\nBy prosperous " - "voyages I often made\nTo Epidamnum, till my factor's death,", - "{\"blog\":\"egeon.example.com\"}", "William Shakespeare"}; - -static const eden::new_member_profile bertie_profile{ - "Bertie Wooster", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", - "I'm a bit short on brain myself; the old bean would appear to have been constructed more for " - "ornament than for use, don't you know; but give me five minutes to talk the thing over with " - "Jeeves, and I'm game to advise any one about anything.", - "{\"blog\":\"bertie.example.com\"}"}; - -static const eden::new_member_profile ahab_profile{ - "Captain Ahab", "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", - "Ahab's been in colleges, as well as 'mong the cannibals; been used to deeper wonders than the " - "waves; fixed his fiery lance in mightier, stranger foes than whales. His lance! aye, the " - "keenest and the surest that out of all our isle" - "{\"blog\":\"ahab.example.com\"}"}; - -struct eden_tester -{ - test_chain chain; - user_context eosio_token = chain.as("eosio.token"_n); - user_context eden_gm = chain.as("eden.gm"_n); - user_context alice = chain.as("alice"_n); - user_context pip = chain.as("pip"_n); - user_context egeon = chain.as("egeon"_n); - user_context bertie = chain.as("bertie"_n); - user_context ahab = chain.as("ahab"_n); - - explicit eden_tester(std::function f = [] {}) - { - chain_setup(chain); - token_setup(chain); - chain.create_code_account("eden.gm"_n); - f(); - eden_setup(chain); - for (auto account : {"alice"_n, "pip"_n, "egeon"_n, "bertie"_n, "ahab"_n}) - { - chain.create_account(account); - chain.as("eosio.token"_n) - .act("eosio.token"_n, account, s2a("1000.0000 EOS"), "memo"); - chain.as("eosio.token"_n) - .act("eosio.token"_n, account, s2a("1000.0000 OTHER"), - "memo"); - } - } - void genesis() - { - eden_gm.act("Eden", eosio::symbol("EOS", 4), s2a("10.0000 EOS"), - std::vector{"alice"_n, "pip"_n, "egeon"_n}, - "QmTYqoPYf7DiVebTnvwwFdTgsYXg2RnuPrt8uddjfW2kHS", - attribute_map{}, s2a("1.0000 EOS"), 7 * 24 * 60 * 60, "", 6, - "15:30"); - - alice.act(1, alice_profile); - pip.act(2, pip_profile); - egeon.act(3, egeon_profile); - - alice.act("alice"_n, "eden.gm"_n, s2a("100.0000 EOS"), "memo"); - pip.act("pip"_n, "eden.gm"_n, s2a("10.0000 EOS"), "memo"); - egeon.act("egeon"_n, "eden.gm"_n, s2a("10.0000 EOS"), "memo"); - - alice.act("alice"_n, 1, s2a("10.0000 EOS")); - pip.act("pip"_n, 2, s2a("10.0000 EOS")); - egeon.act("egeon"_n, 3, s2a("10.0000 EOS")); - } - - void create_accounts(const std::vector& test_accounts) - { - for (auto account : test_accounts) - { - chain.start_block(); - chain.create_account(account); - chain.as("eosio.token"_n) - .act("eosio.token"_n, account, s2a("1000.0000 EOS"), "memo"); - } - } - - void finish_induction(uint64_t induction_id, - eosio::name inviter, - eosio::name invitee, - const std::vector& witnesses) - { - chain.as(invitee).act(invitee, "eden.gm"_n, s2a("10.0000 EOS"), - "memo"); - - std::string video = "QmTYqoPYf7DiVebTnvwwFdTgsYXg2RnuPrt8uddjfW2kHS"; - eden::new_member_profile profile{invitee.to_string(), - "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws", - "Hi, I'm the coolest " + invitee.to_string() + " ever!", - "{\"blog\":\"" + invitee.to_string() + ".example.com\"}"}; - chain.as(invitee).act(induction_id, profile); - chain.as(inviter).act(inviter, induction_id, video); - - auto hash_data = eosio::convert_to_bin(std::tuple(video, profile)); - auto induction_hash = eosio::sha256(hash_data.data(), hash_data.size()); - - chain.as(inviter).act(inviter, induction_id, induction_hash); - for (auto witness : witnesses) - { - chain.as(witness).act(witness, induction_id, induction_hash); - } - chain.as(invitee).act(invitee, induction_id, s2a("10.0000 EOS")); - CHECK(get_eden_membership(invitee).status() == eden::member_status::active_member); - }; - - void induct(eosio::name account) - { - alice.act(42, "alice"_n, account, std::vector{"pip"_n, "egeon"_n}); - finish_induction(42, "alice"_n, account, {"pip"_n, "egeon"_n}); - } - - void induct_n(std::size_t count) - { - auto members = make_names(count); - create_accounts(members); - for (auto a : members) - { - chain.start_block(); - induct(a); - } - } - - void electseed(eosio::time_point_sec block_time, const char* expected = nullptr) - { - // This isn't a valid bitcoin block, but it meets the requirements that we actually check. - char buf[80] = - "\4\0\0\0" - "00000000000000000000000000000000" - "00000000000000000000000000000000" - "\x00\x00\x00\x00" - "\x00\x00\x00\x00" - "\x00\x00\x00"; - uint32_t time = block_time.sec_since_epoch(); - memcpy(buf + 68, &time, 4); - expect(eden_gm.trace(eosio::bytes{std::vector(buf, buf + sizeof(buf))}), - expected); - } - - void skip_to(std::string time) - { - uint64_t value; - eosio::check(eosio::string_to_utc_microseconds(value, time.data(), time.data() + time.size()), - "bad time"); - skip_to(eosio::time_point{eosio::microseconds(value)}); - } - void skip_to(eosio::time_point tp) - { - chain.finish_block(); - auto head_tp = chain.get_head_block_info().timestamp.to_time_point(); - auto skip = (tp - head_tp).count() / 1000 - 500; - chain.start_block(skip); - } - - void setup_election(uint32_t batch_size = 10000) - { - while (true) - { - auto trace = alice.trace(batch_size); - if (trace.except) - { - expect(trace, "Nothing to do"); - break; - } - chain.start_block(); - } - } - - eosio::block_timestamp next_election_time() const - { - return *eden::elections{"eden.gm"_n}.get_next_election_time(); - } - - auto get_current_groups() const - { - std::map> groups; - eden::vote_table_type vote_tb("eden.gm"_n, eden::default_scope); - eden::current_election_state_singleton state("eden.gm"_n, eden::default_scope); - auto config = std::get(state.get()).config; - for (auto row : vote_tb.get_index<"bygroup"_n>()) - { - groups[config.member_index_to_group(row.index)].push_back(row.member); - } - return groups; - }; - - void generic_group_vote(const auto& groups, uint8_t round, bool add_video = false) - { - for (const auto& [group_id, members] : groups) - { - chain.start_block(); - auto winner = *std::min_element(members.begin(), members.end()); - for (eosio::name member : members) - { - chain.as(member).act(round, member, winner); - if (add_video) - chain.as(member).act( - round, member, "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws"); - } - } - chain.start_block(60 * 60 * 1000); - alice.act(256); - }; - - void electdonate(eosio::name member) { chain.as(member).act(member, true); } - - void electdonate_all() - { - eden::member_table_type members_tb{"eden.gm"_n, eden::default_scope}; - std::vector members(members_tb.begin(), members_tb.end()); - int i = 0; - for (auto member : members) - { - if (++i % 25 == 0) - { - chain.start_block(); - } - if (member.election_participation_status() == eden::not_in_election) - { - chain.as(member.account()).act(member.account(), true); - } - } - } - - void run_election(bool auto_donate = true, uint32_t batch_size = 10000, bool add_video = false) - { - if (auto_donate) - { - electdonate_all(); - } - skip_to(next_election_time().to_time_point() - eosio::days(1)); - electseed(next_election_time().to_time_point() - eosio::days(1)); - skip_to(next_election_time().to_time_point()); - - setup_election(batch_size); - - uint8_t round = 0; - - while (get_table_size() > 11) - { - generic_group_vote(get_current_groups(), round++, add_video); - } - - if (get_table_size() != 0) - { - chain.start_block(); - electseed(chain.get_head_block_info().timestamp.to_time_point()); - chain.start_block(2 * 60 * 60 * 1000); - alice.act(256); - } - } - - void distribute(uint32_t batch_size = 256) - { - while (true) - { - auto trace = alice.trace(batch_size); - if (trace.except) - { - expect(trace, "Nothing to do"); - break; - } - chain.start_block(); - } - } - - void set_balance(eosio::asset amount) - { - eden::account_table_type account_tb{"eden.gm"_n, "owned"_n.value}; - auto balance = account_tb.get("master"_n.value).balance(); - if (balance < amount) - { - eosio_token.act("eosio.token"_n, "eden.gm"_n, amount - balance, - "memo"); - } - else if (balance > amount) - { - eden_gm.act("eosio.token"_n, balance - amount, "memo"); - } - } - - template - eosio::asset get_total_balance(const T& table) - { - eosio::asset result{s2a("0.0000 EOS")}; - for (auto item : table) - { - result += item.balance(); - } - return result; - } - - eosio::asset get_total_balance() - { - eden::account_table_type user_accounts{"eden.gm"_n, eden::default_scope}; - eden::account_table_type internal_accounts{"eden.gm"_n, "owned"_n.value}; - return get_total_balance(user_accounts) + get_total_balance(internal_accounts) + - get_total_budget(); - } - - eosio::asset get_total_budget() - { - eden::distribution_account_table_type distributions{"eden.gm"_n, eden::default_scope}; - return get_total_balance(distributions); - }; - - auto get_budgets_by_period() const - { - std::map result; - eden::distribution_account_table_type distributions{"eden.gm"_n, eden::default_scope}; - for (auto t : distributions) - { - auto [iter, _] = result.insert(std::pair(t.distribution_time(), s2a("0.0000 EOS"))); - iter->second += t.balance(); - } - return result; - }; - - void write_dfuse_history(const char* filename) - { - chain.start_block(); - chain.start_block(); - dfuse_subchain::write_history(filename, chain); - } -}; - TEST_CASE("genesis NFT pre-setup") { eden_tester t; @@ -911,7 +412,8 @@ TEST_CASE("induction") "alice"_n, 4, eosio::sha256(hash_data.data(), hash_data.size() - 1)), "Outdated endorsement"); - auto endorse_all = [&] { + auto endorse_all = [&] + { t.alice.act("alice"_n, 4, induction_hash); t.pip.act("pip"_n, 4, induction_hash); t.egeon.act("egeon"_n, 4, induction_hash); @@ -1173,7 +675,8 @@ TEST_CASE("deposit and spend") TEST_CASE("election config") { - auto verify_cfg = [](const auto& config, uint16_t num_participants) { + auto verify_cfg = [](const auto& config, uint16_t num_participants) + { INFO("participants: " << num_participants) if (num_participants < 1) { @@ -1630,10 +1133,11 @@ TEST_CASE("accounting") TEST_CASE("pre-genesis balance") { - eden_tester t{[&] { - t.eosio_token.act("eosio.token"_n, "eden.gm"_n, s2a("3.1415 EOS"), - ""); - }}; + eden_tester t{[&] + { + t.eosio_token.act("eosio.token"_n, "eden.gm"_n, + s2a("3.1415 EOS"), ""); + }}; t.genesis(); CHECK(get_token_balance("eden.gm"_n) == t.get_total_balance()); } @@ -1651,7 +1155,8 @@ TEST_CASE("account migration") { eden_tester t; t.genesis(); - auto sum_accounts = [](eden::account_table_type& table) { + auto sum_accounts = [](eden::account_table_type& table) + { auto total = s2a("0.0000 EOS"); for (auto iter = table.begin(), end = table.end(); iter != end; ++iter) { diff --git a/docker/eden-chain.Dockerfile b/docker/eden-chain.Dockerfile new file mode 100644 index 000000000..9423ac832 --- /dev/null +++ b/docker/eden-chain.Dockerfile @@ -0,0 +1,23 @@ +FROM ghcr.io/eoscommunity/eden-builder:sub-chain + +WORKDIR /app + +COPY ./contracts ./contracts +COPY ./external ./external +COPY ./libraries ./libraries +COPY ./native ./native +COPY ./programs ./programs +COPY ./wasm ./wasm +COPY CMakeLists.txt ./ + +WORKDIR /app/build +RUN cmake -DCMAKE_BUILD_TYPE=Release -DSKIP_TS=Yes -DEDEN_ATOMIC_ASSETS_ACCOUNT=atomicassets -DEDEN_ATOMIC_MARKET_ACCOUNT=atomicmarket -DEDEN_SCHEMA_NAME=members .. + +RUN make -j$(nproc) + +RUN ls -la + +WORKDIR /app/build/clsdk/bin + +RUN ls -la +# CMD ["nodeos"] From eb9568fb30628b1f4bc3aa680a977e2fa233382c Mon Sep 17 00:00:00 2001 From: SparkPlug0025 <79721020+sparkplug0025@users.noreply.github.com> Date: Thu, 28 Oct 2021 01:12:01 -0400 Subject: [PATCH 13/33] Add AtomicHub NFTs queries to Microchain (#581) * Add AtomicHub NFTs queries to Microchain * Complete NFT auctions bidding track * Add by collectedNfts query * Tracks transfer to keep the correct owner * Narrow down dfuse receivers * Lean NFT tracking --- contracts/eden/src/eden-micro-chain.cpp | 214 ++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index c482ac1d9..af37c40c7 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -95,6 +95,8 @@ struct by_invitee; struct by_group; struct by_round; struct by_createdAt; +struct by_member; +struct by_owner; template using mic = boost:: @@ -130,6 +132,16 @@ using ordered_by_createdAt = boost::multi_index::ordered_unique< // boost::multi_index::tag, boost::multi_index::key<&T::by_createdAt>>; +template +using ordered_by_member = boost::multi_index::ordered_unique< // + boost::multi_index::tag, + boost::multi_index::key<&T::by_member>>; + +template +using ordered_by_owner = boost::multi_index::ordered_unique< // + boost::multi_index::tag, + boost::multi_index::key<&T::by_owner>>; + uint64_t available_pk(const auto& table, const auto& first) { auto& idx = table.template get(); @@ -151,6 +163,7 @@ enum tables vote_table, distribution_table, distribution_fund_table, + nft_table, }; struct MemberElection; @@ -176,6 +189,14 @@ using DistributionFundConnection = DistributionFundConnection_name, DistributionFundEdge_name>>; +struct Nft; +constexpr const char NftConnection_name[] = "NftConnection"; +constexpr const char NftEdge_name[] = "NftEdge"; +using NftConnection = + clchain::Connection>; + struct status { bool active = false; @@ -457,6 +478,32 @@ using distribution_fund_index = mic, ordered_by_pk>; +using nft_account_key = std::tuple; + +struct nft_object + : public chainbase::object +{ + CHAINBASE_DEFAULT_CONSTRUCTOR(nft_object) + + id_type id; + eosio::name member; + eosio::name owner; + int32_t templateId; + uint64_t assetId; + uint32_t templateMint; + eosio::block_timestamp createdAt; + + auto by_pk() const { return assetId; } + nft_account_key by_member() const { return {member, createdAt, assetId}; } + nft_account_key by_owner() const { return {owner, createdAt, assetId}; } +}; +using nft_index = mic, + ordered_by_pk, + ordered_by_member, + ordered_by_owner>; + + struct database { chainbase::database db; @@ -471,6 +518,7 @@ struct database chainbase::generic_index votes; chainbase::generic_index distributions; chainbase::generic_index distribution_funds; + chainbase::generic_index nfts; database() { @@ -485,6 +533,7 @@ struct database db.add_index(votes); db.add_index(distributions); db.add_index(distribution_funds); + db.add_index(nfts); } }; database db; @@ -658,6 +707,24 @@ struct Member const std::string* inductionVideo() const { return member ? &member->inductionVideo : nullptr; } bool participating() const { return member && member->participating; } eosio::block_timestamp createdAt() const { return member->createdAt; } + + NftConnection nfts(std::optional gt, + std::optional ge, + std::optional lt, + std::optional le, + std::optional first, + std::optional last, + std::optional before, + std::optional after) const; + + NftConnection collectedNfts(std::optional gt, + std::optional ge, + std::optional lt, + std::optional le, + std::optional first, + std::optional last, + std::optional before, + std::optional after) const; MemberElectionConnection elections(std::optional gt, std::optional ge, @@ -686,6 +753,8 @@ EOSIO_REFLECT2( inductionVideo, participating, createdAt, + method(nfts, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(collectedNfts, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(elections, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(distributionFunds, "gt", "ge", "lt", "le", "first", "last", "before", "after")) @@ -1032,6 +1101,78 @@ void add_genesis_member(const status& status, eosio::name member) }); } +struct Nft { + const nft_object* obj; + + auto member() const { return get_member(obj->member); } + auto owner() const { return get_member(obj->owner); } + auto templateId() const { return obj->templateId; } + auto assetId() const { return obj->assetId; } + auto templateMint() const { return obj->templateMint; } + auto createdAt() const { return obj->createdAt; } +}; +EOSIO_REFLECT2(Nft, + member, + owner, + templateId, + assetId, + templateMint, + createdAt) + +NftConnection Member::nfts(std::optional gt, + std::optional ge, + std::optional lt, + std::optional le, + std::optional first, + std::optional last, + std::optional before, + std::optional after) const +{ + return clchain::make_connection( + gt ? std::optional{nft_account_key{account, *gt, ~uint64_t(0)}} // + : std::nullopt, // + ge ? std::optional{nft_account_key{account, *ge, 0}} // + : std::optional{nft_account_key{account, eosio::block_timestamp{0}, 0}}, // + lt ? std::optional{nft_account_key{account, *lt, 0}} // + : std::nullopt, // + le ? std::optional{nft_account_key{account, *le, ~uint64_t(0)}} // + : std::optional{nft_account_key{account, eosio::block_timestamp::max(), // + ~uint64_t(0)}}, // + first, last, before, after, // + db.nfts.get(), // + [](auto& obj) { return obj.by_member(); }, // + [&](auto& obj) { return Nft{&obj}; }, + [](auto& nfts, auto key) { return nfts.lower_bound(key); }, + [](auto& nfts, auto key) { return nfts.upper_bound(key); }); +} + +NftConnection Member::collectedNfts(std::optional gt, + std::optional ge, + std::optional lt, + std::optional le, + std::optional first, + std::optional last, + std::optional before, + std::optional after) const +{ + return clchain::make_connection( + gt ? std::optional{nft_account_key{account, *gt, ~uint64_t(0)}} // + : std::nullopt, // + ge ? std::optional{nft_account_key{account, *ge, 0}} // + : std::optional{nft_account_key{account, eosio::block_timestamp{0}, 0}}, // + lt ? std::optional{nft_account_key{account, *lt, 0}} // + : std::nullopt, // + le ? std::optional{nft_account_key{account, *le, ~uint64_t(0)}} // + : std::optional{nft_account_key{account, eosio::block_timestamp::max(), // + ~uint64_t(0)}}, // + first, last, before, after, // + db.nfts.get(), // + [](auto& obj) { return obj.by_owner(); }, // + [&](auto& obj) { return Nft{&obj}; }, + [](auto& nfts, auto key) { return nfts.lower_bound(key); }, + [](auto& nfts, auto key) { return nfts.upper_bound(key); }); +} + struct block_state { bool in_withdraw = false; @@ -1070,6 +1211,7 @@ void clearall() clear_table(db.votes); clear_table(db.distributions); clear_table(db.distribution_funds); + clear_table(db.nfts); } eosio::asset add_balance(eosio::name account, const eosio::asset& delta) @@ -1338,6 +1480,71 @@ void electvideo(uint8_t round, eosio::name voter, const std::string& video) db.votes.modify(*vote, [&](auto& vote) { vote.video = video; }); } +void logmint(const action_context& context, + uint64_t asset_id, + eosio::name authorized_minter, + eosio::name collection_name, + eosio::name schema_name, + int32_t template_id, + eosio::name new_asset_owner, + eden::atomicassets::attribute_map immutable_data, + eden::atomicassets::attribute_map mutable_data, + std::vector backed_tokens, + eden::atomicassets::attribute_map immutable_template_data) +{ + if (authorized_minter != eden_account || collection_name != eden_account + || schema_name != eden::schema_name) + return; + + auto account_pos = std::find_if(immutable_template_data.begin(), immutable_data.end(), + [](const auto& attr) { return attr.key == "account"; }); + if (account_pos == immutable_template_data.end()) + return; // this nft has no eden member account value + + eosio::name member_account(std::get(account_pos->value)); + + uint64_t template_mint = 0; + auto& index = db.nfts.get(); + for (auto it = index.lower_bound(nft_account_key{member_account, eosio::block_timestamp(0), 0}); + it != index.end() && it->member == member_account; it++) + { + template_mint++; + } + + db.nfts.emplace([&](auto& nft) { + nft.member = member_account; + nft.owner = new_asset_owner; + nft.templateId = template_id; + nft.assetId = asset_id; + nft.templateMint = template_mint; + nft.createdAt = eosio::block_timestamp(context.block.timestamp); + }); +} + +void logtransfer(const action_context& context, + eosio::name collection_name, + eosio::name from, + eosio::name to, + std::vector asset_ids, + std::string memo) +{ + if (collection_name != eden_account) + return; + + auto& index = db.nfts.get(); + + for (const auto& asset_id : asset_ids) { + auto it = index.find(asset_id); + if (it == index.end()) { + continue; + } + + db.nfts.modify(*it, [&](auto& nft) { + nft.owner = to; + }); + } +} + void handle_event(const eden::election_event_schedule& event) { db.status.modify(get_status(), [&](auto& status) { @@ -1603,6 +1810,13 @@ void filter_block(const subchain::eosio_block& block) for (auto& event : events) handle_event(context, event); } + else if (action.firstReceiver == atomic_account && action.receiver == eden_account) + { + if (action.name == "logmint"_n) + call(logmint, context, action.hexData.data); + else if (action.name == "logtransfer"_n) + call(logtransfer, context, action.hexData.data); + } } // for(action) eosio::check(!block_state.in_withdraw && !block_state.in_manual_transfer, "missing transfer notification"); From b2e7f18764503a04f1e8d0841c0fa1fc8d00e72c Mon Sep 17 00:00:00 2001 From: Brandon Fancher Date: Fri, 29 Oct 2021 12:54:20 -0400 Subject: [PATCH 14/33] Member detail page refactors (#592) * Add AtomicHub NFTs queries to Microchain * Complete NFT auctions bidding track * Add by collectedNfts query * Tracks transfer to keep the correct owner * Narrow down dfuse receivers * Lean NFT tracking * Remove SSR from member detail page. * Remove unused usePagedMembers box query. * Move members box queries into members dir/domain. * Expose bios and inductionVideo. * Update member profile page UI with latest design system. * Use react-icon IconBase for NFT icons. * Member detail NFT layout enhancements. * Adjust styles for mobile. * Refactor member NFT collect(ion/ors) queries. * Remove now-unused code. * Unified MemberData to simplify until we have a better data model for member info. * Lock node version to LTS for eden-webapp runs. * Update election box queries to conform to MemberData. --- docker/eden-webapp.Dockerfile | 6 +- packages/webapp/src/_app/hooks/box-queries.ts | 199 ++---------------- packages/webapp/src/_app/hooks/queries.ts | 13 -- .../src/_app/ui/icons/eos-community-icon.tsx | 11 + packages/webapp/src/_app/ui/icons/index.ts | 1 + packages/webapp/src/_app/ui/icons/nft.tsx | 27 +-- packages/webapp/src/_app/ui/social-button.tsx | 6 +- packages/webapp/src/members/api/members.ts | 59 +----- .../members/components/home/members-list.tsx | 4 +- .../src/members/components/member-card.tsx | 35 +-- .../member-chip-components/nft-info.tsx | 8 +- .../members/components/member-collections.tsx | 196 ++++++++++------- .../members/components/member-holo-card.tsx | 108 +++++----- .../components/member-social-links.tsx | 20 +- .../src/members/components/token-balance.tsx | 10 +- .../webapp/src/members/helpers/formatters.ts | 21 ++ packages/webapp/src/members/helpers/index.ts | 2 + packages/webapp/src/members/hooks.ts | 41 ---- packages/webapp/src/members/hooks/index.ts | 1 + packages/webapp/src/members/hooks/queries.ts | 99 +++++++++ packages/webapp/src/members/index.ts | 1 + packages/webapp/src/members/interfaces.ts | 37 +++- packages/webapp/src/nfts/api/nfts.ts | 28 --- packages/webapp/src/nfts/hooks/index.ts | 1 + packages/webapp/src/nfts/hooks/queries.ts | 63 ++++++ packages/webapp/src/nfts/interfaces.ts | 22 ++ .../src/pages/election/round-video-upload.tsx | 4 +- packages/webapp/src/pages/election/stats.tsx | 4 +- packages/webapp/src/pages/members/[id].tsx | 113 +++++----- 29 files changed, 544 insertions(+), 596 deletions(-) create mode 100644 packages/webapp/src/_app/ui/icons/eos-community-icon.tsx create mode 100644 packages/webapp/src/members/helpers/formatters.ts create mode 100644 packages/webapp/src/members/helpers/index.ts delete mode 100644 packages/webapp/src/members/hooks.ts create mode 100644 packages/webapp/src/members/hooks/index.ts create mode 100644 packages/webapp/src/members/hooks/queries.ts create mode 100644 packages/webapp/src/nfts/hooks/index.ts create mode 100644 packages/webapp/src/nfts/hooks/queries.ts diff --git a/docker/eden-webapp.Dockerfile b/docker/eden-webapp.Dockerfile index c948d5ae2..db980a036 100644 --- a/docker/eden-webapp.Dockerfile +++ b/docker/eden-webapp.Dockerfile @@ -1,5 +1,5 @@ # Install dependencies only when needed -FROM node:alpine AS deps +FROM node:lts-alpine AS deps RUN apk add --no-cache libc6-compat WORKDIR /app @@ -13,7 +13,7 @@ COPY ./packages/webapp/package.json ./packages/webapp/ RUN yarn install --frozen-lockfile # Rebuild the source code only when needed -FROM node:alpine AS builder +FROM node:lts-alpine AS builder WORKDIR /app COPY ./packages/common ./packages/common @@ -27,7 +27,7 @@ COPY --from=deps /app/packages/webapp/node_modules ./packages/webapp/node_module RUN yarn build --stream # Production image, copy all the files and run next -FROM node:alpine AS runner +FROM node:lts-alpine AS runner WORKDIR /app ENV NODE_ENV production diff --git a/packages/webapp/src/_app/hooks/box-queries.ts b/packages/webapp/src/_app/hooks/box-queries.ts index 76157fa68..113708d4d 100644 --- a/packages/webapp/src/_app/hooks/box-queries.ts +++ b/packages/webapp/src/_app/hooks/box-queries.ts @@ -1,124 +1,12 @@ -import { - QueryResult, - PagedQueryResult, - PagedQuery, - usePagedQuery, - useQuery, -} from "@edenos/common/dist/subchain"; +import { QueryResult, useQuery } from "@edenos/common/dist/subchain"; import dayjs from "dayjs"; import { assetFromString } from "_app"; -import { MemberAccountData } from "members"; - -interface MemberQueryEdge { - node: { - account: string; - createdAt: string; - profile: { - name: string; - img: string; - social: string; - }; - }; -} - -interface MembersQuery { - members: { - edges: MemberQueryEdge[]; - }; -} - -export const useMembers = () => { - const result = useQuery(`{ - members { - edges { - node { - account - createdAt - profile { - name - img - social - } - } - } - } - }`); - - let formattedMembers: MemberAccountData[] = []; - - if (result.data) { - const memberNodes = result.data.members.edges; - if (memberNodes) { - formattedMembers = memberNodes - .map((member: MemberQueryEdge) => - formatQueriedMemberAccountData(member.node) - ) - .filter((member): member is MemberAccountData => - Boolean(member) - ); - } - } - - return { ...result, data: formattedMembers }; -}; - -interface PagedMembersQuery { - members: PagedQuery; -} - -export const usePagedMembers = ( - pageSize: number = 20 -): PagedQueryResult => { - const query = `{ - members(@page@) { - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor - } - edges { - node { - account - createdAt - profile { - name - img - social - } - } - } - } -} - `; - - const pagedResult = usePagedQuery( - query, - pageSize, - (result) => result.data?.members.pageInfo - ); - - let formattedMembers: MemberAccountData[] = []; - - if (pagedResult.result.data) { - const memberNodes = pagedResult.result.data.members.edges; - if (memberNodes) { - formattedMembers = memberNodes - .map((member: MemberQueryEdge) => - formatQueriedMemberAccountData(member.node) - ) - .filter((member): member is MemberAccountData => - Boolean(member) - ); - } - } - - return { - ...pagedResult, - result: { ...pagedResult.result, data: formattedMembers }, - }; -}; +import { + formatQueriedMemberData, + MemberData, + MEMBER_DATA_FRAGMENT, +} from "members"; export interface ElectionStatusQuery { status: { @@ -150,8 +38,8 @@ export interface RoundBasicQueryData { } export interface RoundForUserVotingQueryData extends RoundBasicQueryData { - candidate?: MemberAccountData; - winner?: MemberAccountData; + candidate?: MemberData; + winner?: MemberData; video: string; } @@ -160,6 +48,7 @@ export interface CurrentMemberElectionVotingDataQuery { votes: RoundForUserVotingQueryData[]; } +// TODO: Pass type arguments to useQuery within this file export const useCurrentMemberElectionVotingData = ( account?: string ): QueryResult => { @@ -189,21 +78,11 @@ export const useCurrentMemberElectionVotingData = ( numGroups } winner { - account - profile { - name - img - social - } + ${MEMBER_DATA_FRAGMENT} } } candidate { - account - profile { - name - img - social - } + ${MEMBER_DATA_FRAGMENT} } video } @@ -234,12 +113,8 @@ export const useCurrentMemberElectionVotingData = ( votingFinished: voteNode.group.round.votingFinished, resultsAvailable: voteNode.group.round.resultsAvailable, numGroups: voteNode.group.round.numGroups, - candidate: formatQueriedMemberAccountData( - voteNode.candidate - ), - winner: formatQueriedMemberAccountData( - voteNode.group.winner - ), + candidate: formatQueriedMemberData(voteNode.candidate), + winner: formatQueriedMemberData(voteNode.group.winner), video: voteNode.video, })) || []; } @@ -249,13 +124,13 @@ export const useCurrentMemberElectionVotingData = ( }; export interface VoteQueryData { - voter: MemberAccountData; - candidate?: MemberAccountData; + voter: MemberData; + candidate?: MemberData; video: string; } export interface RoundGroupQueryData { - winner?: MemberAccountData; + winner?: MemberData; votes: VoteQueryData[]; } @@ -288,29 +163,14 @@ const currentElectionGlobalDataQuery = ` edges { node { winner { - account - profile { - name - img - social - } + ${MEMBER_DATA_FRAGMENT} } votes { voter { - account - profile { - name - img - social - } + ${MEMBER_DATA_FRAGMENT} } candidate { - account - profile { - name - img - social - } + ${MEMBER_DATA_FRAGMENT} } video } @@ -354,35 +214,20 @@ const mapQueriedRounds = (queriedRoundsEdges: any) => const mapQueriedRoundsGroups = (queriedRoundsGroupsEdges: any) => queriedRoundsGroupsEdges?.map(({ node: groupNode }: any) => ({ - winner: formatQueriedMemberAccountData(groupNode.winner), + winner: formatQueriedMemberData(groupNode.winner), votes: mapQueriedGroupVotes(groupNode.votes), })) || []; const mapQueriedGroupVotes = (votes: any) => { return ( votes?.map((vote: any) => ({ - voter: formatQueriedMemberAccountData(vote.voter), - candidate: formatQueriedMemberAccountData(vote.candidate), + voter: formatQueriedMemberData(vote.voter), + candidate: formatQueriedMemberData(vote.candidate), video: vote.video, })) || [] ); }; -const formatQueriedMemberAccountData = ( - memberAccountData: any -): MemberAccountData | undefined => - memberAccountData - ? { - account: memberAccountData.account, - name: memberAccountData.profile.name, - image: memberAccountData.profile.img, - socialHandles: JSON.parse(memberAccountData.profile.social), - createdAt: memberAccountData.createdAt - ? new Date(memberAccountData.createdAt).getTime() - : 0, - } - : undefined; - export interface ScheduledDistributionTargetAmountQuery { distributions: { edges: [ diff --git a/packages/webapp/src/_app/hooks/queries.ts b/packages/webapp/src/_app/hooks/queries.ts index 568d7007a..7a8a42da2 100644 --- a/packages/webapp/src/_app/hooks/queries.ts +++ b/packages/webapp/src/_app/hooks/queries.ts @@ -3,10 +3,8 @@ import { useQueries, useQuery, UseQueryResult } from "react-query"; import { EdenMember, getEdenMember, - getMember, getMembers, getTreasuryStats, - getNewMembers, getMembersStats, MemberData, MemberStats, @@ -41,7 +39,6 @@ import { CurrentElection, Election, ElectionCompletedRound, - ElectionState, VoteData, } from "elections/interfaces"; import { EncryptionScope, getEncryptedData } from "encryption/api"; @@ -188,21 +185,11 @@ export const queryMembers = ( }; }; -export const queryNewMembers = (page: number, pageSize: number) => ({ - queryKey: ["query_new_members", page, pageSize], - queryFn: () => getNewMembers(page, pageSize), -}); - export const queryMemberByAccountName = (accountName: string) => ({ queryKey: ["query_member", accountName], queryFn: () => getEdenMember(accountName), }); -export const queryMemberData = (account: string) => ({ - queryKey: ["query_member_data", account], - queryFn: () => getMember(account), -}); - export const queryDistributionState = () => ({ queryKey: ["query_distribution_state"], queryFn: getDistributionState, diff --git a/packages/webapp/src/_app/ui/icons/eos-community-icon.tsx b/packages/webapp/src/_app/ui/icons/eos-community-icon.tsx new file mode 100644 index 000000000..48b4ee35b --- /dev/null +++ b/packages/webapp/src/_app/ui/icons/eos-community-icon.tsx @@ -0,0 +1,11 @@ +import { IconBase, IconBaseProps } from "react-icons/lib"; + +export const EosCommunityIcon = (props: IconBaseProps) => ( + + + + + +); + +export default EosCommunityIcon; diff --git a/packages/webapp/src/_app/ui/icons/index.ts b/packages/webapp/src/_app/ui/icons/index.ts index 9ba454142..aabc1da1c 100644 --- a/packages/webapp/src/_app/ui/icons/index.ts +++ b/packages/webapp/src/_app/ui/icons/index.ts @@ -1,4 +1,5 @@ export * from "./circle-x"; +export * from "./eos-community-icon"; export * from "./magnifying-glass"; export * from "./nft"; export * from "./pending-invites"; diff --git a/packages/webapp/src/_app/ui/icons/nft.tsx b/packages/webapp/src/_app/ui/icons/nft.tsx index 71e45eaff..7a6916fa5 100644 --- a/packages/webapp/src/_app/ui/icons/nft.tsx +++ b/packages/webapp/src/_app/ui/icons/nft.tsx @@ -1,33 +1,14 @@ -import { CSSProperties } from "react"; +import { IconBase, IconBaseProps } from "react-icons/lib"; -interface Props { - size?: number; - className?: string; - style?: CSSProperties; -} - -export const NFT = ({ size = 16, className = "", style }: Props) => ( - +export const NFT = (props: IconBaseProps) => ( + - + ); diff --git a/packages/webapp/src/_app/ui/social-button.tsx b/packages/webapp/src/_app/ui/social-button.tsx index 0175d4bc4..5b2de46d4 100644 --- a/packages/webapp/src/_app/ui/social-button.tsx +++ b/packages/webapp/src/_app/ui/social-button.tsx @@ -28,8 +28,10 @@ export const SocialButton = ({ handle, icon, href, className }: Props) => { target="_blank" rel="noopener noreferrer" > - {formattedIcon} - {handle} +
+ {formattedIcon} + {handle} +
); diff --git a/packages/webapp/src/members/api/members.ts b/packages/webapp/src/members/api/members.ts index abdafd1c6..630b8d71b 100644 --- a/packages/webapp/src/members/api/members.ts +++ b/packages/webapp/src/members/api/members.ts @@ -1,11 +1,5 @@ -import { atomicAssets, devUseFixtureData, edenContractAccount } from "config"; -import { - getAccountCollection, - getAuctions, - getOwners, - getTemplate, - getTemplates, -} from "nfts/api"; +import { devUseFixtureData, edenContractAccount } from "config"; +import { getAccountCollection, getAuctions, getTemplates } from "nfts/api"; import { AssetData, AuctionableTemplateData, @@ -14,21 +8,8 @@ import { } from "nfts/interfaces"; import { MemberData } from "../interfaces"; -import { getEdenMember } from "./eden-contract"; import { fixtureMemberData } from "./fixtures"; -export const getMember = async ( - account: string -): Promise => { - if (devUseFixtureData) - return fixtureMemberData.find((member) => member.account === account); - const member = await getEdenMember(account); - if (member && member.nft_template_id > 0) { - const template = await getTemplate(`${member.nft_template_id}`); - return template ? convertAtomicTemplateToMember(template) : undefined; - } -}; - export const getMembers = async ( page: number, limit: number, @@ -40,7 +21,7 @@ export const getMembers = async ( let data = fixtureMemberData; if (ids.length) { data = fixtureMemberData.filter((md) => - ids.includes(md.templateId.toString()) + ids.includes(md.templateId!.toString()) ); } return Promise.resolve(data.slice(0, limit)); @@ -67,40 +48,6 @@ export const getCollection = async (account: string): Promise => { return members.sort((a, b) => a.createdAt - b.createdAt); }; -export const getCollectedBy = async ( - templateId: number -): Promise<{ members: MemberData[]; unknownOwners: string[] }> => { - const [owners, auctions] = await Promise.all([ - getOwners(templateId), - getAuctions(undefined, [`${templateId}`]), - ]); - - const auctionsOwners = auctions - .filter((auction) => auction.seller !== edenContractAccount) - .map((auction) => auction.seller); - - // the real eden owners are the current owners + pending auctions by current owners - const edenAccs = owners.concat(auctionsOwners); - - // TODO: revisit very expensive lookups here, we need to revisit - // maybe not, since each card will not be minted more than 20 times... - // so a given template will have a MAXIMUM number of 20 owners. - // even though, it would generate 20 api calls... not good. - const collectedMembers = edenAccs.map(getMember); - const membersData = await Promise.all(collectedMembers); - - const members = membersData.filter( - (member) => member !== undefined - ) as MemberData[]; - const unknownOwners = edenAccs.filter( - (acc) => - acc !== atomicAssets.marketContract && - !members.find((member) => member.account === acc) - ); - - return { members, unknownOwners }; -}; - export const memberDataDefaults = { templateId: 0, name: "", diff --git a/packages/webapp/src/members/components/home/members-list.tsx b/packages/webapp/src/members/components/home/members-list.tsx index 6599bd100..f8153df8b 100644 --- a/packages/webapp/src/members/components/home/members-list.tsx +++ b/packages/webapp/src/members/components/home/members-list.tsx @@ -64,8 +64,8 @@ export const MembersList = ({ searchValue }: Props) => { rowHeight={77} rowRenderer={({ index, style }: ListRowProps) => ( )} diff --git a/packages/webapp/src/members/components/member-card.tsx b/packages/webapp/src/members/components/member-card.tsx index 479243b63..646962d9f 100644 --- a/packages/webapp/src/members/components/member-card.tsx +++ b/packages/webapp/src/members/components/member-card.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FaVideo } from "react-icons/fa"; -import { SocialButton, ipfsUrl } from "_app"; +import { SocialButton, ipfsUrl, Container } from "_app"; import { MemberData } from "../interfaces"; import { MemberBio } from "./member-bio"; @@ -15,23 +15,24 @@ interface Props { export const MemberCard = ({ member, showBalance }: Props) => { return ( -
-
{showBalance && }
-
+
+ + {showBalance && } + + -
- {member.inductionVideo && ( - - )} -
- + {member.inductionVideo && ( + + )} + + + +
); }; diff --git a/packages/webapp/src/members/components/member-chip-components/nft-info.tsx b/packages/webapp/src/members/components/member-chip-components/nft-info.tsx index 82c9615fe..69b4c7964 100644 --- a/packages/webapp/src/members/components/member-chip-components/nft-info.tsx +++ b/packages/webapp/src/members/components/member-chip-components/nft-info.tsx @@ -56,14 +56,14 @@ const SaleBadge = ({ saleId: string; }) => (
{ e.stopPropagation(); const url = `${atomicAssets.hubUrl}/market/sale/${saleId}`; openInNewTab(url); }} > - +

#{assetData.templateMint} ON SALE

); @@ -85,10 +85,10 @@ interface NFTBadgeProps { const NFTBadge = ({ onClick, children }: NFTBadgeProps) => (
- + {children}
); diff --git a/packages/webapp/src/members/components/member-collections.tsx b/packages/webapp/src/members/components/member-collections.tsx index 9f70e4ebb..85e5dfd73 100644 --- a/packages/webapp/src/members/components/member-collections.tsx +++ b/packages/webapp/src/members/components/member-collections.tsx @@ -1,93 +1,135 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; +import { Tab } from "@headlessui/react"; -import { Button, Container, Heading, LoadingCard } from "_app"; +import { Container, LoadingContainer, MessageContainer, Text } from "_app"; import { MemberChip, MembersGrid } from "members"; -import { getCollection, getCollectedBy, memberDataDefaults } from "../api"; import { MemberData } from "../interfaces"; +import { useMemberNFTCollection, useMemberNFTCollectors } from "nfts/hooks"; interface Props { member: MemberData; } export const MemberCollections = ({ member }: Props) => { - const [tab, setTab] = useState<"collection" | "collectedBy">("collection"); - const [isLoading, setLoading] = useState(false); - const [members, setMembers] = useState(undefined); - - useEffect(() => { - const loadMember = async () => { - if (tab === "collection") { - const members = await getCollection(member.account); - setMembers(members); - } else { - const { members, unknownOwners } = await getCollectedBy( - member.templateId - ); - setMembers([ - ...members, - ...unknownOwners.map(externalOwnersCards), - ]); - } - setLoading(false); - }; - setLoading(true); - loadMember(); - }, [member, tab]); + return ( + + + NFT Collection + NFT Collectors + + + + + + + + + + + ); +}; + +export default MemberCollections; + +const tabClassName = ({ selected }: { selected: boolean }) => { + const baseClass = + "flex-1 lg:flex-none h-14 lg:px-12 border-b-2 focus:outline-none hover:bg-gray-100"; + if (!selected) + return `${baseClass} text-gray-500 border-white hover:border-gray-100`; + return `${baseClass} border-blue-500 text-gray-700`; +}; + +const StyledTab = ({ children }: { children: React.ReactNode }) => ( + +

{children}

+
+); + +const Collection = ({ member: { account, name } }: Props) => { + const { data: nfts, isLoading, isError } = useMemberNFTCollection(account); + + if (isLoading) return ; + + if (isError) { + return ( + + ); + } + + if (!nfts?.length) { + return ( + + ); + } return ( -
- - NFTs -
- - -
- {tab === "collection" ? ( -

- {member.name}{" "} - collects NFTs for the following Eden members: -

- ) : ( -

- The following Eden members or accounts collect one or - more of{" "} - {member.name}'s{" "} - NFTs. -

- )} + <> + + + {name} collects NFTs + for the following Eden members: + - {isLoading ? ( - - ) : ( - - {(member) => ( - - )} - - )} -
+ + {(member) => ( + + )} + + ); }; -const externalOwnersCards = (owner: string): MemberData => ({ - ...memberDataDefaults, - name: owner, -}); +const Collectors = ({ member: { account, name } }: Props) => { + const { data: collectors, isLoading, isError } = useMemberNFTCollectors( + account + ); + + if (isLoading) return ; + + if (isError) { + return ( + + ); + } + + if (!collectors.length) { + return ( + + ); + } + + return ( + <> + + + The following Eden members or accounts collect one or more + of {name}'s NFTs. + + + + {(member) => ( + + )} + + + ); +}; diff --git a/packages/webapp/src/members/components/member-holo-card.tsx b/packages/webapp/src/members/components/member-holo-card.tsx index c44d13824..723c98e7d 100644 --- a/packages/webapp/src/members/components/member-holo-card.tsx +++ b/packages/webapp/src/members/components/member-holo-card.tsx @@ -7,72 +7,74 @@ import { MemberData } from "../interfaces"; interface Props { member: MemberData; inducted?: boolean; + className?: string; } // TODO: 2x, 1x images for sharper images -export const MemberHoloCard = ({ member, inducted = true }: Props) => { +export const MemberHoloCard = ({ + member, + inducted = true, + className = "", +}: Props) => { const { observe, width } = useDimensions(); - const attributions = member.attributions - ? ` Attribution: ${member.attributions}` - : ""; - const memberImageTitle = `Member image for ${member.name}.${attributions}`; - return ( -
- +
- -
- {inducted && ( + +
+ +
+ {inducted && ( +

+ {dayjs(member.createdAt).format("L")} +

+ )} +

+ {member.name} +

- {dayjs(member.createdAt).format("L")} + Eden: @{member.account}

- )} -

- {member.name} -

-

- Eden: @{member.account} -

+
+
-
); }; diff --git a/packages/webapp/src/members/components/member-social-links.tsx b/packages/webapp/src/members/components/member-social-links.tsx index 9e1e3befa..416719d53 100644 --- a/packages/webapp/src/members/components/member-social-links.tsx +++ b/packages/webapp/src/members/components/member-social-links.tsx @@ -1,9 +1,10 @@ import { HiOutlineLink } from "react-icons/hi"; import { FaFacebook, FaLinkedin, FaTelegram, FaTwitter } from "react-icons/fa"; import { IoChatbubblesOutline } from "react-icons/io5"; -import { GenIcon } from "react-icons/lib"; import { explorerAccountUrl, SocialButton } from "_app"; +import { EosCommunityIcon } from "_app/ui/icons"; + import { MemberData } from "../interfaces"; import { getValidSocialLink } from "../helpers/social-links"; @@ -18,7 +19,7 @@ export const MemberSocialLinks = ({ member }: Props) => { const telegramHandle = getValidSocialLink(member.socialHandles.telegram); return (
{ return `//${address.substring(domainBeginIndex)}`; }; - -const EosCommunityIcon = (props: any) => - GenIcon({ - tag: "svg", - attr: { viewBox: "0 0 32.2 48" }, - child: [ - { - tag: "path", - attr: { - d: - "M16.1 0L4.8 15.5 0 38.3 16.1 48l16.1-9.7-4.8-22.9L16.1 0zM7.4 15.9L16.1 4l8.7 11.9L16.1 42 7.4 15.9zM26 19.8l3.6 17.4-11.8 7.1L26 19.8zM2.6 37.2l3.6-17.4 8.2 24.5-11.8-7.1z", - }, - }, - ], - } as any)(props); diff --git a/packages/webapp/src/members/components/token-balance.tsx b/packages/webapp/src/members/components/token-balance.tsx index 9c80b5777..1ff7b3e18 100644 --- a/packages/webapp/src/members/components/token-balance.tsx +++ b/packages/webapp/src/members/components/token-balance.tsx @@ -6,14 +6,12 @@ import { } from "_app"; import { blockExplorerAccountBaseUrl } from "config"; -import { MemberData } from "../interfaces"; - interface Props { - member: MemberData; + account: string; } -export const TokenBalance = ({ member }: Props) => { - const { data: balance } = useTokenBalanceForAccount(member.account); +export const TokenBalance = ({ account }: Props) => { + const { data: balance } = useTokenBalanceForAccount(account); return (
@@ -21,7 +19,7 @@ export const TokenBalance = ({ member }: Props) => { Balance:{" "} {balance ? assetToLocaleString(balance) : "loading..."}{" "} History diff --git a/packages/webapp/src/members/helpers/formatters.ts b/packages/webapp/src/members/helpers/formatters.ts new file mode 100644 index 000000000..da5eafd83 --- /dev/null +++ b/packages/webapp/src/members/helpers/formatters.ts @@ -0,0 +1,21 @@ +import { MemberData, MembersQueryNode } from "members/interfaces"; + +/******************************************** + * MICROCHAIN GRAPHQL QUERY RESULT FORMATTERS + *******************************************/ + +export const formatQueriedMemberData = ( + data: MembersQueryNode +): MemberData | undefined => { + if (!data) return; + return { + createdAt: data.createdAt ? new Date(data.createdAt).getTime() : 0, + account: data.account, + name: data.profile.name, + image: data.profile.img, + attributions: data.profile.attributions, + bio: data.profile.bio, + socialHandles: JSON.parse(data.profile.social), + inductionVideo: data.inductionVideo, + }; +}; diff --git a/packages/webapp/src/members/helpers/index.ts b/packages/webapp/src/members/helpers/index.ts new file mode 100644 index 000000000..f733d53d4 --- /dev/null +++ b/packages/webapp/src/members/helpers/index.ts @@ -0,0 +1,2 @@ +export * from "./formatters"; +export * from "./social-links"; diff --git a/packages/webapp/src/members/hooks.ts b/packages/webapp/src/members/hooks.ts deleted file mode 100644 index f4e78a006..000000000 --- a/packages/webapp/src/members/hooks.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useQuery } from "react-query"; - -import { queryNewMembers, useMembers } from "_app"; -import { MemberData } from "members"; - -const sortMembersByDateDESC = (a: MemberData, b: MemberData) => - b.createdAt - a.createdAt; - -export const useMembersWithAssets = () => { - const NEW_MEMBERS_PAGE_SIZE = 10000; - const newMembers = useQuery({ - ...queryNewMembers(1, NEW_MEMBERS_PAGE_SIZE), - }); - - const allMembers = useMembers(); - - const isLoading = newMembers.isLoading || allMembers.isLoading; - const isError = - newMembers.isError || allMembers.isError || !allMembers.data; - - let members: MemberData[] = []; - - if (allMembers.data.length) { - const mergeAuctionData = (member: MemberData) => { - const newMemberRecord = newMembers.data?.find( - (newMember) => newMember.account === member.account - ); - return newMemberRecord ?? member; - }; - - members = (allMembers.data as MemberData[]) - .sort(sortMembersByDateDESC) - .map(mergeAuctionData); - } - - return { - members, - isLoading, - isError, - }; -}; diff --git a/packages/webapp/src/members/hooks/index.ts b/packages/webapp/src/members/hooks/index.ts new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/packages/webapp/src/members/hooks/index.ts @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/packages/webapp/src/members/hooks/queries.ts b/packages/webapp/src/members/hooks/queries.ts new file mode 100644 index 000000000..5fc953f41 --- /dev/null +++ b/packages/webapp/src/members/hooks/queries.ts @@ -0,0 +1,99 @@ +import { useQuery as useReactQuery } from "react-query"; +import { useQuery as useBoxQuery } from "@edenos/common/dist/subchain"; + +import { formatQueriedMemberData, getNewMembers } from "members"; +import { MemberData, MembersQuery } from "members/interfaces"; + +export const MEMBER_DATA_FRAGMENT = ` + account + createdAt + profile { + name + img + attributions + social + bio + } + inductionVideo +`; + +export const useMembers = () => { + const result = useBoxQuery(`{ + members { + edges { + node { + ${MEMBER_DATA_FRAGMENT} + } + } + } + }`); + + let formattedMembers: MemberData[] = []; + + if (!result.data) return { ...result, data: formattedMembers }; + + const memberEdges = result.data.members.edges; + if (memberEdges) { + formattedMembers = memberEdges.map( + (member) => formatQueriedMemberData(member.node) as MemberData + ); + } + + return { ...result, data: formattedMembers }; +}; + +export const useMemberByAccountName = (account: string) => { + const result = useBoxQuery(`{ + members(ge: "${account}", le: "${account}") { + edges { + node { + ${MEMBER_DATA_FRAGMENT} + } + } + } + }`); + + if (!result.data) return { ...result, data: null }; + + const memberNode = result.data.members.edges[0]?.node; + const member = formatQueriedMemberData(memberNode) ?? null; + return { ...result, data: member }; +}; + +const sortMembersByDateDESC = (a: MemberData, b: MemberData) => + b.createdAt - a.createdAt; + +export const queryNewMembers = (page: number, pageSize: number) => ({ + queryKey: ["query_new_members", page, pageSize], + queryFn: () => getNewMembers(page, pageSize), +}); + +export const useMembersWithAssets = () => { + const NEW_MEMBERS_PAGE_SIZE = 10000; + const newMembers = useReactQuery({ + ...queryNewMembers(1, NEW_MEMBERS_PAGE_SIZE), + }); + + const allMembers = useMembers(); + + const isLoading = newMembers.isLoading || allMembers.isLoading; + const isError = + newMembers.isError || allMembers.isError || !allMembers.data; + + let members: MemberData[] = []; + + if (allMembers.data.length) { + const mergeAuctionData = (member: MemberData) => { + const newMemberRecord = newMembers.data?.find( + (newMember) => newMember.account === member.account + ); + return newMemberRecord ?? member; + }; + + members = allMembers.data + .sort(sortMembersByDateDESC) + .map(mergeAuctionData); + } + + return { members, isLoading, isError }; +}; diff --git a/packages/webapp/src/members/index.ts b/packages/webapp/src/members/index.ts index 17ae0ec82..8aa7d75ac 100644 --- a/packages/webapp/src/members/index.ts +++ b/packages/webapp/src/members/index.ts @@ -1,4 +1,5 @@ export * from "./api"; export * from "./components"; +export * from "./helpers"; export * from "./hooks"; export * from "./interfaces"; diff --git a/packages/webapp/src/members/interfaces.ts b/packages/webapp/src/members/interfaces.ts index 7d30898e0..45c597a8c 100644 --- a/packages/webapp/src/members/interfaces.ts +++ b/packages/webapp/src/members/interfaces.ts @@ -6,19 +6,16 @@ export type VoteDataQueryOptionsByField = { fieldValue: string; }; -export interface MemberAccountData { +export interface MemberData { + createdAt: number; account: string; name: string; image: string; - socialHandles: EdenNftSocialHandles; - createdAt: number; -} - -export interface MemberData extends MemberAccountData { - templateId: number; - bio: string; attributions: string; + bio: string; + socialHandles: EdenNftSocialHandles; inductionVideo: string; + templateId?: number; auctionData?: MemberAuctionData; assetData?: AssetData; saleId?: string; @@ -60,3 +57,27 @@ export interface MemberStats { // NOTE: ranks is set to [] at start of election and has a new entry added at the end of each round ranks: any[]; } + +/********************************* + * MEMBER GRAPHQL QUERY INTERFACES + ********************************/ +export interface MembersQuery { + members: { + edges: { + node: MembersQueryNode; + }[]; + }; +} + +export interface MembersQueryNode { + createdAt: string; + account: string; + profile: { + name: string; + img: string; + attributions: string; + social: string; + bio: string; + }; + inductionVideo: string; +} diff --git a/packages/webapp/src/nfts/api/nfts.ts b/packages/webapp/src/nfts/api/nfts.ts index 893c52143..201c33a70 100644 --- a/packages/webapp/src/nfts/api/nfts.ts +++ b/packages/webapp/src/nfts/api/nfts.ts @@ -1,11 +1,6 @@ import { atomicAssets } from "config"; import { AssetData, AuctionableTemplateData } from "../interfaces"; -export const getTemplate = async (templateId: string) => { - const templates = await getTemplates(1, 20, [templateId]); - return templates.length ? templates[0] : undefined; -}; - const FETCH_AFTER_TIMESTAMP = atomicAssets.fetchAfter ? `&after=${atomicAssets.fetchAfter}` : "&after=1619779033000"; // genesis community launch timestamp @@ -39,29 +34,6 @@ export const getAccountCollection = async ( return data; }; -export const getSalesForTemplates = async ( - ids: string[], - page = 1, - limit = 9999, - sortField = "created", - order = "asc" -): Promise => { - const url = `${atomicAssets.apiMarketUrl}/sales?template_id=${ids.join( - "," - )}&collection_name=${ - atomicAssets.collection - }&page=${page}&limit=${limit}&order=${order}&sort=${sortField}${FETCH_AFTER_TIMESTAMP}`; - const { data } = await executeAtomicAssetRequest(url); - return data; -}; - -export const getOwners = async (templateId: number): Promise => { - const url = `${atomicAssets.apiBaseUrl}/assets?collection_name=${atomicAssets.collection}&schema_name=${atomicAssets.schema}&template_id=${templateId}&page=1&limit=9999&order=asc&sort=created`; - const { data } = await executeAtomicAssetRequest(url); - const owners: string[] = data.map((item: any) => item.owner); - return owners; -}; - export const getAuctions = async ( seller?: string, templateIds?: string[], diff --git a/packages/webapp/src/nfts/hooks/index.ts b/packages/webapp/src/nfts/hooks/index.ts new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/packages/webapp/src/nfts/hooks/index.ts @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/packages/webapp/src/nfts/hooks/queries.ts b/packages/webapp/src/nfts/hooks/queries.ts new file mode 100644 index 000000000..da981c932 --- /dev/null +++ b/packages/webapp/src/nfts/hooks/queries.ts @@ -0,0 +1,63 @@ +import { useQuery as useReactQuery } from "react-query"; +import { useQuery as useBoxQuery } from "@edenos/common/dist/subchain"; + +import { atomicAssets, edenContractAccount } from "config"; +import { formatQueriedMemberData, MEMBER_DATA_FRAGMENT } from "members"; +import { getCollection, memberDataDefaults } from "members/api"; +import { MemberData, MembersQueryNode } from "members/interfaces"; +import { NFTCollectorsQuery } from "nfts/interfaces"; + +export const queryMemberNFTCollection = (account: string) => ({ + queryKey: ["query_member_nft_collection", account], + queryFn: () => getCollection(account), +}); + +export const useMemberNFTCollection = (account: string) => { + return useReactQuery({ + ...queryMemberNFTCollection(account), + }); +}; + +export const useMemberNFTCollectors = (account: string) => { + const result = useBoxQuery(`{ + members(ge: "${account}", le: "${account}") { + edges { + node { + nfts { + edges { + node { + owner { + ${MEMBER_DATA_FRAGMENT} + } + } + } + } + } + } + } + }`); + + let collectors: MemberData[] = []; + + if (!result.data) return { ...result, data: collectors }; + + const collectorEdges = result.data.members.edges[0]?.node.nfts.edges; + if (collectorEdges) { + collectors = collectorEdges + .filter((edge) => !isAuction(edge.node.owner.account)) + .map((edge) => formatCollectorAsMemberData(edge.node.owner)); + } + + return { ...result, data: collectors }; +}; + +// filter new-member and after-market auctions +const isAuction = (account: string) => + [edenContractAccount, atomicAssets.marketContract].includes(account); + +const formatCollectorAsMemberData = (owner: MembersQueryNode) => { + if (owner.profile) { + return formatQueriedMemberData(owner) as MemberData; + } + return { ...memberDataDefaults, name: owner.account }; +}; diff --git a/packages/webapp/src/nfts/interfaces.ts b/packages/webapp/src/nfts/interfaces.ts index d459df620..d2c93c5f1 100644 --- a/packages/webapp/src/nfts/interfaces.ts +++ b/packages/webapp/src/nfts/interfaces.ts @@ -1,4 +1,5 @@ import { Asset } from "_app"; +import { MembersQueryNode } from "members/interfaces"; export interface EdenNftData { name: string; @@ -40,3 +41,24 @@ export interface AuctionableTemplateData extends TemplateData { assetId: string; templateMint: number; } + +/****************************** + * NFT GRAPHQL QUERY INTERFACES + *****************************/ +interface NFTCollectorsQueryNode { + owner: MembersQueryNode; +} + +export interface NFTCollectorsQuery { + members: { + edges: { + node: { + nfts: { + edges: { + node: NFTCollectorsQueryNode; + }[]; + }; + }; + }[]; + }; +} diff --git a/packages/webapp/src/pages/election/round-video-upload.tsx b/packages/webapp/src/pages/election/round-video-upload.tsx index 3a13b4bfc..fc74cacdd 100644 --- a/packages/webapp/src/pages/election/round-video-upload.tsx +++ b/packages/webapp/src/pages/election/round-video-upload.tsx @@ -29,7 +29,7 @@ import { setElectionRoundVideo, } from "elections"; import { RoundHeader } from "elections/components/ongoing-election-components"; -import { MemberAccountData, MemberGateContainer } from "members"; +import { MemberData, MemberGateContainer } from "members"; export const RoundVideoUploadPage = () => { const { @@ -254,7 +254,7 @@ const LoaderSection = () => ( interface HeaderProps { isOngoing: boolean; roundIndex: number; - winner?: MemberAccountData; + winner?: MemberData; roundStartTime?: dayjs.Dayjs; roundEndTime?: dayjs.Dayjs; } diff --git a/packages/webapp/src/pages/election/stats.tsx b/packages/webapp/src/pages/election/stats.tsx index 60a8f93cb..c3bd15de6 100644 --- a/packages/webapp/src/pages/election/stats.tsx +++ b/packages/webapp/src/pages/election/stats.tsx @@ -185,9 +185,7 @@ const GroupSegment = ({ // TODO: revisit this, unfortunately the MembersGrid only accepts MemberData, // even though we don't need it to display the required summarized member // chip data - const members: MemberData[] = group.votes.map( - (vote) => vote.voter as MemberData - ); + const members: MemberData[] = group.votes.map((vote) => vote.voter); const membersStats = group.votes.reduce((membersVotingMap, vote) => { membersVotingMap[vote.voter.account] = { diff --git a/packages/webapp/src/pages/members/[id].tsx b/packages/webapp/src/pages/members/[id].tsx index b187ea10b..54bbb7c20 100644 --- a/packages/webapp/src/pages/members/[id].tsx +++ b/packages/webapp/src/pages/members/[id].tsx @@ -1,85 +1,70 @@ -import { GetServerSideProps } from "next"; -import { QueryClient, useQuery } from "react-query"; -import { dehydrate } from "react-query/hydration"; +import React from "react"; +import { useRouter } from "next/router"; import { - CallToAction, Container, SideNavLayout, - LoadingCard, - queryMemberData, + LoadingContainer, + Heading, + MessageContainer, } from "_app"; -import { ROUTES } from "_app/routes"; - -import { MemberCard, MemberCollections, MemberHoloCard } from "members"; +import { + MemberCard, + MemberCollections, + MemberHoloCard, + useMemberByAccountName, +} from "members"; import { DelegateFundsAvailable } from "delegates/components"; -/** - * We have an issue if the member is not found in the development environment - * due to dehydration with JSON not being able to serialize `undefined`: - * Error: Error serializing `.dehydratedState.queries[0].state.data` - * returned from `getServerSideProps` in "/members/[id]". - * Reason: `undefined` cannot be serialized as JSON. Please use `null` - * or omit this value. - * Let's track this here: https://github.com/tannerlinsley/react-query/issues/1978 - */ -export const getServerSideProps: GetServerSideProps = async ({ params }) => { - const account = params!.id as string; - - const queryClient = new QueryClient(); - await queryClient.prefetchQuery(queryMemberData(account)); - - return { props: { account, dehydratedState: dehydrate(queryClient) } }; -}; - -interface Props { - account: string; -} - -export const MemberPage = ({ account }: Props) => { - const { data: member, isLoading } = useQuery({ - ...queryMemberData(account), - keepPreviousData: true, - }); +export const MemberPage = () => { + const router = useRouter(); + const { data: member, isLoading, isError } = useMemberByAccountName( + router.query.id as string + ); - if (member) { + if (isLoading) { return ( - - - -
-
- -
- -
-
- -
+ + + ); } - if (isLoading) { + if (isError || !member) { return ( - - - + + + ); } return ( - - - This account is not an active Eden member. - - + + + + + + + + ); }; export default MemberPage; + +interface ContainerProps { + pageTitle: string; + children: React.ReactNode; +} + +const MemberPageContainer = ({ pageTitle, children }: ContainerProps) => ( + + + Member Profile + + {children} + +); From 93a4e7915a5ab209523c7cbac34717858a67b15f Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Tue, 26 Oct 2021 21:59:15 -0400 Subject: [PATCH 15/33] Init SHiP receiver --- packages/box/src/handlers/subchain.ts | 10 +- .../{ => history-receivers}/dfuse-receiver.ts | 52 +----- packages/box/src/history-receivers/index.ts | 2 + .../box/src/history-receivers/interfaces.ts | 28 +++ .../src/history-receivers/ship-receiver.ts | 159 ++++++++++++++++++ packages/box/src/utils.ts | 12 ++ 6 files changed, 215 insertions(+), 48 deletions(-) rename packages/box/src/{ => history-receivers}/dfuse-receiver.ts (89%) create mode 100644 packages/box/src/history-receivers/index.ts create mode 100644 packages/box/src/history-receivers/interfaces.ts create mode 100644 packages/box/src/history-receivers/ship-receiver.ts create mode 100644 packages/box/src/utils.ts diff --git a/packages/box/src/handlers/subchain.ts b/packages/box/src/handlers/subchain.ts index 28d53db21..2ae9924ac 100644 --- a/packages/box/src/handlers/subchain.ts +++ b/packages/box/src/handlers/subchain.ts @@ -5,7 +5,7 @@ import path from "path"; import { Storage } from "../subchain-storage"; import logger from "../logger"; import { subchainConfig } from "../config"; -import DfuseReceiver from "../dfuse-receiver"; +import { DfuseReceiver, ShipReceiver } from "../history-receivers"; import { ClientStatus, ServerMessage, @@ -13,7 +13,6 @@ import { } from "@edenos/common/dist/subchain/SubchainProtocol"; const storage = new Storage(); -const dfuseReceiver = new DfuseReceiver(storage); export const subchainHandler = express.Router(); subchainHandler.get("/eden-micro-chain.wasm", (req, res) => { @@ -152,7 +151,12 @@ export async function startSubchain() { subchainConfig.atomic, subchainConfig.atomicMarket ); - await dfuseReceiver.start(); + + // TODO: gate through config + // const dfuseReceiver = new DfuseReceiver(storage); + // await dfuseReceiver.start(); + const shipReceiver = new ShipReceiver(storage); + await shipReceiver.start(); } catch (e: any) { logger.error(e); } diff --git a/packages/box/src/dfuse-receiver.ts b/packages/box/src/history-receivers/dfuse-receiver.ts similarity index 89% rename from packages/box/src/dfuse-receiver.ts rename to packages/box/src/history-receivers/dfuse-receiver.ts index 4bcdf1df1..2d18a4604 100644 --- a/packages/box/src/dfuse-receiver.ts +++ b/packages/box/src/history-receivers/dfuse-receiver.ts @@ -1,11 +1,13 @@ import { createDfuseClient, GraphqlStreamMessage, Stream } from "@dfuse/client"; import * as fs from "fs"; import nodeFetch from "node-fetch"; -import WebSocketClient from "ws"; import { performance } from "perf_hooks"; -import { dfuseConfig, subchainConfig } from "./config"; -import { Storage } from "./subchain-storage"; -import logger from "./logger"; + +import { dfuseConfig, subchainConfig } from "../config"; +import { Storage } from "../subchain-storage"; +import logger from "../logger"; +import { webSocketFactory } from "../utils"; +import { JsonTrx } from "./interfaces"; const query = ` subscription ($query: String!, $cursor: String, $limit: Int64, $low: Int64, @@ -41,47 +43,7 @@ subscription ($query: String!, $cursor: String, $limit: Int64, $low: Int64, } }`; -interface JsonTrx { - undo: boolean; - cursor: string; - irreversibleBlockNum: number; - block: { - num: number; - id: string; - timestamp: string; - previous: string; - }; - trace: { - id: string; - status: string; - matchingActions: [ - { - seq: number; - receiver: string; - account: string; - name: string; - creatorAction: { - seq: number; - receiver: string; - }; - hexData: string; - } - ]; - }; -} - -async function webSocketFactory( - url: string, - protocols: string[] = [] -): Promise { - const webSocket = new WebSocketClient(url, protocols, { - handshakeTimeout: 30 * 1000, // 30s - maxPayload: 10 * 1024 * 1024, - }); - return webSocket; -} - -export default class DfuseReceiver { +export class DfuseReceiver { storage: Storage; stream: Stream | null = null; jsonTransactions: JsonTrx[] = []; diff --git a/packages/box/src/history-receivers/index.ts b/packages/box/src/history-receivers/index.ts new file mode 100644 index 000000000..039520818 --- /dev/null +++ b/packages/box/src/history-receivers/index.ts @@ -0,0 +1,2 @@ +export * from "./dfuse-receiver"; +export * from "./ship-receiver"; diff --git a/packages/box/src/history-receivers/interfaces.ts b/packages/box/src/history-receivers/interfaces.ts new file mode 100644 index 000000000..c2aaa2267 --- /dev/null +++ b/packages/box/src/history-receivers/interfaces.ts @@ -0,0 +1,28 @@ +export interface JsonTrx { + undo: boolean; + cursor: string; + irreversibleBlockNum: number; + block: { + num: number; + id: string; + timestamp: string; + previous: string; + }; + trace: { + id: string; + status: string; + matchingActions: [ + { + seq: number; + receiver: string; + account: string; + name: string; + creatorAction: { + seq: number; + receiver: string; + }; + hexData: string; + } + ]; + }; +} diff --git a/packages/box/src/history-receivers/ship-receiver.ts b/packages/box/src/history-receivers/ship-receiver.ts new file mode 100644 index 000000000..8df649bcc --- /dev/null +++ b/packages/box/src/history-receivers/ship-receiver.ts @@ -0,0 +1,159 @@ +import WebSocket from "ws"; +import * as eosjsSerialize from "eosjs/dist/eosjs-serialize"; + +import { dfuseConfig } from "../config"; +import { Storage } from "../subchain-storage"; +import logger from "../logger"; +import { JsonTrx } from "./interfaces"; + +const SHIP_ADDRESS = "localhost"; +const SHIP_PORT = "8080"; +const FIRST_BLOCK = 1; + +export class ShipReceiver { + storage: Storage; + wsClient: WebSocket | undefined; + jsonTransactions: JsonTrx[] = []; + abi: any; + types: any; + blocksQueue: any[] = []; + + variables = { + query: "", + cursor: "", + low: FIRST_BLOCK, + limit: 0, + irrev: false, + interval: dfuseConfig.interval, + }; + + constructor(storage: Storage) { + this.storage = storage; + } + + async start() { + // TODO: add state loader + await this.connect(); + } // start() + + async connect() { + if (this.wsClient) { + logger.info(`already connected`); + return; + } + + try { + logger.info(`connecting to ${SHIP_ADDRESS}:${SHIP_PORT}`); + + if (this.jsonTransactions.length) + this.variables.cursor = this.jsonTransactions[ + this.jsonTransactions.length - 1 + ].cursor; + + // Connecting to SHiP + this.wsClient = new WebSocket(`ws://${SHIP_ADDRESS}:${SHIP_PORT}`); + this.wsClient.on("open", () => { + logger.info("SHiP is now connected"); + }); + this.wsClient.on("message", (data) => this.onMessage(data)); + this.wsClient.on("close", this.disconnect); + } catch (e: any) { + logger.error(e); + logger.info("scheduling retry in 10 min"); + setTimeout(() => { + this.connect(); + }, 10 * 60 * 1000); + } + } + + onMessage(data: any) { + if (!this.abi) { + this.setupAbi(data); + this.requestStatus(); + } else { + const [type, response] = this.deserialize("result", data); + this[type](response); + } + } + + disconnect(reason: string) { + logger.info("closed connection: %s", reason); + if (this.wsClient) { + this.abi = undefined; + this.wsClient = undefined; + } + setTimeout(() => { + this.connect(); + }, 1000); + } + + setupAbi(data: string) { + this.abi = JSON.parse(data); + logger.info("received SHiP abi"); + this.types = eosjsSerialize.getTypesFromAbi( + eosjsSerialize.createInitialTypes(), + this.abi + ); + } + + requestStatus() { + this.send(["get_status_request_v0", {}]); + } + + requestBlocks() { + this.send([ + "get_blocks_request_v0", + { + start_block_num: this.variables.low, + end_block_num: 0xffffffff, + max_messages_in_flight: 0xffffffff, + have_positions: [], + irreversible_only: false, + fetch_block: true, + fetch_traces: true, + fetch_deltas: false, + }, + ]); + } + + get_status_result_v0(response: any) { + logger.info("get status result head: %s", response.head); + this.requestBlocks(); + } + + get_blocks_result_v0(response: any) { + logger.info("get status result: %s", response.this_block); + this.blocksQueue.push(response); + // this.processBlocks(); + } + + send(request: any) { + this.wsClient.send(this.serialize("request", request)); + } + + serialize(type: string, value: any) { + const buffer = new eosjsSerialize.SerialBuffer({ + textEncoder: new TextEncoder(), + textDecoder: new TextDecoder(), + }); + eosjsSerialize.getType(this.types, type).serialize(buffer, value); + return buffer.asUint8Array(); + } + + deserialize(type: string, bytes: Uint8Array) { + const buffer = new eosjsSerialize.SerialBuffer({ + textEncoder: new TextEncoder(), + textDecoder: new TextDecoder(), + array: bytes, + }); + let result = eosjsSerialize + .getType(this.types, type) + .deserialize( + buffer, + new eosjsSerialize.SerializerState({ bytesAsUint8Array: true }) + ); + if (buffer.readPos != bytes.length) + throw new Error("deserialization error: " + type); // todo: remove check + return result; + } +} diff --git a/packages/box/src/utils.ts b/packages/box/src/utils.ts new file mode 100644 index 000000000..669eecfbd --- /dev/null +++ b/packages/box/src/utils.ts @@ -0,0 +1,12 @@ +import WebSocketClient from "ws"; + +export const webSocketFactory = async ( + url: string, + protocols: string[] = [] +): Promise => { + const webSocket = new WebSocketClient(url, protocols, { + handshakeTimeout: 30 * 1000, // 30s + maxPayload: 10 * 1024 * 1024, + }); + return webSocket; +}; From 91e6250af1f59b66602ec237863cc077c98db072 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Wed, 27 Oct 2021 00:09:35 -0400 Subject: [PATCH 16/33] Init SHiP on microchain --- contracts/eden/src/eden-micro-chain.cpp | 41 +++++++++ .../src/history-receivers/ship-receiver.ts | 85 ++++++++++++------- packages/box/src/subchain-storage.ts | 11 +++ packages/common/src/subchain/EdenSubchain.ts | 8 ++ 4 files changed, 116 insertions(+), 29 deletions(-) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index af37c40c7..070d301f3 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include using namespace eosio::literals; @@ -1869,6 +1870,25 @@ bool add_block(subchain::block&& eden_block, uint32_t eosio_irreversible) return add_block(std::move(bi), eosio_irreversible); } +bool add_block(eosio::ship_protocol::block_position block_position, uint32_t eosio_irreversible, + eosio::ship_protocol::signed_block&& signed_block) +{ + subchain::eosio_block eosio_block; + eosio_block.num = block_position.block_num; + eosio_block.id = block_position.block_id; + eosio_block.previous = signed_block.previous; + eosio_block.timestamp = signed_block.timestamp.to_time_point(); + + //eosio_block.transactions = signed_block.timestamp.to_time_point(); + + subchain::block eden_block; + eden_block.num = block_position.block_num; + eden_block.previous = signed_block.previous; + eden_block.eosioBlock = eosio_block; + + return add_block(std::move(eden_block), eosio_irreversible); +} + // TODO: prevent from_json from aborting [[clang::export_name("addEosioBlockJson")]] bool addEosioBlockJson(const char* json, uint32_t size, @@ -1909,6 +1929,27 @@ bool add_block(subchain::block&& eden_block, uint32_t eosio_irreversible) return add_block(std::move(block), eosio_irreversible); } +[[clang::export_name("pushShipMessage")]] bool pushShipMessage(const char* data, + uint32_t size) +{ + dump("ship received message"); + dump(size); + eosio::input_stream bin{data, size}; + eosio::ship_protocol::result result; + eosio::from_bin(result, bin); + dump("ship deserialized message"); + + if (auto blocks_result = std::get_if(&result)) + { + eosio::ship_protocol::signed_block block; + eosio::from_bin(block, blocks_result->block.value()); + return add_block(blocks_result->this_block.value(), + blocks_result->last_irreversible.block_num, + std::move(block)); + } + return false; +} + [[clang::export_name("setIrreversible")]] uint32_t setIrreversible(uint32_t irreversible) { if (auto* b = block_log.block_before_num(irreversible + 1)) diff --git a/packages/box/src/history-receivers/ship-receiver.ts b/packages/box/src/history-receivers/ship-receiver.ts index 8df649bcc..cc5087b94 100644 --- a/packages/box/src/history-receivers/ship-receiver.ts +++ b/packages/box/src/history-receivers/ship-receiver.ts @@ -13,10 +13,10 @@ const FIRST_BLOCK = 1; export class ShipReceiver { storage: Storage; wsClient: WebSocket | undefined; - jsonTransactions: JsonTrx[] = []; abi: any; types: any; blocksQueue: any[] = []; + inProcessBlocks = false; variables = { query: "", @@ -45,18 +45,13 @@ export class ShipReceiver { try { logger.info(`connecting to ${SHIP_ADDRESS}:${SHIP_PORT}`); - if (this.jsonTransactions.length) - this.variables.cursor = this.jsonTransactions[ - this.jsonTransactions.length - 1 - ].cursor; - // Connecting to SHiP this.wsClient = new WebSocket(`ws://${SHIP_ADDRESS}:${SHIP_PORT}`); this.wsClient.on("open", () => { logger.info("SHiP is now connected"); }); this.wsClient.on("message", (data) => this.onMessage(data)); - this.wsClient.on("close", this.disconnect); + this.wsClient.on("close", (reason) => this.disconnect(reason)); } catch (e: any) { logger.error(e); logger.info("scheduling retry in 10 min"); @@ -66,17 +61,7 @@ export class ShipReceiver { } } - onMessage(data: any) { - if (!this.abi) { - this.setupAbi(data); - this.requestStatus(); - } else { - const [type, response] = this.deserialize("result", data); - this[type](response); - } - } - - disconnect(reason: string) { + disconnect(reason: any) { logger.info("closed connection: %s", reason); if (this.wsClient) { this.abi = undefined; @@ -87,6 +72,19 @@ export class ShipReceiver { }, 1000); } + onMessage(data: WebSocket.Data) { + if (!this.abi) { + this.setupAbi(data as string); + this.requestBlocks(); + } else { + const bytes = (data as ArrayBuffer) as Uint8Array; + logger.info("shipping %s bytes", bytes.length); + this.storage.pushShipMessage((data as ArrayBuffer) as Uint8Array); + // const [type, response] = this.deserialize("result", data); + // this[type](response); + } + } + setupAbi(data: string) { this.abi = JSON.parse(data); logger.info("received SHiP abi"); @@ -116,19 +114,48 @@ export class ShipReceiver { ]); } - get_status_result_v0(response: any) { - logger.info("get status result head: %s", response.head); - this.requestBlocks(); - } - - get_blocks_result_v0(response: any) { - logger.info("get status result: %s", response.this_block); - this.blocksQueue.push(response); - // this.processBlocks(); - } + // get_status_result_v0(response: any) { + // logger.info("status result head: %s", response.head); + // } + + // get_blocks_result_v0(response: any) { + // logger.info( + // "received block: %s, head: %s", + // response.this_block.block_num, + // response.head.block_num + // ); + // this.blocksQueue.push(response); + // this.processBlocks(); + // } + + // async processBlocks() { + // if (this.inProcessBlocks) { + // return; + // } + + // this.inProcessBlocks = true; + + // try { + // while (this.blocksQueue.length) { + // const response = this.blocksQueue.shift(); + // if (response.block && response.block.length) { + // const block = this.deserialize( + // "signed_block", + // response.block + // ); + // logger.info("processed block: %s", block); + // logger.info("transactions: %s", block.transactions); + // } + // } + // } catch (e) { + // logger.error("process blocks failed: %s", e); + // } + + // this.inProcessBlocks = false; + // } send(request: any) { - this.wsClient.send(this.serialize("request", request)); + this.wsClient!.send(this.serialize("request", request)); } serialize(type: string, value: any) { diff --git a/packages/box/src/subchain-storage.ts b/packages/box/src/subchain-storage.ts index 662477d7d..5f14643c0 100644 --- a/packages/box/src/subchain-storage.ts +++ b/packages/box/src/subchain-storage.ts @@ -120,4 +120,15 @@ export class Storage { this.changed(); return result; } + + pushShipMessage(shipMessage: Uint8Array) { + const result = this.protect(() => { + const result = this.blocksWasm!.pushShipMessage(shipMessage); + this.stateWasm!.pushShipMessage(shipMessage); + this.stateWasm!.trimBlocks(); + return result; + }); + this.changed(); + return result; + } } diff --git a/packages/common/src/subchain/EdenSubchain.ts b/packages/common/src/subchain/EdenSubchain.ts index 1a12bd521..84d348da5 100644 --- a/packages/common/src/subchain/EdenSubchain.ts +++ b/packages/common/src/subchain/EdenSubchain.ts @@ -187,6 +187,14 @@ export class EdenSubchain { }); } + pushShipMessage(message: Uint8Array): boolean { + return this.protect(() => { + return this.withData(message, (addr) => { + return this.exports.pushShipMessage(addr, message.length); + }); + }); + } + trimBlocks() { this.protect(() => { this.exports.trimBlocks(); From 64560c2add1f1ea5004b443936f0ba9aa0c52781 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Thu, 28 Oct 2021 01:25:19 -0400 Subject: [PATCH 17/33] SHiP receiver cleanup and Transaction Handling --- contracts/eden/src/eden-micro-chain.cpp | 117 ++++++++++++--- .../src/history-receivers/ship-receiver.ts | 142 ++---------------- packages/box/src/subchain-storage.ts | 4 + packages/common/src/subchain/EdenSubchain.ts | 7 + 4 files changed, 122 insertions(+), 148 deletions(-) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 070d301f3..1f76545fe 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -1870,21 +1870,72 @@ bool add_block(subchain::block&& eden_block, uint32_t eosio_irreversible) return add_block(std::move(bi), eosio_irreversible); } -bool add_block(eosio::ship_protocol::block_position block_position, uint32_t eosio_irreversible, - eosio::ship_protocol::signed_block&& signed_block) +bool add_block(eosio::ship_protocol::block_position block, + eosio::ship_protocol::block_position prev, + uint32_t eosio_irreversible, + eosio::block_timestamp timestamp, + std::vector traces) { + printf("received block %d with %d traces\n", block.block_num, (int)traces.size()); + subchain::eosio_block eosio_block; - eosio_block.num = block_position.block_num; - eosio_block.id = block_position.block_id; - eosio_block.previous = signed_block.previous; - eosio_block.timestamp = signed_block.timestamp.to_time_point(); - - //eosio_block.transactions = signed_block.timestamp.to_time_point(); + eosio_block.num = block.block_num; + eosio_block.id = block.block_id; + eosio_block.previous = prev.block_id; + eosio_block.timestamp = timestamp.to_time_point(); + + for (const auto& transaction_trace : traces) + { + if (auto* trx_trace = + std::get_if(&transaction_trace)) + { + subchain::transaction transaction{ + .id = trx_trace->id, + }; + + for (const auto& action_trace : trx_trace->action_traces) + { + if (auto* act_trace = std::get_if(&action_trace)) + { + printf("action ordinal %d, creator ordinal %d\n", act_trace->action_ordinal.value, + act_trace->creator_action_ordinal.value); + + std::optional creatorAction; + if (act_trace->creator_action_ordinal.value > 0) + { + const auto& creator_action_trace = + std::get( + trx_trace->action_traces[act_trace->creator_action_ordinal.value - 1]); + creatorAction = subchain::creator_action{ + .seq = act_trace->creator_action_ordinal, + .receiver = creator_action_trace.receiver, + }; + } + + std::vector data(act_trace->act.data.pos, act_trace->act.data.end); + eosio::bytes hexData{data}; + + subchain::action action{ + .seq = act_trace->action_ordinal, + .firstReceiver = act_trace->act.account, + .receiver = act_trace->receiver, + .name = act_trace->act.name, + .creatorAction = creatorAction, + .hexData = hexData, + }; + + transaction.actions.emplace_back(action); + } + } + + eosio_block.transactions.emplace_back(transaction); + } + } subchain::block eden_block; - eden_block.num = block_position.block_num; - eden_block.previous = signed_block.previous; - eden_block.eosioBlock = eosio_block; + eden_block.num = eosio_block.num; + eden_block.previous = eosio_block.previous; + eden_block.eosioBlock = eosio_block; return add_block(std::move(eden_block), eosio_irreversible); } @@ -1929,23 +1980,49 @@ bool add_block(eosio::ship_protocol::block_position block_position, uint32_t eos return add_block(std::move(block), eosio_irreversible); } +[[clang::export_name("getShipBlocksRequest")]] bool getShipBlocksRequest() +{ + auto head = block_log.head(); + auto block_num = head ? head->num : 1; + + eosio::ship_protocol::request request = eosio::ship_protocol::get_blocks_request_v0{ + .start_block_num = block_num, + .end_block_num = 0xffff'ffff, + .max_messages_in_flight = 0xffff'ffff, + .fetch_block = true, + .fetch_traces = true, + }; + result = eosio::convert_to_bin(request); + + return true; +} + [[clang::export_name("pushShipMessage")]] bool pushShipMessage(const char* data, uint32_t size) { - dump("ship received message"); - dump(size); eosio::input_stream bin{data, size}; eosio::ship_protocol::result result; eosio::from_bin(result, bin); - dump("ship deserialized message"); - if (auto blocks_result = std::get_if(&result)) + if (auto* blocks_result = std::get_if(&result)) { - eosio::ship_protocol::signed_block block; - eosio::from_bin(block, blocks_result->block.value()); - return add_block(blocks_result->this_block.value(), - blocks_result->last_irreversible.block_num, - std::move(block)); + eosio::ship_protocol::signed_block signed_block; + if (blocks_result->block) + { + eosio::from_bin(signed_block, blocks_result->block.value()); + } + + std::vector traces; + if (blocks_result->traces) + { + eosio::from_bin(traces, blocks_result->traces.value()); + } + + auto prev_block = blocks_result->prev_block ? blocks_result->prev_block.value() + : eosio::ship_protocol::block_position{}; + + return add_block(blocks_result->this_block.value(), prev_block, + blocks_result->last_irreversible.block_num, signed_block.timestamp, traces); } return false; } diff --git a/packages/box/src/history-receivers/ship-receiver.ts b/packages/box/src/history-receivers/ship-receiver.ts index cc5087b94..1e1c4e016 100644 --- a/packages/box/src/history-receivers/ship-receiver.ts +++ b/packages/box/src/history-receivers/ship-receiver.ts @@ -1,40 +1,23 @@ import WebSocket from "ws"; -import * as eosjsSerialize from "eosjs/dist/eosjs-serialize"; -import { dfuseConfig } from "../config"; import { Storage } from "../subchain-storage"; import logger from "../logger"; -import { JsonTrx } from "./interfaces"; const SHIP_ADDRESS = "localhost"; const SHIP_PORT = "8080"; -const FIRST_BLOCK = 1; export class ShipReceiver { storage: Storage; wsClient: WebSocket | undefined; - abi: any; - types: any; - blocksQueue: any[] = []; - inProcessBlocks = false; - - variables = { - query: "", - cursor: "", - low: FIRST_BLOCK, - limit: 0, - irrev: false, - interval: dfuseConfig.interval, - }; + requestedBlocks = false; constructor(storage: Storage) { this.storage = storage; } async start() { - // TODO: add state loader await this.connect(); - } // start() + } async connect() { if (this.wsClient) { @@ -43,15 +26,16 @@ export class ShipReceiver { } try { - logger.info(`connecting to ${SHIP_ADDRESS}:${SHIP_PORT}`); - - // Connecting to SHiP + logger.info( + `Connecting to SHiP on ${SHIP_ADDRESS}:${SHIP_PORT}...` + ); this.wsClient = new WebSocket(`ws://${SHIP_ADDRESS}:${SHIP_PORT}`); this.wsClient.on("open", () => { logger.info("SHiP is now connected"); }); this.wsClient.on("message", (data) => this.onMessage(data)); this.wsClient.on("close", (reason) => this.disconnect(reason)); + this.wsClient.on("error", (error) => this.disconnect(error)); } catch (e: any) { logger.error(e); logger.info("scheduling retry in 10 min"); @@ -64,7 +48,7 @@ export class ShipReceiver { disconnect(reason: any) { logger.info("closed connection: %s", reason); if (this.wsClient) { - this.abi = undefined; + this.requestedBlocks = false; this.wsClient = undefined; } setTimeout(() => { @@ -73,114 +57,16 @@ export class ShipReceiver { } onMessage(data: WebSocket.Data) { - if (!this.abi) { - this.setupAbi(data as string); - this.requestBlocks(); + if (!this.requestedBlocks) { + logger.info("Requesting Blocks from SHiP..."); + const request = this.storage.getShipBlocksRequest(); + this.requestedBlocks = true; + this.wsClient!.send(request); + logger.info("Requested Blocks from SHiP!"); } else { const bytes = (data as ArrayBuffer) as Uint8Array; logger.info("shipping %s bytes", bytes.length); - this.storage.pushShipMessage((data as ArrayBuffer) as Uint8Array); - // const [type, response] = this.deserialize("result", data); - // this[type](response); + this.storage.pushShipMessage(bytes); } } - - setupAbi(data: string) { - this.abi = JSON.parse(data); - logger.info("received SHiP abi"); - this.types = eosjsSerialize.getTypesFromAbi( - eosjsSerialize.createInitialTypes(), - this.abi - ); - } - - requestStatus() { - this.send(["get_status_request_v0", {}]); - } - - requestBlocks() { - this.send([ - "get_blocks_request_v0", - { - start_block_num: this.variables.low, - end_block_num: 0xffffffff, - max_messages_in_flight: 0xffffffff, - have_positions: [], - irreversible_only: false, - fetch_block: true, - fetch_traces: true, - fetch_deltas: false, - }, - ]); - } - - // get_status_result_v0(response: any) { - // logger.info("status result head: %s", response.head); - // } - - // get_blocks_result_v0(response: any) { - // logger.info( - // "received block: %s, head: %s", - // response.this_block.block_num, - // response.head.block_num - // ); - // this.blocksQueue.push(response); - // this.processBlocks(); - // } - - // async processBlocks() { - // if (this.inProcessBlocks) { - // return; - // } - - // this.inProcessBlocks = true; - - // try { - // while (this.blocksQueue.length) { - // const response = this.blocksQueue.shift(); - // if (response.block && response.block.length) { - // const block = this.deserialize( - // "signed_block", - // response.block - // ); - // logger.info("processed block: %s", block); - // logger.info("transactions: %s", block.transactions); - // } - // } - // } catch (e) { - // logger.error("process blocks failed: %s", e); - // } - - // this.inProcessBlocks = false; - // } - - send(request: any) { - this.wsClient!.send(this.serialize("request", request)); - } - - serialize(type: string, value: any) { - const buffer = new eosjsSerialize.SerialBuffer({ - textEncoder: new TextEncoder(), - textDecoder: new TextDecoder(), - }); - eosjsSerialize.getType(this.types, type).serialize(buffer, value); - return buffer.asUint8Array(); - } - - deserialize(type: string, bytes: Uint8Array) { - const buffer = new eosjsSerialize.SerialBuffer({ - textEncoder: new TextEncoder(), - textDecoder: new TextDecoder(), - array: bytes, - }); - let result = eosjsSerialize - .getType(this.types, type) - .deserialize( - buffer, - new eosjsSerialize.SerializerState({ bytesAsUint8Array: true }) - ); - if (buffer.readPos != bytes.length) - throw new Error("deserialization error: " + type); // todo: remove check - return result; - } } diff --git a/packages/box/src/subchain-storage.ts b/packages/box/src/subchain-storage.ts index 5f14643c0..c39303435 100644 --- a/packages/box/src/subchain-storage.ts +++ b/packages/box/src/subchain-storage.ts @@ -121,6 +121,10 @@ export class Storage { return result; } + getShipBlocksRequest(): Uint8Array { + return this.protect(() => this.blocksWasm!.getShipBlocksRequest())!; + } + pushShipMessage(shipMessage: Uint8Array) { const result = this.protect(() => { const result = this.blocksWasm!.pushShipMessage(shipMessage); diff --git a/packages/common/src/subchain/EdenSubchain.ts b/packages/common/src/subchain/EdenSubchain.ts index 84d348da5..a4a72991c 100644 --- a/packages/common/src/subchain/EdenSubchain.ts +++ b/packages/common/src/subchain/EdenSubchain.ts @@ -187,6 +187,13 @@ export class EdenSubchain { }); } + getShipBlocksRequest() { + return this.protect(() => { + if (!this.exports.getShipBlocksRequest()) return null; + return this.resultAsUint8Array(); + }); + } + pushShipMessage(message: Uint8Array): boolean { return this.protect(() => { return this.withData(message, (addr) => { From b9988273233b31896dd6e80da68d2ad39ea0fb6d Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Thu, 28 Oct 2021 02:12:48 -0400 Subject: [PATCH 18/33] Fix block appends --- contracts/eden/src/eden-micro-chain.cpp | 13 ++++++++++--- packages/box/src/history-receivers/ship-receiver.ts | 1 - 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 1f76545fe..a157d5aeb 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -1933,9 +1933,16 @@ bool add_block(eosio::ship_protocol::block_position block, } subchain::block eden_block; - eden_block.num = eosio_block.num; - eden_block.previous = eosio_block.previous; - eden_block.eosioBlock = eosio_block; + eden_block.eosioBlock = std::move(eosio_block); + + auto* eden_prev = block_log.block_before_eosio_num(eden_block.eosioBlock.num); + if (eden_prev) + { + eden_block.num = eden_prev->num + 1; + eden_block.previous = eden_prev->id; + } + else + eden_block.num = 1; return add_block(std::move(eden_block), eosio_irreversible); } diff --git a/packages/box/src/history-receivers/ship-receiver.ts b/packages/box/src/history-receivers/ship-receiver.ts index 1e1c4e016..8e4d08a61 100644 --- a/packages/box/src/history-receivers/ship-receiver.ts +++ b/packages/box/src/history-receivers/ship-receiver.ts @@ -65,7 +65,6 @@ export class ShipReceiver { logger.info("Requested Blocks from SHiP!"); } else { const bytes = (data as ArrayBuffer) as Uint8Array; - logger.info("shipping %s bytes", bytes.length); this.storage.pushShipMessage(bytes); } } From cbd9520e8a179ee7c5f51372c97c4f35ddf4e3a7 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Thu, 28 Oct 2021 20:45:21 -0400 Subject: [PATCH 19/33] Working SHiP --- contracts/eden/src/eden-micro-chain.cpp | 48 ++++++++----------- .../src/history-receivers/ship-receiver.ts | 3 +- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index a157d5aeb..9d3e3ec8b 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -1870,6 +1870,22 @@ bool add_block(subchain::block&& eden_block, uint32_t eosio_irreversible) return add_block(std::move(bi), eosio_irreversible); } +bool add_block(subchain::eosio_block&& eosioBlock, uint32_t eosio_irreversible) { + subchain::block eden_block; + eden_block.eosioBlock = std::move(eosioBlock); + + auto* eden_prev = block_log.block_before_eosio_num(eden_block.eosioBlock.num); + if (eden_prev) + { + eden_block.num = eden_prev->num + 1; + eden_block.previous = eden_prev->id; + } + else + eden_block.num = 1; + + return add_block(std::move(eden_block), eosio_irreversible); +} + bool add_block(eosio::ship_protocol::block_position block, eosio::ship_protocol::block_position prev, uint32_t eosio_irreversible, @@ -1921,7 +1937,7 @@ bool add_block(eosio::ship_protocol::block_position block, .receiver = act_trace->receiver, .name = act_trace->act.name, .creatorAction = creatorAction, - .hexData = hexData, + .hexData = std::move(hexData), }; transaction.actions.emplace_back(action); @@ -1931,20 +1947,8 @@ bool add_block(eosio::ship_protocol::block_position block, eosio_block.transactions.emplace_back(transaction); } } - - subchain::block eden_block; - eden_block.eosioBlock = std::move(eosio_block); - - auto* eden_prev = block_log.block_before_eosio_num(eden_block.eosioBlock.num); - if (eden_prev) - { - eden_block.num = eden_prev->num + 1; - eden_block.previous = eden_prev->id; - } - else - eden_block.num = 1; - - return add_block(std::move(eden_block), eosio_irreversible); + + return add_block(std::move(eosio_block), eosio_irreversible); } // TODO: prevent from_json from aborting @@ -1956,19 +1960,7 @@ bool add_block(eosio::ship_protocol::block_position block, eosio::json_token_stream s(str.data()); subchain::eosio_block eosio_block; eosio::from_json(eosio_block, s); - - subchain::block eden_block; - eden_block.eosioBlock = std::move(eosio_block); - auto* prev = block_log.block_before_eosio_num(eden_block.eosioBlock.num); - if (prev) - { - eden_block.num = prev->num + 1; - eden_block.previous = prev->id; - } - else - eden_block.num = 1; - return add_block(std::move(eden_block), eosio_irreversible); - + return add_block(std::move(eosio_block), eosio_irreversible); // printf("%d blocks processed, %d blocks now in log\n", (int)eosio_blocks.size(), // (int)block_log.blocks.size()); // for (auto& b : block_log.blocks) diff --git a/packages/box/src/history-receivers/ship-receiver.ts b/packages/box/src/history-receivers/ship-receiver.ts index 8e4d08a61..0d65de15c 100644 --- a/packages/box/src/history-receivers/ship-receiver.ts +++ b/packages/box/src/history-receivers/ship-receiver.ts @@ -64,8 +64,9 @@ export class ShipReceiver { this.wsClient!.send(request); logger.info("Requested Blocks from SHiP!"); } else { - const bytes = (data as ArrayBuffer) as Uint8Array; + const bytes = new Uint8Array(data as ArrayBuffer); this.storage.pushShipMessage(bytes); + this.storage.saveState(); } } } From c1703b4848d787d6401dddd3087aeffa1f848971 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Thu, 28 Oct 2021 21:17:59 -0400 Subject: [PATCH 20/33] Address PR reviews and add SHIP config --- packages/box/src/config.ts | 32 +++++++++++++- packages/box/src/handlers/subchain.ts | 21 ++++++--- .../src/history-receivers/dfuse-receiver.ts | 43 ++++++++++++++++++- .../box/src/history-receivers/interfaces.ts | 28 ------------ .../src/history-receivers/ship-receiver.ts | 10 ++--- packages/box/src/utils.ts | 12 ------ 6 files changed, 92 insertions(+), 54 deletions(-) delete mode 100644 packages/box/src/history-receivers/interfaces.ts delete mode 100644 packages/box/src/utils.ts diff --git a/packages/box/src/config.ts b/packages/box/src/config.ts index 8f96a7d80..200fc848a 100644 --- a/packages/box/src/config.ts +++ b/packages/box/src/config.ts @@ -55,6 +55,12 @@ logger.info( JSON.stringify(validUploadActions, undefined, 2) ); +export enum SubchainReceivers { + UNKNOWN, + DFUSE, + SHIP, +} + export const subchainConfig = { enable: !("SUBCHAIN_DISABLE" in process.env), eden: process.env.SUBCHAIN_EDEN_CONTRACT || "genesis.eden", @@ -63,6 +69,7 @@ export const subchainConfig = { atomicMarket: process.env.SUBCHAIN_AA_MARKET_CONTRACT || "atomicmarket", wasmFile: process.env.SUBCHAIN_WASM || "../../build/eden-micro-chain.wasm", stateFile: process.env.SUBCHAIN_STATE || "state", + receiver: SubchainReceivers[process.env.SUBCHAIN_RECEIVER || "DFUSE"], }; console.info(subchainConfig); @@ -78,6 +85,29 @@ export const dfuseConfig = { : 30, preventConnect: "DFUSE_PREVENT_CONNECT" in process.env, }; -console.info({ ...dfuseConfig, apiKey: "" }); + +export const shipConfig = { + address: process.env.SHIP_ADDRESS || "127.0.0.1", + port: process.env.SHIP_PORT || "8080", +}; + +if (subchainConfig.enable) { + logger.info("Using Subchain Receiver: %s", subchainConfig.receiver); + switch (subchainConfig.receiver) { + case SubchainReceivers.DFUSE: { + console.info({ ...dfuseConfig, apiKey: "" }); + break; + } + case SubchainReceivers.SHIP: { + console.info(shipConfig); + break; + } + default: { + const error = "Invalid Subchain Receiver"; + logger.error(error); + throw new Error(error); + } + } +} logger.info("<== Env Configs Loaded!"); diff --git a/packages/box/src/handlers/subchain.ts b/packages/box/src/handlers/subchain.ts index 2ae9924ac..6b8f52050 100644 --- a/packages/box/src/handlers/subchain.ts +++ b/packages/box/src/handlers/subchain.ts @@ -4,7 +4,7 @@ import * as WebSocket from "ws"; import path from "path"; import { Storage } from "../subchain-storage"; import logger from "../logger"; -import { subchainConfig } from "../config"; +import { subchainConfig, SubchainReceivers } from "../config"; import { DfuseReceiver, ShipReceiver } from "../history-receivers"; import { ClientStatus, @@ -152,12 +152,21 @@ export async function startSubchain() { subchainConfig.atomicMarket ); - // TODO: gate through config - // const dfuseReceiver = new DfuseReceiver(storage); - // await dfuseReceiver.start(); - const shipReceiver = new ShipReceiver(storage); - await shipReceiver.start(); + await setupReceiver(); } catch (e: any) { logger.error(e); } } + +const setupReceiver = async () => { + switch (subchainConfig.receiver) { + case SubchainReceivers.DFUSE: { + const dfuseReceiver = new DfuseReceiver(storage); + return dfuseReceiver.start(); + } + case SubchainReceivers.SHIP: { + const shipReceiver = new ShipReceiver(storage); + return shipReceiver.start(); + } + } +}; diff --git a/packages/box/src/history-receivers/dfuse-receiver.ts b/packages/box/src/history-receivers/dfuse-receiver.ts index 2d18a4604..38be037c5 100644 --- a/packages/box/src/history-receivers/dfuse-receiver.ts +++ b/packages/box/src/history-receivers/dfuse-receiver.ts @@ -1,13 +1,12 @@ import { createDfuseClient, GraphqlStreamMessage, Stream } from "@dfuse/client"; import * as fs from "fs"; import nodeFetch from "node-fetch"; +import WebSocketClient from "ws"; import { performance } from "perf_hooks"; import { dfuseConfig, subchainConfig } from "../config"; import { Storage } from "../subchain-storage"; import logger from "../logger"; -import { webSocketFactory } from "../utils"; -import { JsonTrx } from "./interfaces"; const query = ` subscription ($query: String!, $cursor: String, $limit: Int64, $low: Int64, @@ -43,6 +42,46 @@ subscription ($query: String!, $cursor: String, $limit: Int64, $low: Int64, } }`; +interface JsonTrx { + undo: boolean; + cursor: string; + irreversibleBlockNum: number; + block: { + num: number; + id: string; + timestamp: string; + previous: string; + }; + trace: { + id: string; + status: string; + matchingActions: [ + { + seq: number; + receiver: string; + account: string; + name: string; + creatorAction: { + seq: number; + receiver: string; + }; + hexData: string; + } + ]; + }; +} + +const webSocketFactory = async ( + url: string, + protocols: string[] = [] +): Promise => { + const webSocket = new WebSocketClient(url, protocols, { + handshakeTimeout: 30 * 1000, // 30s + maxPayload: 10 * 1024 * 1024, + }); + return webSocket; +}; + export class DfuseReceiver { storage: Storage; stream: Stream | null = null; diff --git a/packages/box/src/history-receivers/interfaces.ts b/packages/box/src/history-receivers/interfaces.ts deleted file mode 100644 index c2aaa2267..000000000 --- a/packages/box/src/history-receivers/interfaces.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface JsonTrx { - undo: boolean; - cursor: string; - irreversibleBlockNum: number; - block: { - num: number; - id: string; - timestamp: string; - previous: string; - }; - trace: { - id: string; - status: string; - matchingActions: [ - { - seq: number; - receiver: string; - account: string; - name: string; - creatorAction: { - seq: number; - receiver: string; - }; - hexData: string; - } - ]; - }; -} diff --git a/packages/box/src/history-receivers/ship-receiver.ts b/packages/box/src/history-receivers/ship-receiver.ts index 0d65de15c..ccfc542bc 100644 --- a/packages/box/src/history-receivers/ship-receiver.ts +++ b/packages/box/src/history-receivers/ship-receiver.ts @@ -2,9 +2,7 @@ import WebSocket from "ws"; import { Storage } from "../subchain-storage"; import logger from "../logger"; - -const SHIP_ADDRESS = "localhost"; -const SHIP_PORT = "8080"; +import { shipConfig } from "../config"; export class ShipReceiver { storage: Storage; @@ -27,9 +25,11 @@ export class ShipReceiver { try { logger.info( - `Connecting to SHiP on ${SHIP_ADDRESS}:${SHIP_PORT}...` + `Connecting to SHiP on ${shipConfig.address}:${shipConfig.port}...` + ); + this.wsClient = new WebSocket( + `ws://${shipConfig.address}:${shipConfig.port}` ); - this.wsClient = new WebSocket(`ws://${SHIP_ADDRESS}:${SHIP_PORT}`); this.wsClient.on("open", () => { logger.info("SHiP is now connected"); }); diff --git a/packages/box/src/utils.ts b/packages/box/src/utils.ts deleted file mode 100644 index 669eecfbd..000000000 --- a/packages/box/src/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import WebSocketClient from "ws"; - -export const webSocketFactory = async ( - url: string, - protocols: string[] = [] -): Promise => { - const webSocket = new WebSocketClient(url, protocols, { - handshakeTimeout: 30 * 1000, // 30s - maxPayload: 10 * 1024 * 1024, - }); - return webSocket; -}; From e2f749fb69dc824475dc308a1f8ea82d344bd9ae Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Thu, 28 Oct 2021 21:48:03 -0400 Subject: [PATCH 21/33] Refactor microchain ship transactions --- contracts/eden/src/eden-micro-chain.cpp | 105 ++++++++++++------------ 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 9d3e3ec8b..66c545134 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -1824,6 +1824,59 @@ void filter_block(const subchain::eosio_block& block) } // for(trx) } // filter_block +std::vector ship_to_eden_transactions( + std::vector& traces) +{ + std::vector transactions; + + for (const auto& transaction_trace : traces) + { + if (auto* trx_trace = + std::get_if(&transaction_trace)) + { + subchain::transaction transaction{ + .id = trx_trace->id, + }; + + for (const auto& action_trace : trx_trace->action_traces) + { + if (auto* act_trace = std::get_if(&action_trace)) + { + std::optional creatorAction; + if (act_trace->creator_action_ordinal.value > 0) + { + const auto& creator_action_trace = + std::get( + trx_trace->action_traces[act_trace->creator_action_ordinal.value - 1]); + creatorAction = subchain::creator_action{ + .seq = act_trace->creator_action_ordinal, + .receiver = creator_action_trace.receiver, + }; + } + + std::vector data(act_trace->act.data.pos, act_trace->act.data.end); + eosio::bytes hexData{data}; + + subchain::action action{ + .seq = act_trace->action_ordinal, + .firstReceiver = act_trace->act.account, + .receiver = act_trace->receiver, + .name = act_trace->act.name, + .creatorAction = creatorAction, + .hexData = std::move(hexData), + }; + + transaction.actions.push_back(action); + } + } + + transactions.push_back(transaction); + } + } + + return transactions; +} + subchain::block_log block_log; void forked_n_blocks(size_t n) @@ -1892,62 +1945,12 @@ bool add_block(eosio::ship_protocol::block_position block, eosio::block_timestamp timestamp, std::vector traces) { - printf("received block %d with %d traces\n", block.block_num, (int)traces.size()); - subchain::eosio_block eosio_block; eosio_block.num = block.block_num; eosio_block.id = block.block_id; eosio_block.previous = prev.block_id; eosio_block.timestamp = timestamp.to_time_point(); - - for (const auto& transaction_trace : traces) - { - if (auto* trx_trace = - std::get_if(&transaction_trace)) - { - subchain::transaction transaction{ - .id = trx_trace->id, - }; - - for (const auto& action_trace : trx_trace->action_traces) - { - if (auto* act_trace = std::get_if(&action_trace)) - { - printf("action ordinal %d, creator ordinal %d\n", act_trace->action_ordinal.value, - act_trace->creator_action_ordinal.value); - - std::optional creatorAction; - if (act_trace->creator_action_ordinal.value > 0) - { - const auto& creator_action_trace = - std::get( - trx_trace->action_traces[act_trace->creator_action_ordinal.value - 1]); - creatorAction = subchain::creator_action{ - .seq = act_trace->creator_action_ordinal, - .receiver = creator_action_trace.receiver, - }; - } - - std::vector data(act_trace->act.data.pos, act_trace->act.data.end); - eosio::bytes hexData{data}; - - subchain::action action{ - .seq = act_trace->action_ordinal, - .firstReceiver = act_trace->act.account, - .receiver = act_trace->receiver, - .name = act_trace->act.name, - .creatorAction = creatorAction, - .hexData = std::move(hexData), - }; - - transaction.actions.emplace_back(action); - } - } - - eosio_block.transactions.emplace_back(transaction); - } - } - + eosio_block.transactions = std::move(ship_to_eden_transactions(traces)); return add_block(std::move(eosio_block), eosio_irreversible); } From 36dcafbd2a98c5135bf1babdd5eaae97ce2f8562 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Fri, 29 Oct 2021 20:37:57 -0400 Subject: [PATCH 22/33] Fix build and update runner --- contracts/eden/tests/include/nodeos-runner.hpp | 5 +++++ packages/box/src/config.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/contracts/eden/tests/include/nodeos-runner.hpp b/contracts/eden/tests/include/nodeos-runner.hpp index 1bd7a10e7..c328e8eff 100644 --- a/contracts/eden/tests/include/nodeos-runner.hpp +++ b/contracts/eden/tests/include/nodeos-runner.hpp @@ -25,10 +25,15 @@ struct nodeos_runner eosio::execute("nodeos -d " + runner_name + " " // "--config-dir config " // + "--plugin eosio::chain_plugin " // "--plugin eosio::chain_api_plugin " // "--plugin eosio::producer_api_plugin " // "--plugin eosio::state_history_plugin " // + "--plugin eosio::http_plugin " // "--trace-history --disable-replay-opts " // + "--access-control-allow-origin \"*\" " // + "--access-control-allow-header \"*\" " // + "--http-validate-host 0 " // "-e -p eosio"); } }; diff --git a/packages/box/src/config.ts b/packages/box/src/config.ts index 200fc848a..6447096db 100644 --- a/packages/box/src/config.ts +++ b/packages/box/src/config.ts @@ -69,7 +69,11 @@ export const subchainConfig = { atomicMarket: process.env.SUBCHAIN_AA_MARKET_CONTRACT || "atomicmarket", wasmFile: process.env.SUBCHAIN_WASM || "../../build/eden-micro-chain.wasm", stateFile: process.env.SUBCHAIN_STATE || "state", - receiver: SubchainReceivers[process.env.SUBCHAIN_RECEIVER || "DFUSE"], + receiver: + SubchainReceivers[ + (process.env.SUBCHAIN_RECEIVER || + "DFUSE") as keyof typeof SubchainReceivers + ], }; console.info(subchainConfig); From d7f2f449f79bfcd2b59959b8edd5c7d5bc0ae526 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 Date: Tue, 2 Nov 2021 09:03:09 -0400 Subject: [PATCH 23/33] Address PR review feedback --- contracts/eden/src/eden-micro-chain.cpp | 18 ++++++++++-------- contracts/eden/tests/include/nodeos-runner.hpp | 2 -- packages/box/src/config.ts | 1 + packages/box/src/handlers/subchain.ts | 2 +- .../box/src/history-receivers/ship-receiver.ts | 4 +++- packages/box/src/subchain-storage.ts | 6 ++++-- packages/common/src/subchain/EdenSubchain.ts | 4 ++-- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 66c545134..874149e62 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -1848,8 +1848,10 @@ std::vector ship_to_eden_transactions( const auto& creator_action_trace = std::get( trx_trace->action_traces[act_trace->creator_action_ordinal.value - 1]); + const auto& receipt = std::get( + *creator_action_trace.receipt); creatorAction = subchain::creator_action{ - .seq = act_trace->creator_action_ordinal, + .seq = receipt.global_sequence, .receiver = creator_action_trace.receiver, }; } @@ -1857,8 +1859,11 @@ std::vector ship_to_eden_transactions( std::vector data(act_trace->act.data.pos, act_trace->act.data.end); eosio::bytes hexData{data}; + const auto& receipt = + std::get(*act_trace->receipt); + subchain::action action{ - .seq = act_trace->action_ordinal, + .seq = receipt.global_sequence, .firstReceiver = act_trace->act.account, .receiver = act_trace->receiver, .name = act_trace->act.name, @@ -1870,7 +1875,7 @@ std::vector ship_to_eden_transactions( } } - transactions.push_back(transaction); + transactions.push_back(std::move(transaction)); } } @@ -1950,7 +1955,7 @@ bool add_block(eosio::ship_protocol::block_position block, eosio_block.id = block.block_id; eosio_block.previous = prev.block_id; eosio_block.timestamp = timestamp.to_time_point(); - eosio_block.transactions = std::move(ship_to_eden_transactions(traces)); + eosio_block.transactions = ship_to_eden_transactions(traces); return add_block(std::move(eosio_block), eosio_irreversible); } @@ -1982,11 +1987,8 @@ bool add_block(eosio::ship_protocol::block_position block, return add_block(std::move(block), eosio_irreversible); } -[[clang::export_name("getShipBlocksRequest")]] bool getShipBlocksRequest() +[[clang::export_name("getShipBlocksRequest")]] bool getShipBlocksRequest(uint32_t block_num) { - auto head = block_log.head(); - auto block_num = head ? head->num : 1; - eosio::ship_protocol::request request = eosio::ship_protocol::get_blocks_request_v0{ .start_block_num = block_num, .end_block_num = 0xffff'ffff, diff --git a/contracts/eden/tests/include/nodeos-runner.hpp b/contracts/eden/tests/include/nodeos-runner.hpp index c328e8eff..f75ccedeb 100644 --- a/contracts/eden/tests/include/nodeos-runner.hpp +++ b/contracts/eden/tests/include/nodeos-runner.hpp @@ -25,11 +25,9 @@ struct nodeos_runner eosio::execute("nodeos -d " + runner_name + " " // "--config-dir config " // - "--plugin eosio::chain_plugin " // "--plugin eosio::chain_api_plugin " // "--plugin eosio::producer_api_plugin " // "--plugin eosio::state_history_plugin " // - "--plugin eosio::http_plugin " // "--trace-history --disable-replay-opts " // "--access-control-allow-origin \"*\" " // "--access-control-allow-header \"*\" " // diff --git a/packages/box/src/config.ts b/packages/box/src/config.ts index 6447096db..d69e6b8b3 100644 --- a/packages/box/src/config.ts +++ b/packages/box/src/config.ts @@ -93,6 +93,7 @@ export const dfuseConfig = { export const shipConfig = { address: process.env.SHIP_ADDRESS || "127.0.0.1", port: process.env.SHIP_PORT || "8080", + firstBlock: +(process.env.SHIP_FIRST_BLOCK as any) || 1, }; if (subchainConfig.enable) { diff --git a/packages/box/src/handlers/subchain.ts b/packages/box/src/handlers/subchain.ts index 6b8f52050..633b3c602 100644 --- a/packages/box/src/handlers/subchain.ts +++ b/packages/box/src/handlers/subchain.ts @@ -158,7 +158,7 @@ export async function startSubchain() { } } -const setupReceiver = async () => { +const setupReceiver = () => { switch (subchainConfig.receiver) { case SubchainReceivers.DFUSE: { const dfuseReceiver = new DfuseReceiver(storage); diff --git a/packages/box/src/history-receivers/ship-receiver.ts b/packages/box/src/history-receivers/ship-receiver.ts index ccfc542bc..f4426d35a 100644 --- a/packages/box/src/history-receivers/ship-receiver.ts +++ b/packages/box/src/history-receivers/ship-receiver.ts @@ -59,7 +59,9 @@ export class ShipReceiver { onMessage(data: WebSocket.Data) { if (!this.requestedBlocks) { logger.info("Requesting Blocks from SHiP..."); - const request = this.storage.getShipBlocksRequest(); + const request = this.storage.getShipBlocksRequest( + shipConfig.firstBlock + ); this.requestedBlocks = true; this.wsClient!.send(request); logger.info("Requested Blocks from SHiP!"); diff --git a/packages/box/src/subchain-storage.ts b/packages/box/src/subchain-storage.ts index c39303435..6200c13fa 100644 --- a/packages/box/src/subchain-storage.ts +++ b/packages/box/src/subchain-storage.ts @@ -121,8 +121,10 @@ export class Storage { return result; } - getShipBlocksRequest(): Uint8Array { - return this.protect(() => this.blocksWasm!.getShipBlocksRequest())!; + getShipBlocksRequest(blockNum: number): Uint8Array { + return this.protect(() => + this.blocksWasm!.getShipBlocksRequest(blockNum) + )!; } pushShipMessage(shipMessage: Uint8Array) { diff --git a/packages/common/src/subchain/EdenSubchain.ts b/packages/common/src/subchain/EdenSubchain.ts index a4a72991c..d3ee39c2f 100644 --- a/packages/common/src/subchain/EdenSubchain.ts +++ b/packages/common/src/subchain/EdenSubchain.ts @@ -187,9 +187,9 @@ export class EdenSubchain { }); } - getShipBlocksRequest() { + getShipBlocksRequest(blockNum: number) { return this.protect(() => { - if (!this.exports.getShipBlocksRequest()) return null; + if (!this.exports.getShipBlocksRequest(blockNum)) return null; return this.resultAsUint8Array(); }); } From e76f8efaccf272e8d67b42d5547dc36bbefde297 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Tue, 2 Nov 2021 11:19:26 -0400 Subject: [PATCH 24/33] build issues --- docker/eden-box.Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docker/eden-box.Dockerfile b/docker/eden-box.Dockerfile index a3c564cde..f72957bb4 100644 --- a/docker/eden-box.Dockerfile +++ b/docker/eden-box.Dockerfile @@ -1,5 +1,5 @@ # Install dependencies only when needed -FROM node:alpine AS deps +FROM node:lts-alpine AS deps RUN apk add --no-cache libc6-compat WORKDIR /app @@ -8,15 +8,17 @@ ENV PATH /app/node_modules/.bin:$PATH COPY package.json yarn.lock ./ COPY ./packages/common/package.json ./packages/common/ +COPY ./packages/eden-subchain-client/package.json ./packages/eden-subchain-client/ COPY ./packages/box/package.json ./packages/box/ RUN yarn install --frozen-lockfile # Rebuild the source code only when needed -FROM node:alpine AS builder +FROM node:lts-alpine AS builder WORKDIR /app COPY ./packages/common ./packages/common +COPY ./packages/eden-subchain-client ./packages/eden-subchain-client COPY ./packages/box ./packages/box COPY .eslintignore .eslintrc.js .prettierrc.json lerna.json package.json tsconfig.build.json tsconfig.json yarn.lock ./ @@ -28,7 +30,7 @@ COPY ./build/eden-micro-chain.wasm /app/build/ RUN yarn build --stream # Production image, copy all the files and run next -FROM node:alpine AS runner +FROM node:lts-alpine AS runner WORKDIR /app ENV NODE_ENV production @@ -44,4 +46,3 @@ USER box EXPOSE 3032 CMD ["yarn", "start", "--stream"] - From cf581718ccebe3722a43ad789a34f54d7abc5ed8 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 <79721020+sparkplug0025@users.noreply.github.com> Date: Wed, 3 Nov 2021 22:50:02 -0400 Subject: [PATCH 25/33] Ephemeral CI (#595) * Fix action move * Ephemeral CI * Fix E2E build requirements * Update build.yml * fix ci env vars * fix box start env * fix build * Fix UAL Softkey Chain ID * Fix UAL Softkey Chain ID * fix softkey chainid * cache clsdk * product_cache Co-authored-by: Todd Fleming --- .github/workflows/build.yml | 90 ++++++++++++++----- contracts/eden/src/eden-micro-chain.cpp | 2 +- docker/eden-chain.Dockerfile | 23 ----- package.json | 3 +- packages/box/.env.test | 10 +++ packages/box/package.json | 1 + packages/webapp/.env.test | 43 ++++++--- packages/webapp/cypress.json | 2 +- .../cypress/integration/inductions.spec.ts | 10 +-- packages/webapp/cypress/support/commands.ts | 2 +- packages/webapp/package.json | 1 + .../ual/softkey/softkey-ual-authenticator.tsx | 5 +- scripts/eden_chain_runner.sh | 22 +++++ 13 files changed, 144 insertions(+), 70 deletions(-) delete mode 100644 docker/eden-chain.Dockerfile create mode 100644 packages/box/.env.test create mode 100755 scripts/eden_chain_runner.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96206d783..a8bc5d51c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,7 +73,9 @@ jobs: - name: show_cache if: steps.filter.outputs.src == 'true' id: show_cache - run: echo "${{ runner.os }}-ccache_whole-${{ steps.ccache_cache_timestamp.outputs.timestamp }}" + run: | + echo "${{ runner.os }}-ccache_whole-${{ steps.ccache_cache_timestamp.outputs.timestamp }}" + echo "${{ runner.os }}-product_cache-${{ steps.ccache_cache_timestamp.outputs.timestamp }}" - name: ccache cache files if: steps.filter.outputs.src == 'true' @@ -84,6 +86,14 @@ jobs: restore-keys: | ${{ runner.os }}-ccache_whole- + - name: product cache files + uses: actions/cache@v1.1.0 + with: + path: product_cache + key: ${{ runner.os }}-product_cache-${{ steps.ccache_cache_timestamp.outputs.timestamp }} + restore-keys: | + ${{ runner.os }}-product_cache- + - name: 🛠 Build if: steps.filter.outputs.src == 'true' run: | @@ -104,6 +114,22 @@ jobs: make -j$(nproc) tar czf clsdk-ubuntu-20-04.tar.gz clsdk + rm -rf ../product_cache + mkdir -p ../product_cache + cp clsdk-ubuntu-20-04.tar.gz ../product_cache + cp atomicassets.abi ../product_cache + cp atomicassets.wasm ../product_cache + cp atomicmarket.abi ../product_cache + cp atomicmarket.wasm ../product_cache + cp bios.wasm ../product_cache + cp boot.wasm ../product_cache + cp eden-micro-chain.wasm ../product_cache + cp eden.abi ../product_cache + cp eden.wasm ../product_cache + cp run-full-election.wasm ../product_cache + cp run-genesis.wasm ../product_cache + cp token.abi ../product_cache + cp token.wasm ../product_cache echo ===== ls -la ${GITHUB_WORKSPACE} @@ -124,41 +150,38 @@ jobs: ccache.log - name: 📃 Upload clsdk - if: steps.filter.outputs.src == 'true' uses: actions/upload-artifact@v2 with: name: clsdk path: | - build/clsdk-ubuntu-20-04.tar.gz + product_cache/clsdk-ubuntu-20-04.tar.gz - name: 📃 Upload Eden Smart Contract - if: steps.filter.outputs.src == 'true' uses: actions/upload-artifact@v2 with: name: Eden Smart Contract path: | - build/eden.abi - build/eden.wasm - build/eden-micro-chain.wasm + product_cache/eden.abi + product_cache/eden.wasm + product_cache/eden-micro-chain.wasm - name: 📃 Upload Ephemeral Eden Chains Runners - if: steps.filter.outputs.src == 'true' uses: actions/upload-artifact@v2 with: name: Ephemeral Eden Chains Runners path: | - build/atomicassets.abi - build/atomicassets.wasm - build/atomicmarket.abi - build/atomicmarket.wasm - build/bios.wasm - build/boot.wasm - build/eden.abi - build/eden.wasm - build/token.abi - build/token.wasm - build/run-genesis.wasm - build/run-full-election.wasm + product_cache/atomicassets.abi + product_cache/atomicassets.wasm + product_cache/atomicmarket.abi + product_cache/atomicmarket.wasm + product_cache/bios.wasm + product_cache/boot.wasm + product_cache/eden.abi + product_cache/eden.wasm + product_cache/token.abi + product_cache/token.wasm + product_cache/run-genesis.wasm + product_cache/run-full-election.wasm build-micro-chain: name: Build Micro Chain @@ -356,7 +379,7 @@ jobs: context: . webapp-e2e: - needs: build-micro-chain + needs: [build-cpp, build-micro-chain] name: WebApp E2E Tests environment: e2e_tests runs-on: ubuntu-latest @@ -392,14 +415,37 @@ jobs: name: Eden Microchain path: build + - name: Download Ephemeral Chain Runners + if: steps.filter.outputs.src == 'true' + uses: actions/download-artifact@v2 + with: + name: Ephemeral Eden Chains Runners + path: build + + - name: Download clsdk + if: steps.filter.outputs.src == 'true' + uses: actions/download-artifact@v2 + with: + name: clsdk + path: build + + - name: Start Genesis Ephemeral Chain + run: | + cp ./scripts/eden_chain_runner.sh ./build + cd build + tar -xvf clsdk-ubuntu-20-04.tar.gz clsdk/bin + ls -la + sh -x ./eden_chain_runner.sh run-genesis.wasm + - name: 🛠 Build and Start WebApp if: steps.filter.outputs.src == 'true' run: | export DFUSE_PREVENT_CONNECT=1 export NODE_ENV=test + env yarn yarn build --stream --ignore @edenos/example-history-app - yarn start --stream --ignore @edenos/example-history-app & + yarn start-test --stream --ignore @edenos/example-history-app & - name: 🧪 Run E2E if: steps.filter.outputs.src == 'true' diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 874149e62..3ee7e160c 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -1871,7 +1871,7 @@ std::vector ship_to_eden_transactions( .hexData = std::move(hexData), }; - transaction.actions.push_back(action); + transaction.actions.push_back(std::move(action)); } } diff --git a/docker/eden-chain.Dockerfile b/docker/eden-chain.Dockerfile deleted file mode 100644 index 9423ac832..000000000 --- a/docker/eden-chain.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM ghcr.io/eoscommunity/eden-builder:sub-chain - -WORKDIR /app - -COPY ./contracts ./contracts -COPY ./external ./external -COPY ./libraries ./libraries -COPY ./native ./native -COPY ./programs ./programs -COPY ./wasm ./wasm -COPY CMakeLists.txt ./ - -WORKDIR /app/build -RUN cmake -DCMAKE_BUILD_TYPE=Release -DSKIP_TS=Yes -DEDEN_ATOMIC_ASSETS_ACCOUNT=atomicassets -DEDEN_ATOMIC_MARKET_ACCOUNT=atomicmarket -DEDEN_SCHEMA_NAME=members .. - -RUN make -j$(nproc) - -RUN ls -la - -WORKDIR /app/build/clsdk/bin - -RUN ls -la -# CMD ["nodeos"] diff --git a/package.json b/package.json index 877563dfd..fe071a918 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "pub": "lerna publish", "test": "lerna run test", "dev": "lerna run dev", - "start": "lerna run start" + "start": "lerna run start", + "start-test": "lerna run start-test" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^4.5.0", diff --git a/packages/box/.env.test b/packages/box/.env.test new file mode 100644 index 000000000..d2b8e393f --- /dev/null +++ b/packages/box/.env.test @@ -0,0 +1,10 @@ +# ephemeral local chain +EOS_CHAIN_ID = "953486e83a647009b04587879db89dee61f960b894c5df847b4d454575443d73" +EOS_RPC_PROTOCOL = "http" +EOS_RPC_HOST = "127.0.0.1" +EOS_RPC_PORT = "8888" +SUBCHAIN_RECEIVER = "SHIP" +DFUSE_FIRST_BLOCK = "0" +DFUSE_JSON_TRX_FILE = "" +SUBCHAIN_EDEN_CONTRACT = "eden.gm" +EDEN_CONTRACT_ACCOUNT = "eden.gm" diff --git a/packages/box/package.json b/packages/box/package.json index 48165c976..6f630416e 100644 --- a/packages/box/package.json +++ b/packages/box/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "ts-node-dev -r tsconfig-paths/register src/index.ts", "start": "NODE_ENV=production node dist/src", + "start-test": "node dist/src", "build": "yarn run clean && yarn run compile", "clean": "rimraf -rf ./dist", "compile": "tsc -p tsconfig.build.json", diff --git a/packages/webapp/.env.test b/packages/webapp/.env.test index 49307419e..8d3183e1a 100644 --- a/packages/webapp/.env.test +++ b/packages/webapp/.env.test @@ -1,17 +1,32 @@ -# GENERAL APP -NEXT_PUBLIC_BASE_URL = "https://cicd-box.dev.eoscommunity.org" +# # GENERAL APP +# NEXT_PUBLIC_BASE_URL = "https://cicd-box.dev.eoscommunity.org" +# +# # CONTRACT +# NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT = "cicd.edev" +# NEXT_PUBLIC_AA_FETCH_AFTER="1634184000000" +# +# # ATOMICHUB +# NEXT_PUBLIC_AA_COLLECTION_NAME = "cicd.edev" +# +# # BOX: SUBCHAIN +# NEXT_PUBLIC_SUBCHAIN_WASM_URL = "https://cicd-box.dev.eoscommunity.org/v1/subchain/eden-micro-chain.wasm" +# NEXT_PUBLIC_SUBCHAIN_STATE_URL = "https://cicd-box.dev.eoscommunity.org/v1/subchain/state" +# NEXT_PUBLIC_SUBCHAIN_WS_URL = "wss://cicd-box.dev.eoscommunity.org/v1/subchain/eden-microchain" +# +# # BOX: IPFS +# NEXT_PUBLIC_BOX_ADDRESS = "https://cicd-box.dev.eoscommunity.org" -# CONTRACT -NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT = "cicd.edev" -NEXT_PUBLIC_AA_FETCH_AFTER="1634184000000" +##### -# ATOMICHUB -NEXT_PUBLIC_AA_COLLECTION_NAME = "cicd.edev" - -# BOX: SUBCHAIN -NEXT_PUBLIC_SUBCHAIN_WASM_URL = "https://cicd-box.dev.eoscommunity.org/v1/subchain/eden-micro-chain.wasm" -NEXT_PUBLIC_SUBCHAIN_STATE_URL = "https://cicd-box.dev.eoscommunity.org/v1/subchain/state" -NEXT_PUBLIC_SUBCHAIN_WS_URL = "wss://cicd-box.dev.eoscommunity.org/v1/subchain/eden-microchain" +# Ephemeral Chain Setup +NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT = "eden.gm" +NEXT_PUBLIC_EOS_CHAIN_ID = "953486e83a647009b04587879db89dee61f960b894c5df847b4d454575443d73" +NEXT_PUBLIC_EOS_RPC_PROTOCOL = "http" +NEXT_PUBLIC_EOS_RPC_HOST = "127.0.0.1" +NEXT_PUBLIC_EOS_RPC_PORT = "8888" +NEXT_PUBLIC_EOS_READ_RPC_URLS = "http://127.0.0.1:8888" +NEXT_PUBLIC_APP_MINIMUM_DONATION_AMOUNT = "10.0000 EOS" -# BOX: IPFS -NEXT_PUBLIC_BOX_ADDRESS = "https://cicd-box.dev.eoscommunity.org" +# ATOMICHUB +NEXT_PUBLIC_AA_COLLECTION_NAME = "eden.gm" +NEXT_PUBLIC_AA_FETCH_AFTER="1" diff --git a/packages/webapp/cypress.json b/packages/webapp/cypress.json index 5f1872199..8cd72d455 100644 --- a/packages/webapp/cypress.json +++ b/packages/webapp/cypress.json @@ -1,6 +1,6 @@ { "baseUrl": "http://localhost:3000", "env": { - "test_users_pk": "" + "test_user_pk": "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" } } diff --git a/packages/webapp/cypress/integration/inductions.spec.ts b/packages/webapp/cypress/integration/inductions.spec.ts index 41f9977b5..ca2128f27 100644 --- a/packages/webapp/cypress/integration/inductions.spec.ts +++ b/packages/webapp/cypress/integration/inductions.spec.ts @@ -13,21 +13,21 @@ describe("Inductions", () => { }); it("should see the invitation button when logged in", () => { - cy.login("alice.edev"); + cy.login("alice"); const inviteButton = getInviteButton(); inviteButton.should("exist"); }); it("should allow to invite a new member", () => { - cy.login("alice.edev"); // TODO: we should keep sessions + cy.login("alice"); // TODO: we should keep sessions const inviteButton = getInviteButton(); inviteButton.click(); - cy.get("#invitee").type("test235.edev"); - cy.get("#witness1").type("egeon.edev"); - cy.get("#witness2").type("pip.edev"); + cy.get("#invitee").type("bertie"); + cy.get("#witness1").type("egeon"); + cy.get("#witness2").type("pip"); cy.get('button[type="submit"]').click(); cy.wait("@eosPushTransaction"); diff --git a/packages/webapp/cypress/support/commands.ts b/packages/webapp/cypress/support/commands.ts index 17c73c7e4..645631b54 100644 --- a/packages/webapp/cypress/support/commands.ts +++ b/packages/webapp/cypress/support/commands.ts @@ -34,7 +34,7 @@ Cypress.Commands.add("login", (account) => { cy.wait(500); cy.get('#ual-box input[type="text"]').type(account); cy.get("#ual-box div").contains("Continue").click(); - cy.get('input[type="password"]').type(Cypress.env("test_users_pk")); + cy.get('input[type="password"]').type(Cypress.env("test_user_pk")); cy.get('button[type="submit"]').click(); }); diff --git a/packages/webapp/package.json b/packages/webapp/package.json index dcacedf85..c91202a91 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -7,6 +7,7 @@ "dev": "next dev", "build": "next build", "start": "next start", + "start-test": "next start", "test": "cypress run", "test-dev": "cypress open" }, diff --git a/packages/webapp/src/_app/eos/ual/softkey/softkey-ual-authenticator.tsx b/packages/webapp/src/_app/eos/ual/softkey/softkey-ual-authenticator.tsx index 0064865f5..229fa4090 100644 --- a/packages/webapp/src/_app/eos/ual/softkey/softkey-ual-authenticator.tsx +++ b/packages/webapp/src/_app/eos/ual/softkey/softkey-ual-authenticator.tsx @@ -18,8 +18,9 @@ import { SoftkeyUser } from "./softkey-user"; export class SoftkeyAuthenticator extends Authenticator { private users: SoftkeyUser[] = []; private readonly supportedChains = { - // SoftkeyAuthenticator only supports WAX Testnet - f16b1833c747c43682f4386fca9cbb327929334a762755ebec17f6f23c9b8a12: {}, + // SoftkeyAuthenticator only supports WAX Testnet and Ephemeral Chain + "f16b1833c747c43682f4386fca9cbb327929334a762755ebec17f6f23c9b8a12": {}, + "953486e83a647009b04587879db89dee61f960b894c5df847b4d454575443d73": {}, }; constructor(chains: Chain[], private loginHook: UALSoftKeyLoginHook) { diff --git a/scripts/eden_chain_runner.sh b/scripts/eden_chain_runner.sh new file mode 100755 index 000000000..fcdab550b --- /dev/null +++ b/scripts/eden_chain_runner.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env sh +set -e + +RUNNER=$1 +WALLET_FILE=./runner-wallet +CONTRACTS_PKS=5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +PATH=$(pwd)/clsdk/bin:$PATH + +echo "Running Eden Ephemeral Chain Runner $RUNNER..." + +cleos wallet create -f $WALLET_FILE || true +cleos wallet import --private-key $CONTRACTS_PKS || true + +echo "Executing $RUNNER" +cltester -v $RUNNER > eden-runner.log 2>&1 & + +sleep 5 + +cleos set abi eosio.token token.abi +cleos set abi atomicassets atomicassets.abi +cleos set abi atomicmarket atomicmarket.abi +cleos set abi eden.gm eden.abi From 918b53cc09aeca5cbda3d902e21b105808a00918 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 <79721020+sparkplug0025@users.noreply.github.com> Date: Fri, 5 Nov 2021 14:32:24 -0400 Subject: [PATCH 26/33] Completes induction with file uploads (#569) * Completes induction with file uploads * Remove unnecessary box e2e * fix pinata jwt env var --- .github/workflows/build.yml | 60 +------- packages/box/src/config.ts | 5 +- packages/webapp/.gitignore | 1 + .../cypress/fixtures/cypress-avatar.jpg | Bin 0 -> 13989 bytes .../cypress/fixtures/fake-induction.mp4 | Bin 0 -> 195922 bytes .../cypress/integration/community.spec.ts | 2 +- .../cypress/integration/inductions.spec.ts | 135 ++++++++++++++---- packages/webapp/cypress/support/commands.ts | 9 +- packages/webapp/cypress/support/index.ts | 4 +- packages/webapp/package.json | 1 + packages/webapp/tsconfig.json | 2 +- yarn.lock | 5 + 12 files changed, 137 insertions(+), 87 deletions(-) create mode 100644 packages/webapp/cypress/fixtures/cypress-avatar.jpg create mode 100644 packages/webapp/cypress/fixtures/fake-induction.mp4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8bc5d51c..7c87ee7f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -253,57 +253,6 @@ jobs: path: | build/eden-micro-chain.wasm - box-e2e: - needs: build-micro-chain - name: Eden Box E2E Tests - runs-on: ubuntu-latest - - steps: - - name: ✅ Checkout code - uses: actions/checkout@v2 - - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - src: - - ".github/workflows/build.yml" - - - ".eslintignore" - - ".eslintrc.js" - - ".prettierrc.json" - - "lerna.json" - - "package.json" - - "packages/common/**" - - "tsconfig.build.json" - - "tsconfig.json" - - "yarn.lock" - - - "docker/eden-box.Dockerfile" - - "packages/box/**" - - - name: Download Eden Microchain - if: steps.filter.outputs.src == 'true' - uses: actions/download-artifact@v2 - with: - name: Eden Microchain - path: build - - - name: 🛠 Build and Start Box - if: steps.filter.outputs.src == 'true' - run: | - export DFUSE_PREVENT_CONNECT=1 - yarn - yarn build --stream - cd packages/box - yarn start & - - - name: 🧪 Run E2E - if: steps.filter.outputs.src == 'true' - # TODO: add real E2E tests... for now it's just a shameless curl ping - run: | - curl localhost:3032 - box-build: needs: build-micro-chain name: Build Eden Box @@ -378,9 +327,9 @@ jobs: tags: ${{ steps.prep.outputs.tags }} context: . - webapp-e2e: + e2e: needs: [build-cpp, build-micro-chain] - name: WebApp E2E Tests + name: E2E Tests environment: e2e_tests runs-on: ubuntu-latest @@ -430,6 +379,7 @@ jobs: path: build - name: Start Genesis Ephemeral Chain + if: steps.filter.outputs.src == 'true' run: | cp ./scripts/eden_chain_runner.sh ./build cd build @@ -446,13 +396,13 @@ jobs: yarn yarn build --stream --ignore @edenos/example-history-app yarn start-test --stream --ignore @edenos/example-history-app & + env: + IPFS_PINATA_JWT: ${{ secrets.IPFS_PINATA_JWT }} - name: 🧪 Run E2E if: steps.filter.outputs.src == 'true' run: | yarn test --stream - env: - cypress_test_users_pk: ${{ secrets.CYPRESS_TEST_USERS_PK }} - name: 🎥 Upload Cypress Results if: always() && steps.filter.outputs.src == 'true' diff --git a/packages/box/src/config.ts b/packages/box/src/config.ts index d69e6b8b3..0c5f0b121 100644 --- a/packages/box/src/config.ts +++ b/packages/box/src/config.ts @@ -35,7 +35,10 @@ export const ipfsConfig = { "https://api.pinata.cloud/pinning/pinFileToIPFS", pinataJwt: process.env.IPFS_PINATA_JWT || "", }; -console.info({ ...ipfsConfig, pinataJwt: "" }); +console.info({ + ...ipfsConfig, + pinataJwt: `${ipfsConfig.pinataJwt.substr(0,8)}..${ipfsConfig.pinataJwt.substr(-8)}` +}); export const validUploadActions: ValidUploadActions = { [edenContractAccount]: { diff --git a/packages/webapp/.gitignore b/packages/webapp/.gitignore index 4ec40474b..84d6a140a 100644 --- a/packages/webapp/.gitignore +++ b/packages/webapp/.gitignore @@ -2,4 +2,5 @@ .env*.local cypress/videos cypress/screenshots +cypress/downloads cypress.env.json diff --git a/packages/webapp/cypress/fixtures/cypress-avatar.jpg b/packages/webapp/cypress/fixtures/cypress-avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de09a5126e7e59aaf94696950c9ccd90755d5dfb GIT binary patch literal 13989 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 GF8wcp-+K@M literal 0 HcmV?d00001 diff --git a/packages/webapp/cypress/fixtures/fake-induction.mp4 b/packages/webapp/cypress/fixtures/fake-induction.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..119f3e12ac0e8dca8bcc05e53844607e3b11336d GIT binary patch literal 195922 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%0DFBz$Rho+1KduT=0U`RW6aa;U{|gGibJ} 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˓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 T9Ci|oDOx~_i;oTCaM^zcYXbjc literal 0 HcmV?d00001 diff --git a/packages/webapp/cypress/integration/community.spec.ts b/packages/webapp/cypress/integration/community.spec.ts index a926f38e6..4d67a6650 100644 --- a/packages/webapp/cypress/integration/community.spec.ts +++ b/packages/webapp/cypress/integration/community.spec.ts @@ -1,6 +1,6 @@ describe("Community", () => { beforeEach(() => { - cy.interceptSubchain(); + cy.interceptBox(); cy.interceptEosApis(); cy.viewport(1000, 1000); cy.visit(`/members`); diff --git a/packages/webapp/cypress/integration/inductions.spec.ts b/packages/webapp/cypress/integration/inductions.spec.ts index ca2128f27..f877420d8 100644 --- a/packages/webapp/cypress/integration/inductions.spec.ts +++ b/packages/webapp/cypress/integration/inductions.spec.ts @@ -1,6 +1,6 @@ describe("Inductions", () => { beforeEach(() => { - cy.interceptSubchain(); + cy.interceptBox(); cy.interceptEosApis(); cy.visit(`/induction`); cy.wait("@boxGetSubchain"); @@ -19,23 +19,120 @@ describe("Inductions", () => { inviteButton.should("exist"); }); - it("should allow to invite a new member", () => { - cy.login("alice"); // TODO: we should keep sessions + describe("inviting a new member", () => { + const participants = { + inviter: "alice", + invitee: "bertie", + witness1: "pip", + witness2: "egeon", + }; - const inviteButton = getInviteButton(); - inviteButton.click(); + it("should allow to invite a new member", () => { + cy.login(participants.inviter); + + const inviteButton = getInviteButton(); + inviteButton.click(); + + cy.get("#invitee").type(participants.invitee); + cy.get("#witness1").type(participants.witness1); + cy.get("#witness2").type(participants.witness2); + cy.get('button[type="submit"]').click(); + cy.wait("@eosPushTransaction"); + + const successMessage = cy.get("main h1"); + successMessage.should("contain", "Success!"); + cy.waitForBlocksPropagation(); + }); + + it("should be able to input induction data", () => { + cy.login(participants.invitee); + + cy.contains("Create my profile").click(); + + cy.get("#name").type("Cypress Smith"); + cy.get("#imgFile").attachFile("cypress-avatar.jpg"); + cy.get("#attributions").type("Cypress Framework"); + cy.get("#bio").type("Thanks to Cypress I'm here!"); + cy.get("#eosCommunity").type(participants.invitee + ".ec"); + cy.get("#telegram").type(participants.invitee + ".tg"); + cy.get("#linkedin").type(participants.invitee + ".li"); + cy.get("#twitter").type(participants.invitee + ".tw"); + cy.get("#blog").type( + participants.invitee + ".example.com/supercool" + ); + cy.get("#facebook").type(participants.invitee + ".fb"); + + cy.get("button").contains("Preview My Profile").click(); + + cy.get("#image").click(); + cy.get("#statement").click(); + cy.get("#links").click(); + cy.get("#handles").click(); + cy.get("#consent").click(); + + cy.get("button").contains("Submit Profile").click(); + cy.wait("@boxUploadFile"); + + const successMessage = cy.get("main h1"); + successMessage.should("contain", "Success!"); + cy.waitForBlocksPropagation(); + }); + + it("should be able to upload induction video and endorse with witness 1", () => { + cy.login(participants.witness1); + + cy.contains("Complete ceremony").click(); + + cy.get("#videoFile0").attachFile("fake-induction.mp4"); + cy.get("button").contains("Upload meeting").click(); + cy.wait("@boxUploadFile"); - cy.get("#invitee").type("bertie"); - cy.get("#witness1").type("egeon"); - cy.get("#witness2").type("pip"); - cy.get('button[type="submit"]').click(); - cy.wait("@eosPushTransaction"); + cy.get("button").contains("Onward!", { timeout: 10000 }).click(); - const successMessage = cy.get("main h1"); - successMessage.should("contain", "Success!"); - cy.waitForBlocksPropagation(); + endorseInvitee(); + }); - cleanupInvitations(); + it("should be able to endorse with witness 2", () => { + cy.login(participants.witness2); + + cy.contains("Review & endorse").click(); + + endorseInvitee(); + }); + + it("should be able to endorse with inviter", () => { + cy.login(participants.inviter); + + cy.contains("Review & endorse").click(); + + endorseInvitee(true); + }); + + it("should be able to complete induction", () => { + cy.login(participants.invitee); + + cy.contains("Donate & complete").click(); + + cy.get("#reviewed").click(); + + cy.get("button").contains("Donate").click(); + cy.wait("@eosPushTransaction"); + }); + + const endorseInvitee = (isLastEndorsement = false) => { + cy.get("#photo").click(); + cy.get("#links").click(); + cy.get("#video").click(); + cy.get("#reviewed").click(); + cy.get("button").contains("Endorse").click(); + cy.wait("@eosPushTransaction"); + cy.contains( + isLastEndorsement + ? "This induction is fully endorsed!" + : "Waiting for all witnesses to endorse.", + { timeout: 10000 } + ).click(); + }; }); }); @@ -43,14 +140,4 @@ const getInviteButton = () => { return cy.get(`[data-testid="invite-button"]`); }; -const cleanupInvitations = () => { - cy.visit(`/induction`); - - const inductionsAvailableForDeleting = cy.get( - `[data-testid="cancel-induction"]` - ); - inductionsAvailableForDeleting.click({ multiple: true }); - cy.wait("@eosPushTransaction"); -}; - export {}; diff --git a/packages/webapp/cypress/support/commands.ts b/packages/webapp/cypress/support/commands.ts index 645631b54..71324f881 100644 --- a/packages/webapp/cypress/support/commands.ts +++ b/packages/webapp/cypress/support/commands.ts @@ -24,6 +24,8 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +import "cypress-file-upload"; + /** * Logins with UAL using Eden SoftKey mode */ @@ -39,10 +41,11 @@ Cypress.Commands.add("login", (account) => { }); /** - * Intercept Subchain Calls + * Intercept Box Calls */ -Cypress.Commands.add("interceptSubchain", () => { +Cypress.Commands.add("interceptBox", () => { cy.intercept("**/v1/subchain/**").as("boxGetSubchain"); + cy.intercept("**/v1/ipfs-upload").as("boxUploadFile"); }); /** @@ -60,7 +63,7 @@ Cypress.Commands.add("interceptEosApis", () => { * Also it makes our tests free of undesirable waits, if we have a new legit * case for `cy.wait()` we can just add a new command here. */ -Cypress.Commands.add("waitForBlocksPropagation", (blocks = 3) => { +Cypress.Commands.add("waitForBlocksPropagation", (blocks = 5) => { cy.wait(blocks * 500); }); diff --git a/packages/webapp/cypress/support/index.ts b/packages/webapp/cypress/support/index.ts index a2c44569f..5ad5c14af 100644 --- a/packages/webapp/cypress/support/index.ts +++ b/packages/webapp/cypress/support/index.ts @@ -25,9 +25,9 @@ declare namespace Cypress { login(account: string): Chainable; /** - * Intercepts and creates default aliases loading up Box Subchain + * Intercepts and creates default aliases loading up Box endpoints calls */ - interceptSubchain(): Chainable; + interceptBox(): Chainable; /** * Intercepts and creates default aliases for main EOS RPC Api Calls diff --git a/packages/webapp/package.json b/packages/webapp/package.json index c91202a91..88ae77359 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -59,6 +59,7 @@ "@types/uuid": "^8.3.1", "autoprefixer": "^10.2.5", "cypress": "^8.4.0", + "cypress-file-upload": "^5.0.8", "next-transpile-modules": "~3.3.0", "postcss": "^8.2.9", "tailwindcss": "^2.0.4", diff --git a/packages/webapp/tsconfig.json b/packages/webapp/tsconfig.json index b779e9152..912326f26 100644 --- a/packages/webapp/tsconfig.json +++ b/packages/webapp/tsconfig.json @@ -14,7 +14,7 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "types": ["cypress"] + "types": ["cypress", "cypress-file-upload"] }, "exclude": ["node_modules", "dist"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] diff --git a/yarn.lock b/yarn.lock index 53e6b6c5c..2120f0826 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4070,6 +4070,11 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +cypress-file-upload@^5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1" + integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g== + cypress@^8.1.0: version "8.2.0" resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.2.0.tgz#1e4e9f6218324e82a95c1b9cad7f3965ba663d7f" From 0cf518f188e0872ff2c4230301c9cd4d4b56fd93 Mon Sep 17 00:00:00 2001 From: Todd Fleming Date: Sat, 6 Nov 2021 13:18:47 -0400 Subject: [PATCH 27/33] repair formatting --- contracts/eden/src/eden-micro-chain.cpp | 118 +++++++++++------------- 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 3ee7e160c..bdd8e2750 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -9,8 +9,8 @@ #include #include #include -#include #include +#include #include using namespace eosio::literals; @@ -27,10 +27,10 @@ constexpr eosio::name pool_account(eosio::name pool) constexpr eosio::name master_pool = pool_account("master"_n); eosio::name distribution_fund; -const eosio::name account_min = eosio::name{0}; -const eosio::name account_max = eosio::name{~uint64_t(0)}; -const eosio::block_timestamp block_timestamp_min = eosio::block_timestamp{0}; -const eosio::block_timestamp block_timestamp_max = eosio::block_timestamp{~uint32_t(0)}; +const eosio::name account_min = eosio::name{0}; +const eosio::name account_max = eosio::name{~uint64_t(0)}; +const eosio::block_timestamp block_timestamp_min = eosio::block_timestamp{0}; +const eosio::block_timestamp block_timestamp_max = eosio::block_timestamp{~uint32_t(0)}; // TODO: switch to uint64_t (js BigInt) after we upgrade to nodejs >= 15 extern "C" void __wasm_call_ctors(); @@ -194,9 +194,7 @@ struct Nft; constexpr const char NftConnection_name[] = "NftConnection"; constexpr const char NftEdge_name[] = "NftEdge"; using NftConnection = - clchain::Connection>; + clchain::Connection>; struct status { @@ -348,12 +346,10 @@ struct member_object : public chainbase::object member member; eosio::name by_pk() const { return member.account; } - MemberCreatedAtKey by_createdAt() const { - return {member.createdAt, member.account}; - } + MemberCreatedAtKey by_createdAt() const { return {member.createdAt, member.account}; } }; -using member_index = mic, +using member_index = mic, ordered_by_pk, ordered_by_createdAt>; @@ -481,8 +477,7 @@ using distribution_fund_index = mic; -struct nft_object - : public chainbase::object +struct nft_object : public chainbase::object { CHAINBASE_DEFAULT_CONSTRUCTOR(nft_object) @@ -504,7 +499,6 @@ using nft_index = mic, ordered_by_owner>; - struct database { chainbase::database db; @@ -708,7 +702,7 @@ struct Member const std::string* inductionVideo() const { return member ? &member->inductionVideo : nullptr; } bool participating() const { return member && member->participating; } eosio::block_timestamp createdAt() const { return member->createdAt; } - + NftConnection nfts(std::optional gt, std::optional ge, std::optional lt, @@ -1102,7 +1096,8 @@ void add_genesis_member(const status& status, eosio::name member) }); } -struct Nft { +struct Nft +{ const nft_object* obj; auto member() const { return get_member(obj->member); } @@ -1112,13 +1107,7 @@ struct Nft { auto templateMint() const { return obj->templateMint; } auto createdAt() const { return obj->createdAt; } }; -EOSIO_REFLECT2(Nft, - member, - owner, - templateId, - assetId, - templateMint, - createdAt) +EOSIO_REFLECT2(Nft, member, owner, templateId, assetId, templateMint, createdAt) NftConnection Member::nfts(std::optional gt, std::optional ge, @@ -1138,7 +1127,7 @@ NftConnection Member::nfts(std::optional gt, : std::nullopt, // le ? std::optional{nft_account_key{account, *le, ~uint64_t(0)}} // : std::optional{nft_account_key{account, eosio::block_timestamp::max(), // - ~uint64_t(0)}}, // + ~uint64_t(0)}}, // first, last, before, after, // db.nfts.get(), // [](auto& obj) { return obj.by_member(); }, // @@ -1165,9 +1154,9 @@ NftConnection Member::collectedNfts(std::optional gt, : std::nullopt, // le ? std::optional{nft_account_key{account, *le, ~uint64_t(0)}} // : std::optional{nft_account_key{account, eosio::block_timestamp::max(), // - ~uint64_t(0)}}, // + ~uint64_t(0)}}, // first, last, before, after, // - db.nfts.get(), // + db.nfts.get(), // [](auto& obj) { return obj.by_owner(); }, // [&](auto& obj) { return Nft{&obj}; }, [](auto& nfts, auto key) { return nfts.lower_bound(key); }, @@ -1491,16 +1480,16 @@ void logmint(const action_context& context, eden::atomicassets::attribute_map immutable_data, eden::atomicassets::attribute_map mutable_data, std::vector backed_tokens, - eden::atomicassets::attribute_map immutable_template_data) + eden::atomicassets::attribute_map immutable_template_data) { - if (authorized_minter != eden_account || collection_name != eden_account - || schema_name != eden::schema_name) + if (authorized_minter != eden_account || collection_name != eden_account || + schema_name != eden::schema_name) return; - + auto account_pos = std::find_if(immutable_template_data.begin(), immutable_data.end(), [](const auto& attr) { return attr.key == "account"; }); if (account_pos == immutable_template_data.end()) - return; // this nft has no eden member account value + return; // this nft has no eden member account value eosio::name member_account(std::get(account_pos->value)); @@ -1534,15 +1523,15 @@ void logtransfer(const action_context& context, auto& index = db.nfts.get(); - for (const auto& asset_id : asset_ids) { + for (const auto& asset_id : asset_ids) + { auto it = index.find(asset_id); - if (it == index.end()) { + if (it == index.end()) + { continue; } - - db.nfts.modify(*it, [&](auto& nft) { - nft.owner = to; - }); + + db.nfts.modify(*it, [&](auto& nft) { nft.owner = to; }); } } @@ -1928,9 +1917,10 @@ bool add_block(subchain::block&& eden_block, uint32_t eosio_irreversible) return add_block(std::move(bi), eosio_irreversible); } -bool add_block(subchain::eosio_block&& eosioBlock, uint32_t eosio_irreversible) { +bool add_block(subchain::eosio_block&& eosioBlock, uint32_t eosio_irreversible) +{ subchain::block eden_block; - eden_block.eosioBlock = std::move(eosioBlock); + eden_block.eosioBlock = std::move(eosioBlock); auto* eden_prev = block_log.block_before_eosio_num(eden_block.eosioBlock.num); if (eden_prev) @@ -2001,8 +1991,7 @@ bool add_block(eosio::ship_protocol::block_position block, return true; } -[[clang::export_name("pushShipMessage")]] bool pushShipMessage(const char* data, - uint32_t size) +[[clang::export_name("pushShipMessage")]] bool pushShipMessage(const char* data, uint32_t size) { eosio::input_stream bin{data, size}; eosio::ship_protocol::result result; @@ -2144,17 +2133,17 @@ struct Query std::optional after) const { return clchain::make_connection( - gt ? std::optional{MemberCreatedAtKey{*gt, account_max}} // - : std::nullopt, // - ge ? std::optional{MemberCreatedAtKey{*ge, account_min}} // - : std::nullopt, // - lt ? std::optional{MemberCreatedAtKey{*lt, account_min}} // - : std::nullopt, // - le ? std::optional{MemberCreatedAtKey{*le, account_max}} // - : std::nullopt, // - first, last, before, after, // - db.members.get(), // - [](auto& obj) { return obj.by_createdAt(); }, // + gt ? std::optional{MemberCreatedAtKey{*gt, account_max}} // + : std::nullopt, // + ge ? std::optional{MemberCreatedAtKey{*ge, account_min}} // + : std::nullopt, // + lt ? std::optional{MemberCreatedAtKey{*lt, account_min}} // + : std::nullopt, // + le ? std::optional{MemberCreatedAtKey{*le, account_max}} // + : std::nullopt, // + first, last, before, after, // + db.members.get(), // + [](auto& obj) { return obj.by_createdAt(); }, // [](auto& obj) { return Member{obj.member.account, &obj.member}; }, @@ -2198,16 +2187,17 @@ struct Query [](auto& distributions, auto key) { return distributions.upper_bound(key); }); } }; -EOSIO_REFLECT2(Query, - blockLog, - status, - masterPool, - distributionFund, - method(balances, "gt", "ge", "lt", "le", "first", "last", "before", "after"), - method(members, "gt", "ge", "lt", "le", "first", "last", "before", "after"), - method(membersByCreatedAt, "gt", "ge", "lt", "le", "first", "last", "before", "after"), - method(elections, "gt", "ge", "lt", "le", "first", "last", "before", "after"), - method(distributions, "gt", "ge", "lt", "le", "first", "last", "before", "after")) +EOSIO_REFLECT2( + Query, + blockLog, + status, + masterPool, + distributionFund, + method(balances, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(members, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(membersByCreatedAt, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(elections, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(distributions, "gt", "ge", "lt", "le", "first", "last", "before", "after")) auto schema = clchain::get_gql_schema(); [[clang::export_name("getSchemaSize")]] uint32_t getSchemaSize() From 35247d644b1700974272036c939ffc4b849f802f Mon Sep 17 00:00:00 2001 From: SparkPlug0025 <79721020+sparkplug0025@users.noreply.github.com> Date: Tue, 9 Nov 2021 07:17:29 -0500 Subject: [PATCH 28/33] Implement runner checkpoint option (#599) * Implement runner checkpoint option * Add proper address ips on nodeos runner --- .../eden/tests/include/nodeos-runner.hpp | 30 ++++++++++++++++++- contracts/eden/tests/run-full-election.cpp | 4 +-- contracts/eden/tests/run-genesis.cpp | 2 -- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/contracts/eden/tests/include/nodeos-runner.hpp b/contracts/eden/tests/include/nodeos-runner.hpp index f75ccedeb..3b07ccf72 100644 --- a/contracts/eden/tests/include/nodeos-runner.hpp +++ b/contracts/eden/tests/include/nodeos-runner.hpp @@ -2,6 +2,23 @@ #include +#define CATCH_CONFIG_RUNNER + +std::string runner_checkpoint; + +int main(int argc, char* argv[]) +{ + Catch::Session session; + auto cli = + session.cli() | Catch::clara::Opt(runner_checkpoint, "checkpoint")["-c"]["--checkpoint"]( + "Stops execution at the given checkpoint label."); + session.cli(cli); + auto ret = session.applyCommandLine(argc, argv); + if (ret) + return ret; + return session.run(); +} + struct nodeos_runner { eden_tester tester; @@ -9,6 +26,15 @@ struct nodeos_runner nodeos_runner(std::string runner_name) : runner_name(runner_name) {} + void checkpoint(const std::string& checkpoint) + { + if (runner_checkpoint == checkpoint) + { + start_nodeos(); + exit(0); + } + } + void start_nodeos() { // tolerance blocks @@ -32,6 +58,8 @@ struct nodeos_runner "--access-control-allow-origin \"*\" " // "--access-control-allow-header \"*\" " // "--http-validate-host 0 " // + "--http-server-address 0.0.0.0:8888 " // + "--state-history-endpoint 0.0.0.0:8080 " // "-e -p eosio"); } -}; +}; \ No newline at end of file diff --git a/contracts/eden/tests/run-full-election.cpp b/contracts/eden/tests/run-full-election.cpp index 7296ad2f4..d2ad76af5 100644 --- a/contracts/eden/tests/run-full-election.cpp +++ b/contracts/eden/tests/run-full-election.cpp @@ -1,5 +1,3 @@ -#define CATCH_CONFIG_MAIN - #include TEST_CASE("Setup Eden chain with full election") @@ -8,7 +6,9 @@ TEST_CASE("Setup Eden chain with full election") r.tester.genesis(); r.tester.run_election(true, 10000, true); + r.checkpoint("small_election"); r.tester.induct_n(100); + r.checkpoint("inductions"); r.tester.run_election(true, 10000, true); r.start_nodeos(); diff --git a/contracts/eden/tests/run-genesis.cpp b/contracts/eden/tests/run-genesis.cpp index 8a244d769..260d54349 100644 --- a/contracts/eden/tests/run-genesis.cpp +++ b/contracts/eden/tests/run-genesis.cpp @@ -1,5 +1,3 @@ -#define CATCH_CONFIG_MAIN - #include TEST_CASE("Setup Eden chain with basic completed genesis") From 50e0bc2a76b3f925d8d6cc3f332db314f82594ce Mon Sep 17 00:00:00 2001 From: Steven Watanabe Date: Wed, 10 Nov 2021 15:22:17 -0500 Subject: [PATCH 29/33] Remove transfer action and change tests that depend on it to use settablerows instead. --- contracts/eden/include/eden.hpp | 3 -- contracts/eden/src/actions/accounts.cpp | 14 -------- contracts/eden/tests/include/tester-base.hpp | 12 ++++++- contracts/eden/tests/test-eden.cpp | 35 +++++++++----------- 4 files changed, 26 insertions(+), 38 deletions(-) diff --git a/contracts/eden/include/eden.hpp b/contracts/eden/include/eden.hpp index d8a427f45..456d524a0 100644 --- a/contracts/eden/include/eden.hpp +++ b/contracts/eden/include/eden.hpp @@ -78,8 +78,6 @@ namespace eden void donate(eosio::name payer, const eosio::asset& quantity); - void transfer(eosio::name to, const eosio::asset& quantity, const std::string& memo); - void genesis(std::string community, eosio::symbol community_symbol, eosio::asset minimum_donation, @@ -195,7 +193,6 @@ namespace eden "eden.gm"_n, action(withdraw, owner, quantity, ricardian_contract(withdraw_ricardian)), action(donate, owner, quantity), - action(transfer, to, quantity, memo), action(fundtransfer, from, distribution_time, rank, to, amount, memo), action(usertransfer, from, to, amount, memo), action(genesis, diff --git a/contracts/eden/src/actions/accounts.cpp b/contracts/eden/src/actions/accounts.cpp index b63554e16..515386086 100644 --- a/contracts/eden/src/actions/accounts.cpp +++ b/contracts/eden/src/actions/accounts.cpp @@ -42,20 +42,6 @@ namespace eden } } - void eden::transfer(eosio::name to, const eosio::asset& quantity, const std::string& memo) - { - require_auth(get_self()); - accounts internal{get_self(), "owned"_n}; - setup_distribution(get_self(), internal); - internal.sub_balance("master"_n, quantity); - accounts{get_self(), "outgoing"_n}.add_balance(to, quantity, false); - eosio::action{{get_self(), "active"_n}, - token_contract, - "transfer"_n, - std::tuple(get_self(), to, quantity, memo)} - .send(); - } - void eden::withdraw(eosio::name owner, const eosio::asset& quantity) { require_auth(owner); diff --git a/contracts/eden/tests/include/tester-base.hpp b/contracts/eden/tests/include/tester-base.hpp index 95a4f85ed..621b30cb2 100644 --- a/contracts/eden/tests/include/tester-base.hpp +++ b/contracts/eden/tests/include/tester-base.hpp @@ -456,7 +456,17 @@ struct eden_tester } else if (balance > amount) { - eden_gm.act("eosio.token"_n, balance - amount, "memo"); +#ifdef ENABLE_SET_TABLE_ROWS + eden_gm.act( + "owned"_n, std::vector{eden::account_v0{"master"_n, amount}}); + eden_gm.act( + "outgoing"_n, + std::vector{eden::account_v0{"eosio.token"_n, balance - amount}}); + eden_gm.act("eden.gm"_n, "eosio.token"_n, balance - amount, + "memo"); +#else + eosio::check(false, "Cannot decrease balance"); +#endif } } diff --git a/contracts/eden/tests/test-eden.cpp b/contracts/eden/tests/test-eden.cpp index c51648cd4..f74c7f4f7 100644 --- a/contracts/eden/tests/test-eden.cpp +++ b/contracts/eden/tests/test-eden.cpp @@ -47,13 +47,11 @@ struct CompareFile for (auto& ttrace : history->traces) { std::visit( - [&](auto& ttrace) - { + [&](auto& ttrace) { for (auto& atrace : ttrace.action_traces) { std::visit( - [&](auto& atrace) - { + [&](auto& atrace) { if (atrace.receiver == "eosio.null"_n && atrace.act.name == "eden.events"_n) { @@ -412,8 +410,7 @@ TEST_CASE("induction") "alice"_n, 4, eosio::sha256(hash_data.data(), hash_data.size() - 1)), "Outdated endorsement"); - auto endorse_all = [&] - { + auto endorse_all = [&] { t.alice.act("alice"_n, 4, induction_hash); t.pip.act("pip"_n, 4, induction_hash); t.egeon.act("egeon"_n, 4, induction_hash); @@ -675,8 +672,7 @@ TEST_CASE("deposit and spend") TEST_CASE("election config") { - auto verify_cfg = [](const auto& config, uint16_t num_participants) - { + auto verify_cfg = [](const auto& config, uint16_t num_participants) { INFO("participants: " << num_participants) if (num_participants < 1) { @@ -1001,6 +997,8 @@ TEST_CASE("budget distribution underflow") CHECK(t.get_budgets_by_period() == expected); } +#ifdef ENABLE_SET_TABLE_ROWS + TEST_CASE("budget distribution min") { eden_tester t; @@ -1023,6 +1021,8 @@ TEST_CASE("budget distribution min") CHECK(t.get_budgets_by_period() == expected); } +#endif + TEST_CASE("budget adjustment on resignation") { eden_tester t; @@ -1124,20 +1124,14 @@ TEST_CASE("accounting") t.genesis(); // should now have 30.0000 EOS, with a 90.0000 EOS deposit from alice CHECK(get_token_balance("eden.gm"_n) == s2a("120.0000 EOS")); - expect(t.eden_gm.trace("eosio"_n, s2a("30.0001 EOS"), ""), - "insufficient balance"); - t.eden_gm.act("eosio"_n, s2a("30.0000 EOS"), ""); - CHECK(get_token_balance("eden.gm"_n) == s2a("90.0000 EOS")); - CHECK(get_token_balance("eosio"_n) == s2a("30.0000 EOS")); } TEST_CASE("pre-genesis balance") { - eden_tester t{[&] - { - t.eosio_token.act("eosio.token"_n, "eden.gm"_n, - s2a("3.1415 EOS"), ""); - }}; + eden_tester t{[&] { + t.eosio_token.act("eosio.token"_n, "eden.gm"_n, s2a("3.1415 EOS"), + ""); + }}; t.genesis(); CHECK(get_token_balance("eden.gm"_n) == t.get_total_balance()); } @@ -1155,8 +1149,7 @@ TEST_CASE("account migration") { eden_tester t; t.genesis(); - auto sum_accounts = [](eden::account_table_type& table) - { + auto sum_accounts = [](eden::account_table_type& table) { auto total = s2a("0.0000 EOS"); for (auto iter = table.begin(), end = table.end(); iter != end; ++iter) { @@ -1199,11 +1192,13 @@ TEST_CASE("account migration") get_token_balance("eden.gm"_n)); } +#ifdef ENABLE_SET_TABLE_ROWS t.set_balance(s2a("0.0000 EOS")); t.alice.act("alice"_n, get_eden_account("alice"_n)->balance()); t.eden_gm.act("eden.gm"_n, eosio::symbol("EOS", 4)); t.eden_gm.act(); t.eden_gm.act(100); +#endif } #ifdef ENABLE_SET_TABLE_ROWS From 371eabeee17661053cea5b0efe93455b8ad5b4db Mon Sep 17 00:00:00 2001 From: SparkPlug0025 <79721020+sparkplug0025@users.noreply.github.com> Date: Wed, 10 Nov 2021 19:56:44 -0500 Subject: [PATCH 30/33] Inductions query in the Microchain (#600) * Add basic Induction query * Add createdAt timestamps * induction wip * Complete inductions queries * Fix microchain * clang-format * fix clang-format --- .github/workflows/build.yml | 4 +- contracts/eden/src/eden-micro-chain.cpp | 116 +++++++++++++++++++++++- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c87ee7f4..bd611b909 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -354,8 +354,8 @@ jobs: - "tsconfig.json" - "yarn.lock" - - "docker/eden-webapp.Dockerfile" - - "packages/webapp/**" + - "packages/**" + - "contracts/**" - name: Download Eden Microchain if: steps.filter.outputs.src == 'true' diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index bdd8e2750..994fc71cb 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -167,6 +167,12 @@ enum tables nft_table, }; +struct Induction; +constexpr const char InductionConnection_name[] = "InductionConnection"; +constexpr const char InductionEdge_name[] = "InductionEdge"; +using InductionConnection = clchain::Connection< + clchain::ConnectionConfig>; + struct MemberElection; constexpr const char MemberElectionConnection_name[] = "MemberElectionConnection"; constexpr const char MemberElectionEdge_name[] = "MemberElectionEdge"; @@ -307,8 +313,11 @@ struct induction std::vector witnesses; eden::new_member_profile profile; std::string video; + eosio::block_timestamp createdAt; }; -EOSIO_REFLECT(induction, id, inviter, invitee, witnesses, profile, video) +EOSIO_REFLECT(induction, id, inviter, invitee, witnesses, profile, video, createdAt) + +using InductionCreatedAtKey = std::pair; struct induction_object : public chainbase::object { @@ -319,11 +328,13 @@ struct induction_object : public chainbase::object by_invitee() const { return {induction.invitee, induction.id}; } + InductionCreatedAtKey by_createdAt() const { return {induction.createdAt, induction.id}; } }; using induction_index = mic, ordered_by_pk, - ordered_by_invitee>; + ordered_by_invitee, + ordered_by_createdAt>; using MemberCreatedAtKey = std::pair; @@ -781,6 +792,20 @@ Member Balance::account() const return *get_member(_account, true); } +struct Induction +{ + uint64_t id; + const induction* induction; + + auto inviteeAccount() const { return induction->invitee; } + auto inviter() const { return get_member(induction->inviter); } + std::vector witnesses() const { return get_members(induction->witnesses); } + auto profile() const { return induction->profile; } + auto video() const { return induction->video; } + eosio::block_timestamp createdAt() const { return induction->createdAt; } +}; +EOSIO_REFLECT2(Induction, id, inviteeAccount, inviter, witnesses, profile, video, createdAt) + struct Status { const status* status; @@ -1363,13 +1388,12 @@ void addtogenesis(eosio::name new_genesis_member) add_genesis_member(status.status, new_genesis_member); } -void inductinit(uint64_t id, +void inductinit(const action_context& context, + uint64_t id, eosio::name inviter, eosio::name invitee, std::vector witnesses) { - // TODO: expire records - // contract doesn't allow inductinit() until it transitioned to active const auto& status = get_status(); if (!status.status.active) @@ -1380,6 +1404,7 @@ void inductinit(uint64_t id, obj.induction.inviter = inviter; obj.induction.invitee = invitee; obj.induction.witnesses = witnesses; + obj.induction.createdAt = eosio::block_timestamp(context.block.timestamp); }); } @@ -1744,6 +1769,33 @@ void call(void (*f)(const action_context&, Args...), std::apply([&](auto&&... args) { f(context, std::move(args)...); }, t); } +void remove_expired_inductions(const subchain::eosio_block& block) +{ + auto& idx = db.status.get(); + if (idx.size() < 1) + return; // skip if genesis is not complete + + const auto& status = get_status(); + if (!status.status.active) + return; // skip if not active + + auto expiration_time = + eosio::block_timestamp(block.timestamp).to_time_point().sec_since_epoch() - + eden::induction_expiration_secs; + + auto& index = db.inductions.get(); + auto it = index.begin(); + + while (it != index.end() && + it->induction.createdAt.to_time_point().sec_since_epoch() < expiration_time) + { + auto next = it; + ++next; + db.inductions.remove(*it); + it = next; + } +} + void filter_block(const subchain::eosio_block& block) { block_state block_state{}; @@ -1808,6 +1860,10 @@ void filter_block(const subchain::eosio_block& block) call(logtransfer, context, action.hexData.data); } } // for(action) + + // garbage collection housekeeping + remove_expired_inductions(block); + eosio::check(!block_state.in_withdraw && !block_state.in_manual_transfer, "missing transfer notification"); } // for(trx) @@ -2151,6 +2207,54 @@ struct Query [](auto& members, auto key) { return members.upper_bound(key); }); } + InductionConnection inductions(std::optional gt, + std::optional ge, + std::optional lt, + std::optional le, + std::optional first, + std::optional last, + std::optional before, + std::optional after) const + { + return clchain::make_connection( + gt, ge, lt, le, first, last, before, after, // + db.inductions.get(), // + [](auto& obj) { return obj.induction.id; }, // + [](auto& obj) { + return Induction{obj.induction.id, &obj.induction}; + }, + [](auto& inductions, auto key) { return inductions.lower_bound(key); }, + [](auto& inductions, auto key) { return inductions.upper_bound(key); }); + } + + InductionConnection inductionsByCreatedAt(std::optional gt, + std::optional ge, + std::optional lt, + std::optional le, + std::optional first, + std::optional last, + std::optional before, + std::optional after) const + { + return clchain::make_connection( + gt ? std::optional{InductionCreatedAtKey{*gt, ~uint64_t(0)}} // + : std::nullopt, // + ge ? std::optional{InductionCreatedAtKey{*ge, 0}} // + : std::nullopt, // + lt ? std::optional{InductionCreatedAtKey{*lt, 0}} // + : std::nullopt, // + le ? std::optional{InductionCreatedAtKey{*le, ~uint64_t(0)}} // + : std::nullopt, // + first, last, before, after, // + db.inductions.get(), // + [](auto& obj) { return obj.by_createdAt(); }, // + [](auto& obj) { + return Induction{obj.induction.id, &obj.induction}; + }, + [](auto& inductions, auto key) { return inductions.lower_bound(key); }, + [](auto& inductions, auto key) { return inductions.upper_bound(key); }); + } + ElectionConnection elections(std::optional gt, std::optional ge, std::optional lt, @@ -2196,6 +2300,8 @@ EOSIO_REFLECT2( method(balances, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(members, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(membersByCreatedAt, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(inductions, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(inductionsByCreatedAt, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(elections, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(distributions, "gt", "ge", "lt", "le", "first", "last", "before", "after")) From 3b5273f8283b476fc04de0eca89a8c125bf3b3b2 Mon Sep 17 00:00:00 2001 From: SparkPlug0025 <79721020+sparkplug0025@users.noreply.github.com> Date: Wed, 10 Nov 2021 20:25:24 -0500 Subject: [PATCH 31/33] Adds endorsing status on microchain (#601) --- contracts/eden/src/eden-micro-chain.cpp | 114 +++++++++++++++++++++--- 1 file changed, 100 insertions(+), 14 deletions(-) diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 994fc71cb..294b32456 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -305,12 +305,14 @@ using balance_history_index = mic, ordered_by_pk>; +using InductionEndorser = std::pair; + struct induction { uint64_t id = 0; - eosio::name inviter; + InductionEndorser inviter; eosio::name invitee; - std::vector witnesses; + std::vector witnesses; eden::new_member_profile profile; std::string video; eosio::block_timestamp createdAt; @@ -792,14 +794,37 @@ Member Balance::account() const return *get_member(_account, true); } +struct InductionEndorsingMemberStatus +{ + eosio::name endorserAccount; + bool endorsed; + + InductionEndorsingMemberStatus(const InductionEndorser& endorser) + { + endorserAccount = endorser.first; + endorsed = endorser.second; + } + + auto member() const { return get_member(endorserAccount); } +}; +EOSIO_REFLECT2(InductionEndorsingMemberStatus, member, endorsed) + struct Induction { uint64_t id; const induction* induction; auto inviteeAccount() const { return induction->invitee; } - auto inviter() const { return get_member(induction->inviter); } - std::vector witnesses() const { return get_members(induction->witnesses); } + auto inviter() const { return InductionEndorsingMemberStatus{induction->inviter}; } + std::vector witnesses() const + { + std::vector endorsers; + for (const auto& witness : induction->witnesses) + { + endorsers.push_back(InductionEndorsingMemberStatus{witness}); + } + return endorsers; + } auto profile() const { return induction->profile; } auto video() const { return induction->video; } eosio::block_timestamp createdAt() const { return induction->createdAt; } @@ -1113,11 +1138,11 @@ void add_genesis_member(const status& status, eosio::name member) { db.inductions.emplace([&](auto& obj) { obj.induction.id = available_pk(db.inductions, 1); - obj.induction.inviter = eden_account; + obj.induction.inviter = {eden_account, false}; obj.induction.invitee = member; for (auto witness : status.initialMembers) if (witness != member) - obj.induction.witnesses.push_back(witness); + obj.induction.witnesses.push_back({witness, false}); }); } @@ -1383,8 +1408,9 @@ void addtogenesis(eosio::name new_genesis_member) db.status.modify(get_status(), [&](auto& obj) { obj.status.initialMembers.push_back(new_genesis_member); }); for (auto& obj : db.inductions) - db.inductions.modify( - obj, [&](auto& obj) { obj.induction.witnesses.push_back(new_genesis_member); }); + db.inductions.modify(obj, [&](auto& obj) { + obj.induction.witnesses.push_back({new_genesis_member, false}); + }); add_genesis_member(status.status, new_genesis_member); } @@ -1392,16 +1418,22 @@ void inductinit(const action_context& context, uint64_t id, eosio::name inviter, eosio::name invitee, - std::vector witnesses) + std::vector witnesses_accounts) { // contract doesn't allow inductinit() until it transitioned to active const auto& status = get_status(); if (!status.status.active) db.status.modify(status, [&](auto& obj) { obj.status.active = true; }); + std::vector witnesses; + for (const auto& witness : witnesses_accounts) + { + witnesses.push_back(InductionEndorser{witness, false}); + } + add_or_replace(db.inductions, id, [&](auto& obj) { obj.induction.id = id; - obj.induction.inviter = inviter; + obj.induction.inviter = {inviter, false}; obj.induction.invitee = invitee; obj.induction.witnesses = witnesses; obj.induction.createdAt = eosio::block_timestamp(context.block.timestamp); @@ -1410,12 +1442,30 @@ void inductinit(const action_context& context, void inductprofil(uint64_t id, eden::new_member_profile profile) { - modify(db.inductions, id, [&](auto& obj) { obj.induction.profile = profile; }); + modify(db.inductions, id, [&](auto& obj) { + obj.induction.profile = profile; + + // reset endorsements + obj.induction.inviter.second = false; + for (auto& witness : obj.induction.witnesses) + { + witness.second = false; + } + }); } void inductvideo(eosio::name account, uint64_t id, std::string video) { - modify(db.inductions, id, [&](auto& obj) { obj.induction.video = video; }); + modify(db.inductions, id, [&](auto& obj) { + obj.induction.video = video; + + // reset endorsements + obj.induction.inviter.second = false; + for (auto& witness : obj.induction.witnesses) + { + witness.second = false; + } + }); } void inductcancel(eosio::name account, uint64_t id) @@ -1429,16 +1479,25 @@ void inductdonate(const action_context& context, eosio::asset quantity) { auto& induction = get(db.inductions, id); + auto& member = db.members.emplace([&](auto& obj) { obj.member.account = induction.induction.invitee; - obj.member.inviter = induction.induction.inviter; - obj.member.inductionWitnesses = induction.induction.witnesses; + obj.member.inviter = induction.induction.inviter.first; + + std::vector inductionWitnesses; + for (const auto& witness : induction.induction.witnesses) + { + inductionWitnesses.push_back(witness.first); + } + obj.member.inductionWitnesses = std::move(inductionWitnesses); + obj.member.profile = induction.induction.profile; obj.member.inductionVideo = induction.induction.video; obj.member.createdAt = eosio::block_timestamp(context.block.timestamp); if (obj.member.inductionVideo.empty()) obj.member.inductionVideo = get_status().status.genesisVideo; }); + transfer_funds(context.block.timestamp, payer, master_pool, quantity, history_desc::inductdonate); @@ -1453,6 +1512,31 @@ void inductdonate(const action_context& context, } } +void inductendors(const action_context& context, + eosio::name account, + uint64_t id, + eosio::checksum256 induction_data_hash) +{ + auto& induction = get(db.inductions, id); + modify(db.inductions, id, [&](auto& obj) { + if (account == obj.induction.inviter.first) + { + obj.induction.inviter.second = true; + } + else + { + for (auto& witness : obj.induction.witnesses) + { + if (witness.first == account) + { + witness.second = true; + break; + } + } + } + }); +} + void resign(eosio::name account) { remove_if_exists(db.members, account); @@ -1832,6 +1916,8 @@ void filter_block(const subchain::eosio_block& block) call(inductcancel, context, action.hexData.data); else if (action.name == "inductdonate"_n) call(inductdonate, context, action.hexData.data); + else if (action.name == "inductendors"_n) + call(inductendors, context, action.hexData.data); else if (action.name == "resign"_n) call(resign, context, action.hexData.data); else if (action.name == "electopt"_n) From 50d5de1cefe79e1f75bc4e4dbd6787fe0bdb7adb Mon Sep 17 00:00:00 2001 From: Brandon Fancher Date: Thu, 11 Nov 2021 09:55:02 -0500 Subject: [PATCH 32/33] Withdraw Options (#597) * Enhanced withdraw and transfer functionality * Componentize and organize. * Fetch distributions from subchain. * Factor out Asset and Account form input components. --- packages/webapp/.env | 3 + packages/webapp/src/_app/hooks/queries.ts | 12 -- packages/webapp/src/_app/styles/inputs.css | 11 ++ packages/webapp/src/_app/ui/form.tsx | 134 ++++++++++++++- packages/webapp/src/_app/utils/asset.ts | 19 +++ packages/webapp/src/config.ts | 16 ++ .../webapp/src/delegates/api/eden-contract.ts | 40 +---- packages/webapp/src/delegates/api/fixtures.ts | 28 +--- .../components/delegate-funds-available.tsx | 157 ------------------ .../webapp/src/delegates/components/index.ts | 1 - packages/webapp/src/delegates/interfaces.ts | 3 +- packages/webapp/src/delegates/transactions.ts | 57 ------- .../components/funds-available-cta.tsx | 91 ++++++++++ .../webapp/src/members/components/index.ts | 2 + .../components/withdraw-funds/index.ts | 6 + .../withdraw-funds/next-disbursement-info.tsx | 44 +++++ .../withdraw-modal-step-confirmation.tsx | 100 +++++++++++ .../withdraw-modal-step-failure.tsx | 30 ++++ .../withdraw-modal-step-form.tsx | 90 ++++++++++ .../withdraw-modal-step-success.tsx | 39 +++++ .../withdraw-funds/withdraw-modal.tsx | 156 +++++++++++++++++ packages/webapp/src/members/transactions.ts | 79 +++++++++ packages/webapp/src/pages/_app.tsx | 3 +- packages/webapp/src/pages/members/[id].tsx | 4 +- .../treasury-disbursements-info.tsx | 5 +- packages/webapp/src/treasury/hooks/index.ts | 1 + packages/webapp/src/treasury/hooks/queries.ts | 88 ++++++++++ 27 files changed, 916 insertions(+), 303 deletions(-) create mode 100644 packages/webapp/src/_app/styles/inputs.css delete mode 100644 packages/webapp/src/delegates/components/delegate-funds-available.tsx delete mode 100644 packages/webapp/src/delegates/transactions.ts create mode 100644 packages/webapp/src/members/components/funds-available-cta.tsx create mode 100644 packages/webapp/src/members/components/withdraw-funds/index.ts create mode 100644 packages/webapp/src/members/components/withdraw-funds/next-disbursement-info.tsx create mode 100644 packages/webapp/src/members/components/withdraw-funds/withdraw-modal-step-confirmation.tsx create mode 100644 packages/webapp/src/members/components/withdraw-funds/withdraw-modal-step-failure.tsx create mode 100644 packages/webapp/src/members/components/withdraw-funds/withdraw-modal-step-form.tsx create mode 100644 packages/webapp/src/members/components/withdraw-funds/withdraw-modal-step-success.tsx create mode 100644 packages/webapp/src/members/components/withdraw-funds/withdraw-modal.tsx create mode 100644 packages/webapp/src/members/transactions.ts create mode 100644 packages/webapp/src/treasury/hooks/index.ts create mode 100644 packages/webapp/src/treasury/hooks/queries.ts diff --git a/packages/webapp/.env b/packages/webapp/.env index 4e2776464..06bd418c9 100644 --- a/packages/webapp/.env +++ b/packages/webapp/.env @@ -15,6 +15,8 @@ NEXT_PUBLIC_TABLE_ROWS_MAX_FETCH_PER_SEC = "10" NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT = "test.edev" NEXT_PUBLIC_AA_FETCH_AFTER="1633883520000" NEXT_PUBLIC_TOKEN_CONTRACT = "eosio.token" +NEXT_PUBLIC_TOKEN_SYMBOL = "WAX" +NEXT_PUBLIC_TOKEN_PRECISION = "8" # ATOMICHUB NEXT_PUBLIC_AA_BASE_URL = "https://test.wax.api.atomicassets.io/atomicassets/v1" @@ -27,6 +29,7 @@ NEXT_PUBLIC_AA_SCHEMA_NAME = "members" # OTHER NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL = "https://wax-test.bloks.io/account" +NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL = "https://wax-test.bloks.io/transaction" NEXT_PUBLIC_APP_MINIMUM_DONATION_AMOUNT = "10.00000000 WAX" NEXT_PUBLIC_ENABLED_WALLETS = "ANCHOR,LEDGER,SOFTKEY" NEXT_PUBLIC_IPFS_BASE_URL = "https://infura-ipfs.io/ipfs" diff --git a/packages/webapp/src/_app/hooks/queries.ts b/packages/webapp/src/_app/hooks/queries.ts index 7a8a42da2..0a2747345 100644 --- a/packages/webapp/src/_app/hooks/queries.ts +++ b/packages/webapp/src/_app/hooks/queries.ts @@ -19,7 +19,6 @@ import { } from "inductions/api"; import { getChiefDelegates, - getDistributionsForAccount, getDistributionState, getMasterPool, getHeadDelegate, @@ -200,11 +199,6 @@ export const queryMasterPool = () => ({ queryFn: getMasterPool, }); -export const queryDistributionsForAccount = (account: string) => ({ - queryKey: ["query_distributions_for_account", account], - queryFn: () => getDistributionsForAccount(account), -}); - export const queryTokenBalanceForAccount = (account: string) => ({ queryKey: ["query_token_balance_for_account", account], queryFn: () => getTokenBalanceForAccount(account), @@ -248,12 +242,6 @@ export const useMemberByAccountName = (accountName?: string) => enabled: Boolean(accountName), }); -export const useDistributionsForAccount = (account: string) => - useQuery({ - ...queryDistributionsForAccount(account), - enabled: Boolean(account), - }); - export const useDistributionState = () => useQuery({ ...queryDistributionState(), diff --git a/packages/webapp/src/_app/styles/inputs.css b/packages/webapp/src/_app/styles/inputs.css new file mode 100644 index 000000000..f841f33e1 --- /dev/null +++ b/packages/webapp/src/_app/styles/inputs.css @@ -0,0 +1,11 @@ +/* Chrome, Safari, Edge, Opera */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +input[type="number"] { + -moz-appearance: textfield; +} diff --git a/packages/webapp/src/_app/ui/form.tsx b/packages/webapp/src/_app/ui/form.tsx index 4eb61b2e1..0c60f8eeb 100644 --- a/packages/webapp/src/_app/ui/form.tsx +++ b/packages/webapp/src/_app/ui/form.tsx @@ -1,19 +1,27 @@ import React, { HTMLProps } from "react"; +import { tokenConfig } from "config"; +import { Button } from "_app"; + export const Label: React.FC<{ htmlFor: string; }> = (props) => ( -