Skip to content
This repository has been archived by the owner on May 2, 2022. It is now read-only.

[WIP] Add offline-first code and contract database (ThreadDB) #83

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
3 changes: 3 additions & 0 deletions packages/app-db/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @canvas-ui/app-db

Connection and schema logic for IPFS back-end (Textile)
21 changes: 21 additions & 0 deletions packages/app-db/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@canvas-ui/app-db",
"private": true,
"version": "0.0.1",
"description": "Connection and schema logic for IPFS back-end (Textile)",
"main": "index.js",
"scripts": {},
"author": "Keith Ingram <[email protected]>",
"maintainers": [
"Keith Ingram <[email protected]>"
],
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.10.2",
"@textile/crypto": "^4.1.1",
"@textile/hub": "^6.1.2",
"@textile/threaddb": "^0.0.6",
"@textile/threads-client": "^2.1.2",
"@textile/threads-id": "^0.5.1"
}
}
135 changes: 135 additions & 0 deletions packages/app-db/src/Database.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright 2017-2021 @canvas-ui/app-db authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { DbProps, UserDocument } from './types';

import { PrivateKey } from '@textile/crypto';
import { KeyInfo } from '@textile/hub';
import { Collection, Database as DB } from '@textile/threaddb';
import { ThreadID } from '@textile/threads-id';
import React, { useEffect, useMemo, useState } from 'react';

import DbContext from './DbContext';
import { code, contract, user } from './schemas';
import { getPrivateKey, publicKeyHex } from './util';

interface Props {
children: React.ReactNode;
rpcUrl: string;
}

async function createUser (db: DB): Promise<string> {
const User = db.collection('User') as Collection<UserDocument>;
const identity = getPrivateKey();

const result = await User.findOne({ publicKey: publicKeyHex(identity) });

if (result) {
return Promise.resolve(result._id);
}

if (identity && !result) {
return User.create({
codeBundles: [],
contracts: [],
publicKey: publicKeyHex(identity) as string
}).save();
}

return Promise.reject(new Error('Invalid identity'));
}

function isLocalNode (rpcUrl: string): boolean {
return !rpcUrl.includes('127.0.0.1');
}

async function initDb (rpcUrl: string, isRemote = false): Promise<[DB, PrivateKey | null]> {
const db = await new DB(
rpcUrl,
{ name: 'User', schema: user },
{ name: 'Contract', schema: contract },
{ name: 'Code', schema: code }
).open(2);

await createUser(db);

const identity = getPrivateKey();

if (isRemote && !isLocalNode(rpcUrl)) {
try {
if (!process.env.HUB_API_KEY || !process.env.HUB_API_SECRET) {
throw new Error('No Textile Hub credentials found');
}

const info: KeyInfo = {
key: process.env.HUB_API_KEY,
secret: process.env.HUB_API_SECRET
};

const remote = await db.remote.setKeyInfo(info);

await remote.authorize(identity);

const threadId = ThreadID.fromString(rpcUrl);

await remote.initialize(threadId);
await remote.pull('User', 'Contract', 'Code');
} catch (e) {
console.error(e);
}
}

return [db, identity];
}

function Database ({ children, rpcUrl }: Props): React.ReactElement<Props> | null {
// TODO: Push to remote peer db if not development/local node
// const { chainName, isDevelopment } = useApi();

const [db, setDb] = useState<DB>(new DB(''));
const [identity, setIdentity] = useState<PrivateKey | null>(null);
const [isDbReady, setIsDbReady] = useState(false);

const isRemote = useMemo(
(): boolean => false, // !isDevelopment
[]
);

// initial initialization
useEffect((): void => {
async function createDb () {
try {
const [db, identity] = await initDb(rpcUrl, isRemote);

setDb(db);
setIdentity(identity);
setIsDbReady(true);
} catch (e) {
console.error(e);
setDb(new DB(''));
}
}

createDb()
.then()
.catch((e) => console.error(e));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const props = useMemo<DbProps>(
() => ({ db, identity, isDbReady }),
[db, identity, isDbReady]
);

if (!db || !props.isDbReady) {
return null;
}

return (
<DbContext.Provider value={props}>
{children}
</DbContext.Provider>
);
}

export default React.memo(Database);
17 changes: 17 additions & 0 deletions packages/app-db/src/DbContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2017-2021 @canvas-ui/app-db authors & contributors
// SPDX-License-Identifier: Apache-2.0

import React from 'react';

import { DbProps } from './types';

const DbContext: React.Context<DbProps> = React.createContext({} as unknown as DbProps);
const DbConsumer: React.Consumer<DbProps> = DbContext.Consumer;
const DbProvider: React.Provider<DbProps> = DbContext.Provider;

export default DbContext;

export {
DbConsumer,
DbProvider
};
14 changes: 14 additions & 0 deletions packages/app-db/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2017-2021 @canvas-ui/app-db authors & contributors
// SPDX-License-Identifier: Apache-2.0

import Database from './Database';

export { default as DbContext } from './DbContext';
export { default as useDatabase } from './useDatabase';
export { default as useCodes } from './useCodes';
export { default as useContracts } from './useContracts';
export { default as useCode } from './useCode';
export { default as useContract } from './useContract';
export * from './types';
export * from './util';
export default Database;
46 changes: 46 additions & 0 deletions packages/app-db/src/schemas/code.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://paritytech.github.io/canvas-ui/code.schema.json",
"title": "Code",
"description": "An uploaded WASM code bundle",
"type": "object",
"properties": {
"_id": {
"description": "Field to contain ulid-based instance id",
"type": "string"
},
"id": {
"description": "Unique url id",
"type": "string"
},
"owner": {
"description": "The public key of the original uploader",
"type": "string"
},
"blockOneHash": {
"description": "The identifying block hash for the code bundle's chain (for distinguishing development/local node instances)",
"type": "string"
},
"codeHash": {
"description": "The code bundle's unique hash",
"type": "string"
},
"abi": {
"description": "The code bundle's associated ABI JSON",
"type": "object"
},
"name": {
"description": "The code bundle's display name",
"type": "string"
},
"tags": {
"description": "The code bundle's associated tags",
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
}
},
"required": [ "owner", "codeHash", "name" ]
}
42 changes: 42 additions & 0 deletions packages/app-db/src/schemas/contract.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://paritytech.github.io/canvas-ui/contract.schema.json",
"title": "Contract",
"description": "An instiated smart contract",
"type": "object",
"properties": {
"_id": {
"description": "Field to contain ulid-based instance id",
"type": "string"
},
"owner": {
"description": "The public key of the original uploader",
"type": "string"
},
"blockOneHash": {
"description": "The identifying block hash for the contract's chain (for distinguishing development/local node instances)",
"type": "string"
},
"address": {
"description": "The contract's instantiation address",
"type": "string"
},
"abi": {
"description": "The code bundle's associated ABI JSON",
"type": "object"
},
"name": {
"description": "The contract's display name",
"type": "string"
},
"tags": {
"description": "The contract's associated tags",
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
}
},
"required": [ "owner", "address", "abi", "name" ]
}
12 changes: 12 additions & 0 deletions packages/app-db/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2017-2021 @canvas-ui/app-db authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { JSONSchema4 } from 'json-schema';

import codeSchema from './code.schema.json';
import contractSchema from './contract.schema.json';
import userSchema from './user.schema.json';

export const user = userSchema as JSONSchema4;
export const code = codeSchema as JSONSchema4;
export const contract = contractSchema as JSONSchema4;
55 changes: 55 additions & 0 deletions packages/app-db/src/schemas/user.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://paritytech.github.io/canvas-ui/user.schema.json",
"title": "User",
"description": "A Canvas UI user identity",
"type": "object",
"properties": {
"_id": {
"description": "Field to contain ulid-based instance id",
"type": "string"
},
"codeBundlesOwned": {
"description": "Addresses of user's uploaded code bundles",
"type": "array",
"items": {
"type": "string"
}
},
"codeBundlesStarred": {
"description": "Addresses of user's starred code bundles",
"type": "array",
"items": {
"type": "string"
}
},
"contractsOwned": {
"description": "Addresses of user's instantiated smart contracts",
"type": "array",
"items": {
"type": "string"
}
},
"contractsStarred": {
"description": "Addresses of user's starred smart contracts",
"type": "array",
"items": {
"type": "string"
}
},
"publicKey": {
"description": "The user's public key",
"type": "string"
},
"email": {
"description": "The user's email address",
"type": "string",
"format": "email"
},
"name": {
"description": "The user's display name",
"type": "string"
}
},
"required": [ "publicKey" ]
}
Loading