Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hello world simple smart contract for use in quickstart guide #1

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions hello-world/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
12 changes: 12 additions & 0 deletions hello-world/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM node:12-alpine AS base
WORKDIR /app

FROM base AS builder
COPY package.json package.json
COPY yarn.lock yarn.lock
RUN yarn --frozen-lockfile --non-interactive --production

FROM base AS release
COPY --chown=1000:1000 ./src ./src
COPY --from=builder --chown=1000:1000 /app/node_modules ./node_modules
USER 1000:1000
80 changes: 80 additions & 0 deletions hello-world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Simple "Hello World" Smart Contract

This contract demonstrates how easy it is to develop on Dragonchain and how quickly you can produce a working smart contract prototype. The SC greeter is implemented as a single function in Nodejs with a single import. Even the Dockerfile is simple, requiring no prior experience.

## Dockerfile

```Dockerfile
FROM node:12-alpine AS base # Using a lightweight base image keeps SC size small
WORKDIR /app # Not strictly necessary, but enforces the working directory instead of determining it at build time

FROM base AS builder # Use a different layer to keep from installing anything unnecessary on the final image
COPY package.json package.json # Copy only what we need to install packages
COPY yarn.lock yarn.lock # Docker COPY commands can only move one file at a time
RUN yarn --frozen-lockfile --non-interactive --production # Run yarn to install node packages

FROM base AS release # This layer will be the final image produced by the Dockerfile
COPY --chown=1000:1000 ./src ./src # Copy source code from local directory
COPY --from=builder --chown=1000:1000 /app/node_modules ./node_modules # Copy node modules from 'builder' layer
USER 1000:1000 # Set user for the docker image for easier local development
```

## Smart Contract Code

Because Dragonchain smart contracts run in isolation using [openfaas](https://www.openfaas.com/), they accept input from stdin. In this example, we use [get-stdin](https://www.npmjs.com/package/get-stdin) to read input as strings.

In most cases, simple strings are not sufficient for smart contract execution. Dragonchain suggests submitting JSON objects as strings for complicated workflows. For this example, the expected object has a single key, `name`, which can be of any Stringifiable data type.

The function that executes smart contract logic must be exported. In this case, we use an index.js file as the entry point to the smart contract that parses stdin and provides the value to the function provided in [hello.js](./src/hello.js).

```js
module.exports = async function (fullTransaction) {
```

Just like any service, a smart contract should validate its inputs. For the greeter function, we want to ensure that the payload provided by the user contains a `name`.

```js
if (!fullTransaction || !fullTransaction.payload || !fullTransaction.payload.name) {
return { error: "I can't say hi if I don't know your name!" };
}
```

After verifying inputs are valid, the smart contract should execute its internal logic. This could include manipulating blockchain heap data, performing arbitrary calculations, ledgering data, managing permissions, etc. Once the SC has finished executing its logic, it can return a string or a JSON object to be ledgered onto the chain as output. By default, all SC executions ledger their output, but this can be optionally disabled.

```js
/* Execute logic here */

// Return JSON or a string to finish execution.
return { greeting: `Hello ${name}!`}
}
```

To see the output from a smart contract, you can query using your transaction id for the invocation request. For example, if you're using dctl to interact with your dragonchain and your transaction create command returns transaction_id `banana`, you would search for transactions with `@invoker:{banana}`.

```sh
$ dctl t c hello-world '{ "name": "dragonchain" }'
{
"status": 201,
"response": {
"transaction_id": "banana"
},
"ok": true
}
$ dctl t q state-test '@invoker:{banana}'
{
...
"header": {
"txn_type": "hello-world",
...
"invoker": "banana"
},
"payload": {
"greeting": "Hello dragonchain!"
},
...
}
```

## Examples

For simplicity, a [small test suite](./spec/hello.spec.js) is included to give some examples how to use a smart contract. Smart contracts that institute business logic should be tested like any other business unit.
18 changes: 18 additions & 0 deletions hello-world/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@dragonchain-dev/hello-world",
"version": "1.0.0",
"description": "demonstrate a simple dragonchain smart contract",
"main": "hello.js",
"scripts": {
"test": "mocha './spec/**/*.spec.js' --exit"
},
"author": "",
"license": "ISC",
"dependencies": {
"get-stdin": "^7.0.0"
},
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^6.2.2"
}
}
17 changes: 17 additions & 0 deletions hello-world/spec/hello.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { expect } = require('chai');
const greeting = require('../src/hello');

describe('greeter', () => {
it('greets a string name', async () => {
const res = await greeting({ payload: { name: 'banana'} });
expect(res).to.deep.equal({ greeting: 'Hello banana!' });
});
it('greets a number name', async () => {
const res = await greeting({ payload: { name: 3.1415} });
expect(res).to.deep.equal({ greeting: 'Hello 3.1415!' });
});
it('Cant greet someone without a name', async () => {
const res = await greeting();
expect(res).to.deep.equal({ error: "I can't say hi if I don't know your name!" });
});
})
6 changes: 6 additions & 0 deletions hello-world/src/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = async function (fullTransaction) {
if (!fullTransaction || !fullTransaction.payload || !fullTransaction.payload.name) {
return { error: "I can't say hi if I don't know your name!" };
}
return { greeting: `Hello ${fullTransaction.payload.name}!`}
}
9 changes: 9 additions & 0 deletions hello-world/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const getStdin = require('get-stdin');
const greeting = require('./hello');

getStdin().then(async val => {
const res = await greeting(JSON.parse(val));
process.stdout.write(JSON.stringify(res))
}).catch(e => {
console.error(e.stack);
});