Skip to content

Quickstart Guide: TypeScript Version

Welcome to GolemDB! Use this guide to get started quickly.

Info

We're using TypeScript here. Looking for the Python version of this doc? Go here.

GolemDB is a database that stores your data inside a blockchain store, bringing the benefits of blockchain to your data, including Tamper proof, Proof of originality, and Immutability. In particular:

  • Immutability and tamper-evident: The data is practically impossible to alter without it being totally obvious it was changed. Data is added through blockchain transactions, and each block of transactions is crytographically linked to the one before it, forming a chain. If anyone tries to modify one of these transactions or blocks, the change could easily be detected. This is ideal where data integrity is required, such as original intellectual property and the timestamp it was created, or, for example, voting records.

  • Decentralized availability: Unlike data stored in a typical database server, which can be taken offline and accessed by a single person or organization, GolemDB data on Ethereum is replicated across dozens, even hundreds of nodes worldwide. This adds to the data resillience, making sure it cannot be censored or removed by any single party.

  • Cryptographically-verifiable provenance: Because the transactions used to add the data are signed by a private cryptographic key, this siganture acts as proof of the data's origin from a specific address. This allows you to prove the data is in its original form and that you were the one who sent it.

  • Unparalleled Transparency and Auditability: Because every transaction in the blockchain is public and verifiable by anyone, your data gets a permanent timestampe, and easily traceable audit trail. Using block explorers, you can easily see who submitted a particular piece of data. (This means, of course, that if you want your data private and not readable by everyone, you need to encrypt it yourself before sending it. But even though others can't read it, your or they can still verify it hasn't been altered.) This can eliminate disputes over when the data was stored and what its original state was.

Data Storage Details

Data is stored as Entities

Data is stored as what we call entities. Each entity consists of

  • The main data you wish to store (also called payload). This can be text or binary data, up to 128,000 bytes.

  • Annotations: These are key/value pairs you define, typically to describe the data. Annotation values can be strings, which together with the keys are called string annotations, or numbers, which together with the keys are called numeric annotations.

Note

Annotation keys must start with a letter or underscore, and can be followed by any combination of letters, numbers, or underscores.

Blocks to live (BTL)

When you create an entity, you specify how many blockchain blocks the entity should live; after that the entity is removed from storage. We call this the BTL, which stands for Blocks to Live.

Operations on entities

You can:

  • Create entities

  • Delete entities

  • Update entities

  • Extend the number of blocks to live on an entity

  • Read the entities, either just the data value, or the metadata, which consists of BTL and annotations.

Transactions

Entities are created through transactions. We have forked the popular op-geth application to allow for entities to be managed by sending the information to a specific address in the form of a transaction. When the node receives a transaction directed towards that address, our own code runs, inspecting the transaction data for the create, delete, update, and extension operations. Further, the data is given an owner based on the address that sent the transaction.

Note

You don't need to use transactions; our SDKs provide simple functions for creating, deleting, updating, and extending entities. Behind the scenes these function calls send transactions through JSON RPC calls.

Note

The data is RLP-encoded; again, our SDKs take care of that part for you.

Querying the data

We also provide a sophisticated query system that allows you to search for entities based on key/value annotation pairs, and by owner.

Getting Started

1. Set up your TypeScript environment

We have a separate guide for configuring your TypeScript environment which you can find here.

Be sure to use the "module" for the "type" attribute in the package.json file.

2. Create code that creates a wallet

First, let's add an additional script to the initial package.json file. Open up package.json, find the scripts section, and the one called wallet as shown here:

    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "tsc",
        "start": "node dist/index.js",
        "build-and-start": "npm run build && npm run start",
        "wallet": "node dist/wallet.js",
        "lint": "eslint \"src/**/*.ts\"",
        "lint:fix": "eslint \"src/**/*.ts\" --fix",
        "format": "prettier --write \"src/**/*.ts\""
    },

Next, make sure you put your TypeScript code in a diretory off the root called src. Create a file in src called wallet.ts. Paste the following into it:

import 'dotenv/config'
import { Wallet } from 'ethers'
import { existsSync } from 'fs'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import xdg from 'xdg-portable'

export const createWalletAtFileWithPassword = async (
    password: string,
    filePath: string
) => {
    try {
        if (!process.env.GOLEMDB_PASS) {
            console.log(`No password provided. Please create a .env file in the root of your project
and include a line such as the following:

    GOLEMDB_PASS=abc123

(Tip: Do not put the .env file in your src directory.)`)
            return
        }

        if (existsSync(filePath)) {
            console.log(`Error: The file '${filePath}' already exists.`)
            console.log(
                'Please delete it manually if you want to create a new one.'
            )
            return // Exit the function gracefully
        }

        // Generate a new random wallet
        const wallet = Wallet.createRandom()
        const accountAddress = wallet.address
        console.log(`New account address created: ${accountAddress}`)

        // Encrypt the wallet with the provided password.
        // The encrypt method from ethers.js requires the options object as the third parameter.
        const encryptedWallet = await wallet.encrypt(password)

        // Write the encrypted JSON string to the specified file path.
        await writeFile(filePath, encryptedWallet)

        console.log(
            `Successfully created encrypted wallet file at: ${filePath}`
        )
    } catch (error) {
        console.error('Failed to create encrypted wallet file:', error)
        throw error
    }
}

const filePath = join(xdg.config(), 'golembase', 'wallet.json')
createWalletAtFileWithPassword(process.env.GOLEMDB_PASS as string, filePath)


Since our purpose here is to build GolemDB apps, we won't go through this code snippet line-by-line; however, we encourage you to do so at a later time.

Now create a file in the root of your project folder called .env and paste the following into it:

GOLEMDB_PASS=abc123

Note

Our wallet.json files are encrypted with a password. To keep things simple in this guide, we're storing it in a file called .env and we're just making the password crazy simple. We recommend treating the wallet being created here as a temporary, throwaway one. As such, you'll be fine using an incredibly easy-to-remember password such as abc123.

Now build it and run it by typing:

npm run build
npm run wallet

You should see output stating that the wallet was created, and it will also print out your address.

Copy the account address it prints out, including the starting 0x. You will need it in the next step.

Tip

If you ever need to locate the address again, you can simply open the wallet.json file found in ~/.config/golembase/wallet.json.

3. Fund your account

Next, fund your account by visiting the faucet for the testnet here, and pasting in your wallet number:

https://kaolin.holesky.golem-base.io/faucet/

Tip

Bookmark the above, and save your account address where you can easily find it so you can request funds periodically.

Using other wallets?

What if you want to use your own wallet, such as MetaMask? We advise against it for now. For these examples, we recommend using a "throwaway" account that you can easily recreate and start over with. The above instructions create just such a wallet account.

5. Build an app

Open src/index.ts and let's start adding code; remove the console.log line if present, and start in on the following steps.

Tip

For the code that follows, you can either paste in the imports all at once, or let the IDE detect them and add them.

Here are the all the import statements to put at the beginning of your app:

import 'dotenv/config'
import xdg from 'xdg-portable'
import { getBytes, Wallet } from 'ethers'
import { readFileSync } from 'fs'
import { join } from 'path'
import {
    AccountData,
    Annotation,
    createClient,
    GolemBaseCreate,
    GolemBaseExtend,
    GolemBaseUpdate,
    Tagged,
} from 'golem-base-sdk'

const encoder = new TextEncoder()
const decoder = new TextDecoder()

Notice we're also instantiating a TextEncoder and TextDecoder for use later.

Reading wallet and extracting key

Now let's follow that with code that opens and decodes your wallet. We'll continue using the password stored in the .env file you created earlier.

Now back to src/index.ts; add the following lines to read the wallet and extract the private key:

// Read wallet

const walletPath = join(xdg.config(), 'golembase', 'wallet.json')
const keystore = readFileSync(walletPath, 'utf8')
const wallet = Wallet.fromEncryptedJsonSync(
    keystore,
    process.env.GOLEMDB_PASS as string
)

// Read key from wallet
const key: AccountData = new Tagged('privatekey', getBytes(wallet.privateKey))

Go ahead and compile and run it just to make sure you have everything correct so far by typing:

npm run build-and-start

Nothing will print out, but you shouldn't get any exception errors either.

(For the remainder of this tutorial, we recommend building and running after each section, but we won't mention it again.)

Connnecting to the TestNet

Now let's add code to connect to our testnet. Add the following code to the end of your index.ts file:

//
// Step 2: Create a client that connects to a GolemDB node
//

const client = await createClient(
    60138453025,
    key,
    'https://kaolin.holesky.golem-base.io/rpc',
    'wss://kaolin.holesky.golem-base.io/rpc/ws'
)

//
// Try connecting to the client.
//     As an example, we'll request the current block number

const block = await client.getRawClient().httpClient.getBlockNumber()

console.log(block)

Creating an entity

For this next step, notice the pattern of our data structures. We'll be creating two entities and storing them with a single call to the node. Again, add this to the end of index.ts:

//
// Step 3: Create two entities and store them
//

const creates: GolemBaseCreate[] = [
    {
        data: encoder.encode('Imagine Dragons'),
        btl: 25,
        stringAnnotations: [
            new Annotation('first_album', 'Nigh Visions'),
            new Annotation('singer', 'Dan Reynolds'),
        ],
        numericAnnotations: [new Annotation('year_formed', 2008)],
    },
    {
        data: encoder.encode('Foo Fighters'),
        btl: 25,
        stringAnnotations: [
            new Annotation('first_album', 'Foo Fighters'),
            new Annotation('singer', 'Dave Grohl'),
        ],
        numericAnnotations: [new Annotation('year_formed', 1994)],
    },
]

const receipts = await client.createEntities(creates)
console.log('Receipts from create (entity key and expiration block):')
console.log(receipts)

Tip

The test net gets a lot of activity. Monitor it and see if 25 for BTL is really enough for your application's needs.

Deleting an entity

Now let's try out deleting an entity. Because this is a sample app, we'll just hardcode the indexes for the keys received from the receipts. Add the following two lines (we'll be using the first_key variable in a later step):

// Let's grab keys. This is just a sample so we're safe
// hardcoding the indexes, 0 and 1.
const first_key = receipts[0].entityKey
const second_key = receipts[1].entityKey

And now let's delete an entity. Then we'll check how many entities we have. (If you run this code multiple times, you might not get the number you're expecting, because entities eventually expire, possibly sooner than you expect.)

Here's the next code to add:

// Notice we pass an array; we're allowed to delete multiple entities
await client.deleteEntities([second_key])

// Let's print out how many entities we now own.

// Get the owner
let owner = await client.getOwnerAddress()

// Get the count; watch closely if you this app multiple times,
// and factor in blocks to live as well.
let entity_count = (await client.getEntitiesOfOwner(owner)).length
console.log(`Number of entities after delete: ${entity_count}`)

Updating an entity

Updating an entity is a simple matter of sending over a revised entity with any changes you need, along with the key of the entity to change.

Tip

Behind the scenes, you're just replacing the entity's value and metadata with what you send to the update function. At present we don't support an "update" where you update only parts of the data, so be sure to include all the annotations, not just the ones you're changing.

Here's the next code to add:

// Let's update the Imagine Dragons entity by adding the second album.
// First, we'll read the existing entity

const imagine_data = await client.getStorageValue(first_key)
console.log(decoder.decode(imagine_data))

const imagine_metadata = await client.getEntityMetaData(first_key)
console.log(imagine_metadata)

const updates: GolemBaseUpdate[] = [
    {
        entityKey: first_key,
        btl: 40,
        data: imagine_data,
        stringAnnotations: [
            ...imagine_metadata.stringAnnotations,
            new Annotation('second_album', 'Smoke + Mirrors'),
        ],
        numericAnnotations: [],
    },
]

await client.updateEntities(updates)

console.log('Entities updated!')

// Let's verify

const imagine_metadata2 = await client.getEntityMetaData(first_key)
console.log(imagine_metadata2)

Extending the blocks to live

To extend the number of blocks to live, you send over an array of items, each of which consists of a key and the number of blocks to extend by. Here's the code to add:

//
// Step 6: Extend the blocks to live
//

// Let's extend the blocks for the first entity to live by 40

const extensions: GolemBaseExtend[] = [
    {
        entityKey: first_key,
        numberOfBlocks: 40,
    },
]

await client.extendEntities(extensions)

// Let's verify again

const imagine_metadata3 = await client.getEntityMetaData(first_key)
console.log(imagine_metadata3.expiresAtBlock - imagine_metadata2.expiresAtBlock)

To verify, we're retreiving the metadata again for the same entity, and subtracting from its expiresAtBlock member the same from the previous run. It should print out 40.

A query

And finally, let's do a query. Although we might only have one block, we'll still do a query with an "and" operator in it so you can try out the query system. Here's the code to add:

//
// Step 7: A simple query
//

// Let's do a quick query for demo purposes, even though we have only one entity
const result = await client.queryEntities(
    'first_album="Night Visions" && singer="Dan Reynolds"'
)
console.log(result)

Tip

If you run this app and quickly run it again before the entities expire from the previous run, you might see more than one result in the query.

The Final Code

Here is the final code you created above.

import 'dotenv/config'
import xdg from 'xdg-portable'
import { getBytes, Wallet } from 'ethers'
import { readFileSync } from 'fs'
import { join } from 'path'
import {
    AccountData,
    Annotation,
    createClient,
    GolemBaseCreate,
    GolemBaseExtend,
    GolemBaseUpdate,
    Tagged,
} from 'golem-base-sdk'

const encoder = new TextEncoder()
const decoder = new TextDecoder()

//
// Step 1: Read the wallet and from that the key
//         Note: We're reading the password from the .env file
//         Normally you would NOT check the .env file into
//         version control; however, for this sample we are
//         so you can see where to put it and what format
//         it should be in.
//

// Read wallet

const walletPath = join(xdg.config(), 'golembase', 'wallet.json')
const keystore = readFileSync(walletPath, 'utf8')
const wallet = Wallet.fromEncryptedJsonSync(
    keystore,
    process.env.GOLEMDB_PASS as string
)

// Read key from wallet
const key: AccountData = new Tagged('privatekey', getBytes(wallet.privateKey))

//
// Step 2: Create a client that connects to a GolemDB node
//

const client = await createClient(
    60138453025,
    key,
    'https://kaolin.holesky.golem-base.io/rpc',
    'wss://kaolin.holesky.golem-base.io/rpc/ws'
)

//
// Try connecting to the client.
//     As an example, we'll request the current block number

const block = await client.getRawClient().httpClient.getBlockNumber()

console.log(block)

//
// Step 3: Create two entities and store them
//

const creates: GolemBaseCreate[] = [
    {
        data: encoder.encode('Imagine Dragons'),
        btl: 25,
        stringAnnotations: [
            new Annotation('first_album', 'Night Visions'),
            new Annotation('singer', 'Dan Reynolds'),
        ],
        numericAnnotations: [new Annotation('year_formed', 2008)],
    },
    {
        data: encoder.encode('Foo Fighters'),
        btl: 25,
        stringAnnotations: [
            new Annotation('first_album', 'Foo Fighters'),
            new Annotation('singer', 'Dave Grohl'),
        ],
        numericAnnotations: [new Annotation('year_formed', 1994)],
    },
]

const receipts = await client.createEntities(creates)
console.log('Receipts from create (entity key and expiration block):')
console.log(receipts)

// Tip: The test net gets a lot of activity. Monitor it and
// see if 25 for BTL is really enough for your application's needs

//
// Step 4: Delete the second of the two entities
//

// Let's grab keys. This is just a sample so we're safe
// hardcoding the indexes, 0 and 1.
const first_key = receipts[0].entityKey
const second_key = receipts[1].entityKey

// Notice we pass an array; we're allowed to delete multiple entities
await client.deleteEntities([second_key])

// Let's print out how many entities we now own.

// Get the owner
let owner = await client.getOwnerAddress()

// Get the count; watch closely if you this app multiple times,
// and factor in blocks to live as well.
let entity_count = (await client.getEntitiesOfOwner(owner)).length
console.log(`Number of entities after delete: ${entity_count}`)

//
// Step 5: Update an entity
//

// Let's update the Imagine Dragons entity by adding the second album.
// First, we'll read the existing entity

const imagine_data = await client.getStorageValue(first_key)
console.log(decoder.decode(imagine_data))

const imagine_metadata = await client.getEntityMetaData(first_key)
console.log(imagine_metadata)

const updates: GolemBaseUpdate[] = [
    {
        entityKey: first_key,
        btl: 40,
        data: imagine_data,
        stringAnnotations: [
            ...imagine_metadata.stringAnnotations,
            new Annotation('second_album', 'Smoke + Mirrors'),
        ],
        numericAnnotations: [],
    },
]

await client.updateEntities(updates)

console.log('Entities updated!')

// Let's verify

const imagine_metadata2 = await client.getEntityMetaData(first_key)
console.log(imagine_metadata2)

//
// Step 6: Extend the blocks to live
//

// Let's extend the blocks for the first entity to live by 40

const extensions: GolemBaseExtend[] = [
    {
        entityKey: first_key,
        numberOfBlocks: 40,
    },
]

await client.extendEntities(extensions)

// Let's verify again

const imagine_metadata3 = await client.getEntityMetaData(first_key)
console.log(imagine_metadata3.expiresAtBlock - imagine_metadata2.expiresAtBlock)

//
// Step 7: A simple query
//

// Let's do a quick query for demo purposes, even though we have only one entity
const result = await client.queryEntities(
    'first_album="Night Visions" && singer="Dan Reynolds"'
)
console.log(result)

// Tip: If you run this app and quickly run it again before the entities expire,
// you might see more than one result in the query.

Final Links

Ready for the API? Find the TypeScript API here