Commit 03d8d582 authored by Cédric Moreau's avatar Cédric Moreau
Browse files

[enh] First implementation of GVA

parent f468d58e
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
}
......
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
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`))
}
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!]!
}
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)
})