Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add backend lambda functions #20

Merged
merged 26 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c684c81
Add initial deps for AWS lambda functions
abhidtu2014 Mar 19, 2024
f0530a8
Add hello world for testing
abhidtu2014 Mar 19, 2024
0cd645b
Add discord function|Use dummy IDL
abhidtu2014 Mar 19, 2024
ca51586
Add todos
abhidtu2014 Mar 19, 2024
ead1657
Run prettier
abhidtu2014 Mar 19, 2024
bb23d14
Run frontend workflow only for frontend folder
abhidtu2014 Mar 19, 2024
f0ca9ec
Make changes to frontend workflow
abhidtu2014 Mar 19, 2024
aee457f
Fix type issue
abhidtu2014 Mar 19, 2024
c6ce722
Fix pre-commit github workflow in CI
abhidtu2014 Mar 19, 2024
7566e2a
single index for lambdas
mat1asm Mar 19, 2024
041586e
Merge remote-tracking branch 'origin/feat/add-backend' into feat/add-…
mat1asm Mar 19, 2024
3534baf
Merge remote-tracking branch 'origin/main' into feat/add-backend
mat1asm Mar 19, 2024
4ad5677
Add fundTransaction implementation
abhidtu2014 Mar 20, 2024
f0e20fd
Use prettier same as frontend
abhidtu2014 Mar 20, 2024
3fbb9ef
Minor fix
abhidtu2014 Mar 20, 2024
4bc8706
Add validate fund txn request body
abhidtu2014 Mar 20, 2024
aad4c7a
Fix pre-commit hook in CI
abhidtu2014 Mar 20, 2024
7ce05bf
backend: adding integration test (#27)
mat1asm Mar 21, 2024
7cb9688
BE: serve lambdas locally + validation fixes (#32)
mat1asm Mar 22, 2024
21d96ea
Merge main
abhidtu2014 Mar 22, 2024
a8763ac
Update IDL json
abhidtu2014 Mar 22, 2024
37a785b
Minor updates|Downgrade prettier
abhidtu2014 Mar 22, 2024
10526a7
include json in build
mat1asm Mar 22, 2024
3d853bc
readme prettier
mat1asm Mar 22, 2024
bacee8f
Merge remote-tracking branch 'origin/main' into feat/add-backend
mat1asm Mar 22, 2024
97fd1e3
Merge remote-tracking branch 'origin/main' into feat/add-backend
mat1asm Mar 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Backend Tests
on:
pull_request:
paths: [backend/**]
push:
branches: [main]
paths: [backend/**]

jobs:
test:
runs-on: ubuntu-latest

defaults:
run:
working-directory: ./backend

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: yarn
cache-dependency-path: backend/yarn.lock
- name: Install deps
run: yarn install --frozen-lockfile
- name: Prettier check
run: yarn prettier:check
- name: Lint
run: yarn lint
- name: Build
run: yarn build
- name: Test
run: yarn test
2 changes: 2 additions & 0 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: Frontend Tests
on:
pull_request:
paths: [frontend/**]
push:
branches: [main]
paths: [frontend/**]

jobs:
test:
Expand Down
12 changes: 12 additions & 0 deletions backend/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +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"
}
}
41 changes: 41 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build
/dist

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# env
.env

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel

# typescript
*.tsbuildinfo
5 changes: 5 additions & 0 deletions backend/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none"
}
14 changes: 14 additions & 0 deletions backend/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"minify": false,
"jsc": {
"target": "es2016",
"parser": {
"syntax": "typescript",
"preserveAllComments": false
}
},
"module": {
"type": "commonjs",
"strict": true
}
}
31 changes: 31 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Backend

This module contains two functions meant to be executed as AWS Lambda functions.

## Run locally

Set some env vars:

```bash
export DISPENSER_WALLET_KEY = [your, solana, private, key]
export FUNDING_WALLET_KEY = [your, solana, private, key]
```

Then run:

```bash
yarn serve
```

Will expose two endpoints on localhost:8002:

`GET /api/grant/v1/discord_signed_message` => signDiscordMessageHandler
`POST /api/grant/v1/fund_transaction` => fundTransactionHandler

## Deployment environment variables

Following env vars are required:

- `DISPENSER_KEY_SECRET_NAME`: private key of the wallet that will be used to sign the discord message
- `FUNDER_WALLET_KEY_SECRET_NAME`: private key of the wallet that will be used to fund the transactions
- `TOKEN_DISPENSER_PROGRAM_ID`: the program id of the token dispenser
46 changes: 46 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "tsc --noemit && rm -rf dist && swc ./src -d ./dist --copy-files --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",
"test": "jest",
"test:coverage": "jest --coverage",
"serve": "node -r @swc-node/register src/serve.ts"
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.535.0",
"@coral-xyz/anchor": "^0.29.0",
"@solana/web3.js": "^1.91.1",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@swc-node/register": "^1.9.0",
"@swc/cli": "^0.3.10",
"@swc/core": "^1.4.8",
"@swc/jest": "^0.2.36",
"@swc/types": "^0.1.6",
"@types/aws-lambda": "^8.10.136",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"body-parser": "^1.20.2",
"eslint": "^8.57.0",
"express": "^4.19.1",
"jest": "^29.7.0",
"msw": "^2.2.9",
"prettier": "^2.7.1",
"typescript": "^5.4.2"
},
"jest": {
"transform": {
"^.+\\.(t|j)sx?$": "@swc/jest"
}
}
}
26 changes: 26 additions & 0 deletions backend/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default {
discord: {
baseUrl: process.env.DISCORD_URL ?? 'https://discord.com'
},
aws: {
region: process.env.AWS_REGION ?? 'us-east-2'
},
tokenDispenserProgramId: () => process.env.TOKEN_DISPENSER_PROGRAM_ID,
keys: {
dispenserGuard: {
/** optional. mostly for local testing */
key: process.env.DISPENSER_WALLET_KEY,
/** required. with a default value and used when when key not set */
secretName:
process.env.DISPENSER_KEY_SECRET_NAME ?? 'xl-dispenser-guard-key'
},
funding: {
/** optional. mostly for local testing */
key: process.env.FUNDING_WALLET_KEY,
/** required. with a default value and used when when key not set */
secretName:
process.env.FUNDER_WALLET_KEY_SECRET_NAME ??
'xli-test-secret-funder-wallet'
}
}
}
94 changes: 94 additions & 0 deletions backend/src/handlers/discord-signed-digest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Keypair, PublicKey } from '@solana/web3.js'
import { getDispenserKey } from '../utils/secrets'
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
import { getDiscordUser, signDiscordDigest } from '../utils/discord'
import { HandlerError } from '../utils/errors'

export interface DiscordSignedDigestParams {
publicKey: string
}

export const signDiscordMessage = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const publicKey = (event.queryStringParameters ?? {})['publicKey']
validatePublicKey(publicKey)

const accessToken = event.headers['x-auth-token']
const discordId = await getDiscordId(accessToken)

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

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')
})
}
} catch (err: HandlerError | unknown) {
console.error('Error generating signed discord digest', err)
if (err instanceof HandlerError) {
return {
statusCode: err.statusCode,
body: JSON.stringify(err.body)
}
}

return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
}
}
}

async function loadDispenserGuard() {
const secretData = await getDispenserKey()
const dispenserGuardKey = secretData.key

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

return dispenserGuard
}

function validatePublicKey(publicKey?: string) {
if (!publicKey) {
throw new HandlerError(400, {
error: "Must provide the 'publicKey' query parameter"
})
}

if (typeof publicKey !== 'string') {
throw new HandlerError(400, {
error: "Invalid 'publicKey' query parameter"
})
}

try {
new PublicKey(publicKey)
} catch {
throw new HandlerError(400, {
error: "Invalid 'publicKey' query parameter"
})
}
}

async function getDiscordId(accessToken?: string) {
if (!accessToken) {
throw new HandlerError(400, { error: 'Must provide discord auth token' })
}

try {
const user = await getDiscordUser(accessToken)
return user.id
} catch (err) {
throw new HandlerError(403, { error: 'Invalid discord access token' })
}
}
72 changes: 72 additions & 0 deletions backend/src/handlers/fund-transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
import { getFundingKey } from '../utils/secrets'
import {
checkTransactions,
deserializeTransactions
} from '../utils/fund-transactions'
import { Keypair } from '@solana/web3.js'
import { HandlerError } from '../utils/errors'

export type FundTransactionRequest = Uint8Array[]

export const fundTransactions = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const requestBody = JSON.parse(event.body!)
validateFundTransactions(requestBody)
const transactions = deserializeTransactions(requestBody)
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.map((tx) => Buffer.from(tx.serialize()))
)
}
} catch (err: HandlerError | unknown) {
console.error('Error signing transactions', err)

if (err instanceof HandlerError) {
return {
statusCode: err.statusCode,
body: JSON.stringify(err.body)
}
}

return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
}
}
}

function validateFundTransactions(transactions: unknown) {
if (!Array.isArray(transactions) || transactions.length === 0) {
throw new HandlerError(400, { error: 'Must provide transactions' })
}

if (transactions.length >= 10) {
throw new HandlerError(400, { error: 'Too many transactions' })
}
}

async function loadFunderWallet(): Promise<NodeWallet> {
const secretData = await getFundingKey()
const funderWalletKey = secretData.key

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

return new NodeWallet(keypair)
}
Loading
Loading