Skip to content

Commit

Permalink
Merge branch 'main' of github.com:wormhole-foundation/example-grant-p…
Browse files Browse the repository at this point in the history
…rogram into backend/add-priority-fee-check
  • Loading branch information
abhidtu2014 committed Mar 27, 2024
2 parents fd512cc + 7cbc1ec commit 297c591
Show file tree
Hide file tree
Showing 28 changed files with 198 additions and 224 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
name: Frontend Tests
on:
pull_request:
paths: [frontend/**]
paths: [frontend/**, token-dispenser/**]
push:
branches: [main]
paths: [frontend/**]
paths: [frontend/**, token-dispenser/**]

jobs:
test:
runs-on: ubuntu-latest
container:
image: jetprotocol/builder:rust-1.68.0-node-18.15.0-solana-1.14.17-anchor-0.27.0-1
env:
ENDPOINT: http://localhost:8899

env:
PROGRAM_ID: Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS

defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@aws-sdk/client-secrets-manager": "^3.535.0",
"@coral-xyz/anchor": "^0.29.0",
"@solana/web3.js": "^1.91.1",
"bs58": "^5.0.0",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
Expand Down
52 changes: 30 additions & 22 deletions backend/src/handlers/discord-signed-digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,62 @@ import { getDispenserKey } from '../utils/secrets'
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
import { getDiscordUser, signDiscordDigest } from '../utils/discord'
import { HandlerError } from '../utils/errors'
import { asJsonResponse } from '../utils/response'

export interface DiscordSignedDigestParams {
publicKey: string
}

let guardKeyPair: Keypair

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

const accessToken = event.headers['x-auth-token']
const accessToken =
event.headers['Authorization'] ??
event.headers['authorization'] ??
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')
})
}
return asJsonResponse(200, {
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 asJsonResponse(err.statusCode, err.body)
}

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

async function loadDispenserGuard() {
if (guardKeyPair) {
return guardKeyPair
}

const secretData = await getDispenserKey()
const dispenserGuardKey = secretData.key

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

return dispenserGuard
guardKeyPair = dispenserGuard
console.log('Loaded dispenser guard key')
return guardKeyPair
}

function validatePublicKey(publicKey?: string) {
Expand All @@ -80,13 +83,18 @@ function validatePublicKey(publicKey?: string) {
}
}

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

const tokenParts = tokenHeaderValue.split(' ')
if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer') {
throw new HandlerError(403, { error: 'Invalid authorization header' })
}

try {
const user = await getDiscordUser(accessToken)
const user = await getDiscordUser(tokenParts[1])
return user.id
} catch (err) {
throw new HandlerError(403, { error: 'Invalid discord access token' })
Expand Down
58 changes: 37 additions & 21 deletions backend/src/handlers/fund-transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import {
checkTransactions,
deserializeTransactions
} from '../utils/fund-transactions'
import { Keypair } from '@solana/web3.js'
import { Keypair, VersionedTransaction } from '@solana/web3.js'
import bs58 from 'bs58'
import { HandlerError } from '../utils/errors'
import { asJsonResponse } from '../utils/response'

export type FundTransactionRequest = Uint8Array[]

let funderWallet: NodeWallet

export const fundTransactions = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
Expand All @@ -20,35 +24,25 @@ export const fundTransactions = async (
const isTransactionsValid = await checkTransactions(transactions)

if (!isTransactionsValid) {
return {
statusCode: 403,
body: JSON.stringify({ error: 'Unauthorized transactions' })
}
return asJsonResponse(403, { 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()))
)
}
logSignatures(signedTransactions)
return asJsonResponse(
200,
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 asJsonResponse(err.statusCode, err.body)
}

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

Expand All @@ -63,10 +57,32 @@ function validateFundTransactions(transactions: unknown) {
}

async function loadFunderWallet(): Promise<NodeWallet> {
if (funderWallet) {
return funderWallet
}

const secretData = await getFundingKey()
const funderWalletKey = secretData.key

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

funderWallet = new NodeWallet(keypair)
console.log('Loaded funder wallet')
return funderWallet
}

function getSignature(tx: VersionedTransaction): string {
if (tx.signatures.length > 0) {
return bs58.encode(tx.signatures[0])
}

return 'unkown signature'
}

return new NodeWallet(keypair)
function logSignatures(signedTransactions: VersionedTransaction[]) {
const sigs: string[] = []
signedTransactions.forEach((tx) => {
sigs.push(getSignature(tx))
})
console.log(`Signed transactions: ${sigs}`)
}
12 changes: 12 additions & 0 deletions backend/src/utils/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { APIGatewayProxyResult } from 'aws-lambda'

export const asJsonResponse = (
statusCode: number,
body: unknown
): APIGatewayProxyResult => {
return {
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}
}
27 changes: 22 additions & 5 deletions backend/src/utils/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,45 @@ import config from '../config'
const client = new SecretsManagerClient({ region: config.aws.region })

export async function getDispenserKey() {
let key: string
if (config.keys.dispenserGuard.key) {
return { key: JSON.parse(config.keys.dispenserGuard.key) }
console.log('Using dispenser guard key from config')
key = config.keys.dispenserGuard.key
}

return getSecret(config.keys.dispenserGuard.secretName)
key = await getSecretKey(config.keys.dispenserGuard.secretName, 'key')
return { key: JSON.parse(key) }
}

export async function getFundingKey() {
export async function getFundingKey(): Promise<{ key: Uint8Array }> {
let key: string
if (config.keys.funding.key) {
return { key: JSON.parse(config.keys.funding.key) }
console.log('Using funding key from config')
key = config.keys.funding.key
}

return getSecret(config.keys.funding.secretName)
key = await getSecretKey(config.keys.funding.secretName, 'key')
return { key: JSON.parse(key) }
}

export async function getSecretKey(secretName: string, keyName: string) {
const secret = await getSecret(secretName)
return secret[keyName]
}

export async function getSecret(secretName: string) {
try {
const command = new GetSecretValueCommand({ SecretId: secretName })
const response = await client.send(command)
if (response.SecretString) {
console.log(
`Retrieved secret: ${secretName}. ${response.SecretString.length} characters long`
)
return JSON.parse(response.SecretString)
} else if (response.SecretBinary) {
console.log(
`Retrieved binary secret: ${secretName}. ${response.SecretBinary.length} characters long`
)
// For binary secrets, use Buffer to decode
const buff = Buffer.from(response.SecretBinary.toString(), 'base64')
return JSON.parse(buff.toString('ascii'))
Expand Down
4 changes: 2 additions & 2 deletions backend/test/handlers/discord-signed-digest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const givenDownstreamServicesWork = () => {
}),
http.all('https://secretsmanager.us-east-2.amazonaws.com', () => {
return HttpResponse.json({
SecretString: JSON.stringify({ key: [...new Keypair().secretKey] })
SecretString: JSON.stringify({ key: `[${new Keypair().secretKey}]` })
})
})
)
Expand All @@ -83,7 +83,7 @@ const whenSignDiscordMessageCalled = async (
) => {
response = await signDiscordMessage({
queryStringParameters: queryParams,
headers: { 'x-auth-token': 'token' }
headers: { Authorization: 'Bearer token' }
} as unknown as APIGatewayProxyEvent)
}

Expand Down
2 changes: 1 addition & 1 deletion backend/test/handlers/fund-transactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ const givenDownstreamServicesWork = () => {
server.use(
http.all('https://secretsmanager.us-east-2.amazonaws.com', () => {
return HttpResponse.json({
SecretString: JSON.stringify({ key: [...FUNDER_KEY.secretKey] })
SecretString: JSON.stringify({ key: `[${[...FUNDER_KEY.secretKey]}]` })
})
})
)
Expand Down
12 changes: 12 additions & 0 deletions backend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,11 @@ base-x@^3.0.2:
dependencies:
safe-buffer "^5.0.1"

base-x@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==

base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
Expand Down Expand Up @@ -2440,6 +2445,13 @@ bs58@^4.0.0, bs58@^4.0.1:
dependencies:
base-x "^3.0.2"

bs58@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
dependencies:
base-x "^4.0.0"

bser@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/buttons/ProceedButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function ProceedButton({
return (
<Tooltip content={tooltipContent} placement={'bottom'}>
<Button onClick={onProceed} type={'primary'} disabled={disabled}>
{hideText ? '' : 'proceed'} <Arrow />
{hideText ? '' : 'Proceed'} <Arrow />
</Button>
</Tooltip>
)
Expand Down
2 changes: 1 addition & 1 deletion frontend/hooks/useDiscordAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,5 @@ export default function useDiscordAuth() {
// Fetch user info on getDiscordUserInfo change, that should mutate on token or isAuthenticated change
useEffect(() => getDiscordUserInfo(), [getDiscordUserInfo])

return { authenticate, clear, isConnecting, isAuthenticated, profile }
return { authenticate, clear, isConnecting, isAuthenticated, profile, token }
}
9 changes: 5 additions & 4 deletions frontend/hooks/useSignMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { fetchDiscordSignedMessage } from 'utils/api'
import { useTokenDispenserProvider } from './useTokenDispenserProvider'
import { ChainName } from '@components/wallets/Cosmos'
import { useWallet as useAlgorandWallet } from '@components/Ecosystem/AlgorandProvider'
import useDiscordAuth from './useDiscordAuth'

// SignMessageFn signs the message and returns it.
// It will return undefined:
Expand Down Expand Up @@ -177,11 +178,11 @@ export function useSuiSignMessage(): SignMessageFn {

export function useDiscordSignMessage(): SignMessageFn {
const tokenDispenser = useTokenDispenserProvider()

const { token } = useDiscordAuth()
return useCallback(async () => {
if (tokenDispenser?.claimant === undefined) return
return await fetchDiscordSignedMessage(tokenDispenser.claimant)
}, [tokenDispenser?.claimant])
if (tokenDispenser?.claimant === undefined || token === null) return
return await fetchDiscordSignedMessage(tokenDispenser.claimant, token)
}, [tokenDispenser?.claimant, token])
}

// This hook returns a function to sign message for the Algorand wallet.
Expand Down
2 changes: 1 addition & 1 deletion frontend/integration/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function handlerAmountAndProof(
} else {
res.status(200).json({
amount: result.amount,
proof: result.proof,
hashes: result.proof_of_inclusion,
address: identity,
})
}
Expand Down
Loading

0 comments on commit 297c591

Please sign in to comment.