-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat/add backend lambda functions (#20)
* Add initial deps for AWS lambda functions * Add hello world for testing * Add discord function|Use dummy IDL * Add todos * Run prettier * Run frontend workflow only for frontend folder * Make changes to frontend workflow * Fix type issue * Fix pre-commit github workflow in CI * single index for lambdas * Add fundTransaction implementation * Use prettier same as frontend * Minor fix * Add validate fund txn request body * Fix pre-commit hook in CI * backend: adding integration test (#27) * use swcrc * jest cfg in package json * backend gh action * discord sign integration test * BE: serve lambdas locally + validation fixes (#32) * local lambda server + move process.env to config file * program id as cfg instead of secret * simple readme * fix input validation for discord-signed-digest * fund-transactions test * fix fund txs validation * Update IDL json * Minor updates|Downgrade prettier * include json in build * readme prettier --------- Co-authored-by: matias martinez <matias@xlabs.xyz> Co-authored-by: Matías Martínez <131624652+mat1asm@users.noreply.github.com>
- Loading branch information
1 parent
fb4f8ae
commit 7de14d5
Showing
24 changed files
with
7,001 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"semi": false, | ||
"singleQuote": true, | ||
"trailingComma": "none" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.