diff --git a/index.ts b/index.ts
index e8edd9a5955c961727056b33666a39c3db105f7d..71750b70edb43eb2e05a99cae083635d4b1b1b84 100644
--- a/index.ts
+++ b/index.ts
@@ -1,22 +1,58 @@
-import * as express from 'express';
 import {Duniter} from 'duniter';
+import {gvaHttpListen} from "./src/network";
+import * as Http from "http";
 import {UpnpProvider} from "duniter/app/modules/upnp-provider";
-import graphqlHTTP = require("express-graphql");
-import {makeExecutableSchema} from "graphql-tools";
+
+const DEFAULT_GVA_PORT = 15000
 
 Duniter.run([{
   name: 'gva',
   required: {
     duniter: {
+
+      cliOptions: [
+        { value: '--gva-port <port>',     desc: 'Force usage of a specific port for GVA.', parser: (val: string) => parseInt(val) },
+        { value: '--gva-host <porthost>', desc: 'Force usage of a specific host for GVA.' },
+        { value: '--gva-upnp',            desc: 'Use UPnP for port opening.' },
+        { value: '--gva-noupnp',          desc: 'Do not use UPnP for port opening.' },
+      ],
+
+      config: {
+        onLoading: (conf: any, program: any) => {
+          if (program.gvaPort || program.gvaHost || program.noupnp || program.upnp) {
+            if (!conf.gva) {
+              conf.gva = {}
+            }
+            if (program.gvaPort) {
+              conf.gva.port = program.gvaPort
+            }
+            if (program.gvaHost) {
+              conf.gva.host = program.gvaHost
+            }
+            if (program.noupnp) {
+              conf.gva.upnp = false
+            }
+            if (program.upnp) {
+              conf.gva.upnp = true
+            }
+          }
+        },
+        beforeSave: (conf, program, logger, confDAL) => {
+        }
+      },
+
       cli: [{
         name: 'gva',
         desc: 'Starts the UI',
         onDatabaseExecute: async (server, conf, program, params, start, stop) => {
-
           // Never ending
           await new Promise(async (res, rej) => {
             try {
-              await start();
+              const { available, host, port } = { available: true, host: 'localhost', port: 15000 }
+              // const { available, host, port } = await api.startRegular()
+              if (available) {
+                gvaHttpListen(server, port, host)
+              }
             } catch (e) {
               rej(e)
             }
@@ -24,74 +60,29 @@ Duniter.run([{
         }
       }],
       service: {
-        neutral: (server, conf, logger) => {
+        neutral: (server, conf: any) => {
 
+          let app: Http.Server|undefined
           let api = new UpnpProvider(
-            15000,
-            16000,
+            (conf.gva && conf.gva.port) || DEFAULT_GVA_PORT,
+            (conf.gva && conf.gva.port) || DEFAULT_GVA_PORT + 1000,
             ':gva:' + conf.pair.pub.substr(0, 6))
-
-          let app = express()
-
           return {
             startService: async () => {
-              const schema = makeExecutableSchema({
-                typeDefs: `
-              
-                  type Certification {
-                    from: String!
-                    to: String!
-                  }
-                
-                  type Identity {
-                    uid: String!
-                    pub: String!
-                    certs: [Certification!]!
-                  }
-                
-                  type Query {
-                    hello: String
-                    identity(uid: String!): Identity
-                  }
-                `,
-                resolvers: {
-
-                  Query: {
-                    hello: () => 'Hello world!',
-
-                    identity: async (_, { uid }: { uid: string }) => {
-                      return server.dal.iindexDAL.getFullFromUID(uid)
-                    },
-
-                  },
-
-                  Identity: {
-                    certs: async (identity: { pub:string }) => {
-                      return (await server.dal.cindexDAL.getValidLinksTo(identity.pub)).map(c => ({
-                        from: c.issuer,
-                        to: c.receiver,
-                      }))
-                    }
-                  }
+              if (!conf.gva || conf.gva.upnp) {
+                // Using UPnP
+                const { available, host, port } = await api.startRegular()
+                if (available) {
+                  app = gvaHttpListen(server, port, host)
                 }
-              })
-              // app.use('/graphql', graphqlHTTP({
-              //   schema,
-              //   graphiql: true,
-              // }))
-              // app.listen(15000, () => console.log(`Now browse to ${'192.168.1.24'}:${15000}/graphql`))
-              const { available, host, port } = { available: true, host: 'localhost', port: 15000 }
-              // const { available, host, port } = await api.startRegular()
-              if (available) {
-                app.use('/graphql', graphqlHTTP({
-                  schema,
-                  graphiql: true,
-                }))
-                app.listen(port, () => console.log(`Now browse to ${host}:${port}/graphql`))
+              } else if (!conf.gva) {
+                // Static usage of port + host, no UPnP
+                app = gvaHttpListen(server, DEFAULT_GVA_PORT)
               }
             },
             stopService: async () => {
-              return undefined;
+              await new Promise(res => app && app.close(res))
+              await (api && api.stopRegular())
             }
           } as any
         }
diff --git a/package.json b/package.json
index 3751d049ea620a2541877f78dadad4beca00b4bc..12d8a3fc601cf2988b69e7af501ad405afa50bdd 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,17 @@
     "@types/express": "^4.16.0",
     "@types/express-graphql": "^0.6.2",
     "@types/graphql": "^14.0.3",
+    "@types/mocha": "^5.2.5",
+    "@types/node": "^10.12.3",
+    "@types/request-promise": "^4.1.42",
+    "@types/should": "^13.0.0",
+    "assert": "^1.4.1",
     "duniter": "file:///home/cgeek/dev/duniter",
+    "mocha": "^5.2.0",
+    "request": "^2.88.0",
+    "request-promise": "^4.2.2",
+    "should": "^13.2.3",
+    "ts-node": "^7.0.1",
     "typescript": "^3.1.6"
   }
 }
diff --git a/src/gva.ts b/src/gva.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b3aecc6fb75e37a3af9ac16da0bd6061536ec8bc
--- /dev/null
+++ b/src/gva.ts
@@ -0,0 +1,151 @@
+import * as path from "path";
+import * as fs from "fs";
+import {Server} from "duniter/server";
+import {makeExecutableSchema} from "graphql-tools";
+import {FullIindexEntry, FullMindexEntry} from "duniter/app/lib/indexer";
+import {hashf} from "duniter/app/lib/common";
+
+export function plugModule(server: Server) {
+  const schemaFile = path.join(__dirname, 'schema.graphqls');
+  const typeDefs = fs.readFileSync(schemaFile, 'utf8');
+  return makeExecutableSchema({
+    typeDefs,
+    resolvers: {
+
+      Query: {
+        hello: () => 'Welcome to Duniter GVA API.',
+
+        currency: () => server.conf.currency,
+
+        block: async (_, { number }: { number?: number }) => {
+          if (number !== undefined) {
+            return server.dal.getBlock(number)
+          }
+          const b = await server.dal.getCurrentBlockOrNull()
+          return b
+        },
+
+        member: async (_, { uid, pub }: { uid: string, pub: string }) => {
+          if (uid) {
+            return server.dal.iindexDAL.getFullFromUID(uid)
+          }
+          return server.dal.iindexDAL.getFromPubkey(pub)
+        },
+
+        pendingIdentities: async (_, { search }: { search: string }) => {
+          if (!search) {
+            return server.dal.idtyDAL.getPendingIdentities()
+          }
+          return server.dal.idtyDAL.searchThoseMatching(search)
+        },
+
+        pendingIdentityByHash: async (_, { hash }: { hash: string }) => {
+          return server.dal.idtyDAL.getByHash(hash)
+        },
+
+        pendingTransactions: async () => {
+          return server.dal.txsDAL.getAllPending(1)
+        },
+
+        transactionByHash: async (_, { hash }: { hash: string }) => {
+          return server.dal.txsDAL.getTX(hash)
+        },
+
+        transactionsOfIssuer: async (_, { issuer }: { issuer: string }) => {
+          return (await server.dal.txsDAL.getLinkedWithIssuer(issuer))
+            .concat(await server.dal.txsDAL.getPendingWithIssuer(issuer))
+        },
+
+        transactionsOfReceiver: async (_, { receiver }: { receiver: string }) => {
+          return (await server.dal.txsDAL.getLinkedWithRecipient(receiver))
+            .concat(await server.dal.txsDAL.getPendingWithRecipient(receiver))
+        },
+
+        sourcesOfPubkey: async (_, { pub }: { pub: string }) => {
+          const txSources = await server.dal.sindexDAL.getAvailableForPubkey(pub)
+          const udSources = await server.dal.dividendDAL.getUDSources(pub)
+          const sources: {
+            type: string
+            noffset: number
+            identifier: string
+            amount: number
+            base: number
+            conditions: string
+            consumed: boolean
+          }[] = []
+          txSources.forEach(s => sources.push({
+            type: 'T',
+            identifier: s.identifier,
+            noffset: s.pos,
+            amount: s.amount,
+            base: s.base,
+            conditions: s.conditions,
+            consumed: false
+          }))
+          udSources.forEach(s => sources.push({
+            type: 'D',
+            identifier: pub,
+            noffset: s.pos,
+            amount: s.amount,
+            base: s.base,
+            conditions: `SIG(${pub})`,
+            consumed: false
+          }))
+          return sources
+        },
+
+      },
+
+      Identity: {
+        certsIssued: async (identity: FullIindexEntry) => {
+          return server.dal.cindexDAL.getValidLinksFrom(identity.pub)
+        },
+        certsReceived: async (identity: FullIindexEntry) => {
+          return server.dal.cindexDAL.getValidLinksTo(identity.pub)
+        },
+        pendingIssued: async (identity: FullIindexEntry) => {
+          return server.dal.certDAL.getFromPubkeyCerts(identity.pub)
+        },
+        pendingReceived: async (identity: FullIindexEntry) => {
+          return server.dal.certDAL.getNotLinkedToTarget(identity.hash)
+        },
+        membership: async (identity: { pub:string }) => {
+          const ms = (await server.dal.mindexDAL.getReducedMS(identity.pub)) as FullMindexEntry
+          return {
+            revokes_on: ms.revokes_on,
+            expires_on: ms.expires_on,
+            chainable_on: ms.chainable_on,
+          }
+        },
+      },
+
+      PendingIdentity: {
+        certs: async (identity: { hash:string }) => {
+          return server.dal.certDAL.getNotLinkedToTarget(identity.hash)
+        },
+        memberships: async (identity: { hash:string }) => {
+          return server.dal.msDAL.getPendingINOfTarget(identity.hash)
+        },
+      },
+
+      Mutation: {
+        submitIdentity(_, { raw }: { raw: string }) {
+          return server.writeRawIdentity(raw)
+        },
+        async submitCertification(_, { raw }: { raw: string }) {
+          const res = await server.writeRawCertification(raw)
+          const targetHash = hashf(res.idty_uid + res.idty_buid + res.idty_issuer)
+          return server.dal.idtyDAL.getByHash(targetHash)
+        },
+        async submitMembership(_, { raw }: { raw: string }) {
+          const res = await server.writeRawMembership(raw)
+          const targetHash = hashf(res.userid + res.blockstamp + res.pub)
+          return server.dal.idtyDAL.getByHash(targetHash)
+        },
+        async submitTransaction(_, { raw }: { raw: string }) {
+          return server.writeRawTransaction(raw)
+        },
+      }
+    }
+  })
+}
\ No newline at end of file
diff --git a/src/network.ts b/src/network.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc67b800f22366245b8f669c26dbc41e1dc97ba8
--- /dev/null
+++ b/src/network.ts
@@ -0,0 +1,14 @@
+import {plugModule} from "./gva";
+import * as express from "express";
+import graphqlHTTP = require("express-graphql");
+import {Server} from "duniter/server";
+import * as http from "http";
+
+export function gvaHttpListen(server: Server, port: number, host = 'localhost'): http.Server {
+  const app = express()
+  app.use('/graphql', graphqlHTTP({
+    schema: plugModule(server),
+    graphiql: true,
+  }))
+  return app.listen(port, host, () => console.log(`Now browse to ${host}:${port}/graphql`))
+}
diff --git a/src/schema.graphqls b/src/schema.graphqls
new file mode 100644
index 0000000000000000000000000000000000000000..c3651303f66b76d9c8acee7f27b44941b60ed20b
--- /dev/null
+++ b/src/schema.graphqls
@@ -0,0 +1,198 @@
+type Membership {
+  pub: String!
+  created_on: String!
+  written_on: String!
+  expires_on: Int!
+  expired_on: Int
+  revokes_on: Int!
+  revoked_on: String!
+  leaving: Boolean!
+  revocation: String!
+  chainable_on: Int!
+  writtenOn: Int!
+}
+
+type Certification {
+  issuer: String!
+  receiver: String!
+  created_on: Int!
+  sig: String!
+  chainable_on: Int!
+  expires_on: Int!
+  expired_on: Int!
+}
+
+type Source {
+  type: String!
+  noffset: Int!
+  identifier: String!
+  amount: Int!
+  base: Int!
+  conditions: String!
+  consumed: Boolean!
+}
+
+type Identity {
+  uid: String!
+  pub: String!
+  hash: String!
+  sig: String!
+  created_on: String!
+  written_on: String!
+  member: Boolean!
+  wasMember: Boolean!
+  certsIssued: [Certification!]!
+  certsReceived: [Certification!]!
+  pendingIssued: [PendingCertification!]!
+  pendingReceived: [PendingCertification!]!
+  membership: Membership!
+}
+
+type PendingIdentity {
+  revoked: Boolean!
+  buid: String!
+  member: Boolean!
+  kick: Boolean!
+  leaving: Boolean
+  wasMember: Boolean!
+  pubkey: String!
+  uid: String!
+  sig: String!
+  revocation_sig: String
+  hash: String!
+  written: Boolean!
+  revoked_on: Int
+  expires_on: Int!
+  certs: [PendingCertification!]!
+  memberships: [PendingMembership!]!
+}
+
+type PendingCertification {
+  linked: Boolean!
+  written: Boolean!
+  written_block: Int
+  written_hash: String
+  sig: String!
+  block_number: Int!
+  block_hash: String!
+  target: String!
+  to: String!
+  from: String!
+  block: Int!
+  expired: Boolean
+  expires_on: Int!
+}
+
+type PendingMembership {
+  membership: String!
+  issuer: String!
+  number: Int!
+  blockNumber: Int!
+  blockHash: String!
+  userid: String!
+  certts: String!
+  block: String!
+  fpr: String!
+  idtyHash: String!
+  written: Boolean!
+  written_number: Int
+  expires_on: Int!
+  signature: String!
+  expired: Boolean
+  block_number: Int
+}
+
+type Transaction {
+  hash: String!
+  block_number: Int
+  locktime: Int!
+  version: Int!
+  currency: String!
+  comment: String!
+  blockstamp: String!
+  blockstampTime: Int
+  time: Int
+  inputs: [String!]!
+  unlocks: [String!]!
+  outputs: [String!]!
+  issuers: [String!]!
+  signatures: [String!]!
+  written: Boolean
+  removed: Boolean
+  received: Int
+  output_base: Int!
+  output_amount: Int!
+  written_on: String
+  writtenOn: Int
+}
+
+type BlockTransaction {
+  version: Int!
+  currency: String!
+  locktime: Int!
+  hash: String!
+  blockstamp: String!
+  blockstampTime: Int!
+  issuers: [String!]!
+  inputs: [String!]!
+  outputs: [String!]!
+  unlocks: [String!]!
+  signatures: [String!]!
+  comment: String!
+}
+
+type Block {
+  version: Int!
+  number: Int!
+  currency: String!
+  hash: String!
+  inner_hash: String!
+  signature: String!
+  previousHash: String
+  issuer: String!
+  previousIssuer: String
+  time: Int!
+  powMin: Int!
+  unitbase: Int!
+  membersCount: Int!
+  issuersCount: Int!
+  issuersFrame: Int!
+  issuersFrameVar: Int!
+  identities: [String!]!
+  joiners: [String!]!
+  actives: [String!]!
+  leavers: [String!]!
+  revoked: [String!]!
+  excluded: [String!]!
+  certifications: [String!]!
+  transactions: [BlockTransaction!]!
+  medianTime: Int!
+  nonce: String!
+  parameters: String!
+  monetaryMass: Int!
+  dividend: Int
+  UDTime: Int
+  writtenOn: Int!
+  written_on: String!
+}
+
+type Mutation {
+  submitIdentity(raw: String!): PendingIdentity!
+  submitCertification(raw: String!): PendingIdentity!
+  submitMembership(raw: String!): PendingIdentity!
+  submitTransaction(raw: String!): Transaction!
+}
+
+type Query {
+  hello: String
+  currency: String!
+  block(number: Int): Block
+  member(uid: String, pub: String): Identity
+  pendingIdentities(search: String): [PendingIdentity!]!
+  pendingIdentityByHash(hash: String!): PendingIdentity
+  pendingTransactions: [Transaction!]!
+  transactionByHash(hash: String!): Transaction
+  transactionsOfIssuer(issuer: String!): [Transaction!]!
+  transactionsOfReceiver(receiver: String!): [Transaction!]!
+  sourcesOfPubkey(pub: String!): [Source!]!
+}
diff --git a/tests/gva.ts b/tests/gva.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6bbda23cf1b40f4c9da8f8cdd8e7eda1e10ddadc
--- /dev/null
+++ b/tests/gva.ts
@@ -0,0 +1,138 @@
+import * as assert from "assert"
+import {NewTestingServer, TestingServer} from "duniter/test/integration/tools/toolbox";
+import {gvaHttpListen} from "../src/network";
+import {Underscore} from "duniter/app/lib/common-libs/underscore";
+import {TestUser} from "duniter/test/integration/tools/TestUser";
+import {TestGvaClient} from "./test-gva-client";
+import * as http from "http";
+import {GvaTestUser} from "./test-user";
+import {DBBlock} from "duniter/app/lib/db/DBBlock";
+
+export const prepareDuniterServer = async (options:any) => {
+
+  const catKeyring = { pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP'};
+  const tacKeyring = { pub: '2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc', sec: '2HuRLWgKgED1bVio1tdpeXrf7zuUszv1yPHDsDj7kcMC4rVSN9RC58ogjtKNfTbH1eFz7rn38U1PywNs3m6Q7UxE'};
+
+  const s1 = NewTestingServer(Underscore.extend({ pair: catKeyring }, options || {}));
+
+  const cat = new TestUser('cat', catKeyring, { server: s1 });
+  const tac = new TestUser('tac', tacKeyring, { server: s1 });
+
+  await s1._server.initWithDAL()
+
+  return { s1, cat, tac };
+}
+
+describe('GVA', () => {
+
+  const port = 15000
+  const now = 1480000000
+
+  let s1: TestingServer
+  let cat: GvaTestUser
+  let tac: GvaTestUser
+  let gva: http.Server
+  let gvaClient: TestGvaClient
+
+  before(async () => {
+    const { s1: _s1, cat: _cat, tac: _tac } = await prepareDuniterServer({
+      dt: 1000,
+      ud0: 200,
+      udTime0: now - 1, // So we have a UD right on block#1
+      medianTimeBlocks: 1 // Easy: medianTime(b) = time(b-1)
+    })
+    s1 = _s1
+    gva = gvaHttpListen(s1._server, port)
+    gvaClient = new TestGvaClient(`http://localhost:${port}/graphql`)
+    cat = new GvaTestUser(
+      _cat.uid,
+      _cat.pub,
+      _cat.sec,
+      gvaClient
+    )
+    tac = new GvaTestUser(
+      _tac.uid,
+      _tac.pub,
+      _tac.sec,
+      gvaClient
+    )
+  })
+
+  it('GVA should answer', async () => {
+    const res = await gvaClient.hello()
+    assert.deepEqual(res.data, {
+      "hello": "Welcome to Duniter GVA API."
+    })
+    assert.equal(null, await gvaClient.current())
+  })
+
+  it('cat & tac should be able to create its identity', async () => {
+    await cat.createIdentityAndSubmit()
+    assert.equal(1, (await gvaClient.pendingIdentities()).length)
+    await tac.createIdentityAndSubmit()
+    assert.equal(2, (await gvaClient.pendingIdentities()).length)
+  })
+
+  it('cat & tac should be able to certify each other', async () => {
+    await cat.createCertAndSubmit(tac)
+    assert.equal(1, (await gvaClient.pendingIdentities())[1].certs.length)
+    assert.equal(0, (await gvaClient.pendingIdentities())[0].certs.length)
+    await tac.createCertAndSubmit(cat)
+    assert.equal(1, (await gvaClient.pendingIdentities())[1].certs.length)
+    assert.equal(1, (await gvaClient.pendingIdentities())[0].certs.length)
+  })
+
+  it('cat & tac should be able to submit membership', async () => {
+    await cat.createJoinAndSubmit()
+    assert.equal(1, ((await gvaClient.pendingIdentities())[0] as any).memberships.length)
+    assert.equal(0, ((await gvaClient.pendingIdentities())[1] as any).memberships.length)
+    await tac.createJoinAndSubmit()
+    assert.equal(1, ((await gvaClient.pendingIdentities())[0] as any).memberships.length)
+    assert.equal(1, ((await gvaClient.pendingIdentities())[1] as any).memberships.length)
+  })
+
+  it('root block should be generated', async () => {
+    await s1.commit({ time: now })
+    const current = await gvaClient.current()
+    assert.notEqual(null, current)
+    assert.equal(0, (current as DBBlock).number)
+    assert.equal(cat.pub, (current as DBBlock).issuer)
+    assert.equal(0, (current as DBBlock).issuersCount)
+  })
+
+  it('UD should be available at block#1', async () => {
+    await s1.commit({ time: now })
+    const current = await gvaClient.current()
+    assert.notEqual(null, current)
+    assert.equal(1, (current as DBBlock).number)
+    assert.equal(200, (current as DBBlock).dividend)
+  })
+
+  it('cat & tac should both have 200 units', async () => {
+    assert.equal(200, (await cat.balance()))
+    assert.equal(200, (await tac.balance()))
+  })
+
+  it('cat should be able to send money to tac', async () => {
+    await cat.createTxAndSubmit(tac, 100)
+    await s1.commit({ time: now })
+    const current = await gvaClient.current()
+    assert.notEqual(null, current)
+    assert.equal(2, (current as DBBlock).number)
+    assert.equal(null, (current as DBBlock).dividend)
+    assert.equal(1, (current as DBBlock).transactions.length)
+  })
+
+  it('cat should have 100 units', async () => {
+    assert.equal(100, (await cat.balance()))
+  })
+
+  it('tac should have 300 units', async () => {
+    assert.equal(300, (await tac.balance()))
+  })
+
+  after(() => Promise.all([
+    s1.closeCluster(),
+    new Promise(res => gva.close(res)),
+  ]))
+})
diff --git a/tests/test-gva-client.ts b/tests/test-gva-client.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ee47fc0d2cb76cdd045d73b0bc532a81ac34ec1a
--- /dev/null
+++ b/tests/test-gva-client.ts
@@ -0,0 +1,310 @@
+import * as requestPromise from "request-promise";
+import {DBBlock} from "duniter/app/lib/db/DBBlock";
+import {DBIdentity} from "duniter/app/lib/dal/sqliteDAL/IdentityDAL";
+
+export class TestGvaClient {
+
+  constructor(protected uri: string) {}
+
+  graphQLPost(req: string) {
+    return requestPromise(this.uri, {
+      method: 'POST',
+      json: true,
+      body: {
+        query: req
+      }
+    })
+  }
+
+  async currency(): Promise<string> {
+    return (await this.graphQLPost(`{ currency }`) as any).data.currency
+  }
+
+  async current(): Promise<DBBlock|null> {
+    const res = await (await this.graphQLPost(`{ block {
+      version
+      number
+      currency
+      hash
+      inner_hash
+      signature
+      previousHash
+      issuer
+      previousIssuer
+      time
+      powMin
+      unitbase
+      membersCount
+      issuersCount
+      issuersFrame
+      issuersFrameVar
+      identities
+      joiners
+      actives
+      leavers
+      revoked
+      excluded
+      certifications
+      transactions {
+        version
+        currency
+        locktime
+        hash
+        blockstamp
+        blockstampTime
+        issuers
+        inputs
+        outputs
+        unlocks
+        signatures
+        comment
+      }
+      medianTime
+      nonce
+      parameters
+      monetaryMass
+      dividend
+      UDTime
+      writtenOn
+      written_on
+    } }`) as any)
+    return res.data.block
+  }
+
+  hello() {
+    return this.graphQLPost('{ hello }')
+  }
+
+  submitIdentity(raw: string) {
+    return this.graphQLPost(`mutation {
+      submitIdentity(raw: "${raw.replace(/\n/g, '\\n')}") {
+        revoked
+        buid
+        member
+        kick
+        leaving
+        wasMember
+        pubkey
+        uid
+        sig
+        revocation_sig
+        hash
+        written
+        revoked_on
+        expires_on
+      }
+    }`)
+  }
+
+  submitCertification(raw: string) {
+    return this.graphQLPost(`mutation {
+      submitCertification(raw: "${raw.replace(/\n/g, '\\n')}") {
+        revoked
+        buid
+        member
+        kick
+        leaving
+        wasMember
+        pubkey
+        uid
+        sig
+        revocation_sig
+        hash
+        written
+        revoked_on
+        expires_on
+        certs {
+          linked
+          written
+          written_block
+          written_hash
+          sig
+          block_number
+          block_hash
+          target
+          to
+          from
+          block
+          expired
+          expires_on
+        }
+        memberships {
+          membership
+          issuer
+          number
+          blockNumber
+          blockHash
+          userid
+          certts
+          block
+          fpr
+          idtyHash
+          written
+          written_number
+          expires_on
+          signature
+          expired
+          block_number
+        }
+      }
+    }`)
+  }
+
+  submitMembership(raw: string) {
+    return this.graphQLPost(`mutation {
+      submitMembership(raw: "${raw.replace(/\n/g, '\\n')}") {
+        revoked
+        buid
+        member
+        kick
+        leaving
+        wasMember
+        pubkey
+        uid
+        sig
+        revocation_sig
+        hash
+        written
+        revoked_on
+        expires_on
+        certs {
+          linked
+          written
+          written_block
+          written_hash
+          sig
+          block_number
+          block_hash
+          target
+          to
+          from
+          block
+          expired
+          expires_on
+        }
+        memberships {
+          membership
+          issuer
+          number
+          blockNumber
+          blockHash
+          userid
+          certts
+          block
+          fpr
+          idtyHash
+          written
+          written_number
+          expires_on
+          signature
+          expired
+          block_number
+        }
+      }
+    }`)
+  }
+
+  async submitTransaction(raw: string) {
+    return (await this.graphQLPost(`mutation {
+      submitTransaction(raw: "${raw.replace(/\n/g, '\\n')}") {
+        hash
+        block_number
+        locktime
+        version
+        currency
+        comment
+        blockstamp
+        blockstampTime
+        time
+        inputs
+        unlocks
+        outputs
+        issuers
+        signatures
+        written
+        removed
+        received
+        output_base
+        output_amount
+        written_on
+        writtenOn
+      }
+    }`))
+  }
+
+  async pendingIdentities(search = ''): Promise<DBIdentity[]> {
+    return (await this.graphQLPost(`{
+      pendingIdentities(search: "${search}") {
+        revoked
+        buid
+        member
+        kick
+        leaving
+        wasMember
+        pubkey
+        uid
+        sig
+        revocation_sig
+        hash
+        written
+        revoked_on
+        expires_on
+        certs {
+          linked
+          written
+          written_block
+          written_hash
+          sig
+          block_number
+          block_hash
+          target
+          to
+          from
+          block
+          expired
+          expires_on
+        }
+        memberships {
+          membership
+          issuer
+          number
+          blockNumber
+          blockHash
+          userid
+          certts
+          block
+          fpr
+          idtyHash
+          written
+          written_number
+          expires_on
+          signature
+          expired
+          block_number
+        }
+      }
+    }`) as any).data.pendingIdentities
+  }
+
+  async sourcesOfPubkey(pub: string): Promise<{
+    type: string
+    noffset: number
+    identifier: string
+    amount: number
+    base: number
+    conditions: string
+    consumed: boolean
+  }[]> {
+    return (await this.graphQLPost(`{
+      sourcesOfPubkey(pub: "${pub}") {
+        type
+        noffset
+        identifier
+        amount
+        base
+        conditions
+        consumed
+      }
+    }`) as any).data.sourcesOfPubkey
+  }
+}
\ No newline at end of file
diff --git a/tests/test-user.ts b/tests/test-user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6534c4858ff4b5af3536a6676b2d5728fedccc83
--- /dev/null
+++ b/tests/test-user.ts
@@ -0,0 +1,213 @@
+import {Buid} from "duniter/app/lib/common-libs/buid";
+import {CommonConstants} from "duniter/app/lib/common-libs/constants";
+import {IdentityDTO} from "duniter/app/lib/dto/IdentityDTO";
+import {KeyGen} from "duniter/app/lib/common-libs/crypto/keyring";
+import {TestGvaClient} from "./test-gva-client";
+import {CertificationDTO} from "duniter/app/lib/dto/CertificationDTO";
+import {MembershipDTO} from "duniter/app/lib/dto/MembershipDTO";
+
+export class GvaTestUser {
+
+  protected createdIdentity: IdentityDTO
+
+  constructor(
+    public readonly uid: string,
+    public readonly pub: string,
+    public readonly sec: string,
+    public readonly gva: TestGvaClient) {}
+
+  public async createIdentity(useRoot?:boolean|null) {
+    const { current, currency } = await this.getCurrentAndCurrency()
+    const buid = !useRoot && current ? Buid.format.buid(current.number, current.hash) : CommonConstants.SPECIAL_BLOCK
+    this.createdIdentity = IdentityDTO.fromJSONObject({
+      buid,
+      uid: this.uid,
+      issuer: this.pub,
+      currency
+    })
+    const raw = this.createdIdentity.getRawUnSigned()
+    this.createdIdentity.sig = KeyGen(this.pub, this.sec).signSync(raw)
+  }
+
+  private async getCurrentAndCurrency() {
+    return {
+      current: await this.gva.current(),
+      currency: await this.gva.currency(),
+    }
+  }
+
+  async createIdentityAndSubmit() {
+    await this.createIdentity()
+    await this.gva.submitIdentity(this.createdIdentity.getRawSigned())
+  }
+
+  public async createCert(user: GvaTestUser) {
+    const { current, currency } = await this.getCurrentAndCurrency()
+    const idty = (await this.gva.pendingIdentities(user.pub))[0]
+    let buid = current ? Buid.format.buid(current.number, current.hash) : CommonConstants.SPECIAL_BLOCK
+    const cert = {
+      "version": CommonConstants.DOCUMENTS_VERSION,
+      "currency": currency,
+      "issuer": this.pub,
+      "idty_issuer": user.pub,
+      "idty_uid": idty.uid,
+      "idty_buid": idty.buid,
+      "idty_sig": idty.sig,
+      "buid": buid,
+      "sig": ""
+    }
+    const rawCert = CertificationDTO.fromJSONObject(cert).getRawUnSigned()
+    cert.sig = KeyGen(this.pub, this.sec).signSync(rawCert)
+    return CertificationDTO.fromJSONObject(cert)
+  }
+
+  public async createCertAndSubmit(user: GvaTestUser) {
+    const cert = await this.createCert(user)
+    await this.gva.submitCertification(cert.getRawSigned())
+  }
+
+  public async makeMembership(type:string) {
+    const { current, currency } = await this.getCurrentAndCurrency()
+    const idty = (await this.gva.pendingIdentities(this.pub))[0]
+    const block = Buid.format.buid(current);
+    const join = {
+      "version": CommonConstants.DOCUMENTS_VERSION,
+      "currency": currency,
+      "issuer": this.pub,
+      "block": block,
+      "membership": type,
+      "userid": this.uid,
+      "certts": idty.buid,
+      "signature": ""
+    };
+    const rawJoin = MembershipDTO.fromJSONObject(join).getRaw()
+    join.signature = KeyGen(this.pub, this.sec).signSync(rawJoin)
+    return MembershipDTO.fromJSONObject(join)
+  }
+
+  async createJoinAndSubmit() {
+    const join = await this.makeMembership('IN')
+    await this.gva.submitMembership(join.getRawSigned())
+  }
+
+  async balance(): Promise<number> {
+    const sources = await this.gva.sourcesOfPubkey(this.pub)
+    return sources.reduce((sum, src) => sum + src.amount * Math.pow(10, src.base), 0)
+  }
+
+
+
+  public async prepareITX(amount:number, recipient:GvaTestUser|string, comment?:string) {
+    let sources = []
+    if (!amount || !recipient) {
+      throw 'Amount and recipient are required'
+    }
+    const { current, currency } = await this.getCurrentAndCurrency()
+    const version = current && Math.min(CommonConstants.LAST_VERSION_FOR_TX, current.version)
+    const json = await this.gva.sourcesOfPubkey(this.pub)
+    let i = 0
+    let cumulated = 0
+    let commonbase = 99999999
+    while (i < json.length) {
+      const src = json[i]
+      sources.push({
+        'type': src.type,
+        'amount': src.amount,
+        'base': src.base,
+        'noffset': src.noffset,
+        'identifier': src.identifier
+      })
+      commonbase = Math.min(commonbase, src.base);
+      cumulated += src.amount * Math.pow(10, src.base);
+      i++;
+    }
+    if (cumulated < amount) {
+      throw 'You do not have enough coins! (' + cumulated + ' ' + currency + ' left)';
+    }
+    let sources2 = [];
+    let total = 0;
+    for (let j = 0; j < sources.length && total < amount; j++) {
+      let src = sources[j];
+      total += src.amount * Math.pow(10, src.base);
+      sources2.push(src);
+    }
+    let inputSum = 0;
+    sources2.forEach((src) => inputSum += src.amount * Math.pow(10, src.base));
+    let inputs = sources2.map((src) => {
+      return {
+        src: ([src.amount, src.base] as any[]).concat([src.type, src.identifier, src.noffset]).join(':'),
+        unlock: 'SIG(0)'
+      };
+    });
+    let outputs = [{
+      qty: amount,
+      base: commonbase,
+      lock: 'SIG(' + (typeof recipient === 'string' ? recipient : recipient.pub) + ')'
+    }];
+    if (inputSum - amount > 0) {
+      // Rest back to issuer
+      outputs.push({
+        qty: inputSum - amount,
+        base: commonbase,
+        lock: "SIG(" + this.pub + ")"
+      });
+    }
+    let raw = this.prepareTX(inputs, outputs, {
+      version: version,
+      blockstamp: current && [current.number, current.hash].join('-'),
+      comment: comment
+    }, currency)
+    return this.signed(raw)
+  }
+
+  public prepareTX(inputs:TestInput[], outputs:TestOutput[], theOptions:any, currency: string) {
+    let opts = theOptions || {};
+    let issuers = opts.issuers || [this.pub];
+    let raw = '';
+    raw += "Version: " + (opts.version || CommonConstants.TRANSACTION_VERSION) + '\n';
+    raw += "Type: Transaction\n";
+    raw += "Currency: " + (opts.currency || currency) + '\n';
+    raw += "Blockstamp: " + opts.blockstamp + '\n';
+    raw += "Locktime: " + (opts.locktime || 0) + '\n';
+    raw += "Issuers:\n";
+    issuers.forEach((issuer:string) => raw += issuer + '\n');
+    raw += "Inputs:\n";
+    inputs.forEach(function (input) {
+      raw += input.src + '\n';
+    });
+    raw += "Unlocks:\n";
+    inputs.forEach(function (input, index) {
+      if (input.unlock) {
+        raw += index + ":" + input.unlock + '\n';
+      }
+    });
+    raw += "Outputs:\n";
+    outputs.forEach(function (output) {
+      raw += [output.qty, output.base, output.lock].join(':') + '\n';
+    });
+    raw += "Comment: " + (opts.comment || "") + "\n";
+    return raw;
+  }
+
+  private signed(raw:string) {
+    let signatures = [KeyGen(this.pub, this.sec).signSync(raw)]
+    return raw + signatures.join('\n') + '\n'
+  }
+
+  async createTxAndSubmit(recipient: GvaTestUser, amount: number, comment = '') {
+    const raw = await this.prepareITX(amount, recipient, comment)
+    await this.gva.submitTransaction(raw)
+  }
+}
+
+
+export interface TestInput {
+  src:string
+  unlock:string
+}
+
+export interface TestOutput {
+  qty:number
+  base:number
+  lock:string
+}
diff --git a/tsconfig.json b/tsconfig.json
index 7892102dfbfbaabc308e6f19beeacfeaf32b92cf..1bb40cad23588e3d5ac4af34c7a66664061a4c1d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,7 +13,8 @@
     "lib": ["es6", "dom", "esnext"]
   },
   "include": [
-    "*.ts"
+    "*.ts",
+    "tests/"
   ],
   "compileOnSave": true
 }
diff --git a/yarn.lock b/yarn.lock
index c8cc8459a66f58071a8765077c1cf1271225a323..6125b5be099af214ef974395942976ebdd70a8c5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6,6 +6,10 @@
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz#3c7750d0186b954c7f2d2f6acc8c3c7ba0c3412e"
 
+"@types/bluebird@*":
+  version "3.5.24"
+  resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.24.tgz#11f76812531c14f793b8ecbf1de96f672905de8a"
+
 "@types/body-parser@*":
   version "1.17.0"
   resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c"
@@ -13,6 +17,10 @@
     "@types/connect" "*"
     "@types/node" "*"
 
+"@types/caseless@*":
+  version "0.12.1"
+  resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.1.tgz#9794c69c8385d0192acc471a540d1f8e0d16218a"
+
 "@types/connect@*":
   version "3.4.32"
   resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
@@ -46,6 +54,12 @@
     "@types/express-serve-static-core" "*"
     "@types/serve-static" "*"
 
+"@types/form-data@*":
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e"
+  dependencies:
+    "@types/node" "*"
+
 "@types/fs-extra@5.0.1":
   version "5.0.1"
   resolved "http://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.1.tgz#cd856fbbdd6af2c11f26f8928fd8644c9e9616c9"
@@ -109,14 +123,38 @@
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
 
+"@types/mocha@^5.2.5":
+  version "5.2.5"
+  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073"
+
 "@types/node@*":
   version "10.12.2"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.2.tgz#d77f9faa027cadad9c912cd47f4f8b07b0fb0864"
 
+"@types/node@^10.12.3":
+  version "10.12.3"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.3.tgz#3918b73ceed484e58367be5acb79d1775239e393"
+
 "@types/range-parser@*":
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d"
 
+"@types/request-promise@^4.1.42":
+  version "4.1.42"
+  resolved "https://registry.yarnpkg.com/@types/request-promise/-/request-promise-4.1.42.tgz#a70a6777429531e60ed09faa077ead9b995204cd"
+  dependencies:
+    "@types/bluebird" "*"
+    "@types/request" "*"
+
+"@types/request@*":
+  version "2.48.1"
+  resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.1.tgz#e402d691aa6670fbbff1957b15f1270230ab42fa"
+  dependencies:
+    "@types/caseless" "*"
+    "@types/form-data" "*"
+    "@types/node" "*"
+    "@types/tough-cookie" "*"
+
 "@types/serve-static@*":
   version "1.13.2"
   resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48"
@@ -131,6 +169,16 @@
     "@types/glob" "*"
     "@types/node" "*"
 
+"@types/should@^13.0.0":
+  version "13.0.0"
+  resolved "https://registry.yarnpkg.com/@types/should/-/should-13.0.0.tgz#96c00117f1896177848fdecfa336313c230c879e"
+  dependencies:
+    should "*"
+
+"@types/tough-cookie@*":
+  version "2.3.4"
+  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.4.tgz#821878b81bfab971b93a265a561d54ea61f9059f"
+
 "@types/ws@^5.1.2":
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/@types/ws/-/ws-5.1.2.tgz#f02d3b1cd46db7686734f3ce83bdf46c49decd64"
@@ -276,6 +324,10 @@ array-flatten@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
 
+arrify@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
 asn1@~0.2.3:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@@ -290,6 +342,12 @@ assert-plus@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
 
+assert@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+  dependencies:
+    util "0.10.3"
+
 async@2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/async/-/async-2.2.0.tgz#c324eba010a237e4fbd55a12dee86367d5c0ef32"
@@ -433,6 +491,10 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
+browser-stdout@1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+
 bs58@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
@@ -458,6 +520,10 @@ buffer-fill@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
 
+buffer-from@^1.0.0, buffer-from@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+
 buffer-shims@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
@@ -584,6 +650,10 @@ combined-stream@^1.0.5, combined-stream@^1.0.6, combined-stream@~1.0.5, combined
   dependencies:
     delayed-stream "~1.0.0"
 
+commander@2.15.1:
+  version "2.15.1"
+  resolved "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+
 commander@2.9.0:
   version "2.9.0"
   resolved "http://registry.npmjs.org/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
@@ -706,6 +776,12 @@ debug@2.6.9, debug@^2.2.0:
   dependencies:
     ms "2.0.0"
 
+debug@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  dependencies:
+    ms "2.0.0"
+
 debug@~2.2.0:
   version "2.2.0"
   resolved "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
@@ -764,6 +840,10 @@ dicer@0.2.5:
     readable-stream "1.1.x"
     streamsearch "0.1.2"
 
+diff@3.5.0, diff@^3.1.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+
 "duniter@file:../duniter":
   version "1.7.1"
   dependencies:
@@ -821,6 +901,63 @@ dicer@0.2.5:
     wotb "^0.6.5"
     ws "1.1.5"
 
+"duniter@file:///home/cgeek/dev/duniter":
+  version "1.7.1"
+  dependencies:
+    "@types/leveldown" "^4.0.0"
+    "@types/levelup" "^3.1.0"
+    "@types/memdown" "^3.0.0"
+    "@types/ws" "^5.1.2"
+    archiver "1.3.0"
+    async "2.2.0"
+    bindings "1.2.1"
+    body-parser "1.17.1"
+    bs58 "^4.0.1"
+    cli-table "^0.3.1"
+    colors "1.1.2"
+    commander "2.9.0"
+    cors "2.8.2"
+    daemonize2 "0.4.2"
+    ddos "0.1.16"
+    errorhandler "1.5.0"
+    event-stream "3.3.4"
+    express "4.15.2"
+    express-fileupload "0.0.5"
+    inquirer "3.0.6"
+    jison "0.4.17"
+    js-yaml "3.8.2"
+    leveldown "^4.0.1"
+    levelup "^3.1.1"
+    lokijs "^1.5.3"
+    memdown "^3.0.0"
+    merkle "0.5.1"
+    moment "2.19.3"
+    morgan "1.8.1"
+    multimeter "0.1.1"
+    naclb "1.3.10"
+    nat-upnp "^1.1.1"
+    node-pre-gyp "0.6.34"
+    node-uuid "1.4.8"
+    optimist "0.6.1"
+    q-io "^1.13.5"
+    querablep "^0.1.0"
+    request "2.81.0"
+    request-promise "4.2.0"
+    scryptb "6.0.5"
+    seedrandom "^2.4.3"
+    sha1 "1.1.1"
+    socks-proxy-agent "^3.0.1"
+    sqlite3 "3.1.13"
+    tail "^1.2.1"
+    tweetnacl "0.14.3"
+    typedoc "^0.11.1"
+    underscore "1.8.3"
+    unzip "0.1.11"
+    unzip2 "0.2.5"
+    winston "2.3.1"
+    wotb "^0.6.5"
+    ws "1.1.5"
+
 duplexer@~0.1.1:
   version "0.1.1"
   resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
@@ -910,7 +1047,7 @@ escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
 
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 
@@ -1241,6 +1378,17 @@ github-from-package@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
 
+glob@7.1.2:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 glob@^7.0.0, glob@^7.0.5:
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
@@ -1282,6 +1430,10 @@ graphql@^14.0.2:
   dependencies:
     iterall "^1.2.2"
 
+growl@1.10.5:
+  version "1.10.5"
+  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
+
 handlebars@^4.0.6:
   version "4.0.12"
   resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5"
@@ -1324,6 +1476,10 @@ has-color@~0.1.0:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
 
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+
 has-unicode@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -1343,6 +1499,10 @@ hawk@3.1.3, hawk@~3.1.3:
     hoek "2.x.x"
     sntp "1.x.x"
 
+he@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+
 highlight.js@^9.0.0:
   version "9.13.1"
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
@@ -1421,6 +1581,10 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, i
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
 
+inherits@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
 ini@~1.3.0:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
@@ -1641,6 +1805,10 @@ ltgt@~2.2.0:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
 
+make-error@^1.1.1:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
+
 map-stream@~0.1.0:
   version "0.1.0"
   resolved "http://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
@@ -1724,7 +1892,7 @@ mimic-response@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
 
-minimatch@^3.0.0, minimatch@^3.0.4:
+minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   dependencies:
@@ -1742,12 +1910,28 @@ minimist@~0.0.1:
   version "0.0.10"
   resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
 
-mkdirp@0.5, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5, mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
     minimist "0.0.8"
 
+mocha@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6"
+  dependencies:
+    browser-stdout "1.3.1"
+    commander "2.15.1"
+    debug "3.1.0"
+    diff "3.5.0"
+    escape-string-regexp "1.0.5"
+    glob "7.1.2"
+    growl "1.10.5"
+    he "1.1.1"
+    minimatch "3.0.4"
+    mkdirp "0.5.1"
+    supports-color "5.4.0"
+
 moment@2.19.3:
   version "2.19.3"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.3.tgz#bdb99d270d6d7fda78cc0fbace855e27fe7da69f"
@@ -2272,6 +2456,15 @@ request-promise@4.2.0:
     request-promise-core "1.1.1"
     stealthy-require "^1.0.0"
 
+request-promise@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.2.tgz#d1ea46d654a6ee4f8ee6a4fea1018c22911904b4"
+  dependencies:
+    bluebird "^3.5.0"
+    request-promise-core "1.1.1"
+    stealthy-require "^1.1.0"
+    tough-cookie ">=2.3.3"
+
 request@2.81.0:
   version "2.81.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
@@ -2299,7 +2492,7 @@ request@2.81.0:
     tunnel-agent "^0.6.0"
     uuid "^3.0.0"
 
-request@2.x, request@^2.79.0, request@^2.81.0:
+request@2.x, request@^2.79.0, request@^2.81.0, request@^2.88.0:
   version "2.88.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
   dependencies:
@@ -2489,6 +2682,44 @@ shelljs@^0.8.1:
     interpret "^1.0.0"
     rechoir "^0.6.2"
 
+should-equal@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
+  dependencies:
+    should-type "^1.4.0"
+
+should-format@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1"
+  dependencies:
+    should-type "^1.3.0"
+    should-type-adaptors "^1.0.1"
+
+should-type-adaptors@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a"
+  dependencies:
+    should-type "^1.3.0"
+    should-util "^1.0.0"
+
+should-type@^1.3.0, should-type@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3"
+
+should-util@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063"
+
+should@*, should@^13.2.3:
+  version "13.2.3"
+  resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10"
+  dependencies:
+    should-equal "^2.0.0"
+    should-format "^3.0.3"
+    should-type "^1.4.0"
+    should-type-adaptors "^1.0.1"
+    should-util "^1.0.0"
+
 signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -2535,7 +2766,14 @@ socks@^1.1.10:
     ip "^1.1.4"
     smart-buffer "^1.0.13"
 
-source-map@^0.6.1, source-map@~0.6.1:
+source-map-support@^0.5.6:
+  version "0.5.9"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
 
@@ -2592,7 +2830,7 @@ statuses@~1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
 
-stealthy-require@^1.0.0:
+stealthy-require@^1.0.0, stealthy-require@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
 
@@ -2659,6 +2897,12 @@ strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 
+supports-color@5.4.0:
+  version "5.4.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
+  dependencies:
+    has-flag "^3.0.0"
+
 supports-color@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -2757,19 +3001,19 @@ toidentifier@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
 
-tough-cookie@~2.3.0:
-  version "2.3.4"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
-  dependencies:
-    punycode "^1.4.1"
-
-tough-cookie@~2.4.3:
+tough-cookie@>=2.3.3, tough-cookie@~2.4.3:
   version "2.4.3"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
   dependencies:
     psl "^1.1.24"
     punycode "^1.4.1"
 
+tough-cookie@~2.3.0:
+  version "2.3.4"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
+  dependencies:
+    punycode "^1.4.1"
+
 traverse@>=0.2.4:
   version "0.6.6"
   resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
@@ -2778,6 +3022,19 @@ traverse@>=0.2.4:
   version "0.3.9"
   resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
 
+ts-node@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf"
+  dependencies:
+    arrify "^1.0.0"
+    buffer-from "^1.1.0"
+    diff "^3.1.0"
+    make-error "^1.1.1"
+    minimist "^1.2.0"
+    mkdirp "^0.5.1"
+    source-map-support "^0.5.6"
+    yn "^2.0.0"
+
 tunnel-agent@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -2904,6 +3161,12 @@ util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
 
+util@0.10.3:
+  version "0.10.3"
+  resolved "http://registry.npmjs.org/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+  dependencies:
+    inherits "2.0.1"
+
 utils-merge@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
@@ -2986,6 +3249,10 @@ xtend@^4.0.0, xtend@~4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
 
+yn@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"
+
 zen-observable-ts@^0.8.10:
   version "0.8.10"
   resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.10.tgz#18e2ce1c89fe026e9621fd83cc05168228fce829"