Skip to content

Commit

Permalink
api: port to new nx workspace (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
0x0ece authored May 9, 2024
1 parent 56d7647 commit fdcb29d
Show file tree
Hide file tree
Showing 21 changed files with 11,638 additions and 11,740 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/pr_test_api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies and run tests
- name: Install dependencies
run: pnpm install

- name: Run tests
shell: bash
run: |
cd api_v0/ && pnpm install && pnpm run test
pnpm api-test
20 changes: 14 additions & 6 deletions anchor/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {

import { Glam, GlamIDL, GlamProgram, getGlamProgramId } from "./glamExports";
import { GlamClientConfig } from "./clientConfig";
import { FundModel } from "./models";
import { FundModel, FundOpenfundsModel } from "./models";

type FundAccount = IdlAccounts<Glam>["fundAccount"];
type FundMetadataAccount = IdlAccounts<Glam>["fundMetadataAccount"];
Expand Down Expand Up @@ -105,7 +105,7 @@ export class GlamClient {
getFundName(fundModel: FundModel) {
return (
fundModel.name ||
fundModel.rawOpenfunds.legalFundNameIncludingUmbrella ||
fundModel.rawOpenfunds?.legalFundNameIncludingUmbrella ||
fundModel.shareClasses[0]?.name ||
""
);
Expand All @@ -125,14 +125,19 @@ export class GlamClient {
manager: null
};

if (!fundModel.rawOpenfunds) {
fundModel.rawOpenfunds = new FundOpenfundsModel({}) as FundOpenfundsModel;
}

if (fundModel.shareClasses?.length == 1) {
// fund with a single share class
const shareClass = fundModel.shareClasses[0];
fundModel.name = fundModel.name || shareClass.name;

fundModel.rawOpenfunds.fundCurrency =
fundModel.rawOpenfunds.fundCurrency ||
shareClass.rawOpenfunds.shareClassCurrency;
fundModel.rawOpenfunds?.fundCurrency ||
shareClass.rawOpenfunds?.shareClassCurrency ||
null;
} else {
// fund with multiple share classes
// TODO
Expand All @@ -142,7 +147,7 @@ export class GlamClient {

if (fundModel.isEnabled) {
fundModel.rawOpenfunds.fundLaunchDate =
fundModel.rawOpenfunds.fundLaunchDate ||
fundModel.rawOpenfunds?.fundLaunchDate ||
new Date().toISOString().split("T")[0];
}

Expand All @@ -156,7 +161,10 @@ export class GlamClient {

// share classes
fundModel.shareClasses.forEach((shareClass, i) => {
if (shareClass.rawOpenfunds.shareClassLifecycle == "active") {
if (
shareClass.rawOpenfunds &&
shareClass.rawOpenfunds.shareClassLifecycle === "active"
) {
shareClass.rawOpenfunds.shareClassLaunchDate =
shareClass.rawOpenfunds.shareClassLaunchDate ||
new Date().toISOString().split("T")[0];
Expand Down
6 changes: 3 additions & 3 deletions anchor/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "./glamExports";
export * from "./offchain";
// export * from "./models";
// export * from "./clientConfig";
// export * from "./client";
export * from "./models";
export * from "./clientConfig";
export * from "./client";
24 changes: 14 additions & 10 deletions anchor/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,22 @@ export const FundModel = class<FundModel> {
assetsWeights: obj.assetsWeights || [],
shareClasses: obj.shareClasses
? obj.shareClasses.map(
(shareClass) => new ShareClassModel(shareClass) as ShareClassModel
(shareClass: any) =>
new ShareClassModel(shareClass) as ShareClassModel
)
: [],
company: obj.company
? (new CompanyModel(obj.company) as CompanyModel)
: null,
manager: obj.manager
? (new ManagerModel(obj.manager) as ManagerModel)
: null,
rawOpenfunds: obj.fundDomicileAlpha2
? (new FundOpenfundsModel(obj) as FundOpenfundsModel)
: null
// company: obj.company
// ? (new CompanyModel(obj.company) as CompanyModel)
// : null,
// manager: obj.manager
// ? (new ManagerModel(obj.manager) as ManagerModel)
// : null,
// rawOpenfunds: obj.fundDomicileAlpha2
// ? (new FundOpenfundsModel(obj) as FundOpenfundsModel)
// : null
company: new CompanyModel(obj.company || {}),
manager: new ManagerModel(obj.manager || {}),
rawOpenfunds: new FundOpenfundsModel(obj)
};
return result;
}
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions api/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
runtime: nodejs20
env_variables:
SOLANA_RPC: "https://api.devnet.solana.com"
File renamed without changes
File renamed without changes.
180 changes: 178 additions & 2 deletions api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,193 @@
*/

import express, { Express, Request, Response } from "express";
import * as cors from "cors";
import * as path from "path";
import { Connection, PublicKey } from "@solana/web3.js";
import { AnchorProvider } from "@coral-xyz/anchor";

import { createCanvas, loadImage } from "canvas";
import {
PythHttpClient,
PythCluster,
getPythClusterApiUrl,
getPythProgramKeyForCluster
} from "@pythnetwork/client";

import { GlamClient } from "@glam/anchor";
import { validatePubkey } from "./validation";
import { priceHistory, fundPerformance } from "./prices";
import { openfunds } from "./openfunds";

const BASE_URL = "https://api.glam.systems";
const SOLANA_RPC = process.env.SOLANA_RPC || "http://localhost:8899";

const app: Express = express();

const connection = new Connection(SOLANA_RPC, "confirmed");
const provider = new AnchorProvider(connection, null, {
commitment: "confirmed"
});
const client = new GlamClient({ provider });

const PYTHNET_CLUSTER_NAME: PythCluster = "pythnet";
const pythClient = new PythHttpClient(
new Connection(getPythClusterApiUrl(PYTHNET_CLUSTER_NAME)),
getPythProgramKeyForCluster(PYTHNET_CLUSTER_NAME),
"confirmed"
);

app.use("/assets", express.static(path.join(__dirname, "assets")));

app.get("/api", (req: Request, res: Response) => {
res.send({ message: "Welcome to api!" });
res.send({ message: "Welcome to Glam!" });
});

app.get("/openfunds", async (req, res) => {
return openfunds(
req.query.funds.split(","),
req.query.template,
req.query.format,
client,
res
);
});

app.get("/openfunds/:pubkey.:ext", async (req, res) => {
return openfunds([req.params.pubkey], "auto", req.params.ext, client, res);
});

app.get("/openfunds/:pubkey", async (req, res) => {
return openfunds([req.params.pubkey], "auto", "json", client, res);
});

app.get("/prices", async (req, res) => {
const data = await pythClient.getData();
res.set("content-type", "application/json");
res.send(
JSON.stringify({
btc: data.productPrice.get("Crypto.BTC/USD").price,
eth: data.productPrice.get("Crypto.ETH/USD").price,
sol: data.productPrice.get("Crypto.SOL/USD").price,
usdc: data.productPrice.get("Crypto.USDC/USD").price
})
);
});

app.get("/fund/:pubkey/perf", async (req, res) => {
const { w_btc = 0.4, w_eth = 0, w_sol = 0.6 } = req.query;
// TODO: validate input
// TODO: Should we fetch weights from blockchain, or let client side pass them in?
// Client side should have all fund info including assets and weights
console.log(`btcWeight: ${w_btc}, ethWeight: ${w_eth}, solWeight: ${w_sol}`);
const { timestamps, closingPrices: ethClosingPrices } = await priceHistory(
"Crypto.ETH/USD"
);
const { closingPrices: btcClosingPrices } = await priceHistory(
"Crypto.BTC/USD"
);
const { closingPrices: solClosingPrices } = await priceHistory(
"Crypto.SOL/USD"
);
// const { closingPrices: usdcClosingPrices } = await priceHistory(
// "Crypto.USDC/USD"
// );
const { weightedChanges, btcChanges, ethChanges, solChanges } =
fundPerformance(
w_btc,
btcClosingPrices,
w_eth,
ethClosingPrices,
w_sol,
solClosingPrices
);
res.set("content-type", "application/json");
res.send(
JSON.stringify({
timestamps,
// usdcClosingPrices,
fundPerformance: weightedChanges,
btcPerformance: btcChanges,
ethPerformance: ethChanges,
solPerformance: solChanges
})
);
});

app.get("/metadata/:pubkey", async (req, res) => {
const key = validatePubkey(req.params.pubkey);
if (!key) {
return res.sendStatus(404);
}

// TODO: Fetch name and symbol from blockchain

const imageUri = `${BASE_URL}/image/${req.params.pubkey}.png`;
res.set("content-type", "application/json");
res.send(
JSON.stringify({
name: "name_placeholder",
symbol: "symbol_placeholder",
description: "",
external_url: "https://glam.systems",
image: imageUri
})
);
});

app.get("/image/:pubkey.png", async (req, res) => {
// convert pubkey from base58 to bytes[32]
const key = validatePubkey(req.params.pubkey);
if (!key) {
return res.sendStatus(404);
}

// fetch params from the key bytes
const angle = 6.28 * (key[0] / 256.0); // between 0.0 and 2*pi
const alpha = key[1] / 256.0 / 4 + 0.75; // between 0.5 and 1.0
const colorR = key[2]; // between 0 and 255
const colorG = key[3];
const colorB = key[4];

const fullSize = 512; // size of the input
const size = fullSize / 2; // size of the output
const offset = size / 2; // offset for rotation/translation

function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}

function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}

// canvas
const canvas = createCanvas(size, size);
const ctx = canvas.getContext("2d");

// base color
ctx.fillStyle = rgbToHex(colorR, colorG, colorB);
ctx.fillRect(0, 0, size, size);

// rotation (relative to image center)
ctx.translate(offset, offset);
ctx.rotate(angle);
ctx.translate(-offset, -offset);

// render the image full size, on half size canvas
// so that we don't see broken corners
ctx.globalAlpha = alpha;
const image = await loadImage(path.join(__dirname, "assets/glam.png"));
ctx.drawImage(image, -offset, -offset, fullSize, fullSize);

// return the image
const buffer = canvas.toBuffer("image/png");
res.set("content-type", "image/png");
res.send(buffer);
});

const port = process.env.PORT || 3333;
const port = process.env.PORT || 8080;
const server = app.listen(port, () => {
console.log(`Listening at http://localhost:${port}/api`);
});
Expand Down
Loading

0 comments on commit fdcb29d

Please sign in to comment.