@windingtree/wt-write-api

API to write data to the Winding Tree platform

Usage no npm install needed!

<script type="module">
  import windingtreeWtWriteApi from 'https://cdn.skypack.dev/@windingtree/wt-write-api';
</script>

README

WT Write API

Greenkeeper badge

API server written in node.js to interact with the Winding Tree platform. It is capable of:

  • Create and register new hotels in a segment directory
  • Update existing hotel records
  • Delete hotels from a segment directory

It also automatically publishes notifications about changes via the WT Update API.

Requirements

  • Nodejs >=10

Development

In order to install and run tests, we must:

git clone git@github.com:windingtree/wt-write-api.git
nvm install
npm install
npm run resolve-swagger-references
npm test

Running in dev mode

With all the dependencies installed, you can start the dev server.

First step is to initialize the SQLite database used to store your settings. If you want to use a different database, feel free to change the connection settings in the appropriate configuration file in src/config/.

npm run createdb-dev

If you'd like to start afresh later, just delete the .dev.sqlite file.

Second step is starting Ganache (local Ethereum network node). You can skip this step if you have a different network already running.

npm run dev-net

For trying out the interaction with the running dev-net and wt-write-api in general, you can use the Winding Tree demo wallet protected by password windingtree. It is initialized on dev-net with enough ether. For sample interaction scripts, check out our Developer guides.

!!!NEVER USE THIS WALLET FOR ANYTHING IN PRODUCTION!!! Anyone has access to it.

{"version":3,"id":"7fe84016-4686-4622-97c9-dc7b47f5f5c6","address":"d037ab9025d43f60a31b32a82e10936f07484246","crypto":{"ciphertext":"ef9dcce915eeb0c4f7aa2bb16b9ae6ce5a4444b4ed8be45d94e6b7fe7f4f9b47","cipherparams":{"iv":"31b12ef1d308ea1edacc4ab00de80d55"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"d06ccd5d9c5d75e1a66a81d2076628f5716a3161ca204d92d04a42c057562541","n":8192,"r":8,"p":1},"mac":"2c30bc373c19c5b41385b85ffde14b9ea9f0f609c7812a10fdcb0a565034d9db"}};

Now we can run our dev server.

npm run dev

When using a dev config, we internally run a script to deploy WT Index. It is not immediate, so you might experience some errors in a first few seconds. And that's the reason why it is not used in the same manner in integration tests.

You can fiddle with the configuration in src/config/.

Running this server

Docker

You can run the whole API in a docker container, and you can control which config will be used by passing an appropriate value to WT_CONFIG variable at runtime. Database will be setup during the container startup in the current setup. You can skip this with SKIP_DB_SETUP environment variable.

$ docker build -t windingtree/wt-write-api .
$ docker run -p 8080:8000 -e ETH_NETWORK_PROVIDER=address_to_node -e WT_CONFIG=playground windingtree/wt-write-api
  • After that you can access the wt-write-api on local port 8080
  • This deployment is using a Ropsten configuration that can be found in src/config/playground
  • Warning - User wallets (although protected by password) will be stored in the image, be careful where your API is running and who can access it.
  • Warning - Once your docker container (and its associated volumes, if any) is deleted, all accounts (wallets and configuration) will disappear.

NPM

You can install and run this from NPM as well:

$ npm install -g @windingtree/wt-write-api
$  -e ETH_NETWORK_PROVIDER=address_to_node WT_CONFIG=playground wt-write-api

This will also create a local SQLite instance in the directory where you run the wt-write-api command. To prevent that, you can suppress DB creation with SKIP_DB_SETUP environment variable.

Probably the easiest way of getting an Ethereum Node API is to register with Infura.

Running in production

You can customize the behaviour of the instance by many environment variables which get applied if you run the API with WT_CONFIG=envvar. These are:

  • WT_CONFIG - Which config will be used. Defaults to dev.
  • ADAPTER_SWARM_GATEWAY - Address of a Swarm HTTP Gateway, for example https://swarm.windingtree.com or https://swarm-gateways.net
  • ADAPTER_SWARM_READ_TIMEOUT - Read timeout in milliseconds for Swarm, defaults to 1000
  • ADAPTER_SWARM_WRITE_TIMEOUT - Write timeout in milliseconds for Swarm, defaults to 2500
  • WT_ENTRYPOINT_ADDRESS - On chain address of Winding Tree Entrypoint
  • PORT - HTTP Port where the API will lsiten, defaults to 8000.
  • LOG_LEVEL - Set log level for winston.
  • BASE_URL - Base URL of this API instance, for example https://playground-api.windingtree.com
  • ETH_NETWORK_NAME - Name of Ethereum network for informational purposes, for example ropsten or mainnet
  • ETH_NETWORK_PROVIDER - Address of Ethereum node, for example https://ropsten.infura.io/v3/my-project-id
  • DB_CLIENT - Knex database client name, for example sqlite3.
  • DB_CLIENT_OPTIONS - Knex database client options as JSON string, for example {"filename": "./envvar.sqlite"}.
  • SKIP_DB_SETUP - Whether to not setup new database upon startup.

For example the playground configuration can be emulated with the following command (the actual values are probably obsolete, check src/config/playground.js for current values):

docker run -p 8080:8000 \
  -e WT_CONFIG=envvar \
  -e ADAPTER_SWARM_GATEWAY=https://swarm.windingtree.com \
  -e WT_ENTRYPOINT_ADDRESS=0xa268937c2573e2AB274BF6d96e88FfE0827F0D4D \
  -e ETH_NETOWRK_NAME=ropsten \
  -e ETH_NETWORK_PROVIDER=https://ropsten.infura.io/v3/my-project-id \
  -e DB_CLIENT_OPTIONS='{"filename": "./envvar.sqlite"}' \
  -e DB_CLIENT=sqlite3 \
  -e BASE_URL=https://playground-write-api.windingtree.com \
  windingtree/wt-write-api \

In production, this API should not be accessible from the open internet as it internally holds potentially lots of funds in ETH.

How tos

Account setup

In order to use the API, you need to create an account that stores your configuration. The account consists of Ethereum wallet in JSON format and a configuration of uploaders. The uploaders are telling the API where to put data about hotels managed by that Ethereum wallet. The API does not store Wallet passwords.

The JSON-format wallet can easily be created locally with mycrypto.

In this case, we are setting up swarm as our preferred storage, make sure it is actually accessible (via the swarmProvider gateway url from config) before you try to create hotel.

{
  "wallet": {"version":3,"id":"7fe84016-4686-4622-97c9-dc7b47f5f5c6","address":"d037ab9025d43f60a31b32a82e10936f07484246","crypto":{"ciphertext":"ef9dcce915eeb0c4f7aa2bb16b9ae6ce5a4444b4ed8be45d94e6b7fe7f4f9b47","cipherparams":{"iv":"31b12ef1d308ea1edacc4ab00de80d55"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"d06ccd5d9c5d75e1a66a81d2076628f5716a3161ca204d92d04a42c057562541","n":8192,"r":8,"p":1},"mac":"2c30bc373c19c5b41385b85ffde14b9ea9f0f609c7812a10fdcb0a565034d9db"}},
  "uploaders": {
    "root": {
      "swarm": {}
    }
  }
}
$ curl -X POST localhost:8000/accounts -H 'Content-Type: application/json' --data @create-account.json

# These values are generated and will be different
{"accountId":"aa43edaf8266e8f8","accessKey":"usgq6tSBW+wDYA/MBF367HnNp4tGKaCTRPy3JHPEqJmFBuxq1sA7UhFOpuV80ngC"}

Uploaders

We currently support two types of uploaders, and each uploader can contain a configuration. These configuration values are not encrypted in any way.

  • Swarm - No configuration needed, we will use the Swarm gateway configured for the whole wt-write-api instance and reasonable defaults for timeout. Timeouts are in milliseconds. You can override these settings with:
    • providerUrl - address of a Swarm gateway. Mandatory.
    • timeout - General timeout. Will be used for both read and write.
    • timeoutRead - Read timeout, overwrites the general timeout.
    • timeoutWrite - Write timeout, overwrites the general timeout.
  • AWS S3 - We recommend to use a separate IAM account for this with a limited set of permissions. All of the following options are required unless stated otherwise.
    • accessKeyId - AWS credentials
    • secretAccessKey - AWS credentials
    • region - AWS region
    • bucket - S3 bucket to upload to. This is the name that is used in URL.
    • keyPrefix - a prefix ("directory") to upload hotel data to. Serves to differentiate between different hotels stored in the same s3 bucket. Optional.

You can use any combination of these uploaders for the following list of objects:

  • root - Hotel data index object. This object is required.
  • description - Hotel description object
  • ratePlans - RatePlans object
  • availability - Availability object ...

If you, for example, would like to store your description on Swarm, but everything else in your S3, you would use the following account object for your initial POST request. For any object type not explicitely stated in the configuration, the root configuration will be used.

{
  "wallet": {"....": "...."},
  "uploaders": {
    "root": {
      "s3": {
        "accessKeyId": "AX...",
        "secretAccessKey": "1234...",
        "region": "eu-west-1",
        "bucket": "hotel-data"
      }
    },
    "description": {
      "swarm": {
        "providerUrl": "https://swarm-gateways.net",
        "timeout": 3000,
        "timeoutRead": 5000,
        "timeoutWrite": 7000
      }
    }
  }
}

In another example, you want everything on swarm except availability and ratePlans:

{
  "wallet": {"....": "...."},
  "uploaders": {
    "root": {
      "swarm": {}
    },
    "availability": {
      "s3": {
        "accessKeyId": "AX...",
        "secretAccessKey": "1234...",
        "region": "eu-west-1",
        "bucket": "hotel-data"
      }
    },
    "ratePlans": {
      "s3": {
        "accessKeyId": "AX...",
        "secretAccessKey": "1234...",
        "region": "eu-west-1",
        "bucket": "hotel-data"
      }
    }
  }
}

However, there is a third option. If you plan to host all of the data by yourself, for example on your own webserver, you might not need to specify any uploaders:

{
  "wallet": {"....": "...."},
  "uploaders": {}
}

Create hotel

Registering a hotel

In case you plan to manage all of your data on your own, you can only register a hotel with a single orgJson and a keccak256 hash of its contents.

{
  "orgJson": "https://example.com/my-hotel-org.json",
  "hash": "0x123456..."
}
$ curl -X POST localhost:8000/hotels -H 'Content-Type: application/json' \
  -H 'X-Access-Key: usgq6tSBW+wDYA/MBF367HnNp4tGKaCTRPy3JHPEqJmFBuxq1sA7UhFOpuV80ngC' \
  -H 'X-Wallet-Password: windingtree' \
  --data @hotel-description.json

# This value will be different
{"address":"0xA603FF7EA9A1B81FB45EF6AeC92A323a88211f40"}

You can verify that the hotel data was saved by calling

$ curl localhost:8000/hotels/0xa8c4cbB500da540D9fEd05BE7Bef0f0f5df3e2cc

In this case, the Write API will try to resolve all of the data from the provided URI as well.

Registering a hotel with data

A full-fledged usage of Write API will take care of uploading the data and generating the ORG.JSON hash for you. This is just an example data that we know will pass all the validations. For a better description of the actual data model, have a look at src/services/validators/, where you will find JSON schemas used to validate incoming data.

The legalEntity is part of the ORG.ID standard. Go check it out.

{
  "legalEntity": {
    "name": "Hostel Marilena",
    "contact": {
      "email": "info@marilena.ro",
      "phone": "+40213191564",
      "url": "https://geotowns.ro/marilena"
    },
    "address": {
      "road": "Horse Saddle",
      "houseNumber": "34",
      "postcode": "33200",
      "city": "Dragolm",
      "countryCode": "RO"
    }
  },
  "hotel": {
    "description": {
      "name": "Random hotel",
      "description": "**Beautiful** hotel located in the center of _Prague, Czech Republic_.",
      "location": {
        "latitude": 50.075388,
        "longitude": 14.414170
      },
      "contacts": {
        "general": {
          "email": "chadima.jiri@gmail.com",
          "phone": "00420224371111",
          "url": "https://jirkachadima.cz",
          "ethereum": "windingtree.eth"
        }
      },
      "address": {
        "line1": "Rašínovo nábřeží 1981/80",
        "line2": "Nové Město",
        "postalCode": "12000",
        "city": "Prague",
        "country": "CZ"
      },
      "timezone": "Europe/Prague",
      "currency": "CZK",
      "amenities": [],
      "images": [
        "https://raw.githubusercontent.com/windingtree/media/master/logo-variants/tree/png/tree--gradient-on-white.png",
        "https://raw.githubusercontent.com/windingtree/media/master/logo-variants/full-logo/png/logo--black-on-green.png"
      ],
      "updatedAt": "2018-06-19T15:53:00+0200",
      "defaultCancellationAmount": 30,
      "roomTypes": {
        "1234-abcd": {
          "name": "string",
          "description": "string",
          "totalQuantity": 0,
          "occupancy": {
            "min": 1,
            "max": 3
          },
          "amenities": [
            "TV"
          ],
          "images": [
            "https://raw.githubusercontent.com/windingtree/media/web-assets/logo-variants/full-logo/png/logo--white.png"
          ],
          "updatedAt": "2018-06-27T14:59:05.830Z",
          "properties": {
            "nonSmoking": "some"
          }
        }
      }
    }
  }
}
$ curl -X POST localhost:8000/hotels -H 'Content-Type: application/json' \
  -H 'X-Access-Key: usgq6tSBW+wDYA/MBF367HnNp4tGKaCTRPy3JHPEqJmFBuxq1sA7UhFOpuV80ngC' \
  -H 'X-Wallet-Password: windingtree' \
  --data @hotel-description.json

# This value will be different
{"address":"0xA603FF7EA9A1B81FB45EF6AeC92A323a88211f40"}

You can verify that the hotel data was saved by calling

$ curl localhost:8000/hotels/0xa8c4cbB500da540D9fEd05BE7Bef0f0f5df3e2cc

Update hotel

You can also update previously created hotels. The top-level properties (e.g. description) are always replaced as a whole.

{
  "legalEntity": {
    "name": "Hostel Marilena",
    "contact": {
      "email": "info@marilena.ro",
      "phone": "+40213191564",
      "url": "https://geotowns.ro/marilena"
    },
    "address": {
      "road": "Horse Saddle",
      "houseNumber": "34",
      "postcode": "33200",
      "city": "Dragolm",
      "countryCode": "RO"
    }
  },
  "hotel": {
    "description": {
      "name": "Changed hotel name",
      "description": "**Beautiful** hotel located in the center of _Prague, Czech Republic_.",
      "location": {
        "latitude": 50.075388,
        "longitude": 14.414170
      },
      "contacts": {
        "general": {
          "email": "chadima.jiri@gmail.com",
          "phone": "00420224371111",
          "url": "https://jirkachadima.cz",
          "ethereum": "windingtree.eth"
        }
      },
      "address": {
        "line1": "Rašínovo nábřeží 1981/80",
        "line2": "Nové Město",
        "postalCode": "12000",
        "city": "Prague",
        "country": "CZ"
      },
      "timezone": "Europe/Prague",
      "currency": "CZK",
      "amenities": [],
      "images": [
        "https://raw.githubusercontent.com/windingtree/media/master/logo-variants/tree/png/tree--gradient-on-white.png",
        "https://raw.githubusercontent.com/windingtree/media/master/logo-variants/full-logo/png/logo--black-on-green.png"
      ],
      "updatedAt": "2018-06-19T15:53:00+0200",
      "defaultCancellationAmount": 30,
      "roomTypes": {
        "1234-abcd": {
          "name": "string",
          "description": "string",
          "totalQuantity": 0,
          "occupancy": {
            "min": 1,
            "max": 3
          },
          "amenities": [
            "TV"
          ],
          "images": [
            "https://raw.githubusercontent.com/windingtree/media/web-assets/logo-variants/full-logo/png/logo--white.png"
          ],
          "updatedAt": "2018-06-27T14:59:05.830Z",
          "properties": {
            "nonSmoking": "some"
          }
        }
      }
    }
  }
}

Alternatively, you can update only the orgJson as well:

{
  "orgJson": "https://example.com/my-updated-hotel-org.json",
  "hash": "0x123456..."
}
$ curl -X PATCH localhost:8000/hotels -H 'Content-Type: application/json' \
  -H 'X-Access-Key: usgq6tSBW+wDYA/MBF367HnNp4tGKaCTRPy3JHPEqJmFBuxq1sA7UhFOpuV80ngC' \
  -H 'X-Wallet-Password: windingtree' \
  --data @hotel-description.json

The data format version indicator in hotel data index is always updated to the write api's declared data format version, no matter how many parts of a hotel you update.

Publicly available instances

For currently available public instances of wt-write-api, please see this page.