diff --git a/README.md b/README.md index 3abfc45..8165ea9 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,51 @@ yarn add db3.js ## Use db3.js in action +### Build db3 client + ```typescript -/* -|----------------------------| -| use db3js open a database | -|----------------------------| -*/ - -// build sign function -const sign = await getSign() - -// build database factory -const dbFactory = new DB3Factory({ - node: 'http://127.0.0.1:26659', - sign, - nonce -}) +// the key seed +const mnemonic ='...' +// create a wallet +const wallet = DB3BrowserWallet.createNew(mnemonic, 'DB3_SECP259K1') +// build db3 client +const client = new DB3Client('http://127.0.0.1:26659', wallet) +``` +### Create a database -// open database with an address -const db = dbFactory.open("0x5ca8d43c15fb366d80e221d11a34894eb0975da6") +```typescript +const [dbId, txId] = await client.createDatabase() +const db = initializeDB3('http://127.0.0.1:26659', dbId, wallet) +``` + +### Create a collection + +```typescript +// add a index to collection +const indexList: Index[] = [ + { + name: 'idx1', + id: 1, + fields: [ + { + fieldPath: 'name', + valueMode: { + oneofKind: 'order', + order: Index_IndexField_Order.ASCENDING, + }, + }, + ], + }, +] +// create a collecion +const collectionRef = await collection(db, 'cities', indexList) +// add a doc to collection +const result = await addDoc(collectionRef, { + name: 'beijing', + address: 'north', +}) +// get all docs from collection +const docs = await getDocs(collectionRef) ``` ## Show Your Support diff --git a/package.json b/package.json index ebfbc39..8e9c24b 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,7 @@ "types": "./dist/index.d.ts", "type": "module", "scripts": { - "build": "microbundle --entry src/index.ts --external none", - "build:wpt": "microbundle --entry src/index.ts --external none -f umd -o thirdparty/wpt/indexedDB/resources/indexedDB3.js ", + "build": "microbundle --entry src/index.ts --external none -f modern,esm", "test": "jest", "benny-sdk": "ts-node-esm --experimental-specifier-resolution=node ./src/benches/sdk_query.ts", "benny-mutation": "ts-node-esm --experimental-specifier-resolution=node ./src/benches/sdk_mutation.ts", diff --git a/src/client/client.ts b/src/client/client.ts index 97c9c16..55bdd04 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -28,6 +28,8 @@ import { StorageProvider } from '../provider/storage_provider' import { Wallet } from '../wallet/wallet' import { DbId } from '../crypto/id' import { toB64, fromHEX, toHEX } from '../crypto/crypto_utils' +import { Database, Index } from '../proto/db3_database' +import { QuerySessionInfo } from '../proto/db3_session' // // @@ -35,6 +37,11 @@ import { toB64, fromHEX, toHEX } from '../crypto/crypto_utils' // // export class DB3Client { + readonly provider: StorageProvider + readonly accountAddress: string + querySessionInfo: QuerySessionInfo | undefined + sessionToken: string | undefined + /** * new a db3 client with db3 node url and wallet * @@ -74,11 +81,11 @@ export class DB3Client { * get a database information * */ - async getDatabase(addr: string) { + async getDatabase(addr: string): Promise { const token = await this.keepSessionAlive() const response = await this.provider.getDatabase(addr, token) this.querySessionInfo!.queryCount += 1 - return response?.db + return response.db } async listCollection(databaseAddress: string) { @@ -130,7 +137,8 @@ export class DB3Client { ) { const documentMutation: DocumentMutation = { collectionName, - document: [BSON.serialize(document)], + documents: [BSON.serialize(document)], + ids: [], } const meta: BroadcastMeta = { @@ -171,21 +179,55 @@ export class DB3Client { })) } + async deleteDocument( + databaseAddress: string, + collectionName: string, + ids: string[] + ) { + const meta: BroadcastMeta = { + nonce: this.provider.getNonce().toString(), + chainId: ChainId.MainNet, + chainRole: ChainRole.StorageShardChain, + } + + const documentMutation: DocumentMutation = { + collectionName, + documents: [], + ids, + } + + const dm: DatabaseMutation = { + meta, + collectionMutations: [], + documentMutations: [documentMutation], + dbAddress: fromHEX(databaseAddress), + action: DatabaseAction.DeleteDocument, + } + const payload = DatabaseMutation.toBinary(dm) + return this.provider.sendMutation(payload, PayloadType.DatabasePayload) + } + async keepSessionAlive() { - if (!this.querySessionInfo) { + if (!this.querySessionInfo || !this.sessionToken) { const response = await this.provider.openSession() this.sessionToken = response.sessionToken this.querySessionInfo = response.querySessionInfo return this.sessionToken } // TODO - if (this.querySessionInfo!.queryCount > 1000) { - await this.provider.closeSession() + if (this.querySessionInfo.queryCount > 1000) { + await this.provider.closeSession( + this.sessionToken, + this.querySessionInfo + ) const response = await this.provider.openSession() this.sessionToken = response.sessionToken this.querySessionInfo = response.querySessionInfo return this.sessionToken } + if (!this.sessionToken) { + throw new Error('sessioToken is not found') + } return this.sessionToken } } diff --git a/src/crypto/crypto_utils.ts b/src/crypto/crypto_utils.ts index 6848e98..33a7b5e 100644 --- a/src/crypto/crypto_utils.ts +++ b/src/crypto/crypto_utils.ts @@ -25,7 +25,12 @@ export const pathRegex = new RegExp("^m(\\/[0-9]+')+$") export const replaceDerive = (val: string): string => val.replace("'", '') -export const getMasterKeyFromSeed = (seed: Hex): Keys => { +interface Keys { + key: Uint8Array + chainCode: Uint8Array +} + +export const getMasterKeyFromSeed = (seed: string): Keys => { const h = hmac.create(sha512, ED25519_CURVE) const I = h.update(fromHEX(seed)).digest() const IL = I.slice(0, 32) @@ -152,10 +157,10 @@ export function toB64(aBytes: Uint8Array): string { } export const derivePath = ( - path: Path, - seed: Hex, + path: string, + seed: string, offset = HARDENED_OFFSET -): Keys => { +) => { if (!isValidPath(path)) { throw new Error('Invalid derivation path') } diff --git a/src/crypto/ed25519_keypair.ts b/src/crypto/ed25519_keypair.ts index 01a4ceb..33cdfd8 100644 --- a/src/crypto/ed25519_keypair.ts +++ b/src/crypto/ed25519_keypair.ts @@ -16,7 +16,7 @@ // import nacl from 'tweetnacl' -import type { Keypair } from './keypair' +import type { ExportedKeypair, Keypair } from './keypair' import { SignatureScheme, SIGNATURE_SCHEME_TO_FLAG } from './publickey' import { Ed25519PublicKey } from './ed25519_publickey' import { mnemonicToSeedHex, isValidHardenedPath } from './mnemonics' @@ -36,13 +36,14 @@ export interface Ed25519KeypairData { } export class Ed25519Keypair implements Keypair { + keypair: Ed25519KeypairData | nacl.SignKeyPair /** * Create a new Ed25519 keypair instance. * Generate random keypair if no {@link Ed25519Keypair} is provided. * * @param keypair Ed25519 keypair */ - constructor(keypair?: Ed25519KeypairData) { + constructor(keypair: Ed25519KeypairData) { if (keypair) { this.keypair = keypair } else { diff --git a/src/crypto/id.ts b/src/crypto/id.ts index 92059b4..9925126 100644 --- a/src/crypto/id.ts +++ b/src/crypto/id.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // - +// @ts-nocheck import sha3 from 'js-sha3' import { toB64, fromHEX } from './crypto_utils' import { Uint64BE } from 'int64-buffer' @@ -23,6 +23,7 @@ const TX_ID_LENGTH = 32 const DB_ID_LENGTH = 20 export class TxId { + data: Uint8Array constructor(data: Uint8Array) { const inputDataLength = data.length if (inputDataLength != TX_ID_LENGTH) { @@ -37,7 +38,7 @@ export class TxId { // from the broadcast response // static from(hash: Uint8Array): TxId { - return new TxId(data) + return new TxId(hash) } getB64(): string { @@ -46,11 +47,12 @@ export class TxId { } export class DbId { + addr: string constructor(sender: string, nonce: number) { const binary_addr = fromHEX(sender) const nonceBuf = new Uint64BE(nonce) let tmp = new Uint8Array(DB_ID_LENGTH + 8) - tmp.set(nonceBuf.toBuffer()) + tmp.set(nonceBuf.buffer || nonceBuf.toBuffer()) tmp.set(binary_addr, 8) this.addr = '0x' + sha3.sha3_256(tmp).slice(0, 40) } diff --git a/src/crypto/secp256k1_publickey.ts b/src/crypto/secp256k1_publickey.ts index ec17c97..af0bb19 100644 --- a/src/crypto/secp256k1_publickey.ts +++ b/src/crypto/secp256k1_publickey.ts @@ -19,6 +19,7 @@ import sha3 from 'js-sha3' import { fromB64, toB64 } from './crypto_utils' import { bytesEqual, + PublicKey, PublicKeyInitData, SIGNATURE_SCHEME_TO_FLAG, } from './publickey' diff --git a/src/index.ts b/src/index.ts index 3e498ab..aada777 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,3 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export { DB3BrowserWallet } from './wallet/db3_browser_wallet' +export { DB3Client } from './client/client' +export { initializeDB3 } from './store/app' +export { collection } from './store/collection' +export { addDoc, getDocs, deleteDoc } from './store/document' diff --git a/src/provider/storage_provider.ts b/src/provider/storage_provider.ts index fcf9ee1..68c9646 100644 --- a/src/provider/storage_provider.ts +++ b/src/provider/storage_provider.ts @@ -14,18 +14,20 @@ // See the License for the specific language governing permissions and // limitations under the License. // - +// @ts-nocheck import { GrpcWebFetchTransport, GrpcWebOptions, } from '@protobuf-ts/grpcweb-transport' import { StorageNodeClient } from '../proto/db3_node.client' -import { WriteRequest, Mutation, PayloadType } from '../proto/db3_mutation' +import { WriteRequest, PayloadType } from '../proto/db3_mutation' import { OpenSessionRequest, BroadcastRequest, GetAccountRequest, ListDocumentsRequest, + CloseSessionRequest, + ShowDatabaseRequest, } from '../proto/db3_node' import { CloseSessionPayload, @@ -40,6 +42,8 @@ import { fromHEX } from '../crypto/crypto_utils' // the db3 storage node provider implementation which provides low level methods to exchange with db3 network // export class StorageProvider { + readonly client: StorageNodeClient + readonly wallet: Wallet /** * new a storage provider with db3 storage grpc url */ @@ -74,11 +78,12 @@ export class StorageProvider { return new TxId(response.hash) } + // @ts-nocheck /** * build a session with storage node for querying data */ async openSession() { - let header = '' + let header if (typeof window === 'undefined') { header = new Date().getTime() + @@ -127,9 +132,9 @@ export class StorageProvider { * get the account information with db3 address * */ - async getAccount(addr: string) { + async getAccount(addr: Uint8Array) { const getAccountRequest: GetAccountRequest = { - addr: address, + addr, } const { response } = await this.client.getAccount(getAccountRequest) return response diff --git a/src/store/app.ts b/src/store/app.ts new file mode 100644 index 0000000..806996a --- /dev/null +++ b/src/store/app.ts @@ -0,0 +1,12 @@ +import { DB3Client } from '../client/client' +import { Wallet } from '../wallet/wallet' +import { DB3Store } from './database' + +export function initializeDB3( + node: string, + dbAddress: string, + wallet: Wallet +): DB3Store { + const dbClient = new DB3Client(node, wallet) + return new DB3Store(dbAddress, dbClient) +} diff --git a/src/store/collection.ts b/src/store/collection.ts new file mode 100644 index 0000000..f5c7059 --- /dev/null +++ b/src/store/collection.ts @@ -0,0 +1,32 @@ +import { Index } from '../proto/db3_database' +import { DB3Store } from './database' +import { DocumentData, DocumentReference } from './document' +import { Query } from './query' + +export class CollectionReference { + readonly type = 'collection' + readonly db: DB3Store + readonly name: string + constructor(db: DB3Store, name: string) { + this.db = db + this.name = name + } +} + +export async function collection( + db: DB3Store, + name: string, + index: Index[] +): Promise + +export async function collection( + db: DB3Store, + name: string, + index: Index[] +): Promise { + const collections = await db.getCollections() + if (!collections || !collections[name]) { + await db.client.createCollection(db.address, name, index) + } + return new CollectionReference(db, name) +} diff --git a/src/store/database.ts b/src/store/database.ts new file mode 100644 index 0000000..809ed1d --- /dev/null +++ b/src/store/database.ts @@ -0,0 +1,20 @@ +import { DB3Client } from '../client/client' +import { Collection } from '../proto/db3_database' + +export class DB3Store { + readonly address: string + readonly client: DB3Client + _collections: Record | undefined + constructor(address: string, client: DB3Client) { + this.address = address + this.client = client + } + async getCollections() { + if (!this._collections) { + const collections = await this.client.listCollection(this.address) + this._collections = collections + } + + return this._collections + } +} diff --git a/src/store/document.ts b/src/store/document.ts new file mode 100644 index 0000000..f013978 --- /dev/null +++ b/src/store/document.ts @@ -0,0 +1,68 @@ +import { CollectionReference } from './collection' +import { DB3Store } from './database' + +export interface DocumentData { + [field: string]: any +} + +export class DocumentReference { + /** The type of this Firestore reference. */ + readonly type = 'document' + + /** + * The {@link Firestore} instance the document is in. + * This is useful for performing transactions, for example. + */ + readonly db3Store: DB3Store + + /** @hideconstructor */ + constructor( + db3Store: DB3Store + /** + * If provided, the `FirestoreDataConverter` associated with this instance. + */ + ) { + this.db3Store = db3Store + } + + get name(): string { + return '' + } + + /** + * A string representing the path of the referenced document (relative + * to the root of the database). + */ + get address(): string { + return '' + } +} + +export async function addDoc( + reference: CollectionReference, + data: any +): Promise { + const db = reference.db + const result = await db.client.createDocument( + db.address, + reference.name, + data + ) + return result +} + +export async function getDocs(reference: CollectionReference) { + const db = reference.db + const docs = await db.client.listDocuments(db.address, reference.name) + return docs +} + +export async function deleteDoc(reference: CollectionReference, ids: string[]) { + const db = reference.db + const result = await db.client.deleteDocument( + db.address, + reference.name, + ids + ) + return result +} diff --git a/src/store/query.ts b/src/store/query.ts new file mode 100644 index 0000000..d8bb4a8 --- /dev/null +++ b/src/store/query.ts @@ -0,0 +1,72 @@ +import { DB3Store } from './database' +import { DocumentData } from './document' + +export class Query { + /** The type of this Firestore reference. */ + readonly type: 'query' | 'collection' = 'query' + + /** + * The `Firestore` instance for the Firestore database (useful for performing + * transactions, etc.). + */ + readonly db3store: DB3Store + + // This is the lite version of the Query class in the main SDK. + + /** @hideconstructor protected */ + constructor( + db3store: DB3Store + /** + * If provided, the `FirestoreDataConverter` associated with this instance. + */ + ) { + this.db3store = db3store + } +} + +export type QueryConstraintType = + | 'where' + | 'orderBy' + | 'limit' + | 'limitToLast' + | 'startAt' + | 'startAfter' + | 'endAt' + | 'endBefore' + +export abstract class AppliableConstraint { + /** + * Takes the provided {@link Query} and returns a copy of the {@link Query} with this + * {@link AppliableConstraint} applied. + */ + abstract _apply(query: Query): Query +} + +/** + * A `QueryConstraint` is used to narrow the set of documents returned by a + * Firestore query. `QueryConstraint`s are created by invoking {@link where}, + * {@link orderBy}, {@link startAt}, {@link startAfter}, {@link + * endBefore}, {@link endAt}, {@link limit}, {@link limitToLast} and + * can then be passed to {@link query} to create a new query instance that + * also contains this `QueryConstraint`. + */ +export abstract class QueryConstraint extends AppliableConstraint { + /** The type of this query constraint */ + abstract readonly type: QueryConstraintType + + /** + * Takes the provided {@link Query} and returns a copy of the {@link Query} with this + * {@link AppliableConstraint} applied. + */ + abstract _apply(query: Query): Query +} + +export function query( + query: Query, + ...queryConstraints: QueryConstraint[] +): Query + +export function query( + query: Query, + queryConstraint: QueryConstraint +): Query {} diff --git a/src/wallet/db3_browser_wallet.ts b/src/wallet/db3_browser_wallet.ts index 16376e9..a499e31 100644 --- a/src/wallet/db3_browser_wallet.ts +++ b/src/wallet/db3_browser_wallet.ts @@ -22,6 +22,7 @@ import { fromB64 } from '../crypto/crypto_utils' const WALLET_KEY = '_db3_wallet_key_' export class DB3BrowserWallet implements Wallet { + keypair: Ed25519Keypair | Secp256k1Keypair constructor(keypair: Ed25519Keypair | Secp256k1Keypair) { this.keypair = keypair } @@ -35,7 +36,7 @@ export class DB3BrowserWallet implements Wallet { } static hasKey(): boolean { - const key = JSON.parse(localStorage.getItem(WALLET_KEY) ?? {}) + const key = JSON.parse(localStorage.getItem(WALLET_KEY) ?? '{}') if (!key.hasOwnProperty('schema')) { return false } @@ -43,7 +44,7 @@ export class DB3BrowserWallet implements Wallet { } static recover(): DB3BrowserWallet { - const key = JSON.parse(localStorage.getItem(WALLET_KEY) ?? {}) + const key = JSON.parse(localStorage.getItem(WALLET_KEY) ?? '{}') if (!key.hasOwnProperty('schema')) { throw new Error('no key in browser') } diff --git a/tests/client.test.ts b/tests/client.test.ts index 2586b4c..8631d77 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -19,6 +19,7 @@ import { describe, expect, test } from '@jest/globals' import { toHEX } from '../src/crypto/crypto_utils' import { DB3Client } from '../src/client/client' import { DB3BrowserWallet } from '../src/wallet/db3_browser_wallet' +import { Index, Index_IndexField_Order } from '../src/proto/db3_database' describe('test db3.js client module', () => { test('create database smoke test', async () => { @@ -30,14 +31,17 @@ describe('test db3.js client module', () => { await new Promise((r) => setTimeout(r, 2000)) const db = await client.getDatabase(dbId) expect(dbId).toEqual(`0x${toHEX(db!.address)}`) - const indexList = [ + const indexList: Index[] = [ { name: 'idx1', id: 1, fields: [ { fieldPath: 'name', - valueMode: { oneofKind: 'order', order: 1 }, + valueMode: { + oneofKind: 'order', + order: Index_IndexField_Order.ASCENDING, + }, }, ], }, @@ -54,5 +58,8 @@ describe('test db3.js client module', () => { const books = await client.listDocuments(dbId, 'books') expect(books.length).toBe(1) expect(books[0]['doc']['name']).toBe('book1') + const bookId = books[0].id + const result = await client.deleteDocument(dbId, 'books', [bookId]) + expect(result).toBeDefined() }) }) diff --git a/tests/provider.test.ts b/tests/provider.test.ts index ac458b7..44f8723 100644 --- a/tests/provider.test.ts +++ b/tests/provider.test.ts @@ -59,7 +59,7 @@ describe('test db3.js provider module', () => { PayloadType.DatabasePayload ) expect(txId.getB64()).toBe( - 'MiuDkHefVUg0AyHQtuq76QwBcraNwNoqbl1QL3Wj78U=' + 'HzyHbZjgfdCcZUW8KERvjQAsjyDTGjt7TLTlhU4wwWE=' ) localStorage.clear() }) @@ -88,7 +88,7 @@ describe('test db3.js provider module', () => { PayloadType.DatabasePayload ) expect(txId.getB64()).toBe( - 'zHkR2KQa9y6n31PjezDTrfi+McVMGQKE9ocMFMsXIJE=' + 'uWECy6GSliva8MJv1F6yg0Qu9nZPjxFPMgIsmejjWiE=' ) localStorage.clear() }) diff --git a/tests/store.test.ts b/tests/store.test.ts new file mode 100644 index 0000000..9dce9ff --- /dev/null +++ b/tests/store.test.ts @@ -0,0 +1,62 @@ +// +// crypto.test.ts +// Copyright (C) 2023 db3.network Author imotai +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + DB3BrowserWallet, + initializeDB3, + DB3Client, + collection, + addDoc, + getDocs, +} from '../src/index' +import { Index, Index_IndexField_Order } from '../src/proto/db3_database' + +describe('test db3.js store module', () => { + const mnemonic = + 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss' + const wallet = DB3BrowserWallet.createNew(mnemonic, 'DB3_SECP259K1') + test('test document curd', async () => { + const client = new DB3Client('http://127.0.0.1:26659', wallet) + const [dbId, txId] = await client.createDatabase() + const db = initializeDB3('http://127.0.0.1:26659', dbId, wallet) + const indexList: Index[] = [ + { + name: 'idx1', + id: 1, + fields: [ + { + fieldPath: 'BJ', + valueMode: { + oneofKind: 'order', + order: Index_IndexField_Order.ASCENDING, + }, + }, + ], + }, + ] + const collectionRef = await collection(db, 'cities', indexList) + await new Promise((r) => setTimeout(r, 2000)) + const result = await addDoc(collectionRef, { + name: 'beijing', + address: 'north', + }) + await new Promise((r) => setTimeout(r, 2000)) + const docs = await getDocs(collectionRef) + expect(docs.length).toBe(1) + expect(docs[0]['doc']['name']).toBe('beijing') + }) +}) diff --git a/thirdparty/db3 b/thirdparty/db3 index 5db0b5e..bd3d80f 160000 --- a/thirdparty/db3 +++ b/thirdparty/db3 @@ -1 +1 @@ -Subproject commit 5db0b5ee417f6f6bd9d8c3c2deefeaa63881d9f1 +Subproject commit bd3d80f114460dd9ca2b5a4f0fe1eddccdad3189 diff --git a/tools/start_localnet.sh b/tools/start_localnet.sh index 233aac4..c6d3c07 100644 --- a/tools/start_localnet.sh +++ b/tools/start_localnet.sh @@ -2,7 +2,7 @@ # # start_localnet.sh killall db3 tendermint -DB3_VERSION="v0.2.7" +DB3_VERSION="v0.2.8" test_dir=`pwd` BUILD_MODE='debug' if [[ $1 == 'release' ]] ; then