Skip to content

Commit

Permalink
Treasury selection (#58)
Browse files Browse the repository at this point in the history
* add lookup table back to config for ease of use in sdk

* manually update program IDLs in claim_sdk

* formatting

* fix integration tests

* more integration test fixes

* fix compilation by putting stub in place

* formatting

* adds treasury pub-key to ClaimEvent

* minor fix

* multi-treasury deployment

* mt-02

* WIP treasury and funders pull and selection

* treasury list without env

* support multiple funders and use same one than frontend to sign

* sign transaction in the frontend and pass the funder used to the backend

* fix mayority of tests

* remove injective test to be up to date with main

* fix BE tests

* FE prettier

* BE prettier

* fix linter

* updated treasury addresses

* fix linting

* Fix algorand claim info identity serialization (#56)

* Add message for EVM wallet selector (#59)

* backend: modifies private key secret format

We expect a list of private keys as an array of number arrays.

* backend: adds types for node v18

* frontend: use `JSON.parse` to parse private key in test

* backend: prettier run

* all: replaces token dispenser program id env var with constant

* update toStringWithDecimals from 9 to 6

* remove injective test

* fix yarn.lock

* fix backend prettier

* frontend fix prettier

* token dispenser fix prettier

* run pre-commit locally

* linter

* added solana/web3 dep

* multi-treasury deployment 03

* use tokenDispenserProgramId from same workspace

* prettier

* remove dep between front and back

* update treasuries and funders

* frontend prettier

---------

Co-authored-by: nonergodic <[email protected]>
Co-authored-by: solanoe <[email protected]>
Co-authored-by: Martin Picco <[email protected]>
Co-authored-by: Valentino Conti <[email protected]>
Co-authored-by: Sebastián Claudio Nale <[email protected]>
  • Loading branch information
6 people authored Apr 1, 2024
1 parent f27cef4 commit 32be07b
Show file tree
Hide file tree
Showing 41 changed files with 2,150 additions and 2,173 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ jobs:
env:
ENDPOINT: http://localhost:8899

env:
PROGRAM_ID: WApA1JC9eJLaULc2Ximo5TffuqCESzf47JZsuhYvfzC

defaults:
run:
working-directory: ./frontend
Expand Down
1 change: 0 additions & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,3 @@ 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
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@types/aws-lambda": "^8.10.136",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"body-parser": "^1.20.2",
Expand Down
2 changes: 1 addition & 1 deletion backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default {
aws: {
region: process.env.AWS_REGION ?? 'us-east-2'
},
tokenDispenserProgramId: () => process.env.TOKEN_DISPENSER_PROGRAM_ID,
tokenDispenserProgramId: () => 'WapFw9mSyHh8trDDRy7AamUn1V7QiGaVvtouj5AucQA',
secrets: {
dispenserGuard: {
/** optional. mostly for local testing */
Expand Down
43 changes: 28 additions & 15 deletions backend/src/handlers/fund-transactions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
import { getFundingKey } from '../utils/secrets'
import { getFundingKeys } from '../utils/secrets'
import {
checkTransactions,
deserializeTransactions,
extractCallData
} from '../utils/fund-transactions'
import { Keypair, VersionedTransaction } from '@solana/web3.js'
import { VersionedTransaction } from '@solana/web3.js'
import bs58 from 'bs58'
import { HandlerError } from '../utils/errors'
import { asJsonResponse } from '../utils/response'
Expand All @@ -15,7 +15,7 @@ import { saveSignedTransactions } from '../utils/persistence'

export type FundTransactionRequest = Uint8Array[]

let funderWallet: NodeWallet
const funderWallets: Record<string, NodeWallet> = {}

export const fundTransactions = async (
event: APIGatewayProxyEvent
Expand All @@ -24,15 +24,29 @@ export const fundTransactions = async (
const requestBody = JSON.parse(event.body!)
validateFundTransactions(requestBody)
const transactions = deserializeTransactions(requestBody)
const isTransactionsValid = await checkTransactions(transactions)
const isTransactionsValid = await checkTransactions(
transactions.map((txWithFunder) => txWithFunder.transaction)
)

if (!isTransactionsValid) {
return asJsonResponse(403, { error: 'Unauthorized transactions' })
}

const wallet = await loadFunderWallet()
const wallets = await loadFunderWallets()

const signedTransactions: VersionedTransaction[] = []

for (const txWithFunder of transactions) {
const funderWallet = wallets[txWithFunder.funder]
if (!funderWallet) {
return asJsonResponse(403, { error: 'Unauthorized funder' })
}

signedTransactions.push(
await funderWallet.signTransaction(txWithFunder.transaction)
)
}

const signedTransactions = await wallet.signAllTransactions(transactions)
await saveSignedTransactions(getSignatures(signedTransactions))

return asJsonResponse(
Expand Down Expand Up @@ -60,19 +74,18 @@ function validateFundTransactions(transactions: unknown) {
}
}

async function loadFunderWallet(): Promise<NodeWallet> {
if (funderWallet) {
return funderWallet
async function loadFunderWallets(): Promise<Record<string, NodeWallet>> {
if (Object.keys(funderWallets).length > 0) {
return funderWallets
}

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

const keypair = Keypair.fromSecretKey(Uint8Array.from(funderWalletKey))
secretData.forEach((keypair) => {
funderWallets[keypair.publicKey.toBase58()] = new NodeWallet(keypair)
})

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

function getSignature(tx: VersionedTransaction): string {
Expand Down
31 changes: 24 additions & 7 deletions backend/src/utils/fund-transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,29 @@ const SET_COMPUTE_UNIT_PRICE_DISCRIMINANT = 3

const MAX_COMPUTE_UNIT_PRICE = BigInt(1_000_000)

export type TransactionWithFunder = {
transaction: VersionedTransaction
funder: string
}

export type SerializedTransactionWithFunder = {
tx: Uint8Array
funder: string
}

export function deserializeTransactions(
transactions: unknown
): VersionedTransaction[] {
): TransactionWithFunder[] {
try {
return (transactions as Uint8Array[]).map((serializedTx) =>
VersionedTransaction.deserialize(Buffer.from(serializedTx))
return (transactions as SerializedTransactionWithFunder[]).map(
(serializedTx) => {
return {
transaction: VersionedTransaction.deserialize(
Buffer.from(serializedTx.tx)
),
funder: serializedTx.funder
}
}
)
} catch (err) {
console.error('Failed to deserialize transactions', err)
Expand All @@ -31,14 +48,14 @@ export function deserializeTransactions(
}

async function loadWhitelistedProgramIds(): Promise<PublicKey[]> {
const programId = config.tokenDispenserProgramId()
if (!programId) {
const tokenDispenserProgramId = config.tokenDispenserProgramId()
if (!tokenDispenserProgramId) {
throw new Error('Token dispenser program ID not set')
}

const PROGRAM_ID = new PublicKey(programId)
const tokenDispenserPublicKey = new PublicKey(tokenDispenserProgramId)
return [
PROGRAM_ID,
tokenDispenserPublicKey,
Secp256k1Program.programId,
Ed25519Program.programId,
ComputeBudgetProgram.programId
Expand Down
21 changes: 11 additions & 10 deletions backend/src/utils/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import {
GetSecretValueCommand
} from '@aws-sdk/client-secrets-manager'
import config from '../config'
import { Keypair } from '@solana/web3.js'

const client = new SecretsManagerClient({ region: config.aws.region })

interface SecretPrivateKeys {
keys: string
}

export async function getDispenserKey() {
let key: string
if (config.secrets.dispenserGuard.key) {
Expand All @@ -19,16 +24,12 @@ export async function getDispenserKey() {
return { key: JSON.parse(key) }
}

export async function getFundingKey(): Promise<{ key: Uint8Array }> {
let key: string
if (config.secrets.funding.key()) {
console.log('Using funding key from config')
key = config.secrets.funding.key()!
} else {
key = await getSecretKey(config.secrets.funding.secretName, 'key')
}

return { key: JSON.parse(key) }
export async function getFundingKeys(): Promise<Keypair[]> {
const secret = (await getSecret(
config.secrets.funding.secretName
)) as SecretPrivateKeys
const keys = JSON.parse(secret.keys) as number[][]
return keys.map((key) => Keypair.fromSecretKey(Uint8Array.from(key)))
}

export async function getInfluxToken(): Promise<string> {
Expand Down
27 changes: 19 additions & 8 deletions backend/test/handlers/fund-transactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import { IDL, TokenDispenser } from '../../src/token-dispenser'
import { fundTransactions } from '../../src/handlers/fund-transactions'
import { GenericContainer, StartedTestContainer } from 'testcontainers'
import { InfluxDB } from '@influxdata/influxdb-client'
import config from '../../src/config'

const RANDOM_BLOCKHASH = 'HXq5QPm883r7834LWwDpcmEM8G8uQ9Hqm1xakCHGxprV'
const tokenDispenserProgramId = config.tokenDispenserProgramId()
const TokenDispenserPublicKey = new PublicKey(tokenDispenserProgramId)
const INFLUX_TOKEN =
'jsNTEHNBohEjgKqWj1fR8fJjYlBvcYaRTY68-iQ5Y55X_Qr3VKGSvqJz78g4jV8mPiUTQLPYq2tLs_Dy8M--nw=='
const PROGRAM_ID = new PublicKey('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS')
const FUNDER_KEY = new Keypair()
const server = setupServer()
const influx = new GenericContainer('influxdb')
Expand Down Expand Up @@ -55,9 +57,9 @@ describe('fundTransactions integration test', () => {
8086
)}`
process.env.INFLUXDB_FLUSH_ENABLED = 'true'

process.env.AWS_ACCESS_KEY_ID = 'key'
process.env.AWS_SECRET_ACCESS_KEY = 'secret'
process.env.FUNDING_WALLET_KEY = `[${FUNDER_KEY.secretKey}]`
process.env.TOKEN_DISPENSER_PROGRAM_ID = PROGRAM_ID.toString()
}, 20_000)

afterAll(async () => {
Expand Down Expand Up @@ -230,7 +232,7 @@ describe('fundTransactions integration test', () => {
const instructions = [
tokenDispenserInstruction,
createComputeUnitLimitInstruction(200),
createComputeUnitPriceInstruction(BigInt(10000000)),
createComputeUnitPriceInstruction(BigInt(10_000_000)),
createSecp256k1ProgramInstruction()
]

Expand All @@ -246,7 +248,7 @@ describe('fundTransactions integration test', () => {
await createTokenDispenserProgramInstruction()
])

const callData = extractCallData(versionedTx, PROGRAM_ID.toBase58())
const callData = extractCallData(versionedTx, tokenDispenserProgramId)

expect(callData).not.toBeNull()
expect(callData?.amount.toNumber()).toBe(3000000)
Expand All @@ -272,7 +274,9 @@ 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({
keys: JSON.stringify([[...FUNDER_KEY.secretKey]])
})
})
})
)
Expand All @@ -295,7 +299,14 @@ const givenCorrectTransaction = async () => {

const whenFundTransactionsCalled = async () => {
response = await fundTransactions({
body: JSON.stringify(input.map((tx) => Buffer.from(tx.serialize())))
body: JSON.stringify(
input.map((tx) => {
return {
tx: Buffer.from(tx.serialize()),
funder: FUNDER_KEY.publicKey
}
})
)
} as unknown as APIGatewayProxyEvent)
}

Expand Down Expand Up @@ -354,7 +365,7 @@ const createTestLegacyTransactionFromInstructions = (
const createTokenDispenserProgramInstruction = async () => {
const tokenDispenser = new Program(
IDL,
PROGRAM_ID,
TokenDispenserPublicKey,
new AnchorProvider(
new Connection('http://localhost:8899'),
new NodeWallet(new Keypair()),
Expand Down
7 changes: 7 additions & 0 deletions backend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2038,6 +2038,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==

"@types/node@^18":
version "18.19.28"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.28.tgz#c64a2c992c8ebbf61100a4570e4eebc1934ae030"
integrity sha512-J5cOGD9n4x3YGgVuaND6khm5x07MMdAKkRyXnjVR6KFhLMNh2yONGiP7Z+4+tBOt5mK+GvDTiacTOVGGpqiecw==
dependencies:
undici-types "~5.26.4"

"@types/node@^18.11.18":
version "18.19.26"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729"
Expand Down
1 change: 0 additions & 1 deletion frontend/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ FUNDER_KEYPAIR=[145,197,43,77,224,103,196,174,132,195,48,31,177,97,237,163,15,19
DEPLOYER_WALLET=[145,197,43,77,224,103,196,174,132,195,48,31,177,97,237,163,15,196,217,142,181,204,104,107,98,82,213,0,155,140,218,180,30,119,201,38,51,176,207,221,193,222,235,244,163,250,125,66,68,196,45,208,212,201,232,178,100,163,24,21,106,83,66,174]


PROGRAM_ID=WApA1JC9eJLaULc2Ximo5TffuqCESzf47JZsuhYvfzC
CSV_DIR=/tmp

# Datadog Event Subscriber Configs
Expand Down
Loading

0 comments on commit 32be07b

Please sign in to comment.