Skip to content

Commit

Permalink
Initialize WM server
Browse files Browse the repository at this point in the history
  • Loading branch information
raducristianpopa committed Jan 26, 2024
1 parent af47068 commit 50cd9d6
Show file tree
Hide file tree
Showing 12 changed files with 445 additions and 109 deletions.
2 changes: 2 additions & 0 deletions packages/openapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"name": "@interledger/wm-openapi",
"homepage": "https://github.com/interledger/web-monetization-extension/tree/main/packages/openapi",
"private": true,
"main": "src/index.ts",
"types": "src/index.ts",
"repository": {
"type": "git",
"url": "[email protected]:interledeger/web-monetization-extension.git",
Expand Down
1 change: 1 addition & 0 deletions packages/openapi/schemas/web-monetization-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ info:
paths:
/:
post:
operationId: connect-wallet
security:
- bearer: []
summary: Connect a wallet
Expand Down
58 changes: 31 additions & 27 deletions packages/openapi/src/generated/wm-server-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,7 @@
export interface paths {
"/": {
/** Connect a wallet */
post: {
requestBody: {
content: {
"application/json": {
/** Format: uri */
walletAddressUrl: string;
amount: number;
recurring: boolean;
};
};
};
responses: {
/** @description OK */
200: {
content: {
"application/json": {
/** Format: uri */
interactionUrl: string;
/** Format: uri */
continueUrl: string;
continueToken: string;
};
};
};
};
};
post: operations["connect-wallet"];
};
"/incoming-payment": {
/** Create incoming payment */
Expand Down Expand Up @@ -103,4 +78,33 @@ export type $defs = Record<string, never>;

export type external = Record<string, never>;

export type operations = Record<string, never>;
export interface operations {

/** Connect a wallet */
"connect-wallet": {
requestBody: {
content: {
"application/json": {
/** Format: uri */
walletAddressUrl: string;
amount: number;
recurring: boolean;
};
};
};
responses: {
/** @description OK */
200: {
content: {
"application/json": {
/** Format: uri */
interactionUrl: string;
/** Format: uri */
continueUrl: string;
continueToken: string;
};
};
};
};
};
}
1 change: 1 addition & 0 deletions packages/openapi/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./generated/wm-server-types";
4 changes: 4 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@
},
"dependencies": {
"@interledger/http-signature-utils": "^2.0.0",
"@interledger/open-payments": "6.1.0",
"@koa/router": "^12.0.1",
"koa": "^2.14.2",
"koa-bodyparser": "^4.4.1"
},
"devDependencies": {
"@interledger/wm-openapi": "workspace:^",
"@types/koa": "^2.14.0",
"@types/koa-bodyparser": "^4.3.12",
"@types/koa__router": "^12.0.4",
"tsx": "^4.7.0"
}
}
53 changes: 53 additions & 0 deletions packages/server/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { type AuthenticatedClient } from '@interledger/open-payments';
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import { Server } from 'node:http';

import { type Config } from './config';
import { type Context, Router } from './context';
import { registerPublicRoutes } from './routes/public';

export class Application {
#config: Config;
#koa: Koa<Koa.DefaultState, Context>;
#router: Router;
#server?: Server;
#client: AuthenticatedClient;

constructor(config: Config, client: AuthenticatedClient) {
this.#config = config;
this.#client = client;
this.#koa = new Koa();
this.#router = new Router();

this.#decorateContext();
this.#koa.use(bodyParser());

registerPublicRoutes(this.#router);

this.#koa.use(this.#router.routes());
this.#koa.use(this.#router.allowedMethods());
}

#decorateContext(): void {
this.#koa.context.client = this.#client;
}

async start(): Promise<void> {
await new Promise<void>(res => {
this.#server = this.#koa.listen(this.#config.PORT, res);
});
}

async stop(): Promise<void> {
await new Promise<void>((resolve, reject) => {
this.#server?.close(err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
}
21 changes: 21 additions & 0 deletions packages/server/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createAuthenticatedClient } from '@interledger/open-payments';

import { Application } from './app';
import { config } from './config';

async function test() {
console.log('creating op client');
return await createAuthenticatedClient({
walletAddressUrl: config.WALLET_ADDRESS,
privateKey: Buffer.from(config.PRIVATE_KEY, 'base64'),
keyId: config.KEY_ID,
});
}

export async function bootstrap() {
const client = await test();

const app = new Application(config, client);
await app.start();
console.log(`WM Server listening on port ${config.PORT}`);
}
16 changes: 16 additions & 0 deletions packages/server/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function getEnvVariable(name: string, defaultValue?: any): string {
if (!process.env[name]) {
return defaultValue;
}

return process.env[name]!;
}

export type Config = typeof config;

export const config = {
PORT: parseInt(getEnvVariable('PORT', 3000), 10),
WALLET_ADDRESS: getEnvVariable('WALLET_ADDRESS'),
PRIVATE_KEY: getEnvVariable('PRIVATE_KEY'),
KEY_ID: getEnvVariable('KEY_ID'),
} as const;
24 changes: 24 additions & 0 deletions packages/server/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type AuthenticatedClient } from '@interledger/open-payments';
import { operations as Operations } from '@interledger/wm-openapi';
import KoaRouter from '@koa/router';
import Koa from 'koa';
import { type ParsedUrlQuery } from 'node:querystring';

export interface ContextExtension<TResponseBody = unknown>
extends Koa.ParameterizedContext<Koa.DefaultState, Koa.DefaultContext, TResponseBody> {
client: AuthenticatedClient;
}

interface Request<TBody = never, TQuery = ParsedUrlQuery>
extends Omit<ContextExtension['request'], 'body'> {
body: TBody;
query: ParsedUrlQuery & TQuery;
}

export interface ConnectWalletContext extends ContextExtension {
request: Request<Operations['connect-wallet']['requestBody']['content']['application/json']>;
}

export type Context = Koa.ParameterizedContext<Koa.DefaultState, ContextExtension>;

export class Router extends KoaRouter<Koa.DefaultState, ContextExtension> {}
82 changes: 2 additions & 80 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,3 @@
import {
createHeaders,
Headers,
loadBase64Key,
RequestLike,
} from '@interledger/http-signature-utils';
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import { bootstrap } from './bootstrap';

interface Context<TResponseBody = unknown>
extends Koa.ParameterizedContext<Koa.DefaultState, Koa.DefaultContext, TResponseBody> {}

interface GenerateSignatureRequestBody extends RequestLike {}

function validateBody(body: any): body is GenerateSignatureRequestBody {
return !!body.headers && !!body.method && !!body.url;
}

async function validatePath(ctx: Context, next: Koa.Next): Promise<void> {
if (ctx.path !== '/') {
ctx.status = 403;
} else {
await next();
}
}

async function validateMethod(ctx: Context, next: Koa.Next): Promise<void> {
if (ctx.method !== 'POST') {
ctx.status = 405;
} else {
await next();
}
}

async function createHeadersHandler(ctx: Context<Headers>): Promise<void> {
const { body } = ctx.request;

if (!validateBody(body)) {
ctx.throw('Invalid request body', 400);
}

let privateKey: ReturnType<typeof loadBase64Key>;

try {
privateKey = loadBase64Key(BASE64_PRIVATE_KEY);
} catch {
ctx.throw('Not a valid private key', 400);
}

if (privateKey === undefined) {
ctx.throw('Not an Ed25519 private key', 400);
}

const headers = await createHeaders({
request: body,
privateKey,
keyId: KEY_ID,
});

delete headers['Content-Length'];
delete headers['Content-Type'];

ctx.body = headers;
}

const PORT = 3000;
const BASE64_PRIVATE_KEY =
'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSUUvVlJTRVUzYS9CTUE2cmhUQnZmKzcxMG10YWlmbkF6SzFsWGpDK0QrSTkKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQ==';
const KEY_ID = 'f0ac2190-54d5-47c8-b061-221e7068d823';

const app = new Koa<Koa.DefaultState, Context>();

app.use(bodyParser());
app.use(validatePath);
app.use(validateMethod);
app.use(createHeadersHandler);

app.listen(3000, () => {
// eslint-disable-next-line no-console
console.log(`Local signatures server started on port ${PORT}`);
});
bootstrap();
13 changes: 13 additions & 0 deletions packages/server/src/routes/public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { DefaultState } from 'koa';

import type { ConnectWalletContext, Router } from '../context';

export function registerPublicRoutes(router: Router) {
router.get<DefaultState, ConnectWalletContext>('/', async ctx => {
const wallet = await ctx.client.walletAddress.get({
url: 'https://ilp.rafiki.money/radu',
});

ctx.body = wallet;
});
}
Loading

0 comments on commit 50cd9d6

Please sign in to comment.