Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/feat/add-backend' into feat/add-…
Browse files Browse the repository at this point in the history
…backend-utils
  • Loading branch information
mat1asm committed Mar 21, 2024
2 parents b2b7991 + aad4c7a commit b968c41
Show file tree
Hide file tree
Showing 14 changed files with 421 additions and 162 deletions.
20 changes: 10 additions & 10 deletions backend/.eslintrc
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "script"
},
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"rules": {
"no-undef": "off"
}
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "script"
},
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"rules": {
"no-undef": "off"
}
}
7 changes: 3 additions & 4 deletions backend/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"tabWidth": 2,
"trailingComma": "all",
"arrowParens": "avoid",
"printWidth": 140
"semi": false,
"singleQuote": true,
"trailingComma": "none"
}
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "rm -rf dist && swc ./src -d ./dist --copy-files --ignore \"**/*.test.ts\"",
"build": "tsc --noemit && rm -rf dist && swc ./src -d ./dist --ignore \"**/*.test.ts,**/__test__/**\"",
"lint": "eslint \"src/**/*.{json,js,jsx,ts,tsx}\" && tsc --noemit",
"prettier": "prettier \"./**/*.{json,js,jsx,ts,tsx}\" --write",
"prettier:check": "prettier \"./**/*.{json,js,jsx,ts,tsx}\" --check",
Expand Down
8 changes: 4 additions & 4 deletions backend/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export default {
discord: {
baseUrl: () => process.env.DISCORD_URL ?? "https://discord.com",
baseUrl: () => process.env.DISCORD_URL ?? 'https://discord.com'
},
aws: {
region: process.env.AWS_REGION ?? "us-east-2",
},
};
region: process.env.AWS_REGION ?? 'us-east-2'
}
}
95 changes: 53 additions & 42 deletions backend/src/handlers/discord-signed-digest.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,107 @@
import { Keypair, PublicKey } from "@solana/web3.js";
import { getSecret } from "../utils/secrets";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { isAccessTokenValid, signDiscordDigest } from "../utils/discord";
import { Keypair, PublicKey } from '@solana/web3.js'
import { getSecret } from '../utils/secrets'
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
import { isAccessTokenValid, signDiscordDigest } from '../utils/discord'

export interface DiscordSignedDigestRequest {
publicKey: string;
discordId: string;
publicKey: string
discordId: string
}

export const signDiscordMessage = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
export const signDiscordMessage = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
// TODO: no need to receive disordId really, as we should can just get it using the auth token.
// TODO: publicKey was expected as query param in pyth version
const { publicKey, discordId } = JSON.parse(event.body!) as DiscordSignedDigestRequest;
const accessToken = event.headers["x-auth-token"];
const { publicKey, discordId } = JSON.parse(
event.body!
) as DiscordSignedDigestRequest
const accessToken = event.headers['x-auth-token']

validatePublicKey(publicKey);
validateAccessTokenAndDiscordId(accessToken, discordId);
validatePublicKey(publicKey)
validateAccessTokenAndDiscordId(accessToken, discordId)

await isAccessTokenValid(discordId, accessToken!);
await isAccessTokenValid(discordId, accessToken!)

const claimant = new PublicKey(publicKey!);
const dispenserGuard = await loadDispenserGuard();
const claimant = new PublicKey(publicKey!)
const dispenserGuard = await loadDispenserGuard()

const signedDigest = signDiscordDigest(discordId, claimant, dispenserGuard);
const signedDigest = signDiscordDigest(discordId, claimant, dispenserGuard)

return {
statusCode: 200,
body: JSON.stringify({
signature: Buffer.from(signedDigest.signature).toString("hex"),
publicKey: Buffer.from(signedDigest.publicKey).toString("hex"), // The dispenser guard's public key
fullMessage: Buffer.from(signedDigest.fullMessage).toString("hex"),
}),
};
signature: Buffer.from(signedDigest.signature).toString('hex'),
publicKey: Buffer.from(signedDigest.publicKey).toString('hex'), // The dispenser guard's public key
fullMessage: Buffer.from(signedDigest.fullMessage).toString('hex')
})
}
} catch (err) {
console.error("Error generating signed discord digest", err);
console.error('Error generating signed discord digest', err)
return {
statusCode: 500,
body: JSON.stringify({ error: "Error generating signed discord digest" }),
};
body: JSON.stringify({ error: 'Internal server error' })
}
}
};
}

async function loadDispenserGuard() {
// TODO: Update secret name based on the secret you created in the AWS Secrets Manager
const secretData = await getSecret(process.env.DISPENSER_KEY_SECRET_NAME ?? "xli-test-secret-dispenser-guard");
const dispenserGuardKey = secretData.target;
const secretData = await getSecret(
process.env.DISPENSER_KEY_SECRET_NAME ?? 'xl-dispenser-guard-key'
)
const dispenserGuardKey = secretData.key

const dispenserGuard = Keypair.fromSecretKey(Uint8Array.from(dispenserGuardKey));
const dispenserGuard = Keypair.fromSecretKey(
Uint8Array.from(dispenserGuardKey)
)

return dispenserGuard;
return dispenserGuard
}

function validatePublicKey(publicKey?: string) {
if (!publicKey) {
return {
statusCode: 400,
body: JSON.stringify({
error: "Must provide the 'publicKey' query parameter",
}),
};
error: "Must provide the 'publicKey' query parameter"
})
}
}

if (typeof publicKey !== "string") {
if (typeof publicKey !== 'string') {
return {
statusCode: 400,
body: JSON.stringify({ error: "Invalid 'publicKey' query parameter" }),
};
body: JSON.stringify({ error: "Invalid 'publicKey' query parameter" })
}
}

try {
new PublicKey(publicKey);
new PublicKey(publicKey)
} catch {
return {
statusCode: 400,
body: JSON.stringify({ error: "Invalid 'publicKey' query parameter" }),
};
body: JSON.stringify({ error: "Invalid 'publicKey' query parameter" })
}
}
}

function validateAccessTokenAndDiscordId(AccessToken?: string, discordId?: string) {
function validateAccessTokenAndDiscordId(
AccessToken?: string,
discordId?: string
) {
if (!AccessToken) {
return {
statusCode: 400,
body: JSON.stringify({ error: "Must provide discord auth token" }),
};
body: JSON.stringify({ error: 'Must provide discord auth token' })
}
}

if (!discordId) {
return {
statusCode: 400,
body: JSON.stringify({ error: "Must provide discord id" }),
};
body: JSON.stringify({ error: 'Must provide discord id' })
}
}
}
73 changes: 73 additions & 0 deletions backend/src/handlers/fund-transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
import { getSecret } from '../utils/secrets'
import {
checkTransactions,
deserializeTransactions
} from '../utils/fundTransactions'
import { Keypair } from '@solana/web3.js'

interface FundTransactionRequest {
transactions: unknown
}

export const fundTransaction = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const requestBody = JSON.parse(event.body!) as FundTransactionRequest
validateFundTransactions(requestBody.transactions)
const transactions = deserializeTransactions(requestBody.transactions)
const isTransactionsValid = await checkTransactions(transactions)

if (!isTransactionsValid) {
return {
statusCode: 403,
body: JSON.stringify({ error: 'Unauthorized transactions' })
}
}

const wallet = await loadFunderWallet()

const signedTransactions = await wallet.signAllTransactions(transactions)
return {
statusCode: 200,
body: JSON.stringify({
signedTransactions: signedTransactions.map((tx) => tx.serialize())
})
}
} catch (err) {
console.error('Error fully signing transactions', err)
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
}
}
}

function validateFundTransactions(transactions: unknown) {
if (!Array.isArray(transactions) || transactions.length === 0) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Must provide transactions' })
}
}

if (transactions.length >= 10) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Too many transactions' })
}
}
}

async function loadFunderWallet(): Promise<NodeWallet> {
const secretData = await getSecret(
process.env.FUNDER_WALLET_KEY_SECRET_NAME ?? 'xli-test-secret-funder-wallet'
)
const funderWalletKey = secretData.key

const keypair = Keypair.fromSecretKey(new Uint8Array(funderWalletKey))

return new NodeWallet(keypair)
}
18 changes: 9 additions & 9 deletions backend/src/handlers/helloworld.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { getSecret } from "../utils/secrets";
import { getSecret } from '../utils/secrets'

export const handler = async () => {
if (!process.env.SECRET_NAME) {
return {
statusCode: 500,
body: JSON.stringify({ error: "SECRET_NAME is not set" }),
};
body: JSON.stringify({ error: 'SECRET_NAME is not set' })
}
}

const secretData = await getSecret(process.env.SECRET_NAME!);
const target = secretData.target;
const secretData = await getSecret(process.env.SECRET_NAME!)
const target = secretData.target

console.log(`Hello ${target}`);
console.log(`Hello ${target}`)
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello ${target}` }),
};
};
body: JSON.stringify({ message: `Hello ${target}` })
}
}
10 changes: 6 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { signDiscordMessage } from "./handlers/discord-signed-digest";
import { handler as helloworld } from "./handlers/helloworld";
import { signDiscordMessage } from './handlers/discord-signed-digest'
import { fundTransaction } from './handlers/fund-transactions'
import { handler as helloworld } from './handlers/helloworld'

export const signDiscordMessageHandler = signDiscordMessage;
export const fundTransactionHandler = helloworld;
export const signDiscordMessageHandler = signDiscordMessage
export const helloworldHandler = helloworld
export const fundTransactionHandler = fundTransaction
9 changes: 7 additions & 2 deletions backend/src/token_dispenser.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"instructions": [
{
"name": "initialize",
"docs": ["This can only be called once and should be called right after the program is deployed."],
"docs": [
"This can only be called once and should be called right after the program is deployed."
],
"accounts": [
{
"name": "payer",
Expand Down Expand Up @@ -84,7 +86,10 @@
"name": "claimantFund",
"isMut": true,
"isSigner": false,
"docs": ["Claimant's associated token account to receive the tokens", "Should be initialized outside of this program."]
"docs": [
"Claimant's associated token account to receive the tokens",
"Should be initialized outside of this program."
]
},
{
"name": "config",
Expand Down
10 changes: 5 additions & 5 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type SignedMessage = {
publicKey: Uint8Array;
signature: Uint8Array;
publicKey: Uint8Array
signature: Uint8Array
// recoveryId is undefined for ed25519
recoveryId: number | undefined;
fullMessage: Uint8Array;
};
recoveryId: number | undefined
fullMessage: Uint8Array
}
Loading

0 comments on commit b968c41

Please sign in to comment.